Reading kovo check & kovo explain
When you want to know what updates when a button is clicked, you ask kovo explain instead of reading
through app code. When you want CI to hold a rule — "every component showing cart data refreshes when
the cart changes" — you assert it with kovo check. Bare kovo check derives those facts from current
source; kovo explain reads a materialized graph for stable artifact inspection. Both print stable,
diffable text that agents and humans consume the same way. This guide is the working vocabulary: what
each command says, how to assert it in CI, and how the same facts show up in the Network panel.
The graph workflow#
Everything runs off app facts — components, queries, mutations, pages, optimistic coverage, and the touch graph (the derived map of which writes refresh which queries). Use the source-backed command for the ordinary loop, then use an explicit materialized graph when the artifact itself is under inspection:
kovo check # derive current ./src/app.tsx
kovo check source ./src/admin-app.tsx # choose another authored entry
kovo check graph.json # compatibility check of an explicit artifact
kovo explain query cart graph.json # read one node of the graph
kovo explain mutation cart/add --optimistic graph.json
kovo explain page /cart graph.json
kovo explain component CartBadge graph.json
kovo explain unguarded graph.json
kovo explain revealed graph.jsonPrefer the current-source check or construct a graph in focused tests instead of committing generated app artifacts. A graph-consuming form fails when its graph is absent; it never treats missing facts as a passing app.
Read the output#
The output blocks below are generated from fixture graphs by the docs gate with the real CLI. Every
format starts with a version line (kovo-explain/v1), and the formats are snapshot-locked so your
scripts and CI assertions don't rot.
A query — what it reads, who consumes it, what refreshes it:
kovo explain query cart graph.jsonkovo-explain/v1
QUERY cart
reads: cart
consumers: component:CartBadge,page:/cart
invalidated-by: cart/add
domain-writes: cart.addItemA mutation — guard chain, input surface, writes, derived invalidations, and the full update fan-out, with optimistic coverage when asked:
kovo explain mutation cart/add --optimistic graph.jsonkovo-explain/v1
MUTATION cart/add
guards: authed
session: commerceSession
enctype: multipart/form-data
input-fields: productId,quantity,receipt
file-fields: receipt
writes: cart,product
invalidates: cart
manual-invalidates: product
updates: cart->component:CartBadge,page:/cart; recommendations->component:Recommendations
OPTIMISTIC cart hand-written
OPTIMISTIC recommendations await-fragment
OPTIMISTIC-SUMMARY total=2 derived=0 hand-written=1 await-fragment=1 UNHANDLED=0 PUNTED=0The updates: line answers "what updates when this button is clicked," mechanically, without reading
a line of app code. That property is a framework acceptance criterion: an agent given only
kovo explain output answers the question with 100% accuracy.
A page — what a route ships:
kovo explain page /cart graph.jsonkovo-explain/v1
PAGE /cart
prefetch: false
meta: title=Kovo Commerce (1) description=Browse products and checkout with 1 verifiable cart item. image=-
i18n: en-US:cartLabel,productStock
modulepreloads: -
stylesheets: /assets/site.css
queries: cart,productGrid,orderHistory
view-transitions: -A component — its queries, fragment targets, DOM identity, and the wiring the compiler extracted from its TSX: handlers (with their capture channels and platform substitutions), derives, and mount triggers:
kovo explain component CartBadge graph.jsonkovo-explain/v1
COMPONENT CartBadge
queries: cart
fragments: cart-badge
dom-name: cart-badge
effective-dom-name: components/cart/cart-badge/cart-badge
STYLE class=kv-button-bg-a1b2c3 source=button.tsx#root style-ref=base.root
HANDLER click export=CartBadge$button_click ref=/c/cart-badge.client.js#CartBadge$button_click captures=ctx,element-params params=itemId substitution=-
SUBSTITUTION dialog tag=button event=click target=cart-drawer action=show-modal
DERIVE CartBadge$isEmpty inputs=cart ref=/c/cart-badge.client.js#CartBadge$isEmpty target=button[data-bind:disabled]
TRIGGER visible export=CartBadge$mountChart ref=/c/cart-badge.client.js#CartBadge$mountChart deps=cart justification=chart boots when visible
MERGE button attr=aria-expanded rule=aria-author-override decision=author-wins diagnostics=KV232Each HANDLER line names the extracted client handler, the /c/* module ref the served on:*
attribute points at, its capture channels (ctx, element-params), and any platform substitution;
SUBSTITUTION lines record where the compiler swapped an author event for a native platform behavior
(here, show-modal on a <dialog>); DERIVE lines are the computed bindings and their DOM targets.
This is how on:* refs, captures, and substitutions become inspectable without reading the lowered IR
or generated modules.
The grammar is consistent: key: value lines, - for empty, comma-separated sets, ;-separated
a->b edges. You parse it with grep and awk; that's the intent.
Run the gates with kovo check#
kovo check graph.json runs focused semantic checks over a named artifact rather than current source: optimistic
exhaustiveness, update coverage, touch-graph consistency, and the audits. Healthy output is short:
kovo-check/v1
OKUnhealthy output names the edge and exits non-zero. This is the commerce graph with one hand-written transform deleted:
kovo-check/v1
WARN KV310 cart/add -> cart Invalidated query lacks optimistic transform.Coverage diagnostics are warnings with teeth: suppressible, but the suppression is recorded in source rather than left silent.
Turn product rules into CI assertions#
When a product rule matters — "every component that shows cart data must refresh when the cart changes" — you assert it as a set operation over the printed graph. Here is an app-owned shell recipe:
kovo explain query cart graph.json > .kovo/cart.query.txt
awk -F': ' '/^consumers: / { print $2 }' .kovo/cart.query.txt \
| tr ',' '\n' | grep '^component:' | sort > .kovo/cart.consumers.txt
printf '%s\n' component:CartBadge component:CartPanel | sort > .kovo/expected.txt
diff -u .kovo/expected.txt .kovo/cart.consumers.txt
grep '^invalidated-by: .*cart/add' .kovo/cart.query.txt
kovo explain mutation cart/add --optimistic graph.json | grep '^OPTIMISTIC-SUMMARY .*UNHANDLED=0'If the rule should live in CI, put the script version in your app. This is app-owned code, not a
file create-kovo generates:
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
const kovoExplain = (args) =>
execFileSync('kovo', ['explain', ...args, 'graph.json'], { encoding: 'utf8' });
const cartAdd = kovoExplain(['mutation', 'cart/add', '--optimistic']);
assert.match(cartAdd, /^updates: cart->component:CartBadge,page:\/cart/m);
assert.match(cartAdd, /^OPTIMISTIC-SUMMARY .*UNHANDLED=0$/m);Run that script next to kovo check only after you add it to your app. A graph assertion differs in
kind from a rendering test: it states intent ("cart consumers are exactly these") and holds as the
app grows — a new component that reads cart data either joins the consumer set correctly or turns CI
red.
The security review modes#
Explain modes answer security review's first questions from the same artifact:
kovo explain unguarded graph.json # every mutation, route, and query reachable without authed
kovo explain unscoped graph.json # owner-annotated tables whose key predicate isn't session-traceable (IDOR)
kovo explain endpoints graph.json # machine ingress: endpoints, webhooks, file/stream routes + auth/CSRF posture
kovo explain revealed graph.json # confidential fields intentionally revealed
kovo explain trust graph.json # trusted HTML/SQL/URL escapes and their evidence
kovo explain capabilities graph.json # held capabilities + static external-Postgres lease contract
kovo explain access graph.json # explicit public/authenticated/machine access decisions
kovo explain cookies graph.json # cookie posture and downgrade findings
kovo explain sources-sinks # source/sink inventorykovo-explain/v1
UNGUARDED
SUMMARY total=0The commerce app's review output is clean (total=0). A finding adds a line per item above its
summary; in CI you run the blocking modes with fail-on-findings so a guard removed in a refactor
can't land quietly.
kovo explain unguardedlists everything reachable without anauthedguard — the first question of any security review.kovo explain unscopedis the IDOR audit: queries and writes touching anowner:-annotated table whose key predicate the analyzer can't trace back toreq.session. In other words, data that should be scoped to its owner but provably might not be.kovo explain endpointsis the machine-ingress table — name, path, auth scheme, CSRF posture, and for webhooks the write→domain chain — so "what can reach this app and what can it touch?" is answerable without executing anything.kovo explain revealedlists confidential data reveals, including exact typed declassification-policy rows that need human review.kovo explain trustlists trusted HTML, SQL, URL, and similar escape hatches with the evidence that made them reviewable.kovo explain capabilitieslists held dangerous capabilities and the static external-Postgres posture-lease contract. It cannot see a running process, so current lease status, digest, and expiry remainnot-observed.kovo explain accesslists explicit public/authenticated/machine access decisions, including missing decisions that block underkovo check.kovo explain cookieslists cookie posture and downgrade findings.kovo explain sources-sinksemits the source/sink inventory used by the security gates.
Capability-style review currently happens through the concrete shipped surfaces:
kovo explain revealed,
kovo explain trust, kovo explain endpoints, kovo explain sources-sinks, and the audit-only
kovo explain capabilities table. The static
lease rows are a contract review, not live health telemetry.
Debug from the Network panel#
The wire and the graph speak the same vocabulary, which gives you a tight debugging loop:
- Click the thing. In the Network panel you see
POST /_m/cart/add— mutations are named POSTs, so you know which mutation fired without source maps. - Read the request.
Kovo-Targets: cart-badge=cart; …is the live DOM's dependency claim. If a fragment you expected isn't listed, the island is missing itskovo-depsstamp; inspect the element. - Read the response.
Kovo-Changesnames the committed domains, and the body's<kovo-query>and<kovo-fragment>chunks are exactly what will patch in. If data is stale, check whether the query's<kovo-query>chunk is present. - Cross-check against intent.
kovo explain mutation cart/addsays what should have happened — theinvalidates:andupdates:lines. The wire says what did. The diff localizes the bug: missing frominvalidates:is a touch-graph problem (see queries & invalidation); present there but absent on the wire is a rendering problem. - For a 422, the body is the re-rendered form with
data-error-code, readable as HTML (see the 422 path).
Debugging proceeds down into plainer artifacts — graph text, HTTP, HTML — instead of up into framework internals.
Next#
- Testing with @kovojs/test — the runtime half: observed ⊆ static ∪ declared.
- Optimistic updates — the coverage these checks enforce.
Spec & diagnostics
The kovo explain / kovo check artifact formats: SPEC §5.3, §11.4. The committed graph and its
diffs: SPEC §11.1. Optimistic exhaustiveness is KV310 (SPEC §10.6); update coverage is KV311
(SPEC §4.9). The "agent answers from kovo explain alone" acceptance criterion:
rules/v1-acceptance.md. Security review modes and guard reachability: SPEC §10.3, §11.4;
owner: annotations behind kovo explain unscoped: SPEC §10.1. Confidential reveal review: SPEC §6.6, KV435.
Capability review through the shipped source/sink and ingress surfaces: SPEC §6.6. The wire
vocabulary behind Network-panel debugging: SPEC §9.1. Debugging downward into plainer artifacts:
SPEC §1.
API reference: @kovojs/test, @kovojs/cli.