Menu

Guides

View as Markdown

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:

sh
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.json

Prefer 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:

sh
kovo explain query cart graph.json
kovo-explain/v1
QUERY cart
reads: cart
consumers: component:CartBadge,page:/cart
invalidated-by: cart/add
domain-writes: cart.addItem

A mutation — guard chain, input surface, writes, derived invalidations, and the full update fan-out, with optimistic coverage when asked:

sh
kovo explain mutation cart/add --optimistic graph.json
kovo-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=0

The 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:

sh
kovo explain page /cart graph.json
kovo-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:

sh
kovo explain component CartBadge graph.json
kovo-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=KV232

Each 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
OK

Unhealthy 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:

sh
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:

ts
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:

sh
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 inventory
kovo-explain/v1
UNGUARDED
SUMMARY total=0

The 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 unguarded lists everything reachable without an authed guard — the first question of any security review.
  • kovo explain unscoped is the IDOR audit: queries and writes touching an owner:-annotated table whose key predicate the analyzer can't trace back to req.session. In other words, data that should be scoped to its owner but provably might not be.
  • kovo explain endpoints is 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 revealed lists confidential data reveals, including exact typed declassification-policy rows that need human review.
  • kovo explain trust lists trusted HTML, SQL, URL, and similar escape hatches with the evidence that made them reviewable.
  • kovo explain capabilities lists held dangerous capabilities and the static external-Postgres posture-lease contract. It cannot see a running process, so current lease status, digest, and expiry remain not-observed.
  • kovo explain access lists explicit public/authenticated/machine access decisions, including missing decisions that block under kovo check.
  • kovo explain cookies lists cookie posture and downgrade findings.
  • kovo explain sources-sinks emits 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:

  1. 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.
  2. 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 its kovo-deps stamp; inspect the element.
  3. Read the response. Kovo-Changes names 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.
  4. Cross-check against intent. kovo explain mutation cart/add says what should have happened — the invalidates: and updates: lines. The wire says what did. The diff localizes the bug: missing from invalidates: is a touch-graph problem (see queries & invalidation); present there but absent on the wire is a rendering problem.
  5. 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#

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.