@ontahi/devtools
v1.0.0-alpha.12
Published
Development-only runtime diagnostics and React tooling for Ontahi clients
Readme
@ontahi/devtools
Experimental, development-only diagnostics for Ontahí web clients.
The package currently provides a bounded in-memory diagnostic store, a compositional
RuntimeTransport instrument, and an opt-in React panel. The panel leads with application intent,
keeps transport families as supporting metadata, and lets each request and response move between a
semantic projection, body JSON, and its complete Runtime Protocol envelope. It does not patch
fetch, WebSocket, or browser globals, and it does not persist or upload diagnostic data.
Activity Graph Read summaries follow Settings → Authoring language in the list, detail heading,
and Visual request (Selection and ordering included). Filtering matches the displayed dialect.
This is a pure projection of captured canonical data, not a CodeMirror editor or a conversion of
previously rendered text. Changing the preference reprojects existing entries without traffic or
payload mutation; Body JSON, Envelope, and copied JSON remain the captured protocol data.
Summaries are compact diagnostic labels, not guaranteed executable Console documents: they may
include Views, multiple ordering keys, redacted values, or Reference selections outside today's
Console grammar. Nullable get is shown as first, not guessed to be exists. Command and
Operation labels remain unchanged until those authoring dialects are defined.
import { entity, field } from '@ontahi/core/data-graph';
import { createRuntimeTransportRouter } from '@ontahi/core/runtime/protocol';
import { createOntahiDiagnostics, instrumentRuntimeTransport } from '@ontahi/devtools';
import { OntahiDevtools } from '@ontahi/devtools/react';
import { createFetchRuntimeTransport, createWebSocketRuntimeTransport } from '@ontahi/react/graph';
const TodoItem = entity('TodoItem', {
id: field.id(),
completed: field.boolean(),
});
const diagnostics = createOntahiDiagnostics();
const runtimeTransport = createRuntimeTransportRouter({
transports: {
http: instrumentRuntimeTransport({
diagnostics,
id: 'http',
kind: 'fetch',
transport: createFetchRuntimeTransport(),
}),
websocket: instrumentRuntimeTransport({
diagnostics,
id: 'websocket',
kind: 'websocket',
transport: createWebSocketRuntimeTransport(),
}),
},
routing: {
'graph.read': 'websocket',
'graph.command': 'websocket',
operation: 'websocket',
'durable.operation.observe': 'websocket',
},
});
<OntahiDevtools
console={{
entities: [TodoItem],
initialDocument: 'TodoItem.where(completed = false).many()',
}}
diagnostics={diagnostics}
runtimeTransport={runtimeTransport}
/>;The Console supports filtered and unfiltered read terminals. Tag.count() and
TodoItem.where(completed = false).count() lower directly to the canonical Graph Read count
mode; count requests do not inherit the Console row limit or a row cardinality.
Tag.exists() and TodoItem.where(completed = false).exists() return a Boolean in both Visual
and JSON. They reuse nullable get with a limit of one and the existing read policy, then project
the successful result to presence/absence. Errors remain errors, never false. exists() accepts
neither limit nor orderBy, and has no table controls. Activity retains the actual get exchange.
Many reads may override the default row limit in source, for example Tag.limit(10).many() or
TodoItem.where(completed = false).limit(5).many().
The composer includes TS / Declarative controls. For example, TodoItem where completed = false
selects many by default; append order by title descending limit 10, or an explicit terminal such
as first, one, count, or exists with the existing modifier restrictions. Both dialects keep
the same runtime and policy boundary, Boolean/enum controls, and permission-aware completion.
Header sorting and limit changes edit the active dialect and submit through Runtime Transport.
Accepting order by/orderBy reopens completion for its permitted Fields. Ordering suggestions
load through a metadata-only graph.read request as soon as the draft names a reflected Entity,
including incomplete queries. No initial Run is required. Field and direction dropdowns edit the
source in both dialects without executing; undo/redo and Escape work like Boolean/enum controls.
Loading, denied/unavailable metadata (with retry), and a known empty policy are distinguished.
Typing and accepting suggestions never execute data queries.
Switching dialect does not run a query. Undo/redo restores the original source and dialect together; equivalent converted queries keep the current result without a false stale notice. Invalid drafts (including unsupported comments) block switching with an explanation and are never discarded. Empty drafts may switch unchanged. Settings → Authoring language saves the preferred dialect for Ontahí authoring editors on this browser origin, including other tabs. With no saved preference, TS is the default. The Console switch is a local override; it does not change Settings. Visiting Settings or Activity preserves the mounted Console draft, result, and undo history. A preference change converts valid drafts without running; incomplete drafts retain their dialect with a notice.
An explicit console.initialDialect is a host override of the saved preference. If supplied,
initialDocument must use that initial dialect (TS when omitted); a saved preference then converts
it safely. Explorer Selection predicates already share their syntax across both dialects, and
plain-text searches remain plain text. Syntax highlighting uses a dedicated dark palette for the
Console, including where, many, other clauses, Fields, and literals.
Query ordering is shared by the source editor and the Visual result table:
Tag.orderBy(name).limit(10).many()
Tag.orderBy(name, desc).many()Run reflects source ordering in the table header. Clicking a scalar Field header cycles through
ascending, descending, and no explicit order: it edits only the ordering source ranges as one
undoable transaction and submits a new Graph Read. Sorting happens in the runtime before the
limit, never just over visible rows. The first slice supports one ordering Field; first() and
one() also accept textual ordering, while count() and exists() do not.
Typing or undoing does not execute. The table and arrow describe the last successful execution.
The result uses one compact toolbar: last successful round-trip duration (including transport),
editable limit for many reads, and Visual/JSON. It does not repeat the query, success message, or
row-count/limit summary above the table. Draft changes, pending reads, and failures leave the
snapshot visible with a short toolbar notice; actionable errors remain visible in the result body.
Controls are disabled while running or when the
draft is invalid, targets another Entity, or is no longer a many read. A valid same-Entity draft
is preserved and submitted with the new sort. Headers intersect intrinsically sortable Fields with
the receiver's ordering capabilities, discovered independently of data execution. Denied
headers remain focusable but inactive, with a tooltip explaining the policy restriction. Missing
or malformed capabilities preserve readable results but disable ordering with a refresh explanation.
Replacing Runtime Transport or changing its graph.read route automatically refreshes metadata;
old results still require a Run on that transport before table edits. An access_denied response
also refreshes capabilities. Pass console.identity the same ExecutionIdentity used by the
application's Graph provider. Changing its principal or cacheScope immediately hides prior
results, cancels pending reads, and refreshes ordering metadata, without losing the draft or undo
history. Use cacheScope for tenant/role/policy revisions that do not change the principal.
Equivalent identity values do not trigger discovery. This is local cache invalidation, not a
credential or a client-supplied permission grant; it is never added to protocol requests.
When omitted, identity defaults to anonymous. Hosts must propagate authority changes, including
login/logout; unreported cookie or server policy changes cannot be detected automatically.
Receiver policy remains authoritative on every request. Within an unchanged identity, rejections
are shown without replacing the successful result data. Activity remains an explicit diagnostic
history; this invalidation does not erase its captured exchanges.
The many-result toolbar's numeric
Limit control accepts non-negative safe integers, including zero. Apply or Enter edits only the
existing limit literal (or inserts .limit(...)) and runs the current same-Entity draft, preserving
its filters and ordering as one undoable source transaction. Typing in the control alone does not
execute; Apply appears only while its numeric draft differs from the executed limit, which is also
available in the input tooltip. Textual limit changes appear in the control after a successful Run; pending
or rejected reads retain the old result and executed limit. Invalid/non-many/other-Entity drafts,
pending reads, or a replaced transport disable the control. Server maximum-limit policy remains
authoritative; the control does not grant a higher limit. Multi-Field ordering and pagination remain
follow-ups. The existing 50-row visual preview cap is
reported separately when reached.
orderBy(...) autocomplete uses that same capability snapshot for the matching Entity and
transport, even in incomplete drafts. It suggests only permitted scalar Fields. While discovery is
pending or unavailable, it offers no ordering Fields; retry loads metadata, not rows.
Changing Entity or replacing the transport discards stale
suggestions. Other completions and manual source authoring remain schema-based; this assistance
does not grant authority or prevent the server from rejecting a manually authored order.
When ordering is the rejected capability, the receiver reports the requested Entity and Field,
for example Ordering by TodoItem.completed is not allowed by the Graph Read policy. The Console
displays that server message; the protocol body retains access_denied and optional
details: { reason: 'ordering_not_allowed', entityName, fieldName }. Other authorization failures
remain generic. Textual ordering can still be authored independently of header availability.
createRuntimeTransportRouter(...) owns effective routing, capability validation, inspection, and
subscription. Devtools recognizes that configurable Runtime Transport and owns its generic Settings
projection; applications do not provide settings UI, React state, or presets. Profiles are derived
from the registered transports and their supported capabilities. The host still chooses the
initial routing and may subscribe for application policies such as cache invalidation or local
persistence. Changing a setting never replays requests or moves an active observation between
transports.
instrumentRuntimeTransport(...) preserves and delegates this routing capability when it wraps a
configurable transport.
The default Visual detail projects Operation requests to their input and successful responses to their returned value, flattening Entity Refs to their locator identity. Body JSON and Envelope keep the complete Runtime Protocol evidence available when transport-level inspection is needed.
The React surface opens as a full-width bottom drawer at a compact default height. Drag its top handle, or focus the handle and use the arrow keys, to resize it while the application remains visible above.
When the host supplies Console Entity definitions, Devtools adds a Console panel backed by the
shared Ontahí Lezer and CodeMirror language packages. The first walking skeleton accepts
Entity.where(Selection).many(), nullable Entity.where(Selection).first(), and exact-cardinality
Entity.where(Selection).one(), lowers them to the canonical Graph Read body, and sends them through
the same configured Runtime Transport as application traffic. Submission is explicit through Run or
Mod-Enter; results default to the same semantic visual projection used by Activity, can be switched
to JSON, remain in the panel, and the exchange appears in Activity.
Contextual Entity Selections are also available as Book.parts.chapters.many() or declarative
Book through parts through chapters many. Completion, finite-value widgets, capability discovery
and result ordering use the current destination Entity. The shared language model preserves source
factories and hops through dialect switching and table-driven sort/limit edits. Contextual reads
negotiate Graph Read v2 support on the active transport before execution; unsupported providers do
not receive a downgraded read. Activity renders the expanded relation membership in the chosen
dialect, without guessing which named factory produced it.
The Console loads Graph Read capability metadata for all configured base Entities when mounted,
without running row/count queries. Variants registered on a base read policy appear automatically
as roots (for example Chapter.many() / Chapter many), labeled with their base Entity. No separate
variant client export or Console configuration is required. Both dialects share inherited Fields,
narrowed enum values and declared by factories; ordering uses the owning base policy, including
table-driven source edits. The server imposes classification rather than trusting a client filter.
Contextual factories targeting variants also support Book.parts.chapters.many() and declarative
Book through parts through chapters many. Completion and table sorting use the final classified
destination's base policy, not the starting Book policy. Graph Read v2 independently enforces every
source and target classification plus base scopes. Variant-root Views remain unsupported.
The catalog is scoped to the active transport, route and execution identity; switching any of these
drops old metadata and ignores late replies. A failed base lookup does not hide successful roots
from other bases. Changing the draft between known roots does not issue another metadata lookup.
Hosts must keep passing the same console.identity as their transport's execution identity.
Payload capture is disabled by default. Enabling it requires a host-owned redactor:
createOntahiDiagnostics({
capturePayloads: true,
redact: value => removeApplicationSecrets(value),
});The Todo example exercises this integration. Transport connection-state evidence remains a later Plan 148 slice.
Cache: local runtime state
Views are ordered Console (when configured), Activity, Cache, Settings. Activity remains the
initial view. Pass the same clientCache used by the application's graph provider/client:
<OntahiDevtools diagnostics={diagnostics} clientCache={graphClient.clientCache} />Cache is a live, read-only view of canonical entity records, locator aliases, freshness markers, and normalized output skeletons. Instances are grouped by entity type with collapsible groups and counts. Search names, identities or aliases across groups; matching groups expand while searching. Names and titles supplement canonical identities when available. In Data, follow normalized field references to cached instances; the Console entity definitions also identify embedded relationship rows by their declared identity. Missing targets are marked unavailable. Back restores the previous selection, search and detail section, including navigation between outputs and entities. These values come directly from the local cache; Activity payload capture/redaction settings do not transform them. Mount Devtools only in the development contexts where inspecting application data is intended.
Output entries are not hook instances. The current inspector does not track active observers, Operation execution state, historical writers, field-level coverage, or indirect/transitive references. Missing fields are not classified as null or stale. Invalidating an entity may leave an output skeleton with an unresolved reference, visible in its normalized JSON.
Output entries use semantic read titles, View/Selection summaries, and visible identity scopes. Operation query outputs carry explicit source labels; arbitrary custom keys remain generic rather than being guessed to be Operations. Full keys remain available under “Cache key / JSON”. Source labels are descriptive provenance, not a freshness or authority guarantee.
Query observations and entity history
The transport decorator also instruments RuntimeTransport.graph.observe(...). Activity groups
its start, incoming snapshots, and termination into one query observation, with transport, status,
update count, and a semantic query title when the request was captured. Results default to the visual
projection, with JSON available in the detail header. Choose an earlier snapshot
or Follow latest; inspecting a snapshot does not pause the application stream. Observations stay
lazy and preserve cancellation, consumer closure, protocol errors, and transport errors.
Query requests and snapshot payloads follow the same diagnostics capture/redaction policy as exchanges. With payload capture disabled, status and row/update counts remain visible. The bounded Activity store can evict older snapshots; the detail reports when the selected snapshot is lost. These entries describe actual graph transport streams, not React hook instances or every query that reruns after invalidation.
With clientCache connected, enable Settings → Record entity history to capture a baseline and
subsequent entity writes, invalidations, and cache clears. Cache → History shows the timeline,
field differences from the previous retained snapshot in that recording segment, and a detached
snapshot. Missing fields mean absent from that local snapshot, not server deletion. Repeated writes
are retained even when field values are unchanged.
Recording starts disabled and stays in memory: at most 200 entries and approximately 2 MB of serialized UTF-16 payloads, with dropped-entry and capture-error counts. Oversized entries are skipped. Recording continues while the panel is closed; stopping keeps the captured entries, and Clear entity history only clears that history. Reloading, unmounting Devtools, or replacing the cache drops the history; a replacement cache starts with recording disabled.
History captures local cache field values, independently of Activity's payload redactor. It is a debugging record of this client's observed state, not persisted entity versioning or a complete audit. Cache writes do not yet carry observation/exchange IDs, so this release does not infer causal links between an Activity snapshot and a cache write.
Observe from the Console
Use Observe next to Run to subscribe to the current many-query expression. For example,
in Todo enter TodoItem.where(completed = false).many() (or the corresponding declarative query),
then complete an item in the application: a new snapshot removes it from the Console result.
Stop cancels the subscription and keeps the last received snapshot visible.
Observe requires a transport with graph.observe, such as Todo's WebSocket route. The initial
slice supports Graph Read v1 many queries; scalar terminals (first, one, count, exists),
invalid expressions and contextual v2 selections cannot start an observation. No .observe()
Console syntax is introduced. Graph observation frames carry rows rather than read capabilities;
the Console continues using its separate capability discovery requests.
The submitted expression stays fixed while observing. Editor and dialect changes remain drafts;
Run and the result's sort/limit execution controls wait for Stop. Switching to Activity, Cache or
Settings keeps the observation alive. Closing Devtools, unmounting it, replacing its transport or
cache, or changing console.identity cancels it. Late responses cannot update results or the cache.
With clientCache connected, received Entity rows are normalized using the host's Entity
reflection, including base identities of discovered variants. This lets enabled History record
those writes. A row disappearing from a query does not delete its canonical entity. Console
observations do not create retained output skeletons, and historical output semantics remain
independent. The Console result itself uses the received snapshot; it does not live-denormalize
older results through newer cache values.
