Menu
View as Markdown

This is the normative specification, rendered from SPEC.md and any split spec/*.md modules at build time. The docs explain; the spec decides.

Kovo — Technical Specification

Version: 0.2 (Draft) Status: Normative specification for v1, with staged roadmap through v3 Audience: Framework implementers and AI app-builder integrators


1. Vision#

Kovo is a web-platform-native framework for building multi-page applications that never show stale or inconsistent UI and are interactive at first paint with minimal JS and CSS — achieved by making the whole system legible at every layer and statically verifiable end-to-end.

It composes ideas from prior systems (Qwik, htmx/LiveView, RTK Query, Replicache; full prior-art table in the README) around one organizing constraint: every artifact the system produces (compiled output, HTML, wire traffic, dependency graphs) must be readable by a human in devtools and checkable by a machine without executing a browser.

1.1 Primary goals#

Kovo exists to deliver three outcomes, in priority order, all produced by one technique — machine-auditable generation (§1.3). Every other property of the framework — legibility, static verifiability, the auditable wire — is a means to these ends.

  1. Secure by construction. Whole vulnerability classes — cross-site scripting, SQL injection, broken access control and IDOR, confidential-data exposure, mass assignment, SSRF, request forgery, lost-update races — are not runtime hazards to test for but build/check errors that never ship, or fail-closed runtime floors where static proof is impossible. Kovo makes the insecure pattern inexpressible wherever the same static analysis that proves data freshness can prove it, and forces the residue into declared, audited, suppressible-in-source decisions visible to kovo explain. The distinctive claim is not "secure" — every framework says that — but secure by the same machine-auditable construction that eliminates stale UI: one substrate, checked without a browser.
  2. Eliminate stale-UI bugs at compile time. Inconsistent UI states — a badge that disagrees with the cart, a list that didn't reflect its own mutation, two views of one fact drifting apart — are not runtime races to debug but build/check errors that never ship. Kovo makes the staleness it can statically model (§1.2) a tsc/check failure, and forces the residue it cannot prove into declared, suppressible-in-source decisions.
  3. Make loading instant. First paint is interactive, and the bytes to get there are minimal: little-to-no JavaScript on the critical path (global delegation + import() on first interaction, not hydration), compiler-scoped CSS with no runtime style engine, and named incremental wire deltas in prod. Performance is a budget the compiler enforces, not a guideline.

1.2 Thesis statement#

An application's complete behavior — every handler wiring, navigation target, form field, mutation contract, data dependency, and optimistic prediction — should be provable by TypeScript static checking plus static graph queries, and auditable by reading the page source and the Network panel.

For v1 data freshness, that proof covers staleness caused by this client's own statically analyzable, modeled writes. Kovo turns those stale-UI paths into build or check errors, and turns freshness gaps it cannot statically prove — raw-SQL seams, database-engine side effects, the wall clock — into declared, checked, suppressible-in-source decisions. Cross-session liveness is an explicit out-of-guarantee boundary for v1 and belongs to the opt-in live tier (§9.3), not to the core mutation proof.

1.3 Design driver: machine-auditable generation#

Kovo is built to be the most machine-auditable compilation target a code-generation agent can emit: generated apps fail TypeScript static checking if wiring is wrong, and intent is verifiable against printed dependency graphs without headless browsers. Where a design choice trades author convenience for machine-checkability, machine-checkability wins. The corollary holds for every reader, not just agents: debugging always proceeds down into plainer code, never up into compiler internals. Machine-auditable generation is the chief technique by which the three primary goals (§1.1) are reached: the same static analysis that lets an agent's output be checked without a browser is what makes whole vulnerability classes inexpressible (the Prime Principle, §2), turns stale-UI paths into build errors, and lets the compiler hold the byte budget.

1.4 Explicit non-goals#

  • Figma-class shared-workspace apps. Long-lived client sessions over one mutable heap (collaborative canvases, video editors, DAWs) are outside the sweet spot. Kovo islands can host rich widgets, but the framework will not grow a client router or global client store to serve this segment.
  • Offline-first. Server truth is unconditionally authoritative; Kovo does not ship a sync engine.
  • App-authored persistent navigation state in v1. Enhanced navigation may preserve unchanged compiler-stamped layout DOM when JS is present (§8), but the canonical behavior is still real URLs and server-rendered documents.
  • Browser support parity for enhancements. Speculation Rules and invoker commands are Chromium-led; Kovo degrades gracefully (real navigations, real forms) but does not polyfill them.
  • A sanctioned JSON/REST public API in v1. Typed public APIs need their own token-auth and schema-reuse story. Until that exists, ad-hoc JSON APIs live only behind declared endpoint() entries (§9.1), where their auth and CSRF posture stay visible to audits; respond.json() is not a route outcome.

2. The Constitution (Design Tests)#

The framework's overriding commitment is the Prime Principle, which precedes and is served by every test below:

Security is by construction. A feature crossing a trust boundary — data coming in, data going out, who may act, how much — makes the unsafe state inexpressible at compile time wherever static analysis can prove it (over AST symbol-identity provenance, never a branded type or runtime taint, both unsound here; §6.6), falls back to a fail-closed runtime floor where it cannot, and routes every exception through an audited escape hatch surfaced in kovo explain. Default-deny over default-allow; advanced TypeScript types make the safe path explicit wherever they naturally encode the contract, but are defense-in-depth, not the mechanism; runtime floors are labeled as floors, never sold as proofs. This turns XSS, SQL injection, IDOR/broken access control, confidential-data exposure, mass assignment, SSRF, and lost-update races into build errors or fail-closed floors — checkable without a browser, by the same machine-auditable generation (§1.3) that eliminates stale UI. It is the first primary goal (§1.1) and the lead gate here precisely because it is the highest-stakes property and is delivered by the legibility, declare-once, and static-auditability the tests below enforce.

Every feature proposal is then evaluated against five design tests. A feature failing the Prime Principle or any test is redesigned or rejected. These are normative. The objectives are the three primary goals (§1.1); the tests are how those goals — security included — are kept honest under pressure.

# Test Consequence
1 Legibility is load-bearing. Names appear in HTML attributes and wire traffic, so they structurally cannot be mangled. Minifiers cannot rename handler exports; debugging never requires decompiling the framework.
2 No global knowledge at local sites. Any API requiring the author to enumerate distant call sites from memory is a bug factory and is rejected. Killed manual fragment targets, manual per-island optimism, query-side mutation registration, call-site mass-assignment allowlists, and per-handler authorization — security facts (secret/owner/governed/access) are declared once and derived everywhere.
3 Sugar must lower to authorable IR. Every compiler feature emits valid Kovo source. Compiling the output is a no-op (CI-enforced fixpoint). Output is auditable in devtools and mechanically checked; app authors still write TSX.
4 The wire is the documentation. Named POSTs and schema-shaped JSON in every environment; full self-describing HTML fragments in dev; size-optimized but still named/schema-shaped deltas against a version-validated base in prod (§9.1.1). The wire documents what the server chose to send, not all it knows: a secret-classified field is ineligible to reach the client wire or a client module, so legibility and confidentiality coexist by construction — the dual of output-safety (§5.2 rule 10, integrity), now for confidentiality. A dev frame is a complete document auditable from the Network panel; a prod frame shows what changed, reconstructable via kovo explain. Names are never mangled in either mode (#1). A typed secret boundary keeps the readable wire from becoming an over-exposure channel.
5 Server truth always wins. No client cache to invalidate; reconciliation is "morph the authority in." Optimistic predictions are disposable; there is no consistency protocol.

3. Architecture Overview#

                        AUTHORING                    COMPILED IR                  RUNTIME
┌──────────────────┐   ┌──────────────────────────┐   ┌──────────────────────────────────┐
│  cart.tsx        │   │ cart.server.js           │   │ Self-describing HTML             │
│  (JSX, inline    │──▶│   render fns, queries    │──▶│  • plain elements, kovo-c stamps   │
│   closures,      │   │ cart.client.js           │   │  • on:click="cart.js#Cart$remove"│
│   single file)   │   │   named handler exports, │   │  • <script kovo-query="cart"> JSON │
│                  │   │   derives, transforms    │   │  • kovo-deps="cart" stamps         │
└──────────────────┘   └──────────────────────────┘   └──────────────────────────────────┘
        │                        │                                  │
        │ fixpoint:              │ 1:1 file mapping,                │ budgeted bootstrap: global event
        │ compile(IR) ≡ IR       │ source-derived names             │ delegation + import() on
        ▼                        ▼                                  ▼ first interaction
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ MPA SPINE: real URLs + server documents + optional enhanced navigation over the full-doc │
│ oracle + Speculation Rules + cross-document View Transitions + bfcache. No client router.│
├─────────────────────────────────────────────────────────────────────────────────────────┤
│ DATA PLANE: queries (typed reads) ← invalidation graph → mutations (typed writes)        │
│ derived from domain layer / Drizzle AST. Optimistic transforms may be hand-written        │
│ or compiler-derived.                                                                     │
├─────────────────────────────────────────────────────────────────────────────────────────┤
│ WIRE: one fragment/query-JSON vocabulary, transport-agnostic:                            │
│ document load · enhanced fetch (mutations) · SSE live queries                            │
└─────────────────────────────────────────────────────────────────────────────────────────┘

3.1 Rejected from prior art#

Client-owned routers and SPA navigation state; hydration; hash-named heuristic chunks; load-bearing semantic optimizer; single global state blob; runtime signal graphs in the core client — proprietary or TC39 (the client dependency graph is compile-time-known, so the compiler emits a per-query update plan instead; Signals interop is outside the core client); opaque closure capture (useLexicalScope); client-side cache with invalidation lifecycle; manual invalidation calls as the primary mechanism; shadow DOM (tree-scoped IDREFs, form participation, and ARIA all break at the boundary — fatal to L0 platform behaviors and the no-JS form contract; style scoping comes from the compiler instead, plans/open-design-areas.md); custom-element registration (resumability comes from delegation + import(), never from customElements.define; component identity is the kovo-c stamp, dashed tags survive as inert sugar, and native hosts like <tr kovo-c="cart-row"> avoid the table-nesting problem); load-bearing import maps (the compiler and server emit full module URLs with cache-busting they control; import maps remain an optional deployment strategy); portals and runtime context APIs (composition is lexical at render time and the DOM tree is the runtime context, §4.5 — framework code never reparents islands, so closest('[kovo-c]') resolution stays sound; native top-layer promotion (<dialog>, popover) does not reparent, which is exactly why no portal is needed). Enhanced navigation (§8) is not a client router: it starts from a real <a href>, fetches the canonical server document, and falls back to the browser's full GET on uncertainty.


3.2 Authority and module split#

SPEC.md remains Kovo's entry point and highest-level normative authority. The detailed contracts in spec/*.md are incorporated by reference and are normative with the same force as text that appears in this file. When a sub-spec and this root disagree, treat that as a specification bug: follow the more specific sub-spec for its owned domain, preserve the Prime Principle in §2, and fix the conflict in the same change that exposes it.

The split is editorial, not semantic. It reduces the root to the material readers need first: vision, constitution, architecture, an index of normative modules, and short local invariants. No framework behavior changes merely because text moved from the root to a numbered module.

rules/ remains the standing rule layer for agents, releases, conformance, docs, and workflow discipline. docs/ and site/content/ remain explanatory unless this spec explicitly delegates a narrow artifact to them. Active ledgers in plans/ sequence work; they do not override the normative contracts here or in spec/*.md.

3.3 Normative module index#

Old section(s) Normative module Owns
§4, §13.1, §13.2 spec/04-component-model.md Component authoring, rendered output, handlers, loader obligations, composition, primitive merging, update plans, dynamic rendering bounds, StyleX/theme tokens, kovo-key identity.
§5 spec/05-compiler.md Compiler pipeline, hard rules, representation/render/app-build identities, prod render-equivalence, and kovo explain.
§6 spec/06-type-system.md Generated registries, package component prefixes, typed surfaces, mutation typing, routes/links, sessions, and soundness boundaries.
§7, §8 spec/07-navigation.md Interaction ladder, MPA spine, enhanced navigation, bfcache posture, speculation rules, view transitions, and streaming/defer behavior.
§9 spec/09-wire-protocol.md Mutation round trips, prod deltas, error envelopes, live/liveness, typed reads, request shell, HMR, durable tasks, and scheduling.
§10 spec/10-data-plane.md Schema/domain annotations, queries, access decisions, SQL safety, mutations/writes, optimism, derivation algebra, and exhaustiveness.
§11, except §11.3 spec/11-verification.md Touch-set extraction, runtime verification, and the browser-free verification surface.
§11.3 spec/11-diagnostics.md Normative KV### diagnostic registry and generated-reference comparison target.
§12 spec/12-testing.md Framework testing API and proof-surface test model.
§14 spec/14-deploy-skew.md Deploy skew recovery, app-build-token mismatch handling, and prior-version retention floor.

SPEC §6.6's finite-transfer claim is accompanied by the reviewed analyzable-fragment hand argument, which states its compositionality, adequacy, and non-claims without presenting prose as a mechanized proof.

3.4 Compatibility map for section citations#

Existing comments, diagnostics, tests, and docs may still cite SPEC §N.M. Those citations remain understandable by preserving the old top-level numbers in this root and by keeping the detailed numbered headings inside the linked modules. For example, SPEC §4.8 means the update-plan contract in spec/04-component-model.md; SPEC §10.3 means the mutations-and-writes contract in spec/10-data-plane.md; SPEC §11.3 means the diagnostic registry in spec/11-diagnostics.md. New citations may use either the old number (SPEC §10.3) or the file-qualified form (spec/10-data-plane.md §10.3) when the file qualification prevents ambiguity.

Detailed moved-subsection map:

Citation Detailed owner
§4.1 Anatomy of a component spec/04-component-model.md
§4.2 Rendered output spec/04-component-model.md
§4.3 Handlers and closures spec/04-component-model.md
§4.4 The loader spec/04-component-model.md
§4.5 Composition: children, slots, layouts spec/04-component-model.md
§4.6 Primitive composition and attribute merging spec/04-component-model.md
§4.7 Execution triggers spec/04-component-model.md
§4.8 The update plan: bindings, derives, stamps spec/04-component-model.md
§4.9 Update coverage spec/04-component-model.md
§4.10 Registry-bounded dynamic rendering spec/04-component-model.md
§5.1 Pipeline spec/05-compiler.md
§5.2 Hard rules spec/05-compiler.md
§5.2.1 Client representation, render-plan, and app-build identities spec/05-compiler.md
§5.2.2 Prod render-equivalence gate spec/05-compiler.md
§5.2.3 Build artifact provenance spec/05-compiler.md
§5.3 kovo explain spec/05-compiler.md
§6.1 The registries spec/06-type-system.md
§6.1.1 Package component prefixes spec/06-type-system.md
§6.2 Typed surfaces spec/06-type-system.md
§6.3 Mutation typing contract spec/06-type-system.md
§6.4 Routes and links spec/06-type-system.md
§6.5 Session schema spec/06-type-system.md
§6.6 Soundness boundary spec/06-type-system.md
§7 Interaction ladder spec/07-navigation.md
§8 MPA spine and navigation spec/07-navigation.md
§9.1 Enhanced mutation round-trip spec/09-wire-protocol.md
§9.1.1 Prod delta encoding spec/09-wire-protocol.md
§9.2 Errors spec/09-wire-protocol.md
§9.3 Liveness and live spec/09-wire-protocol.md
§9.4 Typed reads: the query endpoint spec/09-wire-protocol.md
§9.5 Request shell spec/09-wire-protocol.md
§9.5.1 Dev HMR spec/09-wire-protocol.md
§9.6 Durable tasks and scheduling spec/09-wire-protocol.md
§10.1 Schema as domain registry spec/10-data-plane.md
§10.2 Queries spec/10-data-plane.md
§10.3 Mutations and writes spec/10-data-plane.md
§10.4 Optimistic updates spec/10-data-plane.md
§10.5 Derivation algebra spec/10-data-plane.md
§10.6 Exhaustiveness spec/10-data-plane.md
§11.1 Touch-set extraction spec/11-verification.md
§11.2 Runtime verification spec/11-verification.md
§11.3 Diagnostic codes spec/11-diagnostics.md
§11.4 Verification surface spec/11-verification.md
§12 Testing API spec/12-testing.md
§13.1 StyleX and theme tokens spec/04-component-model.md
§13.2 kovo-key runtime identity spec/04-component-model.md
§14 Deploy skew and version recovery spec/14-deploy-skew.md

3.5 Diagnostic and generated-reference ownership#

The full diagnostic table lives in spec/11-diagnostics.md. The framework-owned diagnosticDefinitions registry remains the implementation source used by compiler, CLI, MCP, and generated docs. Generated references must compare framework KV### mentions and registry entries against the normative table; they may not invent codes, severities, or local blocking policy outside that source.

3.6 Maintenance rules for spec edits#

Use this root for contracts that shape the whole framework: the Prime Principle, the architecture model, module ownership, cross-reference maps, and deployment-wide authority. Put domain-specific normative detail in the owning spec/*.md file, and link back here only when the root needs to name the invariant at a high level.

When changing a detailed contract, update the smallest owning module first, then update this root only if the module index, compatibility map, or root summary would otherwise mislead a reader. When adding a new KV### code, update the shared diagnosticDefinitions registry and spec/11-diagnostics.md together so generated reference checks compare one implementation registry with one normative table.

When adding explanatory examples, tutorials, or decision history, prefer docs/, site/content/, or plans/ unless the example is itself a normative rule. A docs link from this file is a pointer to supporting material, not authority transfer, unless the text explicitly says the linked artifact owns a narrowly scoped contract.

When changing rules/, keep the relationship one-way: rules may enforce process, conformance, release, accessibility, docs, workflow, or agent discipline around the spec, but they do not silently override Kovo behavior. If a process rule needs a behavior change, make that behavior change in this root or the owning sub-spec.

When moving text between modules, preserve the old numbered heading until its inbound references have been deliberately migrated. A renumbering that makes existing SPEC §... citations ambiguous is a behavior-affecting documentation change and must land with the matching compatibility-map edit.

When in doubt, keep the root terse and make the linked module carry the full proof.

4. Component Model & Authoring#

Normative module: spec/04-component-model.md.

Components are authored as TSX/JSX source with component() definitions. Authors do not repeat compiler-derivable registry strings, stamps, bindings, or lowered IR by hand. The compiler derives component identities from exported bindings and module paths, emits readable runtime stamps, and rejects app-authored lowered output that would make the proof model dishonest.

The local invariant is that rendered output is both browser-valid HTML and machine-checkable IR: handlers load through named event references, data dependencies are visible through query stamps, IDREF and content-model constraints are statically checked, and server-refreshable fragments have enough identity to reconcile without clobbering state-bearing children. StyleX/theme-token extraction and kovo-key runtime identity are part of this component contract, not a separate styling compatibility layer.

Subsection map: §4.1 anatomy, §4.2 rendered output, §4.3 handlers and closures, §4.4 loader, §4.5 composition/slots/layouts, §4.6 primitive merging, §4.7 triggers, §4.8 update plan/bindings/derives/stamps, §4.9 update coverage, §4.10 registry-bounded dynamic rendering, §13.1 StyleX/theme tokens, §13.2 kovo-key.

5. Compiler#

Normative module: spec/05-compiler.md.

The compiler lowers authored TSX into readable server/client modules, generated registries, CSS assets, query/update metadata, and verification artifacts. Its hard rules keep generated output authorable, deterministic, source-derived, and security-preserving. The compiler must prove its own fixpoint and prod render-equivalence instead of relying on opaque optimizer behavior.

The local invariant is that every emitted artifact remains auditable: generated names survive minification, app-authored TSX remains the source of truth, and three derived build-coherence values use only two external carriers. Client-module URLs carry exact-representation digests; document/request/response Kovo-Build carriers use the app build token; the internal render-plan fingerprint moves with shape-changing grammar edits and is folded into that token rather than stamped separately. kovo explain exposes the graph facts needed for review without executing a browser.

6. Type System#

Normative module: spec/06-type-system.md.

Kovo uses TypeScript to make safe wiring the normal authoring path: generated registries type components, routes, queries, mutations, sessions, forms, and public surfaces. These types prevent accidental misspellings and unsafe call shapes, but the security proof still belongs to AST/provenance analysis and fail-closed runtime floors.

The local invariant is the honesty boundary: branded or conditional types are author-time guardrails, not trust proofs. The framework must still verify access, ownership, SQL provenance, output sinks, CSRF posture, session shape, package-prefix uniqueness, and deploy-skew retention through runtime validation, compiler provenance, or generated registries.

7. The Interaction Ladder#

Normative module: spec/07-navigation.md.

The interaction ladder orders behavior from platform-native L0 through lazy islands and enhanced server round trips. Kovo starts from real HTML and real browser semantics, then adds JavaScript only where the compiler can name, load, and verify the enhancement.

The local invariant is progressive capability: an interaction must preserve the no-JS or low-JS path unless its contract explicitly requires a higher rung, and any eager or enhanced behavior must remain visible to static checks and the wire.

8. MPA Spine & Navigation#

Normative module: spec/07-navigation.md.

Navigation is an MPA spine, not a client router. Canonical URLs, server-rendered documents, browser history, forms, bfcache, and full GET fallback remain authoritative. Enhanced navigation may fetch and morph the canonical server document when the proof model allows it, but uncertainty falls back to the browser.

The local invariant is that navigation state is URL/server-owned. Speculation Rules, View Transitions, streaming navigation, and <Defer> improve the experience without creating app-authored persistent navigation state or a global client store.

9. Wire Protocol#

Normative module: spec/09-wire-protocol.md.

The wire is the documentation: mutation posts, fragment responses, query reads, live events, errors, HMR frames, endpoint audits, durable-task scheduling, and prod deltas use named, schema-shaped traffic. Dev can ship full self-describing frames; prod may compress them only when app build tokens and reconstruction rules keep the delta auditable.

The local invariant is that server truth always wins and every trust boundary stays explicit. Client optimism is disposable, /_q/ reads are typed and token-tagged, errors preserve per-region/per-field identity where applicable, and endpoint/webhook surfaces are auditable through declared auth, CSRF, cache, and response-body posture.

10. Data Plane#

Normative module: spec/10-data-plane.md.

The data plane connects schema facts, query read sets, mutation touch sets, access decisions, SQL safety, optimistic transforms, and exhaustiveness checks. Drizzle-backed schema and AST provenance are the blessed path because they let the compiler derive domains, row keys, ownership, secret classification, and read/write edges.

The local invariant is default-deny plus freshness proof. Queries, mutations, endpoints, and routes need explicit access posture; owner/secret/governed facts flow from schema to wire eligibility and invalidation; and Kovo only claims "the engine is the sole authorization/confidentiality door" when the runtime is least-privilege and a closure audit proves every role-reachable object is FORCE-RLS+policy, proven security_invoker, or explicitly allowlisted. In-process PGlite is a single-tenant dev/test database whose bootstrap identity is superuser; production MUST refuse it before serving and requires an external Postgres URL whose runtime login passes the least-privilege boot invariant. Security-relevant boundary crossings are further constrained by C9: sinks must be reconstructed carriers, runtime boxes, or framework-owned doors, with a named sink inventory and hostile-value proof for each class. Owner-scoped/governed rows retain provenance at persistent non-engine sinks, and vector/RAG copies cross only through the request-scoped derived() namespace door. Raw or opaque SQL must declare the facts the analyzer cannot prove and is then runtime-verified.

Capability URLs use replay domain v3, the first domain backed by the durable per-surface reclamation watermark. Pre-watermark v2 tokens are invalid and MUST be rejected before replay-store access because deleted one-time truth from an older runtime cannot be reconstructed safely after database-clock rollback.

11. Static Analysis & Verification#

Normative modules: spec/11-verification.md and spec/11-diagnostics.md.

Kovo's verification surface combines static touch/read extraction, runtime instrumentation, graph inspection, generated diagnostics, and browser-free contract tests. Static analysis over-approximates, runtime instrumentation under-approximates executed paths, and the invariant is that observed behavior remains within static or explicitly declared facts.

For managed Postgres/PGlite writes, dangerous observed-vs-declared escapes are also engine-bounded: owner-table writes are constrained by RLS/WITH CHECK, and unclassified/reference tables are default-denied by writer grants. That engine-door claim is honest only while the runtime itself is non-superuser/NOBYPASSRLS and the closure audit has proved every app-role-reachable object safe; build-time lints are defense-in-depth. The declared-write wrapper still carries the coverage/invalidation contract for benign over-declaration among writable tables, and the C9 sink inventory records the hostile-value proof surface for DB, wire, file, webhook, task, log, and outbound-egress sinks; §10.3 and §11.2 own the detailed layer split.

The diagnostic registry is split out because it is lookup material with independent generated-reference checks. Its authority remains normative: every framework KV### code, severity, and fix posture must agree with the shared diagnosticDefinitions registry and the generated diagnostics reference.

12. Testing API#

Normative module: spec/12-testing.md.

The testing API mirrors the proof surface. Mutations execute as functions with touch checking, pages render to inspectable HTML without a browser, typed error unions stay visible, optimistic transforms are pure enough for property tests, and HTTP integration tests exercise the wire against realistic database semantics.

The local invariant is that application wiring should be testable through generated contracts and HTTP/HTML assertions. Browser tests still matter for framework-owned morph survival and platform behavior, but apps should not need broad browser suites to compensate for unverifiable wiring.

13. Related Rules and Roadmaps#

SPEC.md is the normative source of framework behavior. The following files carry standing conformance rules, release gates, implementation roadmaps, and explanatory examples:

  • Accessibility conformance: rules/accessibility-conformance.md
  • Data-layer policy: rules/data-layer-policy.md
  • v1 acceptance gates: rules/v1-acceptance.md
  • Open design areas: plans/open-design-areas.md
  • Data-layer roadmap: plans/data-layer-roadmap.md
  • Risk register: docs/risk-register.md
  • Worked add-to-cart example: docs/worked-example-add-to-cart.md
  • Integration testing and browser-free test API examples: docs/integration-testing.md
  • Layout authoring examples: site/content/guides/layouts.md
  • Component authoring and copy-in UI examples: site/content/guides/components.md
  • Optimistic derivation examples and expanded grammar: site/content/guides/optimistic.md
  • StyleX, stylesheet, and theme-token guidance: site/content/guides/styling.md

StyleX/theme-token contracts and the kovo-key runtime-identity contract moved to spec/04-component-model.md as §13.1 and §13.2 because they are component/runtime identity rules.

Rules and roadmaps do not weaken the spec. If a plan conflicts with this root or a linked normative module, follow the spec and update the plan or ask before coding through the conflict.

14. Deploy Skew & Version Recovery#

Normative module: spec/14-deploy-skew.md.

Kovo treats long-lived documents, stale prerenders, and redeploy skew as recoverable version mismatches, not silent stale patches. An app build token mismatch prevents delta/query merge, triggers full-value refetch when possible, and escalates to full page reload when document and server tokens cannot be reconciled.

The local invariant is a deployment floor: every supported deployment must retain prior immutable client modules and token-scoped /_q/ reads for at least 24 hours of wall-clock retention across redeploys, or surface KV417 instead of shipping a broken artifact.


Component Model & Authoring (SPEC §4, §13.1-§13.2)

This file is incorporated by reference from ../SPEC.md and is normative for Kovo framework behavior. The root spec remains the entry point and cross-reference index; this module owns the detailed contract below. Owns component identity, authored TSX, rendered IR, handler loading, composition, primitive merging, update coverage, registry-bounded dynamic rendering, StyleX/theme-token extraction, and keyed runtime identity.

4. Component Model & Authoring#

4.1 Anatomy of a component#

tsx
// cart-badge.tsx — what you write
import { component } from '@kovojs/core';
import { cartQuery } from './cart.queries.js';

export const CartBadge = component({
  queries: { cart: cartQuery }, // typed data dependencies
  state: () => ({ bouncing: false }), // LOCAL state: UI-only facts, JsonValue-constrained

  render: ({ cart }, state) => (
    <button
      commandfor="cart-drawer"
      command="show-modal" // L0: platform behavior, zero JS
      class={state.bouncing ? 'bounce' : ''}
    >
      🛒 <span>{cart.count}</span> {/* compiler derives data-bind="cart.count" (§4.8) */}
    </button>
  ),
});

component() accepts the definition object only. The author never supplies a component name string: the compiler derives the DOM wire leaf from the exported binding (CartBadge -> cart-badge) and the registry/type key from the module path plus that leaf (components/cart-badge/cart-badge). This is the component-name application of the §4.8 rule that TSX does not require strings the compiler can derive. Implementation sequencing for the 2026-06-16 migration is tracked in plans/name-derivation.md.

Address strings vs registry identities (normative). A first positional string argument is reserved for externally meaningful addresses or protocol paths: route paths, endpoint mounts, capability URL mounts, and webhook receiver paths. Framework registry identities are source-derived whenever the compiler can prove an exported binding plus module path: components, webhooks, mutations, queries, domains, and tags follow this rule. The derived identity is the stable name printed by explain output and carried by generated registries and internal wire references (/_m/*, /_q/*, kovo-deps, <kovo-query>, replay scopes, touch graphs). App-authored TSX and server modules do not write registry-name strings merely to repeat facts the compiler can derive; emitted IR may retain residual strings only when those strings are validated against the generated graph (§4.8, §6.1, §11.3). Every mutation registry identity is a non-empty string of at most 1,024 JavaScript code units. The compiler/runtime assignment boundary MUST enforce that ceiling before consuming an unkeyed declaration or composing replay scope, so a source-derived name cannot overflow durable identity storage. Explicit strings remain appropriate for conceptual groupings that intentionally span declarations and are not one declaration's identity, such as a shared mutation queue (§10.4).

Query-backed components are ordinary live refresh candidates: a component with queries gets kovo-deps and a derived fragment target when the compiler can prove the root is addressable and the subtree can be reconstructed from declared query data plus serializable stamped props. App authors do not write fragmentTarget: true or kovo-fragment-target="..."; those are derived IR facts. The component-level escape hatch is force-off only: disableServerRefresh: true opts a query-backed component out of server fragment refresh while preserving query bindings and local client updates. There is no force-on mode.

Call-site props are inferred from render. The first render parameter is the single runtime input bag: the server calls render({ ...callSiteProps, ...queryResults }, state, slots), so query result keys are server-owned and override any same-named call-site value. TypeScript therefore derives the public component call-site props from the annotated first render parameter with the component's declared queries keys removed. The second parameter is component-local state, and the third parameter carries render-time slots (children, named slots, form state, and request-only framework slots). A render function whose first parameter is unannotated or any exports no ordinary call-site props; authors must annotate the render input to make props public. props metadata and query.args((props) => ...) declarations are consistency checks against that derived call-site prop shape, not a second source of truth. Component JSX/call expressions still accept framework-owned attributes such as key, kovo-key, style, and styles.

Rules enforced by the type system:

  • state must satisfy JsonValue (no Date, Map, functions, class instances) — serializability is a compile error, not a runtime surprise.
  • A query-backed component that is inferred as server-refreshable has render inputs ⊆ (declared queries ∪ stamped props); otherwise the compiler emits a diagnostic explaining why the target cannot be reconstructed, while §4.8 plan-covered positions may still update from query JSON.
  • Repeated or prop-keyed inferred targets need stable instance identity from authored key or serializable keyed component props; duplicate or ambiguous target identities are compile errors.
  • Query data is shared and server-owned; local state is private and client-owned. A lint (KV301) rejects server facts in local state.
  • Declared clocks are part of the component definition contract. They describe named now.* inputs for time-dependent render positions and derives; undeclared clock reads remain KV312/KV315.

Framework component-library contract (normative). @kovojs/ui exposes components through task-specific subpaths such as @kovojs/ui/card; it has no root barrel. The same manifest owns each component's package import, copy-in command, parts, slots, IDs, state inputs, enhancement tier, roles, keyboard behavior, and accessibility note, and generated registries, catalogs, reference material, examples, and copied source MUST agree with that manifest. Card's anatomy is exactly Card, CardHeader, CardTitle, CardDescription, CardContent, and CardFooter. Generated @kovojs/icons/<glyph> functions return the canonical ComponentRenderResult from @kovojs/core; an icon package MUST NOT define a weaker parallel render-result type.

The public result is the small opaque Component<Props> callable contract. Its call/JSX props are the derived call-site props plus framework-owned JSX attributes; it has no public .definition, query-key metadata, render-slot plumbing, or conditional helper-family fields. The framework keeps the definition in module-private identity state and compiler/server internals resolve it through a private ABI. Public AnyFunction, IsAny, Checked*, ComponentCall*, internal prop/query metadata, and ComponentRenderSlots are not app API. TypeScript opacity is ergonomics; compiler provenance and exact runtime registry membership own definition acceptance.

Mutation slots are inferred from mutation handles named in the component definition or used by a typed mutation form in its render. The third render argument exposes only the resulting handle-indexed forms failures, field errors, children, and named slots; authors do not declare a parallel slot-map type or augment a registry. A mutation code or payload-field rename therefore turns its exact component use red without exposing framework slot-support types.

4.2 Rendered output (the IR's runtime form)#

html
<cart-badge kovo-deps="cart" kovo-fragment-target="cart-badge">
  <button commandfor="cart-drawer" command="show-modal">
    🛒 <span data-bind="cart.count">2</span>
  </button>
</cart-badge>

<!-- Query data ships ONCE per page, as shared client data -->
<script type="application/json" kovo-query="cart">
  { "count": 2, "items": [{ "productId": "p1", "qty": 2, "unitPrice": 1499 }] }
</script>

Components render to light DOM as plain, never-registered elements — no shadow roots, no customElements.define, no upgrade step (rationale in §3.1). The load-bearing DOM identity is the derived kovo-c leaf; the compiler omits it when the host tag already spells the derived leaf (<cart-badge> — dashed tags are inert sugar) and emits it on native hosts (<tr kovo-c="cart-row">, so table content-model nesting works). Query-backed server-refreshable roots also carry a derived kovo-fragment-target; for singleton components this is the DOM leaf (cart-badge), while repeated instances append stable authored identity (product-form:p2). Registry and type identities are separately namespaced by module path (§6.1), so global uniqueness never lengthens the ordinary DOM leaf. If two distinct registry keys would put the same DOM leaf on one page, the composition pass derives a stable disambiguated kovo-c value from the registry key and reports it through component explain output; fragment target identities use the same disambiguated leaf before any instance suffix. StyleX-authored component styles compile to globally collision-free atomic classes and dedupe into declared stylesheet assets; raw co-located CSS remains an escape hatch scoped to the derived host leaf (@scope, donut-scoped out of nested islands) (§13.1). With no shadow boundary, IDREF wiring (commandfor, for, aria-*), native form participation, and find-in-page work document-wide — the L0 layer and no-JS form fallback depend on it. The compiler validates JSX nesting against the HTML content model (KV225): markup the parser would re-parent (<div> in <p>, <tr> outside a table) makes served HTML and parsed DOM disagree, silently breaking morph identity and fragment targets — a compile error, not a runtime surprise.

Declarative Shadow DOM is disabled. A <template> carrying shadowrootmode, shadowrootdelegatesfocus, shadowrootclonable, or shadowrootserializable (ASCII casing insensitive) is KV236, including direct/static/dynamic attributes, binding or derive targets, static spreads, and opaque spreads. There is no app-authored suppression. The server renderer, live binding paths, inline loader, and response-fragment sanitizer remove those controls and their control-targeting stamps before output or detached-tree adoption, while preserving ordinary inert template content. This runtime floor covers uncompiled JSX and hostile response bytes; it does not make declarative shadow roots an alternate component API. For the same finite-output reason, Kovo does not render or adopt unsandboxable object/embed or obsolete frame/frameset primitives; use a reviewed sandboxed iframe or ordinary navigation/download link instead (§4.8, §5.2 rule 10).

Everything is inspectable in the Elements panel: dependencies (kovo-deps), data (the JSON), behavior (on:* attributes), pending mutations (kovo-pending, §10.3).

4.3 Handlers and closures#

You author inline closures; the compiler lowers them (§5). The lowered form is the contract:

tsx
// Authoring (sugar)
<button
  onClick={() => {
    state.pendingRemovalId = String(item.id);
  }}
>
  ×
</button>
js
// cart.client.js — GENERATED compiler IR, never an app-authoring surface
import { securityHandler } from '@kovojs/browser/generated';

/** captures: item.id → element params */
export const Cart$removeItem = securityHandler(
  [{ door: 'compiler-state', kind: 'browser.state.write', target: 'state.pendingRemovalId' }],
  (event, ctx) => {
    ctx.state.pendingRemovalId = String(ctx.params.itemId);
  },
);
html
<button on:click="/c/cart.client.js#Cart$removeItem" data-p-item-id="i_42">×</button>
<!-- full URL + #export: no import-map indirection; cache-busting via query
     strings/ETags the server controls. '#cart'-style aliases exist only at
     the authoring/type level (§6.1); import maps are an optional deployment
     strategy, never load-bearing. -->

Capture channels (exhaustive): component/query state (via ctx), element params (data-p-*, typed — attribute values arrive as strings, so non-string params declare coercion once, schema-style, exactly like form fields §6.3), module scope (shared, not captured). Anything else is compile error KV201, whose message shows what the closure would have compiled to and the three fixes.

Finite browser effects (normative). Every serialized handler is classified before emission into the closed kovo-security-operation-ir/v1 vocabulary. Authored inline handlers currently admit static component-state reads/writes and exact timer schedule/cancel operations; compiler-generated primitive handler references retain their separately reviewed event/DOM behavior. The generated securityHandler(operations, fn) wrapper carries the exact compiler-derived list into the immutable client module. It is a generated ABI and audit witness, not a public authoring helper and not an opcode interpreter. App source cannot construct, widen, or suppress the list.

A native event and every object reached from it remain capability-bearing. Property or method names such as target, value, preventDefault, or focus are not authority proofs: a synthetic event can own-shadow them and invoke attacker-selected getters or functions. Authored raw event/DOM member reads and calls are therefore KV449 until lowering targets a framework-pinned operation rather than dispatching through the raw receiver. The same rule closes raw browser globals, computed terminal operations, and authority that escapes through an alias, destructuring, mutable container, constructor, or unreviewed call. Exact framework import identity alone does not summarize argument positions, callback-bearing containers, or return authority, so authored inline framework-helper calls are also KV449 until that export has a generated exact summary. Direct primitive handler references remain the supported path. Named exceptional doors and the server vocabulary are defined in §6.6, while §5.2 owns lowering and fixpoint behavior.

Closed handler language (normative). A browser handler is synchronous and its public function type returns void. A concise body or explicit return may evaluate only closed JSON/scalar data or an exact finite effect; the loader discards that value. Authority-bearing, executable, or thenable outcomes are KV449. async, generators, await, yield, exception control (try/catch/finally/throw), constructors, instanceof, and every tagged template are outside the language. The loader never performs PromiseResolve, reads a returned then, or awaits a handler return. Asynchronous work must use an explicit finite operation such as a timer whose callback is an exact reviewed zero-parameter callable; source-text timer callbacks and callback parameters/defaults/destructuring are forbidden. Timer cancellation accepts exactly one compiler-proven primitive handle or the exact result of a finite timer schedule. Promise factories and other asynchronous global work are outside the synchronous language. A handler may contain at most 256 distinct finite operations, matching the generated runtime manifest bound. A timer callback cannot read, write, or capture handler state, including through an immutable alias: synchronous dispatch has already committed and retired that state snapshot before the callback runs. Delayed state work requires a future framework-owned scheduled operation that re-enters the state-host queue and performs a fresh snapshot/validate/commit transaction.

State may be read and written only through static member targets. Computed or destructured state targets, binding or assignment default initializers, spreads, for...of, this, authority-bearing computed keys or scalar coercions, opaque call/callback results, accessors, constructors/classes, and implicit object protocols are outside the language. Object.assign cannot hide a state mutation from the compiler-owned state-write IR. A state write expression is built from JSON literals and dense literal containers, immutable aliases of those values, static state reads, ordinary scalar operators, and the compiler's exact scalar intrinsics. The callable state-method vocabulary is closed to pop, push, reverse, shift, unshift, endsWith, includes, indexOf, lastIndexOf, replace, replaceAll, startsWith, toLowerCase, toUpperCase, trim, trimEnd, and trimStart; replacement callbacks are not accepted. The only callback transform on local data is map over an exact dense array literal or its immutable alias, with an exact reviewed callback. Adding a method requires a SPEC change and adversarial proof of its callbacks, coercions, return provenance, and mutation semantics; a familiar JavaScript method name is not evidence.

State is also a runtime boundary. Before module import and the first state snapshot, the loader serializes dispatches per state host so overlapping events cannot read the same stale base. Before the first handler and after every handler in a chain, it replaces ctx.state with a fresh recursive own-data JsonValue snapshot. Arrays must be dense and use the intrinsic array prototype; records must use Object.prototype or null; only enumerable string-keyed data properties are copied; enumerable accessors, cycles, non-finite numbers, non-JSON values, exotic prototypes, and over-budget graphs fail with KV449. The current bounds are 64 levels, 10,000 values, and 1,000,000 string/key code units. Non-enumerable and symbol data is outside JSON and is discarded. This runtime check is defense-in-depth after the compiler's closed verdict, not a substitute for it.

A delegated dispatch is one fail-closed state transaction. After the final handler, the loader first snapshots and serializes the candidate state. It then prepares the complete state-binding update: every direct target and derive reference is snapshotted, every derive module is imported, every owned export and owned run function is strictly validated, every derive runs, and the results are materialized as a closed list of typed text/attribute/property sink operations. Preparation performs no framework DOM writes. Only after preparation succeeds does the loader apply that list, commit the serialized kovo-state, and drain post-commit callbacks, in that order. A missing or invalid derive export/run is a closed failure, never an implicit undefined binding value.

If handler import/export resolution, handler invocation, the post-handler state snapshot or serialization, or binding preparation throws, the loader aborts the remaining transaction, performs no binding write, leaves serialized state unchanged, and discards every post-commit callback collected by that dispatch. If an arbitrary platform DOM setter throws while applying an already prepared list, an earlier presentation write in that list can remain applied: the web platform does not provide a general rollback transaction for setters. Kovo still leaves kovo-state unchanged and does not drain post-commit work because that commit happens only after every prepared write succeeds. Modular and all generated-inline runtimes use the same rule. A later queued dispatch starts from the last successfully committed state snapshot.

4.4 The loader#

A gzip-capped inline bootstrap is the only JavaScript Kovo ever puts in a document. Its enforced ceiling lives in inlineKovoLoaderGzipByteBudget (currently 10,500 gzip bytes). The bootstrap captures first interactions, queues or falls back safely while the runtime loads, promotes deferred styles, and imports the versioned Kovo deferred runtime module from the framework-owned /c/ module registry. The deferred runtime module is not part of the first-paint byte budget and has no SPEC gzip cap; it is versioned, cacheable, and loaded by import() after the bootstrap's first-interaction or post-paint trigger.

Emission is conditional (normative). The bootstrap is emitted only into a document that carries client surface — anything below the deferred runtime's responsibilities: an island or delegated handler or execution trigger, an update-plan binding, an enhanced form, <kovo-query> truth, a deferred region, a deferFull stylesheet the bootstrap alone promotes (§13.1), an app-declared module preload or bootstrap script, or a session-dependent/fingerprinted document whose bfcache restore the runtime must force through the server (§8, §9.3). A document with none of those has nothing for the deferred runtime to do, so it ships zero JavaScript: no inline bootstrap, no import(), and correspondingly no inline-script hash in its Content-Security-Policy — a strictly tighter policy, never a looser one. The test is a deliberate over-approximation read only in the fail-safe direction: any framework-emitted client marker keeps the bootstrap, and only the total absence of every marker drops it. A fully server-rendered document therefore also has no enhanced navigation; it navigates natively, which is the same behavior it already has before the runtime settles.

Ordering inside <head> is fixed by authority class, not by hint kind. Non-executable CSS delivery — inline critical <style> and the <link rel="stylesheet"> (or its deferred rel="preload" + <noscript> form) — is emitted before the bootstrap so the browser can discover the stylesheet within the first congestion window. Everything that can execute — app-declared module preloads, an authored bootstrap script, structured-document head scripts — is emitted after it, so the bootstrap still installs its controls before any app-authored script runs (§6.6, §8).

Deferred runtime responsibilities:

  • Event delegation (capture phase) for all on:* events — including chained refs (§4.6) and the execution triggers on:visible (one shared IntersectionObserver) / on:idle / on:load (§4.7).
  • Ref resolution: parse url#export, import() the URL, invoke synchronously with (event, ctx), discard the return value without thenable inspection, and snapshot ctx.state before invoking the next ref. Module loading and the later update plan may be asynchronous; the handler call frame is not.
  • Per-island AbortSignal (ctx.signal), aborted when the morph layer removes the island (§4.7); no mount/unmount callbacks.
  • Enhanced form interception (§9) and query-data hydration from kovo-query scripts.
  • Update plan (bindings → derives → stamps, §4.8) on query/state change, by walking the self-describing attributes.
  • Refetch on focus/visibility over the typed read endpoint (§9.3, §9.4).
  • Morph application: the morph layer accounts for islands it patches in and aborts the signals of those it removes — nothing is registered.

4.5 Composition: children, slots, layouts#

Composition is render-time function composition — there is no client re-render, so projection happens exactly once, on the server. Three rules:

1. Children are a render-time value. JSX children lower to an opaque Html-typed argument; named slots are just named Html-typed props. The lowered IR is a plain function call — fixpoint-trivial:

tsx
export const Card = component({
  render: (_, state, { children, footer }) => (
    <div class="card">
      {children}
      <div class="card-footer">{footer}</div>
    </div>
  ),
});

// call site — lowers to Card.render(…, { children, footer })
<Card footer={<Totals />}>…</Card>;

2. Compound components coordinate through lexical scope and the DOM — there is no context API. At render time, sub-parts are functions sharing scope (a Dialog.Root generates ids and passes them down as ordinary arguments; KV221 validates the IDREF wiring). Ids are unique by construction: generated ids are keyed to the render site, and a static id in a component the compiler cannot prove renders at most once per page is KV224 (derive it from kovo-key/props instead) — KV221 proves an id exists; KV224 keeps that proof meaningful by forbidding duplicates, including under list stamping and fragment patch-in. At runtime, the tree is the context: a sub-part's handler resolves its island via closest('[kovo-c]'), which ctx already does. This is sound because framework code never reparents islands (normative; dev mode asserts it). Native top-layer promotion (<dialog>, popover) does not reparent — exactly why Kovo needs no portal.

3. Refreshable-target children must remain server-renderable. An inferred server-refreshable query component's subtree must be reconstructible from (declared queries ∪ stamped props) — and call-site children are part of that subtree. They are therefore lowered to component references: the compiler hoists JSX children into a named component (Parent$slot_children) when their free variables fit the stamped-prop channels (the same lowering discipline as handlers, §4.3), records the reference + props in the target's stamps, and re-renders the full subtree on fragment patch. Children that cannot be hoisted (unserializable captures) are compile error KV230, whose message shows the hoisted component that would have been generated and the fixes. Because the full subtree re-renders on every fragment patch from (declared queries ∪ stamped props) and island-local state rides neither channel nor any morph-preserved serialization (§9.1, §4.9), an island declaring local state (or carrying kovo-state) inside another component's inferred server-refreshable fragment target is compile error KV420. The fixes are: lift the child's state into a declared query, mark the child isomorphic: true (§4.8), set disableServerRefresh: true on the enclosing component, move the stateful island outside the refreshable target, or declare genuinely document-lifetime-immutable state as renderOnce. If the component has disableServerRefresh: true, the hoist requirement applies only to positions that still need a server fragment; ordinary §4.8 query bindings remain valid. Fragment responses must fully describe the DOM they produce; prod may encode that refresh as a version-validated delta (§9.1.1), but there are no morph-preserved slot holes.

Layouts are first-class route chrome. v1 has explicit layout() declarations, not a file-system route-tree convention. A layout is still render-time function composition over children, but authors attach it to routes instead of wrapping every page by hand. Layouts may be nested with an explicit parent, may declare queries, guard, and per-segment boundaries, and are shown by kovo explain page <path> --layouts. They are page chrome, not document assembly; documents are owned by the request shell (§9.5). Runtime persistence is not part of v1: every navigation still renders a full document, so later enhanced-navigation layers must preserve the same authored layout declarations. Authoring examples live in site/content/guides/layouts.md.

Routes may declare parallel layout regions at the route boundary with regions, for sibling chrome that a layout positions beside the main page without app-authored runtime stamps:

tsx
const DocsLayout = layout({
  render: (_queries, _state, { regions }) => (
    <DocsShell page={regions.page} sidebar={regions.sidebar} />
  ),
});

route('/guides/:slug', {
  layout: DocsLayout,
  regions: {
    page: ({ params }) => <GuidePage slug={params.slug} />,
    sidebar: ({ params }) => <DocsSidebar activeSlug={params.slug} />,
  },
});

regions.page is the route leaf region when present; additional names are scoped to the declaring route/layout contract. The request shell renders every region from the same route params, search, guard-refined request, and JSX context as the page, then passes the rendered map to layout().render as slots.regions. The compiler owns the stable segment ids and dependency metadata for those regions. JSX marker components or app-authored kovo-nav-* attributes are not part of this API.

Route pages that return JSX are compiler-processed Kovo source, not opaque runtime JSX. The compiler lowers the route page into authorable server IR, records the component calls and serializable props, runs the declared component queries for the initial document, and emits the live-target registry used by enhanced mutation responses (§9.1). Dynamic route composition that cannot be scanned receives a diagnostic rather than falling back to app-authored fragment routing. Every navigation is a full document, so there is no persistent-layout state to manage; cross-document View Transitions carry the visual continuity. A route-tree convention may arrive later as sugar lowering to exactly these calls (Constitution #3).

Payload posture: projected children ship in the initial HTML — all tab panels, dialog bodies, accordion contents. There is no client-side lazy mount; <kovo-defer> (§8) is the escape hatch for expensive subtrees. This is the MPA posture by design. Component authoring examples live in site/content/guides/components.md.

Because projected children ship once and never receive a client mount, an isomorphic: true island (§4.8) that composes children or named slots must, on self-render, leave those projected regions in place and re-render only its own positions (§4.8); a render whose own positions cannot be separated from the projected regions is KV316.

4.6 Primitive composition & attribute merging#

Headless primitives decorate author-owned elements through three spellings of one mechanism: the primitive computes a plain, serializable attribute record (ARIA, data-state, on:* refs, ids) at render time, and it merges into the author's element before emission. The wire shows only the result — a merged element is indistinguishable from one written by hand (Constitution #3, #4) — and merging is deterministic (stable ordering), so the fixpoint and byte-stable IR hold.

Attrs-function children (the normative IR):

tsx
<Tooltip.Trigger>
  {(attrs) => (
    <a {...attrs} href="/pricing" class="nav-link">
      Pricing
    </a>
  )}
</Tooltip.Trigger>

attrs is typed; this is the render-prop pattern minus its runtime cost, because there is no re-render. An Html-returning function whose attrs parameter goes unused is a lint.

asChild (sugar lowering to the attrs-function form): requires a single, statically-known element child; the compiler merges and emits. Dynamic or multiple children → teaching error pointing at the attrs-function form to write instead.

Behavior attributes (trigger-shaped cases): annotate instead of wrap — <a href="/pricing" kovo-tooltip="pricing-tip"> — the invoker-commands idiom (commandfor/command) extended upward from L0; the package prefix comes from §6.1.1 and the IDREF is validated by KV221. This is also the only spelling that works on markup Kovo didn't render (CMS content, markdown).

Rejected: a polymorphic as prop — it composes only with intrinsic tags, and polymorphic typing is the heaviest TS pattern known (type-perf risk; see docs/risk-register.md) for the weakest payoff.

Merge rules (normative). Merging happens once, at render; conflicts resolve per attribute class:

Attribute class Rule
class Concatenate (primitive first, author last), dedupe, stable order
style Concatenate; author declarations last (later wins per property)
on:<event> Chain: space-separated refs, author's first, then primitive's; after each module resolves, the loader invokes its handler synchronously left-to-right, discards its return without thenable inspection, and replaces ctx.state with a fresh bounded own-data JSON snapshot before the next ref. defaultPrevented does not stop the chain (platform semantics) — primitive handlers contractually no-op when event.defaultPrevented (linted in the primitive package, not the loader)
id Author wins; the primitive rewires its IDREF references to the surviving id (KV221 validates the result)
IDREF attrs (commandfor, popovertarget, for, aria-controls, …) Both set → error KV231 (double-wired relationships are ambiguity, not composition)
Descriptive aria-* (aria-label, aria-labelledby, aria-describedby, aria-roledescription), role Author wins, lint KV232 (the escape hatch stays open; the override stays visible). These are not runtime-driven, so author authority cannot freeze a live value.
State aria-* the primitive updates at runtime (aria-expanded, aria-selected, aria-checked, aria-pressed, aria-current, aria-disabled when state-driven, and any aria-* the primitive lists as state-bearing) Primitive wins, lint KV232 — same hazard as data-state: the primitive's runtime derive owns the attribute, so a static author override would be clobbered on first state change. The primitive's runtime updater keeps writing this attribute after the merge regardless of the static winner; the author's static value is used only as the initial server-rendered value when the primitive is silent at render. Authoring a static state aria-* whose value contradicts the primitive's render-time state is error KV317 (a frozen-vs-clobbered ambiguity the author cannot have meant), distinct from the visible-escape-hatch override lint KV232.
data-state & primitive-owned data-* state attrs Primitive wins, lint KV232 (runtime-updated values; a static override would be clobbered on first state change)
data-p-* (handler params) Same param from both → error KV231
Binding attrs (data-bind, data-bind:*) Same target slot → error KV233; distinct targets compose
disabled, aria-disabled, required, readonly Logical OR
Other scalars (type, href, tabindex, value, view-transition-name, …) Author wins; the primitive value is a default (used only when the author is silent)
kovo-deps Union
kovo-c, kovo-state Both present → error KV231 (one element = one island)

4.7 Execution triggers#

"Execute nothing until interaction" is a proxy for the real invariant: execute nothing the page didn't declare, and make every trigger legible in markup. Interaction is the default trigger; three declared alternatives extend the same on:* → delegate → import() → named-export model:

html
<sales-chart on:visible="/c/chart.client.js#SalesChart$mount" kovo-deps="sales"></sales-chart>
<search-index on:idle="/c/search.client.js#Search$warm"></search-index>
<stock-ticker on:load="/c/ticker.client.js#Ticker$start"></stock-ticker>
<!-- lint-gated -->
  • on:visible — one shared IntersectionObserver; fires once on first intersection. Charts, maps, carousels, lazy embeds.
  • on:idlerequestIdleCallback; warm-up work.
  • on:load — fires at parse. The escape hatch: it reintroduces eager JS, so lint KV211 requires a justification comment, and grep 'on:load' is the app's eager-JS budget.

The set is closed — on:media is CSS's job; timers belong inside handlers. Islands patched in by morph are observed like everything else (the morph layer already accounts for islands it patches in, §4.4).

Lifecycle is one primitive: ctx.signal, an AbortSignal aborted when the morph layer removes the island (or the document tears down). Long-running handlers (autoplay loops, map instances, observers) register cleanup on it; there are no mount/unmount callbacks.

4.8 The update plan: bindings, derives, stamps#

The DOM is the plan. There is no separate compiled-plan artifact: binding attributes are self-describing, the loader executes them by walking the tree under kovo-deps islands, and compile-time knowledge is used for typing only. When a query value — or island-local state; same machinery, two data sources — changes, the loader runs, in order:

Each component-local query alias MUST occur once and MUST identify exactly one canonical runtime query; one canonical runtime query MUST NOT be bound under multiple aliases in the same component. Until generated alias-to-instance ownership metadata exists, either duplicate object-key aliases or distinct aliases that resolve to the same source-derived runtime query are compile error KV240: otherwise one store identity could drive multiple update-plan owners without a deterministic mapping from returned query data to the DOM writes it owns.

1. Bindings — path writes. data-bind="cart.count" sets text content; data-bind:<attr> sets attributes (data-bind:value, data-bind:hidden). Contextual encoding is mandatory and by default. Every interpolated value MUST be contextually encoded for the sink it lands in before it reaches the DOM, and the server renderer and the loader MUST use byte-identical encoding (the §5.2 #3 render-equivalence gate binds them — divergent encoding is KV222 drift). A data-bind text write is a textContent/escaped-text write, never an HTML parse: it never opens an element or comment. A data-bind:<attr> write is an attribute-value write with attribute-value escaping; it never opens a new attribute or tag. No binding ever inserts attacker-influenced markup as raw HTML, and no plain binding reaches an unsafe sink — those sinks are enumerated under KV236 below and require the trusted-HTML escape hatch. Grammar: dot paths plus optional segments (deal.contact?.name) — no expressions, no indexing (arrays are stamps' job). Paths type-check against the query's inferred shape (§6.2), and the check is null-aware: a path traversing a nullable or optional segment — the routine shape of leftJoin projections — must mark the traversal ?., or it is compile error KV227; rendering undefined is unrepresentable, not a runtime surprise. ?. has defined empty semantics shared by the server renderer and the loader (the two must not disagree — the KV222 drift rule applied to nullability): a text binding renders the empty string; an attribute binding removes the attribute. Sugar lowers {deal.contact?.name} to exactly this form; item-relative stamp paths (.contact?.name) and data-bind-list paths follow the same rule. When empty-on-null is the wrong rendering, the KV227 fix menu is the usual ladder: extract a named derive that handles null explicitly, or make the projection non-null in the query itself (coalesce), keeping the binding total.

Unsafe output contexts and the trusted-HTML escape hatch (KV236). A safe binding context is one whose contextual encoding (above) provably neutralizes attacker-influenced bytes: HTML text content and ordinary attribute values. The following are unsafe output contexts, and a plain data-bind/data-bind:<attr>/derive value flowing into one is a compile error KV236:

  • raw-HTML insertion (any binding that would parse its value as markup rather than set escaped text);
  • URL-scheme attributes — href, src, action, formaction, xlink:href, ping, poster, and CSS url(...) — whose value's scheme is not on the allowlist http, https, mailto, tel, ftp, relative/path-only, or a fragment; in particular javascript: and data: are denied;
  • event-handler attributes (on*);
  • the style attribute and <style> element text;
  • srcdoc;
  • <script> element text and <script type="application/json"> island bodies (§9.1 governs the byte-level encoding for the latter).
  • element-context execution, request, and isolation controls form one finite denominator of exactly 67 element/attribute tuples: script × (src, href, xlink:href, type, nomodule, integrity, crossorigin, referrerpolicy, charset, nonce, language, attributionsrc); style × (type, media, nonce); link × (href, rel, type, media, disabled, integrity, crossorigin, referrerpolicy, as, nonce); iframe × (src, sandbox, allow, allowfullscreen, allowpaymentrequest, browsingtopics, credentialless, sharedstoragewritable, csp, referrerpolicy, name); annotation-xml[encoding]; geolocation × (autolocate, watch, accuracymode); a × (target, rel, referrerpolicy, ping, attributionsrc, attributiondestination, attributionsourceid, attributionsourcenonce); area × (target, rel, referrerpolicy, ping, attributionsrc); form × (target, rel); button[formtarget]; input[formtarget]; img × (referrerpolicy, crossorigin, attributionsrc, sharedstoragewritable); audio[crossorigin]; video[crossorigin]; SVG image[crossorigin] and feImage[crossorigin]; and meta × (name, http-equiv). Every listed control is static-only or disabled by the stronger value rules below: a direct dynamic value, dynamic removal, or opaque spread is KV236. The parser-request controls are included as tuples, not treated as independent strings: changing a script/link/style type or media, subresource integrity or credential mode, destination, or referrer policy after parsing/request selection cannot retroactively secure that action. Script scheduling hints (async, defer, fetchpriority) are deliberately outside this denominator because they schedule an already-reviewed resource rather than select its authority or isolation posture. The URL halves (script[src|href|xlink:href], link[href], and iframe[src]) may instead hold an exact trustedUrl(value, { reason: auditedReason }); trustedUrl suppresses no other tuple. Live bindings and keyed fragment morphs preserve the reviewed current value (including absence) rather than apply or remove a blocked control; newly adopted fragment nodes pass the same static-value floor before adoption. Compiler, server JSX, modular binding/fragment code, and the generated inline loader consume the same closed classifier, and the conformance corpus asserts every tuple non-vacuously.
  • the finite browser-control denominator has these stronger value rules. referrerpolicy is limited to no-referrer, same-origin, strict-origin, or strict-origin-when-cross-origin, so an element cannot weaken Kovo's strict-origin-when-cross-origin response-header floor. Anchor and area target accepts only _blank, _self, _parent, or _top (never an opener-bearing named browsing context), and their rel token list must not contain opener. Anchor/area ping is disabled outright because its reporting headers can disclose the source URL. meta[name=referrer] is disabled because it can override the response-header posture. meta[http-equiv=refresh] is disabled because it can navigate before framework controls install. Script/link nonce is framework-owned and disabled in authored output under Kovo's hash-locked CSP, and obsolete script[language] is disabled. Attribution registration, browsing-topics disclosure, shared-storage writes, and legacy payment-request delegation controls are disabled pending named reviewed capability doors. geolocation[autolocate|watch|accuracymode] is likewise disabled: supported Kovo responses send Permissions-Policy: geolocation=(), and a future opt-in requires a named permission-policy capability rather than an incidental element attribute. HTML and SVG crossorigin credential modes and style[type|media] activation remain available only as statically reviewed values. These bans have no trusted-value suppression. An iframe[src] additionally requires a present, statically reviewed sandbox attribute; a trusted URL never suppresses that relational boundary. sandbox is an ASCII-whitespace token set with the exact admitted tokens allow-forms, allow-modals, allow-orientation-lock, allow-pointer-lock, allow-presentation, allow-same-origin, and allow-scripts. Unknown or newly introduced tokens fail closed. allow-scripts and allow-same-origin may each appear but MUST NOT be combined, because a same-origin scripted child can remove its own sandbox. Navigation/popup/storage/download escapes — including every allow-top-navigation* token, allow-popups, allow-popups-to-escape-sandbox, allow-storage-access-by-user-activation, and allow-downloads — are not admitted. The compiler rejects missing or unsafe sandbox posture; server JSX and modular/generated live or fragment floors remove the active src (and any unsafe sandbox) before adoption or update.
  • document-wide navigation elements are outside the app-authored output surface: <base> is disabled because even a safe-scheme value retargets every later relative URL, and <meta http-equiv="refresh"> is disabled because http-equiv plus content is one executable navigation sink whose attributes may be committed in either order. The compiler reports KV236 for <base> and for a statically or directly dynamically refresh-capable <meta>; opaque spread values are reconstructed and classified by the element-aware runtime pair sink. Server rendering and live binding updates remove attempts that reach that floor. Ordinary metadata such as <meta name="description" content={...}> and statically non-refresh http-equiv values remain valid. There is no trusted-value suppression for document-wide navigation; use Kovo router or response navigation outcomes and framework deployment-base configuration instead.
  • generic SVG SMIL execution primitives — <animate>, <animateColor>, <animateMotion>, <animateTransform>, <discard>, and <set> — are disabled outright in framework-generated or framework-managed DOM. SMIL's attributeName target and its values/from/to/by transfers are one temporal sink: the target may be an ancestor or an href="#id" sibling, and live bindings may commit target and transfer values in either order. The compiler reports KV236 for these intrinsic elements even when the apparent target is benign; server JSX omits them, and fragment/live-update runtimes inert them before adoption or update. There is no plain-JSX trusted-value suppression for this ban; reviewed raw trustedHtml(...) remains the explicit whole-markup authority described below.
  • unsandboxable active embeds — <object> and <embed> — are disabled outright in framework-generated or framework-managed DOM. Either element can load same-origin HTML with the embedding document's authority, but neither provides the isolation contract of iframe[sandbox]; CSP object-src 'none' remains defense in depth rather than the proof. The compiler reports KV236, server JSX omits the element, and fragment/live-update runtimes remove its attributes and fallback descendants before adoption or update. Use a sandboxed iframe or an ordinary download/navigation link instead; there is no plain-JSX trusted-value suppression for this ban.

Every data-bind:<attr> write into a URL-scheme attribute MUST scheme-allowlist its resolved value at both render and loader update time; a value resolving to a denied scheme is dropped to the attribute's empty semantics (the attribute is removed, per the ?. rule above), never written verbatim. A binding into an unsafe context with no escape hatch is KV236 with the usual teaching menu: change the projection, extract a derive that returns a safe value, or — for genuinely author-trusted markup/URLs — opt in via the trusted-HTML escape hatch.

The escape hatch is a typed, named, public Kovo API (trustedHtml(value, { reason, source? }) / trustedUrl(value, { reason, source? }), importable only from a documented public entrypoint per §5.2 #8) that brands its argument as author-vouched. The structured metadata argument is required. reason is a non-empty, trimmed audit justification; source, when present, is a non-empty, trimmed provenance label. Both are bounded and reject control/format characters, accessors, arrays, and unknown fields. String shorthand, missing metadata, and blank or dynamic reasons do not discharge compiler provenance. A binding may reach an unsafe context only when its lowered value is a trustedHtml/trustedUrl brand; the brand is the only thing that suppresses KV236, it is visible in source and in kovo explain component, and it is never derivable by the compiler (so the author always writes it explicitly — the inverse of the "TSX never requires a string the compiler can derive" rule, applied to trust). A trusted value carries no escaping obligation onto the framework; producing it from unvalidated query data is the documented hazard the brand makes auditable.

For a reactive URL binding, the compiler MUST preserve that source provenance without emitting a runtime call to the author-facing constructor. It lowers the exact trustedUrl(value, { reason: auditedReason, source? }) argument as the derive value and emits a compiler-owned, generated-only sink marker plus a closed query-plan fact. The modular browser runtime and inline loader may mint the runtime brand only from that plan fact immediately before the reviewed URL sink. App-authored copies of the marker are KV235, and an unmarked query value remains subject to the ordinary URL-scheme floor.

Live-property bindings (data-bind-prop:<prop>) — the property-authoritative addendum. A handful of attributes are property-authoritative: once the live DOM property is dirtied by user interaction (or script), the browser stops reflecting the attribute onto the property, so an attribute-only data-bind:<attr> write silently fails to update the observed state — FormData reads input.checked, not the checked attribute; .indeterminate/.scrollTop/.scrollLeft are not HTML attributes at all. For a closed, security-reviewed allowlistchecked, indeterminate, value (form controls), scrollTop, scrollLeft, selected, open — the compiler additionally emits a companion data-bind-prop:<prop> stamp alongside the SSR attribute and data-bind:<attr>, and the loader applies it by assigning the live element property (el[prop] = coerce(prop, value): boolean for checked/indeterminate/selected/open, number for scrollTop/scrollLeft, string for value) on hydration and after every derive/morph re-render — the property write runs after the attribute patch. The SSR attribute is unchanged, so first paint and no-JS stay correct and render-equivalence (§5.2 #3) treats data-bind-prop:* as a non-attribute output (byte-identical visible HTML; the property write is the extra output). This is not an author surface: a component still writes checked={…}/scrollTop={…} and the compiler derives both stamps from the one fact. The allowlist is the security boundarydata-bind-prop:* is never emitted or applied for any other property, and the unsafe sinks (innerHTML/outerHTML/srcdoc/on*) stay forbidden (KV236); the runtime ignores a non-allowlisted property defensively.

2. Named derives — the expression layer. A derive is a named, exported, pure function with declared inputs — exactly parallel to handlers:

ts
// cart.client.ts — public authoring API
import { derive } from '@kovojs/browser';
import { cart } from './cart.server.js';

export const Cart$isEmpty = derive([derive.query(cart)], (cartValue) => cartValue.count === 0);

export const Cart$summary = derive(
  {
    cart: derive.query(cart),
    local: derive.state<{ expanded: boolean }>(),
    now: derive.clock<Date>(),
  },
  ({ cart, local, now }) => `${cart.count}:${local.expanded}:${now.toISOString()}`,
);
html
<button data-bind:disabled="/c/cart.client.js#Cart$isEmpty">Checkout</button>

Each public input is an opaque, fieldless handle minted by the same installed @kovojs/browser instance. derive.query(queryHandle) preserves the query's result type and lets the compiler carry its source-derived runtime name without an app-authored string; derive.state<Value>() and derive.clock<Value>() name the framework-owned state and clock channels while preserving the callback value type. Tuple inputs preserve tuple positions. Object inputs preserve author-selected callback property names while retaining the underlying runtime input names. Casts, structural copies, accessors, foreign-package handles, and raw string inputs fail closed.

The compiler-emitted @kovojs/browser/generated ABI retains raw tuple and object input names so lowered output is inspectable and fixpoint-validatable. App-authored use of that generated subpath or hand-authored raw derive IR is KV235 under §5.2; raw strings are not accepted by the public authoring API.

Declared inputs tell the loader which query changes re-run it — no dependency tracking — and the module loads lazily on the first relevant change, preserving resumability. Inline JSX expressions in bound positions lower to named derives (the KV210 naming nudge applies). Minification cannot rename them (Constitution #1); kovo explain component lists every derive with its inputs.

3. Template stamps — keyed list reconciliation.

html
<ul data-bind-list="cart.items" kovo-key="productId">
  <template kovo-stamp>
    <li><span data-bind=".qty"></span> × <span data-bind=".name"></span></li>
  </template>
  <li kovo-key="p1"><span data-bind=".qty">2</span> × <span data-bind=".name">Mug</span></li>
</ul>

On change, the loader keys existing [kovo-key] children against the new array: clone the template for inserts, remove exits, reorder by key, then run item-relative bindings (.qty, typed against the array element type). key={...} is the authored TSX identity; kovo-key is the lowered runtime identity contract — written once and shared verbatim by stamps, morph, inferred fragment target suffixes, submitted-form identity, and optimistic reordering (§13.2). App source that hand-authors kovo-key where it can write key instead is hand-authored lowered IR under KV235; emitted IR keeps kovo-key so fixpoint validation can recompile it.

Stamps are derived, never required in TSX. {cart.count} and data-bind="cart.count" are one fact; the author writes the typed expression, the compiler emits the stamp. Classification: an expression that is an element's sole text child stamps that element; an expression in mixed content gets a synthesized <span data-bind> (reported in kovo explain component — wrap it yourself if the extra element matters); an expression in attribute position lowers to a named derive (above). Hand-written stamps remain valid compiler input so the fixpoint gate can recompile emitted IR (Constitution #3), but app-authored TSX must not carry derivable stamps: redundant stamps are lint KV223, and a stamp that disagrees with the expression it wraps is an error (KV222). The same rule covers kovo-fragment-target, kovo-deps, and kovo-key: app TSX writes typed queries and key; emitted IR carries residual strings and validates them. A component module in app source that hand-authors the lowered string/template IR instead of TSX is KV235. The general rule, normative framework-wide: a residual string may be validated in emitted IR, but TSX never requires a string the compiler can derive from a typed expression.

The ceiling is explicit, and the escape hatch is defined. Anything beyond paths, derives, and keyed lists flips to a server fragment — or to an isomorphic island: isomorphic: true on a component also emits its render function into the client module; on query/state change the island re-renders itself and self-morphs. It is the same render function the server uses (partials cannot drift), and it is lint-gated (KV318: justification comment required) — this is the sanctioned SPA-creep escape, bounded by KV318 and the §4.9 update-coverage proof.

The "partials cannot drift" guarantee holds only when the client self-render binds the same arguments the server bound. Projected children and named slots are Html-typed arguments supplied at the server render site and ship once in the initial HTML (§4.5); a client self-render has no slot/children arguments. To keep the self-render sound for a children- or slot-accepting isomorphic island, the self-morph must preserve the projected-children DOM regions in place and re-render only the island's own positions — the loader marks each projected-children/slot region (kovo-slot="children", kovo-slot="<name>") at server render, scopes the self-render's morph to the island's own attributes/text/structure, and treats the marked regions as morph-stable holes whose subtrees the self-render does not touch. The island's render therefore reads its slot arguments as the existing region contents, not as fresh Html. A children- or slot-accepting component whose render cannot be partitioned this way — where the island's own positions interleave with projected content such that the slot regions are not contiguous, statically locatable holes — cannot be made isomorphic without drift and is compile error KV316, whose message shows the interleaving position and the fix menu (lift the dynamic part above or below the slot, make the children a stamped-prop-hoistable inferred fragment target per §4.5/KV230, or drop isomorphic: true and use a server fragment).

4.9 Update coverage (exhaustiveness)#

§10.6 proves every invalidated query has an optimistic story; this is the same theorem one hop further down the dataflow: every query- or island-local-state-dependent position in rendered output must have a declared update status, or the page renders data it will never refresh — the silent-staleness bug §10.6 exists to kill, recurring on the client side of the wire. The framework rejected runtime dependency tracking (§3.1), and the thing removed was also the thing that guaranteed coverage in SPA frameworks; a static plan needs a static completeness proof.

During lowering, the compiler classifies every render-output position that reads query data or island-local state:

Status Meaning Latency
plan lowered to a binding, derive, or stamp (§4.8) instant; participates in optimism
isomorphic island self-renders on change (§4.8, KV318); a children/slot-accepting island self-morphs in place over preserved projected-children regions, and a non-partitionable render is KV316 (§4.8) instant; costs the render module
fragment inside an inferred server-refreshable query target — mutation success may re-render it after invalidation ∩ live targets (§9.1); not a state remedy — the morph carries no island-local kovo-state serialization (§9.1), so a nested island declaring local state inside the target is KV420 (§4.5), not a covered position 1 RTT — no optimistic update
renderOnce declared immutable for the document's lifetime (suppression recorded in source) never

A position fitting none of these is KV311. The teaching error shows the classification, why the position exceeds the plan grammar, and the fix menu — extract a derive, lower to a CSS/attribute toggle, make the query-backed component reconstructible as an inferred target, remove disableServerRefresh: true if it is suppressing a valid target, use isomorphic: true, or declare renderOnce:

kovo check coverage
query cart:
  cart-badge   span text          plan: binding ✓
  cart-badge   button class       plan: derive (CartBadge$button_class) ✓
  cart-badge   conditional <dot>  UNHANDLED ⚠ KV311
     → derive + [hidden] toggle, inferred fragment target, isomorphic, or renderOnce
  mini-cart    (subtree)          fragment ✓ — no optimistic update (inferred target)

Like KV310, the check runs at two altitudes off one derived set: in the compiler during lowering (editor-visible) and as kovo check coverage (CI/agents). Together with §10.6 and the touch graph, a mutation's full dataflow is exhaustiveness-checked edge by edge: write → invalidated queries (§11.1) → optimistic prediction (KV310) → every dependent DOM position (KV311) → fragment reconcile (§9.1). No edge from this client's own statically analyzable modeled writes may be silently uncovered; raw-SQL seams, DB-engine fan-outs, wall-clock freshness, and cross-session liveness must be declared through their checked escape hatches or treated as outside the v1 automatic freshness guarantee.

4.10 Registry-bounded dynamic rendering#

Some content is authored by an LLM or stored in a database as rich text that embeds components — well-formed XML tags drawn from a fixed vocabulary (<kovo-chart title="Q3">…</kovo-chart>). The tree's shape is unknown until runtime, but the set of renderable components is fixed ahead of time. This is the one place the static composition model of §4.5 does not reach — the call graph cannot be scanned because it is data — so v1 provides a bounded runtime primitive rather than an app-authored dynamic dispatch (which remains KV230/§4.5).

renderTree(registry, nodes) renders such a tree server-side and once (the §4.5 posture: no client re-render of the dynamic shape). It is framework code, not lowered app TSX, so the static ban does not apply; the bound is the registry:

  • The registry is the pre-approval boundary. renderRegistry({...}) is a closed map of tag → { component, props }. A tag with no entry can never render a component, so the approved set is structural, not conventional. Registered components must be server-renderable; an isomorphic component (§4.8) defeats the lazy posture (it ships its render module) and is not a valid entry.
  • Parsing is the trust boundary. The untrusted string is parsed into a plain-data AST and is never reconstituted into HTML — there is no markup sink for an injection to reach. parseComponentXml is pure and side-effect-free, so validation may run at write time and the AST stored, leaving render-time on already-trusted data.
  • Output is safe by construction (§4.8). The walker HTML-escapes every text node itself (the bare JSX runtime inserts a child verbatim — escaping dynamic text is otherwise the compiler's job), passes only schema-declared props to a component (attributes outside the s.object schema are dropped, never spread through), and never produces trustedHtml/trustedUrl. Attribute and URL emission still pass through the §4.8 attribute-escape, URL-scheme allowlist, and on*/srcdoc refusal. The XSS review therefore reduces to one invariant: no registered component binds untrusted data into a trustedHtml/trustedUrl sink.
  • Attributes validate against the component's own schema (§6.3). The same s.object({...}) that types the component validates the LLM-supplied attributes — one source of truth. Invalid attributes are dropped (and defaulted where the schema declares a default); a tag whose required attributes cannot be satisfied, or an unknown tag, renders its children with the wrapper dropped rather than failing the whole document.

Lazy loading needs no new mechanism: because the tree renders to light-DOM HTML on the server, an unregistered-or-unused component ships no client JS, and a rendered component's handlers still load on first interaction through the §4.4 loader.


13.1 StyleX and Theme Tokens#

Kovo component styles are authored as TSX/JSX source with @kovojs/style objects. The compiler may extract static style.create(...), style.defineVars(...), style.keyframes(...), and compiler-known imported token references into ordinary CSS assets, but it may not turn lowered style IR into a second app-authoring surface (§5.2). Extracted rules are global atomic CSS with stable provenance, not shadow-DOM scoped rules; components remain light DOM so form participation, IDREFs, and accessibility relationships cross component boundaries. Static keyframes resolve to deterministic animation names and are emitted once.

style.create(...) returns module-provenance StyleHandle capabilities, not public rule records. The public runtime representation is fieldless and opaque: style.attrs(...) accepts only handles minted by the same installed @kovojs/style instance, falsy entries, or nested arrays of those values. Type assertions, object spreads, marker-shaped records, raw representation tuples, and handles from a second package instance MUST fail the runtime provenance check. Compiler-owned rule, style-key, and source-attribution metadata is available only through internal build capabilities; it MUST NOT appear in app-public declarations. Rendered HTML may still carry data-style-src provenance because that is inspectable output, not an app-authoring input.

Theme tokens are document CSS custom properties. Components may reference typed public tokens from @kovojs/style, but the runtime value is still resolved by the document. No core runtime theme store, hydration graph, or shadow boundary is introduced for theme selection. The one app-facing theme constructor is defineTheme({ seed, ...options }); variable-override classes and base-theme derivation are not public APIs. Expanded StyleX, stylesheet, and theme-token guidance lives in site/content/guides/styling.md.

13.2 kovo-key runtime-identity contract (normative)#

kovo-key is the single lowered runtime identity for a keyed row (§4.8): the same string is written once and shared verbatim by stamps, morph, inferred fragment-target instance suffixes, submitted-form identity, and optimistic reordering. This subsection pins the order-of-operations every consumer MUST follow so identity stays stable across reconciliation, delta merge, and optimism.

  1. Identity, not position. A row's identity is its kovo-key value, never its array index or DOM order. A key value is stable for the lifetime of the row it names and unique within its data-bind-list (uniqueness is a render-site invariant, asserted in dev). The authored TSX identity is key={...}; the compiler lowers it to kovo-key (§4.8). The kovo-key field MUST be one of the projected query-shape fields that feed the render-plan fingerprint (§5.2.1).
  2. Keyed reconciliation order-of-operations. On any array change the loader reconciles existing [kovo-key] children against the new keyed set in a fixed order: (a) match existing children to new rows by key; (b) remove children whose key is absent from the new set (or named in a delta removed-key list, §9.1.1); (c) insert rows whose key is new by cloning the row template; (d) reorder matched children to the new key order, moving existing nodes rather than recreating them; (e) bind item-relative paths (.qty, …) on every surviving and inserted child. Steps run in this order so a moved row preserves its node identity (and its UA state — focus, selection, in-flight transition) instead of being destroyed and recreated.
  3. Morph identity. A <kovo-fragment>/delta morph matches incoming keyed rows to live DOM by kovo-key, applying the same match/remove/insert/reorder/bind order as (2). The morph MUST NOT key by position; a row whose key is unchanged is morphed in place (same node, same island signal) even if its order moved.
  4. Submitted-form identity. A keyed mutation form lowers its row key to kovo-key on the form element (§6.3), and the post-commit fragment target and any failure re-render (§9.2) resolve back to that same key — so the response patches the originating row, not a positional neighbor, even if the list reordered between submit and response.
  5. Wire-stable identity. A server-authored identity MUST survive UTF-8 serialization, HTML input preprocessing, and (when it is a successful-control routing/name/value) native form encoding as the same string. Kovo fails closed with KV236 before emitting NUL, carriage returns, or lone UTF-16 surrogates in DOM identities; control names and identity-bearing single-line/hidden values additionally reject line feeds because every native form encoding normalizes line endings to CRLF. A hidden <input> MUST NOT use the ASCII-case-insensitive name _charset_: HTML reserves that cross-attribute tuple and replaces its submitted value with the selected encoding label, so Kovo rejects it at compiler, runtime-render, and CSRF-configuration boundaries; an ordinary non-hidden _charset_ field remains valid. An <option> without explicit value must already equal the browser's stripped/collapsed fallback value (use an explicit stable value when its visible label needs formatting whitespace). Ordinary server-prefilled <textarea> content is not identity: it permits CR/LF and follows native multiline form normalization, while still rejecting NUL and lone surrogates that HTML/UTF-8 would replace. Valid Unicode scalar values, including surrogate pairs, remain valid. The identity restrictions apply to native form association/submission routing fields and Kovo identity/target stamps (key/kovo-key, fragment targets, form keys, query/live targets); ordinary business content and non-authority display text are unchanged. Compiler-known violations are build errors and runtime-dynamic violations abort rendering rather than silently normalizing two source-distinct records into one browser identity.
  6. Optimistic reordering. An optimistic transform (§10.4) that inserts, removes, or moves a keyed row predicts the new key order; the predicted rows reconcile by key under (2), so the optimistic prediction and the arriving server truth (§10.3) align on identity and a moved row is not double-rendered. Rebase (§10.4) re-applies pending transforms over the keyed identity, never over array indices, so a reorder that lands between prediction and truth does not misattribute a later transform to the wrong row.

The two prod-delta soundness claims that cite this contract — keyed-collection merge-by-identity and removed-key deletion (§9.1.1) — hold because every consumer above keys off this one stable identity.



Compiler (SPEC §5)

This file is incorporated by reference from ../SPEC.md and is normative for Kovo framework behavior. The root spec remains the entry point and cross-reference index; this module owns the detailed contract below.

5. Compiler#

5.1 Pipeline#

cart.tsx ──parse──▶ analyze ──lower──▶ cart.server.js + cart.client.js ──(prod only)──▶ minify*
                       │
                       ├─▶ generated/registries/*.d.ts   (module aliases, fragment targets, query keys, domains,
                       │                                  routes, element ids, invalidation sets)
                       ├─▶ generated/touch-graph.ts      (§11.3 — reproducible/checkable on demand)
                       └─▶ generated/optimistic/*.ts     (§10.4; emitted output; authored transforms override)

* Minification may never rename exported handler symbols or anything appearing in HTML attributes (Constitution #1 — enforced because those names are load-bearing at runtime); this holds in prod too, where payloads are delta-encoded (§9.1.1) but names stay verbatim. The prod build gives each final client representation one immutable content-addressed URL and stamps the separate app build token defined in §5.2.1 into documents and data/fragment responses, so §9.1.1 base-version validation can fail loud on deploy skew instead of patching stale DOM silently.

5.2 Hard rules (normative)#

  1. Source-derived names and content-addressed modules. Extracted handlers are named Component$fnName, or Component$element_event when anonymous (lint KV210 nudges naming). Framework client modules use only the immutable path grammar /c/__v/<representation-digest>/<module> from §5.2.1. Author version strings, ?v= cache busters, truncated hashes, ETag-selected identity, and render-plan fingerprints are not module identity.
  2. 1:1 file mapping. x.tsx → exactly x.server.js + x.client.js. No heuristic chunking. A prod-only merge pass for tiny modules is opt-in (kovo.config: mergeClientModules), defaulting off.
  3. Fixpoint invariant. compile(compile(src)) === compile(src); the IR is valid input. CI test ships in the starter template. Paired with a semantic gate: render(src) ≡ render(compile(src)) — authored and lowered components must produce byte-identical HTML over the test corpus (a browser-free differential suite), so the fixpoint proves behavior preservation, not merely syntactic idempotence.
  4. Platform-behavior emission. Where the compiler proves a handler equivalent to a declarative platform feature (dialog open/close → invoker commands; popovers; <details>; pure-CSS state via :has()), it emits the attribute and drops the handler. kovo explain reports each substitution.
  5. Teaching errors. Every diagnostic shows the lowering: what would have been generated, why it can't be, and the fix menu.
  6. Registry and app-membership atomicity. Registry .d.ts emission and the app-contract membership check (§6.2.1) are part of every compile; kovo dev, kovo check, and kovo build derive both from one immutable project snapshot before type-checking or authored evaluation. A stale registry, a compiled app-scoped declaration omitted from assemble, or membership from another module generation is unrepresentable, not just unlikely. Emitted registries contain source-derived identities; they are not an ambient runtime registration mechanism.
  7. TSX-only authoring. TSX is the sole app-authoring surface. The lowered IR is an output format: valid Kovo source for fixpoint/render-equivalence gates and readable artifacts, but not something app code hand-authors or vendors. Hand-authored lowered IR in app source is KV235 with a teaching message that shows the TSX equivalent. There is no suppression pragma or ejection workflow in v1; a front-end gap is fixed in the compiler or recorded as a SPEC conflict.
  8. Public imports in app source. App-authored source may import Kovo packages only through documented public entrypoints. Imports from framework-maintenance subpaths (@kovojs/*/internal, kovo/internal) and compiler-emitted ABI subpaths (@kovojs/*/generated) are invalid in app source and must produce a teaching diagnostic. Compiler-emitted modules may import generated ABI subpaths such as @kovojs/browser/generated; those imports are compiler-owned artifacts, not app-authored API. Generated app artifacts are reproducible outputs, not app dependencies: app-authored modules MUST NOT import app-local generated modules such as src/generated/*, and app-local generated artifacts MUST NOT be checked in. App-facing tests and scripts use authored entry points plus public kovo emit/kovo explain/kovo check flows; direct generated reads are reserved for compiler/build internals and on-demand verification artifacts that are created during the command.
  9. Source proof before deployment proof. Standalone kovo check MUST regenerate the nearest TypeScript project's tsc --noEmit result and the compiler/security graph from current authored source, fail rather than accept an absent graph, emit the stable, inspectable kovo-check/v1 surface, and stop before deployment-preset, artifact, least-privilege, and §14 retention proof. kovo build MUST rerun that same source verifier, then enforce those deployment obligations (including KV417) before writing deploy artifacts. Build reuses the verifier as a deployment gate, not a separate policy, and check never invents retention merely to become green.
  10. Post-parse decisions use typed facts, not source strings. After parsing, the compiler's post-parse phases (lower/**, validate/**, analyze/**, emit/**, and graph.ts) MUST decide from typed model facts and spans, never from raw source snippets, regexes, getText()/getFullText(), or ad hoc string slicing; the scanner/parser is the sole boundary that reads source text into typed facts. Permitted source-text uses elsewhere are narrow: diagnostic source-frame rendering, span-based source-patch application by known offsets, generated-artifact body carry and renderSource() emission, generated-artifact verification, IR-header provenance checks (source.startsWith(compilerIrHeader)), binding-path grammar parsing on typed .path fields, URL/route parsing of an extracted literal attribute.value, import-specifier boundary validation for the public/generated/internal Kovo subpath rule above, and name-formatting of model-derived identifiers. A mechanical kovo-check guard enforces this.
  11. Output safety is contextual and default-on. The server renderer and the client update plan MUST contextually encode every interpolated query/state value for its sink — escaped text for text content, attribute-value escaping for attributes, the §9.1 script-data encoding for JSON islands — and MUST encode identically (bound by render-equivalence, rule #3). Pair-dependent HTML sinks MUST classify the browser-effective tuple from the same pinned attribute snapshot and renderer order: attribute names use HTML ASCII-case-insensitive matching, omitted values do not participate, and the first emitted duplicate owns the browser decision. In particular, <meta> refresh content is an executable navigation sink whenever the first rendered http-equiv attribute has the ASCII-case-insensitive value refresh; a later differently-cased duplicate cannot replace that decision. Plain bindings may reach only safe contexts; the unsafe output contexts and the URL-scheme allowlist are defined in §4.8 and gated by KV236. The only suppression is the typed trusted-HTML escape hatch (§4.8); there is no raw-string ejection. A sink renderer or any other app-authored presentation layer that consumes streamed/model output is bound by the same obligation (§9.1).
  12. Security-critical effects lower to a finite compiler-owned IR. The scanner derives every supported browser-handler and structured-server effect as one exact kovo-security-operation-ir/v1 operation before emission (§4.3, §6.6). The same closed union contains two compiler-control records: server.handler.root proves that each supported query/mutation/endpoint/webhook/task root was enrolled, and server.helper.call records an exact same-file authority transfer discharged by the bounded bottom-up summaries in §6.6. Generated client modules carry their browser subset through the compiler-only @kovojs/browser/generated securityHandler ABI; generated server modules carry the corresponding immutable manifest for component-graph and explain consumers. Neither manifest is caller-supplied enforcement or a runtime sandbox: the pre-evaluation compiler gate owns the supported-subset decision, and the C9 sink inventory owns each real runtime door and the capability-closure owner for those two control records. Unknown terminal calls, raw capability/DOM escapes, ambiguous receiver joins, and unreviewed authority transfer fail with KV449 before output. The generated wrapper and manifest are valid only as provenance-marked compiler IR for the rule #3 fixpoint/render-equivalence gates; rule #7/#8 still forbid app-authored lowered IR or generated-ABI imports. An app-scoped root additionally carries the exact proved defineKovo receiver and owning assemble identities from §6.2.1. Direct receiver calls and ordinary immutable local import/re-export aliases are supported; a destructured factory, wrapper result, computed member, cast/structural copy, mutable or ambiguous receiver, duplicate package identity, missing assembly membership, or second assembly is closed before output. Once enrolled, app.query/app.mutation/app.endpoint/app.task emit the same server.handler.root family and callback facts as their primitive counterparts. A missing, spread/computed, imported, aliased, reassigned, or otherwise unresolved callback root is KV449; it cannot disappear by producing no manifest row.
  13. Authored-source provenance stays compiler-owned. Compiler/build graph facts for component, query, mutation, route/page, endpoint, agent, tool, and durable-task declarations carry SourceAnchor { file, start, end }, where offsets are zero-based UTF-16 code-unit positions, end is exclusive, and file names the exact analyzed source input. Compiler-generated form transport and atomic-style facts additionally carry generatedFrom pointing to the authored form or style declaration that owns them. Agent-to-tool bindings, tool-to-mutation bindings, and task-to-query/mutation/task composition edges carry their own exact authored identifier anchor rather than borrowing a declaration anchor. These anchors come from the pinned parser snapshot or an explicit lowering offset map; a post-parse consumer MUST NOT rediscover them with symbol or text matching. An invalid, conflicting, summary-inconsistent, or unresolvable compiler anchor fails closed instead of falling back to a heuristic source match. Source provenance belongs to graph, diagnostic, and development artifacts; it does not enlarge an executable route or browser/server helper ABI.

5.2.1 Client representation, render-plan, and app-build identities (normative)#

Kovo derives exactly three distinct build-coherence values but exposes exactly two external carriers. The immutable client-module URL carries the representation digest; Kovo-Build and its response/meta equivalents carry the app build token. The render-plan fingerprint is an internal input folded into the app build token and is never stamped separately. These values prevent cache aliases and mixed-deploy merges; they are not signatures, do not authenticate a producer, and are never an app security principal.

  1. Client representation digest. Every immutable client-module URL contains one full 64-character lowercase hexadecimal SHA-256 digest. Its domain-separated, UTF-8 byte-length-framed preimage is exactly: the domain kovo-client-module-representation/v1, the fixed media type text/javascript; charset=utf-8, and the exact final well-formed UTF-8 JavaScript bytes after all compiler/browser import rewriting. The canonical URL is /c/__v/<representation-digest>/<module> with no query string. A fragment may select an export at a reference site, but is not part of the stored representation identity. The URL contains no render-plan fingerprint, author version, custom content type, truncated digest, or second content hash. Identical final representations keep the same digest when only render/query grammar changes. A resolver MUST re-verify the returned 200 body and fixed metadata against the requested digest before serving it; mismatch fails closed.
  2. Render-plan fingerprint. The compiler derives a separate full 64-character lowercase hexadecimal SHA-256 fingerprint over canonical byte-length-framed facts that include, at minimum: (a) the projected shape of every query — field set, nesting, nullability, and element type, including each kovo-key field per keyed collection (§4.8); (b) the update-plan grammar version — the binding/derive/stamp lowering vocabulary and §9.1.1 delta deep-merge semantics; and (c) the core-owned wire-input grammar schema defining canonical target, query-dependency, and live-target identities (§9.1). A change to any projected query shape, keyed-collection identity field, update-plan grammar, or wire-input grammar schema MUST change this fingerprint even when every client-module byte remains unchanged. This is one centrally derived compatibility fingerprint, not a module URL component, direct wire token, or authenticator.
  3. App build token. The framework derives one full 64-character lowercase hexadecimal SHA-256 token directly from byte-length-framed values: the domain kovo-app-build-token/v1, the render-plan fingerprint, and the ascending sorted set of exact active immutable client-module hrefs. There is no nested module-graph digest, delimiter-only encoding, truncation, author version, custom content type, or caller-supplied token. The active set MAY contain simultaneous representations of one logical module path and MUST exclude resolver history retained only for skew recovery. A module-less app hashes the empty active set and still has a non-empty token.
  4. Ownership and finalization. An injected VersionedClientModuleStore supplies only four storage operations: retain stores one immutable representation without changing the active deployment; readActiveSnapshot returns the durable exact { modules, renderPlanFingerprint } snapshot; replaceActiveSnapshot atomically commits that complete snapshot; and resolve reads retained representation history. The framework closes it behind a VersionedClientModuleRegistry facade and derives every representation href and app build token itself. Store-supplied hrefs, buildToken, or fingerprint setters have no authority. A store replacement that is not atomic or whose immediate readback differs from the requested exact snapshot fails closed. Production registration/finalization stages every compiler module together with framework-mandatory and stable/manual modules, performs one complete snapshot replacement, and then seals it: after finalization the manifest and token are frozen, request handling performs no hashing or storage writes, and later put attempts fail with KV417. Development/HMR replaces the render fingerprint and complete active module set as one atomic snapshot and publishes the new token only after all entries validate, retain, commit, and read back successfully; retained history is not silently promoted into that snapshot. The framework-mandatory loader participates in the active href set and in §14 retention requirements even when it is the deployment's only client module.
  5. Two external carriers. The app build token — not either component value — is carried by every full page render (document meta, §9.5), every <kovo-query>/<kovo-fragment> delta or full response (§9.1.1), and every /_q/<key> read response (§9.4). Client-module URLs carry only their representation digest. The render-plan fingerprint has no third stamp, header, URL component, or manifest carrier.
  6. Comparison. The client applies a delta only when the response app build token equals the token the held base was produced against (§9.1.1); on mismatch it discards and refetches full (§9.4). Every enhanced typed-read, mutation, and HMR request carries the immutable document token as Kovo-Build; once dispatched to one app build, a missing or unequal value fails before target/query decoding or handler work. A /_q/ or target-bearing response whose token differs from the receiving document's token is a §14 build-skew event. A retained serving layer MAY select the matching immutable app and decoder by exact token, but may never heuristically dual-decode one request. All three identities are opaque to app code; only equality is defined.

5.2.2 Prod render-equivalence gate (normative)#

The prod build is sound only if delta encoding reconstructs the dev full render. The gate, over the differential corpus (§5.2 rule 3): for every query and every change record, apply_delta(base, render_prod(Δ)) ≡ render_dev(full), where apply_delta is the §9.1.1 deep-merge plus update plan and base is the prior full value. The gate MUST also assert the three-value separation and monotonicity plus the two-carrier rule: a projected-shape or update-grammar change moves the §5.2.1 render-plan fingerprint and app build token, while an unchanged final client representation keeps its representation digest and href; the internal fingerprint never becomes a separate external stamp. A prod build whose delta path or these identity properties fail is KV416.

5.2.3 Build artifact provenance (normative)#

Every successful kovo build MUST add a top-level provenance object to the emitted dist/.kovo/graph.json. The object has schema kovo.artifact.provenance/v1 and contains exactly the path-independent inputs later certificates and advisories use to identify the framework posture:

  • graphSchemaVersion is the compiler-owned graph grammar identifier (kovo.graph/v2). Any incompatible meaning or shape change in the graph moves this value.
  • frameworkPackages is the unique, ascending sequence of {name, version} pairs for resolved @kovojs/* packages. Resolution starts from the executing @kovojs/cli package and the nearest app package.json inside the lockfile root, follows declared Kovo dependencies recursively under Node's actual resolution contexts, and retains simultaneous versions of one package as separate pairs. App dependencies, devDependencies, optional dependencies, and peers seed the walk; resolved Kovo packages contribute their production dependencies, optional dependencies, and peers. Missing optional packages are absent; a missing required declared package fails the build.
  • pnpmLock.contentHash is sha256:<lowercase-hex> over the exact bytes of the nearest ancestor pnpm-lock.yaml, with no newline, path, or text normalization. A production build with no such lockfile fails before app or config evaluation.
  • securityGuarantees records schema kovo.security.guarantees/v1 plus canonicalHash. The hash is SHA-256 over UTF-8 canonical JSON of the fenced guarantee register in SECURITY.md: arrays retain order; object keys sort by JavaScript/Unicode UTF-16 code-unit order at every depth; strings, numbers, booleans, and null use ordinary JSON encoding; and the serialization contains no whitespace. The executing CLI's package manifest embeds this identity, and the security-guarantee gate MUST reject a digest that does not match the normative register before that CLI ships.

The stamp contains no absolute paths, filesystem identities, wall-clock time, random values, or output-directory names. Capturing it before authored config/app evaluation and sorting every set-like field makes two no-op builds byte-identical; changing any listed input MUST change the emitted graph bytes. The stamp identifies the build inputs. It is not a signature and does not by itself prove that the artifact is safe or that the package contents match their version labels.

5.2.4 Proof-graph completion and transactional promotion (normative)#

A deploy graph is admissible only when its top-level proof record is the exact kovo.graph.proof/v2 shape. The record binds the complete analyzed source set, config subset, executing compiler version, app build token, canonical declared app identity (or null when the app omitted one), and selected posture profile. Its completion value is complete; a missing record, an unknown field, an invalid digest, an identity that does not recompute from the graph's own build-owned inputs, or any other completion value is not a partial success. Human CLI inspection of this graph requires --artifact <path> so a nearby dist/.kovo/graph.json cannot silently become source authority. Source-backed kovo check and kovo explain instead derive current facts or consume an explicitly selected non-deployment review graph; they never infer a deploy claim from directory layout.

kovo build writes every neutral and preset artifact beneath one unique sibling staging directory. Only after source proof, preset inspection, artifact emission, and the complete proof stamp succeed may it promote that directory to the requested output. Replacement preserves the previous complete output until the staged output is ready and restores it if promotion fails. Any failure before promotion leaves the last known-good output byte-for-byte unchanged. Validate-only builds remove their staging directory and never touch the requested output.

Failed-build evidence is not deploy output. When the operator explicitly enables debug evidence, Kovo may write one bounded, redacted kovo.build-debug/v1 record beneath .kovo/debug/<build-id>/ at the invocation root. That record contains no environment values, authored source, stack, absolute path, or partially emitted artifact, and dist/.kovo is never used as a failed-build cache.

5.3 kovo explain#

The compiler's decision tree, on demand. explain has one discriminated grammar: the token after explain is always a view (component, mutation, query, page, context, task, access, agent, authorization, auth-lifecycle, capabilities, cookies, document, endpoints, grants, model-boundaries, revealed, sources-sinks, tasks, trust, unguarded, unscoped, or the deployment-review view attest). Programmatic command callers use the corresponding exhaustive discriminated request union; graph explanation callers use its non-attestation { view, ... } subset. Flag-shaped view selectors are not a second grammar. All output remains stable, diffable text and agents consume the same artifact humans read:

kovo explain component cart        # lowerings: extracted handlers, derives, capture channels, platform substitutions, attribute merges, triggers
kovo explain mutation cart/add     # writes → domains → invalidated queries → consumers; guard chain
kovo explain mutation cart/add --optimistic   # transform coverage per query; derivation traces + punts (§10.5)
kovo explain query cart            # read set, consumers, every mutation that invalidates it
kovo explain page /products/:id    # emitted modulepreloads, per-route prefetch config, param/search schemas, query payloads
kovo explain capabilities          # held capabilities plus untrusted roots, reviewed doors, exact package verdicts, and closed provenance paths
kovo mcp                           # the same compile/check/explain results over the finite stdio protocol in §11.5

The capability-closure rows are the stable rendering of the pre-evaluation proof from §6.6, not a runtime sandbox trace. Root, door, package-summary, and closed rows are sorted independently of source traversal order; a closed row retains the exact root-to-terminal path also emitted by KV448.

kovo mcp is only a machine-readable command surface for these framework decisions. It MUST use the dependency-free, finite stdio protocol in §11.5; it is not a general MCP server or an extension point for application code.



SPEC §6.6 analyzable-fragment hand argument

Status: reviewed non-mechanized hand argument.

This is not a mechanized proof. It is the repository's explicit, reviewable argument for why the finite transfer rules in SPEC §6.6 compose and what they are adequate to claim. The generated prohibition table and compiler witnesses make its boundary falsifiable; they do not turn this prose into a proof assistant result or a proof of the implementation.

Claim and classification vocabulary#

The claim is intentionally narrow: if a semantic root receives a proved verdict, every authority-bearing value on that root's recorded traces was introduced by a recognized root parameter or reviewed finite operation, preserved only by the transfer rules named in SPEC §6.6, and consumed only by an operation in the finite server operation vocabulary. Any transfer that the relation cannot represent must produce KV449 before output.

The ledger classifications apply to the general prohibition, not to the difficulty of its minimal witness:

  • FUNDAMENTAL means sound and complete acceptance for the general JavaScript family would require information unavailable to this finite source-local decision procedure or would amount to a sound and complete decision over general mutable JavaScript behavior.
  • DELIBERATE means a narrower exact subset could be implemented, but SPEC §6.6 intentionally excludes it to keep invocation identity, authority ownership, and review obligations explicit.
  • BUDGETED means the transfer is in the finite language but evaluation stops at a deterministic resource ceiling. No prohibition row is currently BUDGETED; the four resource-contract rows are the budgeted boundary.

The FUNDAMENTAL label does not say every instance is impossible to analyze. For example, the minimal mutating-authority-alias fixture is easy to reject. It says accepting the whole general family while remaining both sound and complete is outside the claimed decision procedure.

Compositionality hand argument#

Take one semantic root and order its same-file helper summaries callee-first. The argument is by structural induction over the finite transfer tree after unsupported constructs and exhausted budgets have been replaced by closed leaves.

At a leaf, root parameters have the exact authority values assigned by the root contract. A finite operation has a reviewed door and terminal kind. Its result is plain data, except the explicit principal-scope acquisition whose returned scope is itself a named provenance value. A direct unsupported use is therefore a closed leaf with one of the eight closed reasons rather than an unrecorded authority transition.

For an internal expression step, an exact immutable alias preserves its lattice value. Static destructuring applies the reviewed member transition to each named property. Those rules neither invent authority nor erase it. opaque-container, mutating-authority-alias, and mutable-ambiguous-join close precisely where that local substitution argument would stop being valid.

For a helper step, an exact immutable same-file callable and exact positional arguments determine the callee's complete authority-input vector. The context-sensitive summary is keyed by that vector, computed before its caller, and merged back with the original root and ordered transfer prefix. Assuming the callee summary satisfies the claim, substitution of its parameter provenances for the caller's argument provenances preserves the claim. A repeated active key closes as helper-cycle; the resource ceilings close before an unfinished summary can be treated as proved. unsummarized-nested-callable, arguments-rest-spread-recovery, call-apply-bind, and foreign-callable close the cases where callable identity or positional substitution is not exact.

The query no-managed-write posture is an invariant carried in the root state and copied through every helper summary. It is not inferred again from a helper's name. Thus a proved callee cannot silently relax its caller's posture.

By the induction hypothesis, every proved child is authority-preserving and every non-compositional child is closed. The parent can therefore be proved only when all relevant children and summaries are proved. This establishes the stated compositionality claim for the finite relation, subject to the limits below.

Adequacy hand argument#

The relation is adequate for Kovo's claimed purpose only inside that finite language. Within it, authority enters through enumerated root bindings or an explicit operation result, exact aliases and member projections retain provenance, helper summaries retain the complete transfer prefix, and terminal operations retain their reviewed kind and door. Those facts are sufficient for downstream consumers to reconstruct the root-to-transfer-to-sink decision they own without treating an emitted graph as runtime authority.

The prohibition ledger is complete with respect to the exact unsupported sentence in SPEC §6.6: returning-authority, throwing-authority, opaque-container, mutating-authority-alias, mutable-ambiguous-join, unsummarized-nested-callable, arguments-rest-spread-recovery, call-apply-bind, and foreign-callable each have exactly one classification, one of the eight closed reasons, and an app-authored source fixture. The focused compiler test does not grep those fixtures; it compiles every one and requires both an emitted KV449 diagnostic with the named verdict=closed:<reason> and the corresponding closed semantic trace.

The four budgets are adequate as deterministic termination guards, not as semantic evidence. The checked measurement compiles every tracked starter/example source file that currently declares a shipping server root: 11 source files and 29 emitted semantic roots. None currently reaches budget-call-depth, budget-node-count, budget-operation-count, or budget-summary-count. The test recomputes that result from the real files and fails if either the root census or binding set drifts.

Limits and non-claims#

  • This hand argument is not a formal operational semantics, a machine-checked soundness theorem, or a proof that the TypeScript implementation faithfully realizes every stated transfer.
  • The fixtures prove current compiler verdicts for representative authored programs. One witness per prohibition does not prove closure over every JavaScript spelling of that family.
  • The argument is not a completeness claim. Deliberate and fundamental closures can reject programs whose behavior a stronger whole-program analyzer could prove safe.
  • Foreign module behavior, proxies, getters, reflective calls, ambient mutation, and runtime code outside the compiler-owned sink doors remain outside the adequacy claim.
  • The real-root measurement is a regression observation over the named repository corpus. Zero binding roots does not predict downstream application shape and does not justify widening or removing a budget.
  • Emitted graphs and diagnostics are audit evidence only. Runtime capability ownership and C9 sink enforcement remain independent obligations.

Any future widening must update the normative transfer sentence, generated ledger, compiler witnesses, and this argument together. A new accepted construct cannot be justified merely by removing its KV449 diagnostic.


Type System (SPEC §6)

This file is incorporated by reference from ../SPEC.md and is normative for Kovo framework behavior. The root spec remains the entry point and cross-reference index; this module owns the detailed contract below.

6. Type System#

One pattern, applied everywhere: declare facts once → derive every surface → validate residual strings against generated registries. The only codegen is trivial registry .d.ts files; all wiring checks are TypeScript static checks over code that runs as written. Residual strings live in emitted IR and are derived from TSX authoring facts (§4.8); every load-bearing attribute the IR carries (on:*, data-bind*, kovo-deps, kovo-c, kovo-key, kovo-fragment-target, href, IDREFs) has a named validator in §11.3, so "all residual strings are validated" is a checkable claim, not an aspiration.

6.1 The registries (generated)#

ts
// generated/registries.d.ts (excerpt)
interface HandlerModules {
  '#cart': typeof import('../components/cart/cart.client.js'); /* … */
}
// '#cart' is a compile-time alias only — emission resolves it to a full URL (§4.3)
interface FragmentTargets {
  'components/cart-badge/cart-badge': CartBadgeProps; /* … */
}
interface ComponentRegistry {
  'components/cart-badge/cart-badge': typeof import('../components/cart-badge.js').CartBadge; /* … */
}
interface QueryRegistry {
  cart: typeof cartQuery;
  product: typeof productQuery;
}
interface MutationRegistry {
  'cart/add': typeof addToCart;
}
interface RouteRegistry {
  '/products/:id': typeof productRoute; /* … */
}
interface InvalidationSets {
  'cart/add': 'cart' | 'product'; // compiler-owned IR from the touch graph (§11.1);
  // app.mutation query-handle bindings are checked against this set (§10.6)
}
// also: DomainKey (schema domains), PageIds (per-page element ids, §6.4/KV221),
// ComponentPackagePrefixes + ComponentPackageRegistry (§6.1.1)

FragmentTargets is generated from inferred server-refreshable query components, not from an author-written fragmentTarget option. Singleton targets use the component registry key as the type identity and the derived DOM leaf as the ordinary wire target; repeated targets add their typed instance identity at the wire edge (cart-row:p1) while the registry records the serializable prop shape required to reconstruct any instance. disableServerRefresh: true suppresses target generation for that component and appears in explain output.

Component registry keys are derived as <module path relative to the package src root>/<dom leaf>, with tests/integration/fixtures/ used as the fixture root in the integration suite. The DOM leaf remains the exported binding's kebab-case form; the generated registry key is for TypeScript, fragment targets, graph facts, and uniqueness diagnostics only.

The same source-derived registry rule applies to app-authored webhooks, mutations, queries, domains, and tags: their module-relative exported binding identity is the generated graph key unless the primitive declares an external address string instead (§4.1). Routes and endpoints keep explicit path strings because those strings are the public HTTP addresses.

App-scoped handles are the authoring identity and generated registry keys are compiler/runtime IR. An application does not augment QueryRegistry, MutationRegistry, or InvalidationSets by hand and does not read a query key to wire optimism. Registry generation resolves each proved handle's exported binding and assembly membership in one snapshot; an orphan or duplicate handle prevents emission rather than producing a partial registry.

6.1.1 Package component prefixes#

Component packages declare their HTML namespace once in their package manifest:

json
{
  "name": "@acme/primitives",
  "kovo": {
    "prefix": "acme-"
  }
}

The field is required for any dependency that exports Kovo component primitives intended to define a package-owned public HTML vocabulary. A package prefix is lowercase ASCII, dash-terminated, and becomes part of that package vocabulary: package behavior attributes use the effective prefix (acme-menu="account-menu"), kovo explain component <name> uses it for provenance, and packages should encode it in their exported component binding names (AcmeCartBadge -> acme-cart-badge) because component DOM leaves are always derived from bindings (§4.1). App-local components may remain bare-named; vendored source such as @kovojs/ui installed by kovo add is app source, not a component package, so its names are the app's names.

Prefix uniqueness is app-wide. During registry generation the compiler collects every imported component package, applies app aliases, and requires that no two packages have the same effective prefix. The alias escape hatch is app-side and explicit:

ts
// kovo.config.ts
export default {
  packagePrefixes: {
    '@acme/primitives': 'acme-primitives-',
  },
};

Aliases affect only the consuming app's effective package behavior/provenance prefix; they do not rewrite component binding-derived DOM leaves, the package manifest, or the package's documentation. They are for package-vocabulary collision repair, not style preferences, because changing prefixes changes the HTML behavior-attribute vocabulary an app serves.

The kovo- prefix family is reserved for first-party packages. Only packages whose manifest name is in the @kovojs/* scope may declare or be aliased to a prefix beginning with kovo-; @kovojs/ui declares kovo-ui-. This is a reservation check inside the same general prefix-registration rule, not a separate first-party naming mechanism.

Package behavior attributes ride the effective package prefix: kovo-tooltip="pricing-tip", acme-menu="account-menu", and so on. The kovo-* attribute namespace is reserved for framework-owned attributes and future loader/compiler growth. Package behavior attributes are compiler-known attributes supplied by the owning package; when a behavior value is an IDREF, it participates in the same page/component id registry as commandfor, popovertarget, for, and aria-* and is validated by KV221.

A duplicate prefix, invalid prefix, missing prefix on an imported component package, or non-@kovojs/* attempt to use kovo-* is KV234. The teaching error names both packages when there is a collision, shows the effective prefix that would have been emitted into package behavior attributes and component explain provenance, and prints the alias fix:

ERROR KV234 package component prefix conflict.
  prefix: acme-
  packages:
    @acme/primitives (package.json kovo.prefix)
    @other/acme-widgets (package.json kovo.prefix)
  emitted names would collide: acme-tooltip="..."
  fix: add an app alias, for example packagePrefixes["@other/acme-widgets"] = "other-acme-"

6.2 Typed surfaces (summary table)#

Surface Source of truth What TypeScript proves
Handler refs client module exports cart.remove exists; params required & typed; typo = error
Form fields mutation input schema names ∈ schema; types match; completeness (missing required field = error); coercion declared once (KV242)
Fragment targets component registry target exists; patched with the right component's props
Query data / bindings Drizzle select shape ($infer) + JsonValue boundary data-bind paths exist; column rename propagates to every template; nullable traversal requires ?. or a derive (KV227, §4.8); query values are serializable client wire payloads, so Date, Map, functions, class instances, and other non-JSON values are type errors
Invalidations domain layer / touch graph invalidated keys exist; optimistic exhaustiveness in tsc via emitted invalidation sets (§10.6)
Errors declared error codes onError receives exhaustive discriminated union
Guards guard combinators req.session.user non-null under authed; guards receive the validated args/instance key (§10.3) so ownership is expressible; static audit of unguarded mutations, routes, and queries, and IDOR audit (KV414) over owner: tables
State JsonValue constraint serializability by construction
Routes / links route() declarations (§6.4) href/<Link>/redirect() target exists; path params required & typed; search params typed; route rename propagates to every link
GET forms / URL state route search schema field names ∈ search schema; coercion declared once; the §7 URL channel is typed
IDREFs (L0 wiring) compiler id registry commandfor/popovertarget/for/aria-* reference an id that exists in scope (KV221)
Sessions declared session schema (§6.5) req.session fully typed; instance keys (§10.2) and guard refinements rest on typed fields
Derives declared inputs (§4.8) derive inputs exist in QueryRegistry; input types match query shapes; bound attribute targets type-checked
Stamp lists query result element type data-bind-list paths are arrays; item-relative paths exist on the element type; kovo-key names a real field (§4.8)
Slots / children hoisted component refs (§4.5) fragment-target children lower to component references with serializable props (KV230)
Component props first render parameter (§4.1) call sites may pass exactly the annotated render-input props after query result keys are removed; unannotated/any render input means no ordinary props; props metadata must match this derived shape
Query args first render parameter + query args schema (§4.1/§10.2) components bind args from their own derived call-site props; mappers cannot invent props outside the render annotation; coercion declared once; instance keys typed end-to-end (store, wire, optimism)
Update coverage render-output classification (§4.9) every query/state-dependent DOM position has a status — plan / isomorphic / fragment / renderOnce; none is KV311
Opaque projections declared output schema (§10.2) sql<T>/raw projections carry s.* output schemas + a reads: table set (KV410); reads: checked against exemption, folded into the read set; result shape runtime-verified (§11.2)
SQL statement safety managed DB-handle contract (§10.2/§10.3) executable SQL text reaches framework-managed DB handles only as typed builders, parameterized SQL values, or audited trustedSql(...); scalar request data binds as parameters, while identifiers/keywords come from schema facts or typed allowlists (KV422)
Output safety binding sink + value brand (§4.8) every binding/derive into an unsafe output context (raw HTML, URL-scheme attr, on*, style, srcdoc, script/JSON) is trustedHtml/trustedUrl-branded or it is KV236

6.2.1 App-scoped declaration contract#

An app declares its runtime context exactly once with defineKovo({...}). The returned KovoContract is a declaration owner, not a mutable application aggregate. Its receiver methods route, layout, query, mutation, endpoint, and task are the ordinary authoring factories; each returns a named, declaration-emit-stable opaque handle interface. Request, validated session, managed read-only or transactional DB posture, declared environment projection, query input/result, mutation error/payload, route params/search, task input, and endpoint request/result types flow from that one contract. Authors MUST NOT need to name AppRequest, Reader, QueryLoadContext, MutationContext, ComponentRenderSlots, registry augmentations, or explicit app generics for an ordinary declaration.

Focused capability subpaths may construct a standalone advanced EndpointDeclaration, including webhook() and createStorageDownloadEndpoint(). Such a declaration joins an app only through the explicit app.endpoint(declaration) bridge, which returns an opaque endpoint handle owned by that contract. Adoption is deterministic and exact-identity: it may happen once, creates no ambient registry, preserves the capability declaration's runtime/provenance facts, and remains subject to the same single-assembly completeness checks. Passing the raw standalone declaration directly to assemble, adopting it into two contracts, adopting it twice, or assembling a structural copy MUST fail the private runtime ownership check.

Capability-bounded agent() and tool() declarations remain on @kovojs/server/agent; their constructors, model callback, mutation capability, and session runner MUST NOT move to the ordinary server root. One exact AgentDefinition joins an app through app.agent(declaration), which returns only a named opaque bridge. Its session(rawRequest) method is unavailable until app.assemble() has validated and closed the app, then inherits that app's session provider, managed DB provider, validated env projection, error handler, and trusted client-IP policy without asking the author to repeat their types or callbacks. The bridge may expose an explicit response-owned Set-Cookie sink because an agent invocation has no ambient HTTP response; it accepts no alternate provider, DB, env, principal, client-IP, or egress authority. Agent adoption is exact-identity and one-owner like advanced endpoint adoption: a copy, cast, duplicate adoption, second app, or duplicate package instance fails the private runtime check. Adopted agents are not HTTP declaration-inventory members and create no ambient registry; the compiler still derives their finite model/tool operation witnesses from the original task-subpath declarations under the §6.6 agent mediation contract.

defineKovo snapshots provider descriptors and callbacks but MUST NOT invoke a DB, session/auth, environment, CSRF, replay, client-module, or other live provider. Provider evaluation and environment parsing begin only when app.assemble({...}) closes the graph (or when an explicitly documented provider's existing contract requires a later request-time call). Importing the contract or any declaration module is therefore provider-inert. A provider throw during assembly fails that assembly before a KovoApp is returned; it cannot leave a partially registered application.

Every declaration handle is registered by exact object identity in module-private state owned by the KovoContract that minted it. A module-private unique symbol may make accidental structural construction a TypeScript error, but the runtime proof is exact private-map membership. A handle from another contract, a copied/spread object, a structurally similar value, or a handle from a duplicate @kovojs/server package instance MUST fail assembly with an actionable diagnostic that names the declaration kind and the two resolved package identities when available. Public structural brand fields and Symbol.for() are forbidden. The compiler additionally detects duplicate Kovo package instances before authored evaluation; the runtime check is the fail-closed floor for direct/custom hosts.

app.assemble({...}) is the sole public application assembly operation and succeeds at most once for one contract identity. Its declaration arrays are dense, finite, snapshotted in authored order, and contain only handles minted by that exact contract. The result is an opaque minimal KovoApp token; normalized options, providers, route/query/mutation registries, runtime authorities, DB carriers, and generated registries remain framework-private. Public CreateAppOptions and a structural createApp() aggregate are not app API. A custom adapter accepts the same opaque token through a focused adapter entrypoint and cannot inspect or reconstruct its private state.

Assembly membership is compiler-checked, not merely a hand-maintained runtime convention. For each proved defineKovo receiver, every compiled declaration handle reachable from that receiver MUST appear exactly once in the single proved assemble call. A missing handle, duplicate membership, dynamic/spread declaration list, second assembly, unresolved handle, or cross-contract handle is a build diagnostic before output. The diagnostic for a missing handle includes one deterministic source edit that appends the exported handle to the matching kind array; applying the edit twice is a no-op. Runtime assembly repeats the membership and duplicate checks for uncompiled/custom hosts. There is no ambient registration, import-order discovery, process-global pending registry, or fallback from the closed application inventory.

Development HMR constructs a fresh contract and closed graph for the new module generation, then atomically swaps it only after compile and assembly succeed. It discards the old generation's private membership and provider references after in-flight requests release them; late declarations cannot join either generation. A failed update keeps the prior closed graph. HMR never invokes assemble twice on one contract or accumulates process-global registrations.

The app-scoped access algebra reuses the executable self-naming guards of §10.2. The contract exposes app.authenticated, plus parameterized app.role(...), app.rateLimit(...), app.owns(keyOf, ownsRow), and app.all(...); these return the same guard values runtime dispatch executes and preserve their request refinements. They are not type markers or audit-only labels. access: [guard, ...], publicAccess(reason), and verified machine access remain the mutually exclusive §10.2 decisions, and missing or mixed decisions remain KV436. Binding a guard to a contract narrows its request/session type but does not change the authorization proof boundary: compiler census plus the exact runtime guard chain remain authoritative.

Factory results expose only purpose-specific operations. In particular, a query handle exposes its inferred input/result types, component binding, and the §10.4 optimistic constructor; it does not expose a writable registry key. A mutation handle exposes its inferred input/result/error union for forms and tests without exposing private runtime callbacks. A component that binds a mutation handle receives its form failure and field-error slots from that handle (§6.3), so a parallel author-maintained slot registry is not part of the public contract. Public handle types keep conditional machinery behind named interfaces and errors must anchor on the offending definition property rather than expand private witness types.

The ordinary route/query handles come only from the app-scoped factories. The Core root does not offer parallel routeRef/queryRef constructors, declaration option records, writable registry keys, or a human registry-augmentation path: none of those can prove app ownership or rename-safe assembly identity. Typed navigation consumes the compiler-generated facts from the app-owned route declarations.

The compiler recognizes an app-scoped declaration only when the call receiver is proven by exact TypeScript symbol identity to originate from one direct defineKovo result under the receiver provenance rules in §5.2. A same-named local, cast, wrapper, destructured method, computed property, mutable/ambiguous alias, duplicate package identity, or structurally copied receiver does not mint factory provenance. Once proven, the factory lowers to the same finite declaration and server.handler.root facts as the corresponding primitive; the facade never replaces the AST/provenance gate or a runtime sink check.

Client-handler publication has a deliberately narrower value grammar than the general JSON wire. publishToClient(value, { reason }) accepts exactly string | number | boolean | null. It rejects every object, array, symbol, bigint, undefined, and function at runtime using only primitive classification, without reflecting over or coercing caller-owned values; its TypeScript signature exposes the same finite union as an author-time guardrail. In client-handler source, the compiler accepts only a unique, pristine same-file const initialized directly from that literal grammar and snapshots the literal into the generated module. An imported value, re-export, alias to an import, mutable binding, duplicate/shadowed binding, array, or object is refused even when wrapped, because evaluating its source module or carrier could itself execute authority. Only the finite reviewed client-handler import registry grants executable authority (§5.2, §6.6).

6.3 Mutation typing contract#

Where the mutation value is importable — server-rendered templates always can — mutation={addToCart} is the preferred form authoring spelling: inference comes straight off the value, no registry hop. The compiler emits the concrete action="/_m/<key>", mutation key metadata, input coercion metadata, CSRF field, idempotency token, and submitted-form target. The string-keyed form('<key>') helper survives for sites that cannot import the value, but author TSX should not hard-code mutation URLs. An end-to-end add-to-cart walkthrough lives in docs/worked-example-add-to-cart.md.

The typed mutation={definition} path is the sole complete public mutation-form bundle: the framework emits the mutation-audience CSRF field and canonical Kovo-Idem field together, from one proven definition, before authored controls. An exact compiler-recognized {...mutationFormAttributes(definition)} JSX spread is an equivalent typed spelling and receives the same generated field bundle. Standalone CSRF token/field construction is not a mutation-form authoring API because it cannot establish the idempotency half of the protocol. TypeScript prevents the ordinary partial call shape, while compiler provenance and the runtime request lifecycle remain the enforcement boundaries.

Enhanced form failures use the same render function as the no-JS full-page path. Expected failures are typed mutation results: schema validation maps to <FieldError name="...">, declared application codes map to <FormError code="...">, and both helpers are compiler-bound to the enclosing enhanced mutation form. The third render argument still carries typed form state as the escape hatch for custom UI, with each bound mutation exposing forms.<mutation>.failure: null | { code; payload; fieldErrors? }. The failure value is scoped to the submitted form instance for that render and is cleared by the next successful render of that instance. ctx.submit(mutation, { input, onError }) receives the same exhaustive typed-error union. Under the app contract, <mutation handle>.form and component mutation binding infer this surface directly from the handle; ComponentRenderSlots and a separately authored form-state map are not public prerequisites.

Repeated forms must provide stable identity through authored key or serializable keyed component props; the compiler lowers it to kovo-key and derives the submitted-form fragment target. Hidden inputs are submitted data, not identity. An enhanced form in a repeatable position with no stable key is a teaching diagnostic because the server cannot know which live form to re-render.

6.4 Routes & links (typed navigation)#

Navigation is the inter-page wiring of an MPA, and it is typed with the same declare-once pattern — a TanStack-Router-style type layer with none of its runtime, because the server owns navigation (§8). Routes are declared values whose path strings are captured as literal types:

ts
// products.routes.ts
export const productRoute = route('/products/:id', {
  params: s.object({ id: s.string() }), // coercion declared once, like FormData (§6.3)
  guard: authed, // same combinators as mutations (§10.3); pages join the unguarded audit
  search: s.object({ max: s.number().optional() }), // the §7 URL channel, typed
  prefetch: 'conservative', // Speculation Rules config lives here (§8)
  meta: ({ params }, queries) => ({
    /* … */
  }), // §13.5 head/meta, typed, fed by queries
  page: async ({ params, search }, req) => {
    /* rendered page */
  },
});

Path params are extracted from the literal by template-literal types (PathParams<'/products/:id'> = 'id'), so links demand exactly the right params — missing or extra is a compile error, and the params argument exists only when the route has params:

tsx
// Authoring (sugar)
<Link to="/products/:id" params={{ id: item.productId }} search={{ max: 500 }}>
  View
</Link>;

// GET forms — the §7 coordination channel — validate against the route's search schema
const f = form.get('/products');
<f.Form>
  <f.input name="max" type="number" />
</f.Form>;
// ✗ compile error: field name not in search schema — same machinery as mutation forms (§6.3)
html
<!-- Lowered IR / wire: a plain anchor. No client router, no link runtime —
     Constitution #1 (legible), #3 (a string href is valid Kovo source), #4. -->
<a href="/products/p1?max=500">View</a>

Link is JSX-only: <Link to={productRoute} params={...}>...</Link> renders an anchor and has no imperative overload or descriptor result. href(productRoute, { params, search }) is the sole imperative URL-string constructor. GET-form helpers infer their public record directly from the route's search schema and expose only the form and typed control builders; their conditional helper families are private implementation types.

redirect(productRoute, { params }) types the POST-redirect-GET path (§9.1) the same way. Residual literal hrefs in hand-authored IR are validated against the route table at compile time (KV220); full-origin URLs and an external marker opt out. The propagation property of §6.2 holds for navigation too: renaming a route path turns every <Link>, GET form, and redirect() in the app red under kovo check.

Two more route-level affordances close the request shell: guardsguard: on a route() runs the same combinator chain as mutations (§10.3) before page, refines req.session identically, and enrolls the page in the kovo explain unguarded audit; and notFound() — returning notFound() from page renders the app's 404 page with the correct status, so status codes stay part of the typed surface rather than ad-hoc response construction. redirect() and notFound() are the sanctioned non-200 page outcomes in v1.

Routes may also return two sanctioned non-HTML 200/304 outcomes: respond.file(body, { contentType, filename?, etag?, headers? }) and respond.stream(body, { contentType, filename?, etag?, disposition?, headers? }). These are still ordinary route()s: params/search schemas, guards, typed links, KV220 validation, the unguarded audit, and the owner:-powered unscoped audit all apply before the body is served. Content-Type is required, Content-Disposition is declared (respond.file() defaults to attachment; respond.stream() defaults to attachment unless inline is requested), and a matching If-None-Match answers 304 without rendering HTML. Upload filename metadata and every final live/generated Content-Disposition filename sink MUST neutralize Unicode directional-formatting controls U+061C, U+200E/U+200F, U+202A–U+202E, and U+2066–U+2069 before constructing either filename or RFC 8187 filename*; browser-visible filenames cannot retain display-direction authority from a remote uploader. Range/resumable downloads are out of scope for v1; large exports that exceed a request/response window belong to a later background-jobs design.

respond.stream() and raw endpoint() responses are the escape hatch for app-owned streaming protocols. They do not participate in enhanced mutation application, query truth, mutation failure rendering, CSRF/replay semantics, or final fragment reconciliation unless the app builds that protocol itself.

6.5 Session schema#

Sessions are a declared s.object schema, not an any bag: req.session is fully typed everywhere it appears. This is core, not a nicety — query instance keys (§10.2) and guard refinements (req.session.user non-null under authed, §6.2) are load-bearing on session fields, so an untyped session would be a hole directly under the proof surface.

Session provenance is an application capability, not a framework-owned identity system. The app declares a sessionProvider in the server request shell; Kovo runs it once before route, query, or mutation guards and exposes the returned value as req.session. session(schema).provider(provider) MUST snapshot the exact schema and runtime-validate every non-null provider result through it before the value reaches guards or handlers. This applies to synchronous and asynchronous providers and to both plain values and { value, setCookies } envelopes; envelope cookies keep their independently snapshotted forwarding semantics. The validated session is an owned framework value, so undeclared properties, inherited/accessor fields, Proxies, and later provider-object mutation cannot create session authority. TypeScript assignability remains an author-time guardrail, not the proof. A provider returning null or undefined means "anonymous"; guard combinators must treat that as unauthenticated rather than as a malformed request.

Route and query guard failures have fixed outcomes so auth remains part of the typed surface. authed failures run the app's onUnauthenticated handler, whose default is a 303 redirect to the configured login route with the original URL available as next. next is framework-validated: it MUST be a same-origin, single-leading-slash absolute path (no //, no scheme, no host) that resolves against the route table (§6.4); a value failing that check is stripped to a safe default. The framework re-validates next both where it is captured and again wherever it hands next to the post-login redirect, so app-authored login code cannot consume an open-redirect target. Authenticated-but-unauthorized failures render the app's 403 shell with status 403. Mutation guard failures distinguish authentication failure from authorization/validation failure. An unauthenticated mutation guard failure (an authed guard failing because req.session is null/anonymous, §6.5 — e.g. a session that expired between page render and submit) is a distinct outcome from a validation or app-fail() error (§9.2): the enhanced path returns HTTP 401 with a Kovo-Reauth directive carrying the login route and a same-origin next (the original document URL), which the loader follows to re-authenticate exactly as a page route would for the same expired session; the no-JS path returns a 303 redirect to the configured login route with next, mirroring the route/query onUnauthenticated contract. An authenticated-but-unauthorized mutation guard failure (a role()/ownership refinement failing on a valid session) keeps the §9.2 typed-error path — HTTP 403 with forms.<mutation>.failure carrying an unauthorized code — and introduces no redirect body. Only the unauthenticated case crosses into the auth-redirect vocabulary; this prevents a routine session-expiry on submit from surfacing as a generic validation-style error with no path to re-auth.

6.6 Soundness boundary (normative)#

The §1.2 proof claims are claims about TypeScript programs that stay inside the sound subset. The starter therefore ships — and the docs state as a precondition — strict everything plus lint bans on any, non-null assertions, and as casts in app code. Three boundaries are runtime-validated regardless, by design: the wire (every mutation input passes its s.* schema — types-without-validators, raw-tRPC style, was rejected); deploy skew (a long-lived document POSTing yesterday's form shape is answered by schema validation and the 422 path, §9.2 — never undefined behavior); and CSRFkovo-csrf (§9.1) is a synchronizer token stamped into every emitted form and verified before schema parsing, replay lookup, and the guard chain on every mutation POST. When req.session is present the token is bound to it; when it is null/anonymous (§6.5) the token is bound instead to a framework-owned signed-cookie secret that exists independent of sessionProvider, so pre-auth forms (login, signup, password reset) are CSRF-protected even with no session to bind to — anonymous-CSRF is mandatory, not optional. CsrfOptions.sessionId MUST return a stable opaque 1..1,024-character rotation id for a framework-resolved session and undefined only for a genuinely anonymous request; non-string, missing, empty, oversized, anonymous-with-id, and unresolved-session results fail closed. The rotation id has no reserved textual spellings: exact length framing and a separate kind frame distinguish even anonymous-shaped session text from an anonymous cookie. The signing payload domain-separates that session/anonymous kind and, for a framework lifecycle request, both the rotation id and the independently pinned authorization principal, so a shared or namespace-shaped app id cannot cross-bind two principals. On a successful authenticating submit the framework rotates the anonymous token's binding to the new principal; apps should rotate their own session identity on auth (Kovo does not own session identity, §6.5). CSRF is default-on for server-rendered mutation endpoints; an explicit csrf: false is the only per-mutation opt-out and is reserved for non-browser or externally authenticated endpoints. A csrf: false mutation MUST NOT use browser authority: it is compile error KV418 for such a mutation to read req.session, Cookie, Authorization, or Proxy-Authorization; escape an unproven request carrier; run a session/cookie-derived guard (e.g. authed, role(), owns()); or call a browser-state response sink (setCookie, forwardSetCookie, or setSessionRevocationClearSiteData). Skipping CSRF while riding the victim's ambient credential is forgeable, and minting an attacker session or clearing victim storage is login/logout CSRF even when the handler never reads ambient state. The exemption is sound only by construction: a csrf: false mutation is served with no ambient session/browser credential headers and cannot emit Set-Cookie or Clear-Site-Data. Machine callers use an explicit non-ambient custom signature header; browser credential flows keep the anonymous synchronizer token. Raw endpoints may separately declare executable verifier auth. Truly non-browser writes belong in endpoint()/webhook(). Every mutation's CSRF posture (checked or exempt:<justification>) is listed in kovo explain endpoints (§11.4) alongside endpoints and webhooks. The Kovo-Idem replay token (§9.1) is a per-submit, high-entropy value minted fresh by the client on each logical submit and refreshed in the enhanced success response (§10.3) — a freshly stamped hidden field, never a form-instance constant — so re-editing and re-submitting a form is a new mutation rather than a silent replay of the first response. Deploy skew also covers handler modules, normatively: emitted module URLs are immutable and versioned, and the serving layer retains prior versions — an old document's on:* refs keep resolving after a deploy; first interaction on a still-open tab never 404s. Generated ABI subpaths (for example @kovojs/browser/generated) may change when the compiler and runtime ship together because app source regenerates those imports, but already-emitted immutable modules remain governed by the same versioned-module retention rule: old generated modules must keep resolving to the runtime symbols they were emitted against for the supported deploy-skew window.

When replay storage is configured, a csrf: false mutation MUST declare machineReplayPrincipal(request). Kovo invokes it exactly once per request, only after parse/coerce and the guard/access decision succeed, against the pinned post-guard request. It MUST return a primitive non-empty string of at most 1,024 JavaScript code units. Missing declarations, throws/rejections, wrappers, and malformed values collapse to the sanitized 422 idempotency conflict before replay-store or handler authority. A CSRF-protected mutation MUST reject this machine-only declaration. The returned public caller/tenant id is canonically length-framed under a versioned domain and committed by applying the boot-pinned SHA-256 control to the exact UTF-16LE encoding of every JavaScript code unit before it enters replay scope. The encoding MUST preserve lone surrogates rather than replacing them with U+FFFD; the raw value MUST NOT enter replay keys, store metadata, errors, or diagnostics. Enhanced and no-JavaScript delivery share this scope. A retry that changes response vocabulary conflicts under the existing claim and MUST NOT execute the handler again.

Persistent principal revocation epochs (normative). A PrincipalEpochStore is the authoritative identity-lifecycle capability for one persistent, monotone { epoch, changedAtMs, status } row per proven principal, independent of any session. initialize(principal) atomically creates active epoch 1 or returns the existing row without changing it; it never reactivates a tombstone. advance(principal, reason) and tombstone(principal, reason) accept only the finite reason unions exported by Kovo and MUST increase both epoch and change time. Tombstones are permanent. Better Auth bindings supplied this store initialize the row from the provider's sanitized authenticated user id before the session reaches app code. Other identity providers MUST initialize at account creation or authenticated resolution and call advancePrincipalEpoch/tombstonePrincipalEpoch for password, role, tenant, administrative, provider-revocation, and deletion events, including out-of-band changes.

Every persistent credential derived from a principal MUST carry the current epoch at its mint door and compare it with an authoritative current row at every release door. Kovo's closed census owns the capability-URL mint and verify doors plus mutation replay-receipt reservation, response release, handler admission, in-transaction completion, and settlement doors. The exactly-once-continuation callback is explicitly inapplicable: it is closed before its adapter frame returns and never becomes a durable credential. Missing, malformed, unavailable, timed-out, contradictory, stale, or tombstoned state fails closed. The default has no positive application cache; each lookup has a 1,000 ms ceiling, so there is zero successful-lookup staleness beyond the one authoritative read/action race. Expiry remains a second floor and is never freshness evidence. Production accepts only the module-private durable store provenance exposed by createPostgresAppRuntimeDb().principalEpochStore; structural fields or a global symbol cannot forge that provenance. §10.3 defines replay scoping and the explicit mutation transition declaration.

Anonymous-CSRF cookie names are logical unprefixed names; Kovo alone applies the effective __Host-/__Secure- prefix. Across the app-wide and every mutation-local CSRF configuration, one logical anonymous-cookie name MUST have exactly one Path, Max-Age, SameSite, and Secure posture. App construction rejects conflicting or prefixed aliases because Cookie request headers omit those attributes: multiple same-name secrets would otherwise collapse to a browser last-wins value or arrive as indistinguishable duplicate-name pairs and make another emitted form unverifiable. Standalone mintCsrfToken/mintCsrfField calls made during one framework-managed response lifecycle MUST likewise reuse one anonymous binding and one identical Set-Cookie value per logical cookie posture. A conflicting same-name posture or authored browser-prefix alias fails before a second token can be emitted, so a raw response containing multiple forms cannot silently invalidate an earlier form. While that lifecycle is active, token-generation calls through cloned, reconstructed, or other derived Request values resolve browser/session authority from the canonical lifecycle request and share its binding/posture state and response-header commit boundary. An exact framework-retained request can identify that lifecycle after async context is lost. That exact retained context takes precedence over any ambient outer lifecycle: nested dispatches cannot cross-bind canonical authority, personalization witnesses, pending cookies, or seal state. Every createRequestHandler() invocation is a distinct response boundary: it clears an ambient caller frame before pre-dispatch callbacks run and, when passed the caller's exact retained Request, reconstructs a detached native ingress carrier before the nested dispatcher can finalize anything. An arbitrary detached derivative cannot identify a lifecycle. A first-anonymous mint therefore requires an active lifecycle or an exact retained lifecycle receipt. The lifecycle privately records the exact standalone Set-Cookie; finalization atomically seals and snapshots that record before delivering it through the route/document sink or an endpoint response authorized to emit browser state. An exact authored duplicate is emitted once; a non-identical plain/__Host-/__Secure- alias under the same logical name fails closed. Direct runEndpoint() and direct internal renderRoutePageResponse() have no managed cookie sink and reject a first-anonymous mint, while a truly late post-seal mint cannot enter the snapshot. Detached session-bound generation and generation from an already-present anonymous cookie can remain valid only from the exact supplied credential carrier; they do not inherit canonical response authority from a settled ambient frame. An authored raw stream therefore must retain and use its exact handler Request when it needs the response receipt; a reconstructed or otherwise derived request after owner settlement cannot recover that receipt. CSRF validation and replay resolution always use the exact supplied ingress request and never inherit response-generation authority.

Every deployable AsyncLocalStorage authority cell is governed by the versioned kovo.async-context-confinement/v1 census and one framework-owned confinement contract. Cells stay separate and least-authority; the contract does not merge request, response, provenance, egress, build, credential, or module-load values into one ambient bag. A cell is an opaque exact identity whose storage remains module-private. Each store is bound to an exact framework lifecycle witness and generation. A read exposes a value only when the cell, current lifecycle, and open-generation identities all match; missing, inherited-foreign, forged, or stale state yields no authority and never falls back to another ambient store. Entering a cell from a detached descendant of a closed lifecycle fails. createRequestHandler() establishes a fresh authority-empty lifecycle before any pre-dispatch callback; nested request cells share that exact lifecycle, while build evaluation, generated-module loading, and cloud-credential access deliberately open isolated roots. The owner closes its lifecycle on synchronous return, throw, asynchronous fulfillment, or rejection, so work not awaited by the owner cannot retain or reacquire its cells. Exact retained response-lifecycle receipts remain the separately specified identity bridge above and never turn an unrelated ambient cell into authority. The sole framework-owned post-settlement re-entry is deferred-region JSX rendering: Defer captures the same exact JSX context object that registered the region and runs its success, rejection, and timeout rendering in a fresh isolated lifecycle containing only that cell. Each captured re-entry capability is one-shot, and success, rejection, and timeout select one winner before opening a fallback lifecycle. Timeout explicitly revokes an unfinished success lifecycle before rendering the error fallback, so a never-settling or late-rejecting authored promise cannot retain or re-mint even JSX authority. The exact retained request inside that context may resolve its already-sealed response receipt; sibling request, provenance, egress, credential, build, and module-load cells remain absent. Critical and non-collected regions stay in their current owner lifecycle, authored raw streams receive no ambient re-entry, and the census gate rejects any additional consumer of this finite bridge. The @kovojs/test SQL observation carrier is a censused, non-deployable observer rather than app authority; it retains its independent exact-scope revocation so observation can span the request boundary without weakening runtime isolation.

The public mintCsrfToken and mintCsrfField helpers serve only a verified raw endpoint protocol with an explicit custom audience. They reject mutation targeting. The lower-level csrfToken and csrfField helpers are internal/test-only; exposing either at the package root would make an incomplete handwritten mutation form look supported while omitting canonical Kovo-Idem. The closed mint/deliver/validate/rotate/replay surface and its proof anchors are recorded in security/csrf-mint-delivery.json; adding a response or bootstrap surface requires adding a closed matrix row before release.

Every independently resolved authorization principal entering a CSRF or replay identity, and every source-derived mutation identity, MUST likewise be a non-empty string of at most 1,024 JavaScript code units. An inbound anonymous-CSRF cookie secret is accepted only when it is 32..1,024 base64url characters; the framework mints a 43-character secret. A present malformed or oversized credential fails closed and is never replaced by an anonymous fallback within that request.

s.string() rejects raw C0 control characters (U+0000 through U+001F), U+007F DEL, and the JavaScript line-terminator code points (U+000A, U+000D, U+2028, U+2029) by default before any format, pattern, or unsafe-regex refinement runs. This is defense-in-depth for every request-derived string sink: an embedded NUL, CR/LF, tab, or other control character cannot survive validation by relying on a loose or parity-sensitive author regex. Authors who are intentionally accepting textarea-style content must opt in with s.string().multiline(), which admits line terminators while still rejecting the other raw C0 controls and DEL. Authors who intentionally accept arbitrary raw controls must opt in with s.string().allowControlChars(). These opt-ins alter only the base string hygiene gate; all existing chained format/pattern/optional/default behavior still applies normally.

Security soundness (normative). The Prime Principle (§2) rests on the same sound-subset discipline, bounded by six rules. (1) The compiler performs no TypeScript type inference of its own — security classification is carried by AST symbol-identity provenance, sink classification, and fail-closed runtime checks; a branded type (Secret<T>, a public() brand, and the like) is tsc-time ergonomics and defense-in-depth, never the enforcement. (2) Runtime taint is unsound — JS string operations and template literals produce fresh primitives with no surviving metadata, so request-derived provenance for confidentiality, write-eligibility, and input shape is proven statically at the AST (where the path is still code), never by runtime value-tracking; runtime contributes only sink validation (checking a final value's grammar, shape, or resolved IP, which survives transforms). (3) By-construction and defense-in-depth are distinguished and labeled. Where static analysis can prove the unsafe state inexpressible, the guarantee is by-construction (output-safety §5.2 rule 10, the confidentiality boundary, default-deny authorization, write-provenance). Where it cannot — outbound egress, a read-only-handle runtime proxy, Content-Security-Policy / Trusted Types, log redaction — the control is a fail-closed runtime floor: sound at its sink but bypassable by privileged same-process code, and it MUST be documented as defense-in-depth rather than a proof. (4) Advanced TypeScript types are preferred when they narrow author mistakes without becoming the trust boundary. Validated branded constructors are appropriate for strong signing material; module-private unique symbol brands are appropriate for framework-owned sentinels; branded escaped/trusted/rendered HTML values are appropriate for UI composition; exact header-bag and discriminated-union types are appropriate for preserving multi-value headers and explicit posture choices. Public structural brands, casts, and type-only assertions MUST NOT be accepted as security evidence unless a runtime constructor, AST/provenance gate, or fail-closed sink also enforces the invariant. (5) Boundary decisions over caller-owned carriers must classify-and-pin or reconstruct. Once a runtime boundary classifies, normalizes, or validates a caller-owned value, the sink MUST consume either an immutable framework-owned pinned carrier for that exact classified value or a reconstructed fixed output; the sink MUST NOT re-read mutable caller bytes after classification and still claim the earlier decision. Browser sinks MUST classify platform behavior that depends on a tuple of attributes from the same pinned element snapshot, not validate each string in isolation; hidden _charset_ substitution is the canonical HTML example (§13.2). Spec §10.3 C15 names the concrete sink obligations. (6) Authority-bearing controls have a framework-owned bootstrap trust root. Every supported Kovo compiler, dev, build, export, generated-server, worker, and test runner MUST evaluate the framework security bootstrap before any authored app module, Vite/plugin module, generated module, or other caller-controlled dependency in that realm. The bootstrap eagerly captures the ambient bindings, prototypes, framework controls, and reviewed dependency objects used by later security decisions; ordinary late replacement of those captured controls can therefore affect only unused public bindings. This is not a claim that deliberately hostile same-realm code cannot instrument a dependency, discover a module-private object that was not reachable at bootstrap, and mutate that object later; that is privileged application compromise under the trusted application-code boundary below. A security claim that must remain independent of app code MUST run in a fresh process or genuinely isolated realm that never evaluates the app graph; in-process parser/control reconciliation is defense-in-depth only. Function source text, names/arity, native-looking descriptors, and finite positive/negative probe corpora are health diagnostics only and MUST NOT be accepted as provenance for a control captured after caller code ran. A host preload (NODE_OPTIONS, embedding code, loader hook, VM setup, or equivalent) that executes before the supported Kovo entry is privileged same-process host compromise and outside the app-level framework claim; a platform that cannot guarantee bootstrap order MUST move authority computation into a genuinely pristine isolate with a fail-closed typed RPC boundary. Tests for import-order mutation MUST enter through the same bootstrap-first runner and poison controls only after that boundary, including the first entropy/hash/command use rather than relying on second-use detection.

Capability-bounded agent mediation and honesty (normative). Kovo does not claim prompt-injection immunity: an application that lets a model read adversarial text can still receive an authorized-but-undesired decision. In the supported subset, a model-selected application action can reach an effect only by naming a framework-owned tool() declaration. Each tool names one exact compiler-visible mutation binding; the compiler derives its effect closure from the same finite L2 operation IR used for HTTP roots and installs that closure as generated runtime evidence. An opaque model callback, dynamic tool collection, unresolved tool-to-mutation link, or model invocation with raw authority is a KV448/KV449 build error, not a warning. The model receives frozen inert tool descriptors and the exact framework ctx.fetch egress door, never an executable mutation, request, database handle, or ambient principal.

A selected tool executes the exact mutation's input parser, access decision and guard chain, managed-database SQL write policy, RLS principal pinning, and transaction path. The invocation principal is an immutable framework session-provider snapshot established before the model runs; structural request fields and caller-supplied service-principal overrides cannot establish agent authority. Internal agent calls do not impersonate a browser form and therefore do not replay the browser CSRF protocol, but they MUST NOT bypass the mutation's authorization or data-plane policy.

An agent session carries the closed integrity order untrusted < retrieved < validated < principal. Every admitted content value is an exact immutable agentContent() carrier with an explicit integrity, and every tool result is unconditionally classified as untrusted or retrieved; no prose/content classifier participates. The session updates by the lattice meet only, rejects concurrent turns, and filters the next offered tool set by the compiler-derived minimum integrity of each tool's effect closure. Thus, once injected or retrieved content is admitted below principal, it cannot raise the session's integrity or regain a removed high-authority tool for the rest of that session. kovo explain agent MUST print the same model effects, per-tool mutation/effect/minimum/result-integrity facts, and retained closure at every integrity level that runtime enforcement consumes. This proves only that no model-selected action exceeds the invoking principal and that admitted lower-integrity content cannot raise authority. It does not prove that an authorized action was intended, that an app classified direct input honestly, or that malicious prompts and retrieved content are harmless.

Cryptographic authority and lifecycle (normative). Raw secret-crypto acquisition is a capability, not an authority-free implementation detail. The capability-closure vocabulary distinguishes crypto-acquisition from digest. An exact named import of a reviewed non-keyed SHA-256 digest primitive may classify as digest; a namespace/default crypto import, WebCrypto/global crypto, entropy, keyed hashing, password hashing, signing, encryption, or an ambiguous import MUST classify as crypto-acquisition. Either capability reached from an untrusted root closes with KV448 unless the exact framework export is a reviewed door. Kovo's repository gate separately records every remaining production direct acquisition by exact path, class, and operation. That high-authority path set MUST be a non-increasing ratchet: adding or widening a row requires an explicit reviewed architecture change, while deleting or narrowing one requires no compatibility mode.

kovo.certificate/v1 MUST carry the exact same nine-member raw-capability domain: crypto-acquisition, database-driver, digest, dynamic-loader, filesystem, network, process, vm, and worker. Its search-side analyzer and disjoint checker MUST preserve the binding-sensitive distinction between an exact reviewed digest import and broader crypto acquisition; neither kind may be downgraded to an opaque external import or omitted from post-fixpoint closure.

The authority posture for that certificate belongs to a distinct, canonical kovo.certificate-policy/v1 reviewer policy. The policy MUST own the exact sorted {path, sha512} set of packed @kovojs/*/dist/*.mjs modules, the complete installed manifest for every package in scope, and the complete roots, doors, and opaque-assumption rows. The certificate MUST contain only the corresponding sorted artifact paths, the fixed capability domain, capability summaries, import edges, exact copies of the policy's roots/doors/opaque rows, and policySha512 computed over the exact canonical policy bytes. A checker MUST require exact equality for every copied posture row and artifact path before checking coverage, stability, or closure. The policy is an independently obtained review decision: a copy emitted beside an application build is an audit convenience only and MUST NOT become a trust root. Fetching policy, certificate, and artifacts from one mutable location does not establish independent review. A detached signature over a certificate authenticates only those certificate bytes and MUST NOT substitute for obtaining and reviewing the policy bytes named by policySha512.

The standalone directory checker MUST derive the actual @kovojs/* package census from the supplied packed tree and require it to equal the policy package census. It MUST compare each installed package.json as a complete JSON object with the reviewer-owned manifest, reject publishConfig and browser remapping, and reject every non-empty automatic package-manager lifecycle hook named dependencies, install, postinstall, preinstall, prepack, postpack, prepare, preprepare, postprepare, prepublish, or prepublishOnly. Every non-types conditional export, fallback, main, module, or bin arm MUST collapse to one canonical listed runtime target. Package imports aliases MUST be exact non-wildcard #name keys whose non-types arms collapse to one canonical relative target; any alias used by packed runtime code MUST resolve to a listed packed module. Source-only aliases are inert only when the complete packed-tree census proves the source files are absent.

The packed-tree census MUST admit only regular, non-symlink files: the installed manifest, canonical .mjs runtime modules below dist/, reviewed declaration/source-map companions below dist/, and the package README. Root or dist/ JavaScript with any other suffix, extensionless executable files, JSON runtime payloads, native addons, WASM, special files, unexpected documentation, and unreviewed siblings fail coverage. Certificate and policy input files and every manifest/runtime module MUST be read through a no-follow descriptor after path and descriptor identity agree; the checker MUST read through a fixed maximum-plus-one buffer rather than an EOF-growing convenience read, compare size and file identity before and after that same-descriptor read, bind each read to the initial file/directory census, and repeat the complete census after verification. An added, removed, replaced, grown, or otherwise mutated file or directory at any of those boundaries fails closed.

The v1 checker budgets are part of the denial-of-service boundary: policy bytes are at most 1 MiB, CLI certificate bytes at most 2 MiB, one runtime module at most 4 MiB, all runtime modules together at most 32 MiB, the policy may name at most 32 packages, the packed tree at most 4,096 files and depth 16, and certificate plus policy JSON together at most 262,144 nodes and depth 64. After parsing each complete runtime module, reference extraction may consume at most 32,768 reference units for that module and 131,072 across the packed tree. Each import, re-export, or dynamic-import occurrence costs one unit, plus one unit for each raw imported or re-exported binding it names; an export-all or dynamic-import occurrence also costs one wildcard-target unit. Deduplication MUST NOT reduce the charged units. Artifact analysis may retain at most 1,024 findings. Exceeding a reference or artifact-analysis finding limit MUST discard every partial graph, summary, and finding and return exactly one fixed fail-closed budget finding; a syntax error beyond the extraction limit MUST still be observed because complete parsing precedes extraction. The checker MUST snapshot caller-owned JSON and policy bytes once before validation and MUST NOT re-read them after making a decision.

The published @kovojs/verify package is the runtime-independent front door to this checker. Its human-public root MUST retain the certificate family as one coherent 11-declaration surface: KOVO_CERTIFICATE_CAPABILITY_DOMAIN, KovoCertificateCapabilityKind, KovoCertificateRootKind, KovoCertificateV1, KovoCertificatePolicyV1, KovoCertificateFinding, KovoCertificateArtifactSource, KovoCertificateVerificationResult, verifyCertificate, verifyCertificateDirectory, and formatCertificateVerification. The packed package MUST bundle its reviewed parser bytes and MUST have no production dependency on a Kovo compiler, analyzer, server, or runtime package.

The package bin is kovo-verify. -h, --help, and --version MUST write to stdout and exit 0. The verification grammar is one certificate path plus the required --policy <path> and --artifacts <root> flag groups and optional --format <human|json>; those groups may appear in any order. A completed verification with no findings exits 0. A completed verification with one or more certificate findings exits 1. Invalid/ambiguous usage, duplicate or unknown options, file I/O, text decoding, and JSON parsing failures are indeterminate rather than certificate findings and MUST write to stderr and exit 2. Human reports remain kovo-verify/v1. JSON reports MUST carry schema kovo.verify-report/v1, status, ok, the same stats, and the exact same ordered {obligation, code, message} findings as the human report. A JSON-mode indeterminate error uses kovo.verify-command-error/v1; it MUST NOT be shaped like a completed verification report.

Every non-dry release from the authorized main commit MUST publish separate GitHub artifact attestations for the exact committed reviewer-policy and certificate files. The attestation job MUST perform no dependency installation or repository script execution, and dry runs MUST receive no attestation or OIDC authority. Consumers MUST verify those attestations against the intended release workflow and commit, or obtain the exact policy bytes through another independently authenticated channel; the committed SHA-512 joins evidence but does not create that channel.

Certificate doors remain coarse module-plus-capability approvals. Their site field is a reviewer label, not a source-location proof. The checker re-derives lexical import edges and the modeled raw capability vocabulary, but it does not prove the behavior of host globals, eval, new Function, computed runtime loading, or every native/WASM execution route beyond explicit rejection and the reviewed opaque ledger. Those residual limitations remain honesty obligations under §4.6 and the trusted application-code boundary below.

The server runtime has one primitive-owning crypto authority. It captures its Node crypto and byte controls during the bootstrap-before-app boundary and runs known-answer checks for RFC 5869 HKDF-SHA256, HMAC-SHA256, fixed-width equality, and AES-256-GCM before serving. It MUST NOT expose a generic signer, sealer, primitive table, or derived key. Each consumer receives an exact framework-witnessed frozen handle containing only the operations for one registered purpose. The environment-neutral webhook verifier uses the corresponding core-realm WebCrypto authority because core cannot import the Node server runtime; that authority is verify-only, boot-captured, and keeps provider signing material out of public verifier metadata. Types and private brands are ergonomics; the runtime witness, closed registry, and acquisition gates are the enforcement.

The runtime-posture Ed25519 trust anchor also verifies detached privileged-write reviews from §10.3 and detached Metric E escape-root reviews from §11. An escape-review signature is domain-separated by the exact kovo.escape-obligation-review/v1 subject and binds one scanner-owned call-span identity, one structured obligation, and one reviewed artifact subject. This reuses the runtime-attestation fingerprint; a second review root is forbidden. A Metric E signature is separately domain-separated by kovo.escape-census-review/v1 and binds the exact reviewed artifact, closed door, counted root, and complete canonical producer-site set. Build emits only unsigned subjects, and its import graph plus the app-facing/internal execution surface expose verification but no signing handle. Signing authority belongs to the out-of-band review/deployment process and is absent from the build environment and coding-agent capability set. A valid signature is evidence that the pinned key holder approved those exact bytes. It is not evidence that the asserted guard or policy exists, that the cited evidence is sufficient, or that a human reviewer was independent; those are retained operational obligations.

The closed registry is kovo-crypto-purpose-registry/v1. Every framework derivation is HKDF-SHA256 over its root with public salt kovo-crypto-authority-v1. Its fixed-width info is the SHA-256 commitment of the injective, length-framed tuple (registry-version, purpose, audience, algorithm); no bounded audience bytes are truncated to meet provider-specific HKDF info limits. A row fixes the literal purpose, algorithm, allowed operation set, root source, and bounded audience grammar. An absent, dynamic, malformed, algorithm-mismatched, or operation-mismatched row fails before derivation. HMAC-SHA256 is the v1 symmetric signature/PRF algorithm; SHA-256 is the v1 non-keyed digest; AES-256-GCM with a fresh 96-bit IV and 128-bit tag is the v1 confidential-at-rest algorithm. A provider-owned webhook HMAC is verified with the provider's raw protocol key through a verify-only handle and is not HKDF-derived, because changing that key would break the external wire protocol.

Framework key rings are opaque configuration carriers, not generic signing objects. Exactly one entry is active; previous entries require a finite acceptUntil epoch-millisecond deadline; revoked entries carry no usable secret. New signatures and seals use only the active key. Verification and opening may use the active key or a previous key strictly before its deadline; unknown, expired, and revoked ids fail closed. On expiry/revocation the authority overwrites its own retained Buffer copy on a best-effort basis. This is memory hygiene, not a JavaScript zeroization guarantee: caller strings/buffers, VM and native-library copies, allocator snapshots, crash dumps, and keys already copied into a crypto implementation can remain.

The confidential-at-rest envelope is unconditionally kovo-aes256gcm-v2.<key-id>.<iv-base64url>.<tag-base64url>.<ciphertext-base64url>; v1 has no compatibility fallback. The key id is chosen by the active ring, never by the caller. The authority derives the key for registered purpose confidential-at-rest and the bounded declared string audience, then authenticates the exact envelope version, key id, purpose, audience, and caller AAD as one length-framed AES-GCM AAD value. Opening performs a bounded canonical parse, selects only an eligible ring key by the authenticated id, and authenticates before returning plaintext. Tampered version, id, IV, tag, ciphertext, audience, or AAD; an unknown/revoked/expired key; and a raw-key call shape all fail closed.

Trusted application-code boundary (normative). Kovo does not sandbox app-authored server modules or third-party packages that execute in the server realm. The public-import and provenance rules in §5.2 prevent unsupported or accidental authority use inside the supported authoring subset; they are not a claim that deliberately hostile same-realm code cannot recover ambient JavaScript authority through Function, dynamic loading, reflection, native addons, or equivalent language/host facilities. Such code is privileged application compromise, not a remote-input framework boundary. Deployments that execute mutually untrusted plugins or generated server code MUST place that code in a separate process or genuinely isolated realm and expose only a fail-closed typed RPC capability surface. Finite syntax deny-lists and intrinsic pinning may remain defense-in-depth, but MUST NOT be described or tested as a sandbox proof.

Capability-closed untrusted roots (normative, supported-subset static gate). Before evaluating authored app modules, kovo build MUST scan the immutable app-source snapshot and census every proved defineKovo() contract and its single assemble() closure (including lifecycle callbacks), route, layout, query, mutation, endpoint or low-level request adapter, webhook, durable or scheduled task, serialized browser handler, and supported agent/tool callback as an untrusted-data root. For each root, Kovo computes a transitive module/callback graph across eager imports, re-exports, local aliases and wrappers, literal import()/require() edges, conditional local targets, and callbacks or callback-bearing containers transferred through a local wrapper. A non-literal loader, unresolved local target, or reachable raw filesystem, network, process, worker, VM/dynamic-loader, or database-driver capability fails the pre-evaluation build gate with KV448 and a root-to-terminal provenance path. Reviewed framework APIs are the only nodes that may terminate such a path as a capability door; app or package metadata cannot mint a framework door.

The lexical-provenance scan behind this gate is resource-bounded and fail-closed. Its analysis budgets — abstract work, per-value candidates, loop reanalysis, invocation cycles, and the recorded unmodeled-effect-site history — bound analyzer time and memory only; they are not classification inputs, and a completed analysis yields the same verdicts at any budget. Exceeding any budget MUST mark the module's provenance analysis exhausted, and an exhausted module MUST close every framework root discovered in it with KV448 rather than report the partial analysis as exact. The effect-site history budget is derived, not constant: it scales with the module's own syntax-node count, clamped to a floor of 128 and a ceiling of 4,096 recorded sites, so a legitimate module does not become unbuildable purely by growing, while no module can obtain more than the ceiling by inflating its own syntax. The remaining budgets are flat and MUST NOT scale with module size; inflating a module to raise the effect-site budget therefore spends the flat abstract-work budget and still fails closed. Partial effect-site history MUST never be consumed as if complete: any truncation or refused recording is valid only in the same analysis that marks the module exhausted.

A custom Node adapter is privileged host wiring, not request-handler code. Its entry module MUST import @kovojs/server/runtime-bootstrap as its exact literal first side-effect import and MUST pass one directly imported handler from a separate local module to toNodeHandler(). Capability closure starts at that handler module, while the adapter entry retains only the host listener boundary. Inlining createRequestHandler(app), importing the handler before the bootstrap, or importing the bootstrap from the handler graph is unsupported and fails closed with KV448. Generated runners own the equivalent compiler-created separation and bootstrap order.

TASK B layered routing (normative). The pre-evaluation request/process check MUST consume the capability-closure result, dependency manifest, finite-operation diagnostics, and normalized semantic graphs derived from the same immutable source snapshot; running those analyzers beside an unbound legacy pass is not sufficient. The internal kovo-task-b-closure/v2 carrier repeats the exact source census, capability root rows, and kovo-app-dependency-capabilities/v1 manifest. It also carries one immutable kovo-task-b-finite-verdict/v1 snapshot taken at the compiler-result boundary. That snapshot MUST retain every KV449, KV450, and KV452 diagnostic with its exact site, start, length, message, and severity, and MUST bind the complete diagnostic census plus normalized semantic-source carrier with a canonical SHA-256 digest. TASK B accepts only an empty diagnostic census, an accepted status consistent with that census, and a digest recomputed over the exact carrier it consumes. Omitting or substituting any diagnostic or semantic trace after the snapshot fails KV424. This transport-integrity proof does not replace the independent compiler soundness oracles and does not claim resistance to coherent same-realm forgery outside the trusted application-code boundary above. TASK B MUST reconstruct each enrolled createApp, endpoint, layout, mutation, query, route, task, and webhook invocation from its independent parser view and require exactly one capability root at the same module and call site plus the same root kind in that module's dependency-manifest entries. A missing, duplicate, byte-mismatched, or differently rooted row fails KV424 with a stable root -> transfers -> sink -> closed verdict trace; it never falls back to a syntax-only allow.

For endpoint, mutation, query, task, and webhook effect handlers, TASK B MUST additionally require one exact kovo-security-semantic-graph/v3 root whose factory call span, callable span, callback, root identity, all-path verdict, helper summaries, and terminal inventory match the authored root. A missing graph, closed trace, closed helper summary, unknown transfer, or terminal mismatch is KV424 even when the residual analyzer would otherwise find no named sink. KV448 remains the primary diagnostic for raw/module/package authority and KV449 remains the primary diagnostic for a finite-IR operation that cannot lower; the KV424 correspondence check prevents either result from being silently omitted between compiler phases. KV450 and KV452 are likewise authoritative closed verdicts for scoped-key and derived-dataset provenance and MUST remain in the same finite verdict. Existing request/process predicates may remain as a conservative residual and independent C13 oracle until their exact root-and-terminal correspondence is proved, but they MUST NOT discharge a missing L1/L2/L3 proof or mint an allow verdict. The specialized Drizzle KV406/owner-predicate proof remains a separate data-plane correspondence responsibility under §10.3 and §11.1.

Reachable package code requires a least-authority verdict for the exact installed package name, version, security-relevant manifest fingerprint, requested subpath, imported export, and complete conditional-export arm set. Every manifest-public Kovo runtime export and every public subpath's <module> initializer MUST appear exactly once in the compiler-owned, versioned framework export posture ledger with an explicit raw-authority disposition, root kind or none, security role, implementation binding, manifest-target/condition fingerprint, and threat-matrix posture. A posture that can produce an authority-free or framework-door verdict MUST bind the exact installed implementation digest. A package whose complete public runtime surface is explicitly request-closed MAY instead use the unconditional-request-closure binding: after the exact installed package name, requested specifier/export status, reviewed version, and security-relevant manifest fingerprint match, the compiler rejects that package without consulting implementation identity. Such a binding is invalid if any public initializer or export is missing or has a disposition other than request-closed; widening the package therefore restores the exact-implementation requirement rather than inheriting a digest-free allow path. A new, missing, duplicate, stale, or unclassified first-party export fails closed; absence from a shorter door list is never an authority-free verdict. Compiler-emitted private ABI edges may bypass public subpath membership only through one compiler-owned exact table that classifies the initializer and every admitted member separately; that table is consulted only after the installed first-party manifest fingerprint and implementation digest match. A vocabulary match alone cannot mint an authority-free verdict. Explicitly reviewed framework companions use the same compiler-owned, version-pinned verdict model. Other packages use the committed kovo.capabilities.json kovo-package-capability-summaries/v1 ledger, whose entries are versioned independently and may classify exports only as pure or raw. A side-effect-only import is the reserved <module> entry. Every package import, including a named, default, or namespace import, evaluates that initializer and MUST consume one exact <module> verdict in addition to every requested export; an export wildcard cannot stand in for the initializer. An absent, stale, duplicate, contradictory, malformed, export-incomplete, condition-incomplete, or unresolved verdict fails closed with KV448. kovo explain capabilities prints the root census, reviewed doors, exact package-summary versions/fingerprints, and every closed fact with the same provenance used by the diagnostic. This is a conservative proof about accidental authority in Kovo's supported static authoring subset; consistent with the trusted application-code boundary above, it is not a same-realm JavaScript sandbox or a claim about deliberately hostile dependencies.

@kovojs/compiler is a deliberate zero-public-surface instance of unconditional request closure. It has no app-facing public runtime subpath, so an authored or request-reachable import of its exact package name or any subpath is rejected before installed resolution, version, manifest fingerprint, or implementation digest is consulted. The analyzer executable that makes this decision is a trusted release/install subject: its bytes are authenticated externally by verified release-tarball provenance, package-manager integrity, a certificate, or equivalent host provenance. The analyzer does not and cannot self-authenticate by comparing its running bytes with a digest embedded in those same bytes. Accordingly, this app-level proof does not detect arbitrary post-install mutation of the analyzer; such mutation is release/install or privileged-host compromise, outside the application-level claim. If the compiler ever gains an app-public runtime subpath, the zero-public closure is invalid and the posture gate fails until that surface receives an explicit non-circular classification.

For every authored or compiler-derived package edge, including an initializer in a malformed or currently rootless module, the compiler MUST also derive one kovo-app-dependency-capabilities/v1 loader manifest row containing the exact installed identity, specifier and conditional-export arms, retained export dispositions/capabilities, exact importer and sites, and the reachable root kinds. A loader-census-only row uses an explicit empty rootKinds array; that row does not invent a request root or explain fact, but its package initializer still cannot execute by omission. Every supported production Vite path that loads or bundles an approved app source MUST re-resolve that exact package identity before admitting its bare import and reject an absent/duplicate or malformed row, identity drift, a closed package row, or a retained raw or request-closed export. Pre-evaluation SSR MUST force complete dependency traversal and parse every admitted third-party module before execution; every bare child edge, including a Node builtin, and every non-literal module edge fails closed before Vite can externalize it. A relative child edge from a reviewed package MUST retain that package's exact nearest owning package root; physical containment does not admit a nested package.json or node_modules package identity, including one reached through a symlink or package-main redirect. Application aliases MUST NOT match a reviewed package child edge, and reviewed dependencies require Kovo's fixed Vite extension-resolution order; same-root retargeting invalidates the reviewed summary just as an escape does. Every direct reviewed export and resolved relative child MUST have one exact case-sensitive JavaScript/TypeScript module suffix — .cjs, .cts, .js, .jsx, .mjs, .mts, .ts, or .tsx — in both its lexical resolver identity and canonical realpath. Extensionless modules, JSON, CSS, SVG/HTML, WASM/native modules, and image/font/media resources remain closed until a separate pinned semantic and provenance lane admits them; Vite cannot reinterpret reviewed bytes as an asset, stylesheet, executable document, or worker payload. Query/fragment variants fail closed at the pre-evaluation module-edge census. Direct Worker/SharedWorker construction, dynamic-code or timer-handler recovery, and every retained new URL(..., import.meta.url) executable-asset carrier from a reviewed package MUST fail closed in the complete build-client artifact before that artifact can be published or executed. Vite MAY transform or stage bytes during an ultimately rejected build; tree-shaken source that is absent from the retained artifact creates no executable secondary graph. This retained-artifact rule does not weaken the earlier pre-evaluation package/module-edge census. An external module edge from a loaded HTML entry MUST resolve to the immutable approved-source snapshot or one exact framework-owned Vite bootstrap virtual; inline HTML module proxies remain outside the supported source graph. Before Vite resolution, Kovo MUST parse raw HTML with one exact-pinned standards-compatible parser in both the build tool's scripting-disabled state and the browser's ordinary scripting-enabled state. Every script source must be an exact HTML-namespace module URL in the immutable approved-source snapshot; inline scripts other than explicit JSON data blocks, foreign-namespace scripts, raw on* event attributes, JavaScript URL attributes, SVG SMIL execution primitives, and any iframe/frame/frameset/object/embed carrier fail closed with KV448. Raw element controls consume §4.8's finite static-value policy, including target-keyword, no-opener, and meta[http-equiv=refresh] automatic-navigation rules. Raw <base href> and <base target> also fail closed because they can retarget emitted modules or later navigation at browser consumption. Public-asset shadows, vite-ignore, browser-only module-type spellings, and post-resolution aliases cannot weaken the same approved-file binding. An exact framework host-tool external is permitted only when no app dependency manifest row overlaps that package or subpath. Artifact checks distinguish bundle-owned chunk filenames from true unresolved externals without weakening the earlier source and module-graph checks: only a relative runtime specifier normalized from its importing chunk may bind a bundle-owned file, while Rollup file-name metadata is checked separately and cannot bless a bare runtime specifier. Retained non-literal module edges fail closed. Relative artifact specifiers with percent encoding, query/fragment syntax, backslashes, ASCII whitespace/control bytes, or empty path segments fail closed before ownership comparison because browser/Node URL resolution can map those spellings to a different file. Only OutputChunk names — never OutputAsset names — satisfy executable bundle ownership. The same manifest is emitted in graph.json; an explicit empty manifest means the compiler proved that the app graph has no dependency edge. This loader check turns the pre-evaluation census into a fail-closed runtime/build bound for supported production artifacts, but it is defense-in-depth under rule (3): it does not sandbox deliberately hostile same-realm package code or prevent privileged host loaders from bypassing Kovo.

Supported browser event handlers MUST be authored as TSX/JSX event attributes and lowered through the compiler-owned finite browser operation vocabulary. App-authored imperative registration — an on* property write, addEventListener, or an equivalent opaque protocol/call transfer — is outside the supported subset and MUST fail closed with KV424 before authored modules are evaluated. That verdict is rooted in the registration's reachable authority, not in a deny-list of names found in the callback body; adding or renaming a dangerous browser API therefore cannot reopen the raw registration path. Compiler-owned JSX handlers remain governed by KV449 and the finite operation IR.

Compiler-derived browser response posture (normative). Supported build and dev runners MUST derive one kovo-browser-posture/v1 manifest from the immutable project source snapshot and register its reconstructed generated carrier before authored app evaluation. The compiler census uses the final effective intrinsic element/attribute tuple after static spread and primitive composition lowering. It records canonical absolute HTTP(S) origins, CSP directive, file, and source span for script[src], sandboxed iframe[src], img[src|srcset], SVG image[href|xlink:href] and feImage[href|xlink:href], audio[src], video[src|poster], source[src|srcset], track[src], input[type=image][src], and fetch-bearing link[href] relations (stylesheet, modulepreload, icons, and typed preloads). Relative/path-only and fragment URLs remain same-origin and add no origin. A computed URL at one of those positions is KV236 unless it is the exact framework trustedUrl(value, { reason: auditedReason, source? }) call with a non-empty static reason. That escape is recorded as opaque audit evidence; it does not invent an origin or establish isolation. A computed link[rel], opaque spread that could introduce an asset position, unclassifiable external URL, raw browser fetch/worker authority, frame, or popup likewise prevents a positive isolation verdict. Local spelling, structural copies, missing reasons, and self-consistent manifest fields cannot mint compiler provenance.

Framework-rendered page hints are re-witnessed at the document sink. An absolute HTTP(S) stylesheet, modulepreload, or bootstrap-script hint prevents the optional isolation posture because the compiler cannot establish the remote response's CORP/CORS behavior; build-generated same-origin hint paths remain eligible.

Document CSP assembly consumes that registered manifest. Census origins are admitted only to their derived fetch directive. An authored CspAllowlist string MUST be a canonical origin already in the same directive's census; a non-static origin requires the structured { origin, rationale } escape with a non-empty audited rationale. An unused unmatched string is a build/check error, not a silent widening. The manifest and authored config are snapshotted from stable own data before use, and generated registry reconstruction rejects unknown kinds, malformed spans, noncanonical origins, accessors, sparse arrays, and schema drift.

Permissions-Policy has exactly one response assembler with one exhaustive decision for every BrowserSecurityOperationKind; adding an operation without a decision fails the build. Both normal and reporting header bytes come from that assembler. The default document posture remains Cross-Origin-Opener-Policy: same-origin-allow-popups and makes no cross-origin-isolation claim. document.csp.crossOriginIsolation: true is accepted only with the exact generated manifest and no external or opaque resource, dynamic/opaque browser call, frame, popup, or authored origin whose CORP/CORS behavior is unproved. The isolated response is exact: COOP same-origin, COEP require-corp, and document CORP same-origin, plus the derived CSP and Permissions Policy. A route response cannot replace or weaken those selected headers. Missing or contradictory evidence fails before a deployable artifact or document response; Kovo never silently weakens isolation to preserve OAuth, popup, embed, or third-party-resource compatibility.

Finite operation closure (normative, supported-subset static gate). Capability closure answers which code and reviewed doors are reachable; the finite security-operation IR answers which security-relevant effects a supported handler can perform. Its browser vocabulary is closed in §4.3. Its structured-server vocabulary is exactly: principal-scope acquisition; managed database read/write; justified trusted SQL; framework egress; justified trusted HTML; cookie/header/outcome, raw-response, and redirect response effects; storage read/write; task composition; plus the typed server.data.declassify effect; plus the compiler-control records server.handler.root and server.helper.call. The root record enrolls each supported query, mutation, endpoint, webhook, and task body even when it has no terminal effects. The helper-call record names an exact immutable same-file callable that received authority and carries the source-derived handler root on the edge. The normalized interpreter below MUST discharge that edge before the build can treat the root as closed. The inventory in securityOperationKinds is the canonical union. C9 assigns terminal effects to one real boundary owner and the two control records to capability closure. Adding a kind without exactly one owner, or an inventory row that names an unknown/duplicate kind, fails check:c9-sink-inventory.

Classification follows symbol identity and monotone receiver provenance, not variable spelling. Endpoint/query/task context is the declared context parameter; a mutation's request and context are the second and third parameters respectively. Context, principal scope, database, headers, storage, Response, and their destructured method aliases retain authority through direct immutable aliases. An ambiguous/mutable join, computed terminal method, raw database client member, authority-bearing container or constructor, or return of authority is unsupported and MUST fail closed with KV449. Authority may pass to an exact immutable same-file function only by emitting a server.helper.call edge with its local identity. Imported, foreign, computed, aliased, reassigned, or unresolved helpers remain KV449. The finite edge enrollment itself does not guess about the helper body; the normalized interpreter MUST produce an explicit bottom-up summary before Kovo can claim cross-helper effect closure. A helper may always consume plain data returned by a reviewed operation, or the capability-closed module graph may terminate at an exact reviewed framework door. Namespace and named imports of the three exceptional operations preserve exact framework identity: trustedSql and trustedHtml require a static justification, and raw Response use is admitted only where the declared endpoint posture supplies the compiler-owned justification. App spelling, a same-named local, a cast, or a generated manifest cannot mint a door.

Typed declassification door (normative). A secret or untrusted value may be unboxed only with an exact nominal DeclassifyPolicy constructed by the door-specific static constructor exported from @kovojs/core/security: forRevealSecret({ purpose, ownerScope }), forSecretValue({ purpose, ownerScope }), forTrustedReveal({ ownerScope }), forRevealUntrusted({ ownerScope }), or forUntrustedValue({ ownerScope }). There is no generic public constructor that accepts a caller-selected door. The policy vocabulary is closed. ownerScope is one of application, current-principal, current-tenant, or framework. forTrustedReveal fixes purpose to public-projection; the two untrusted-value constructors fix it to request-validation; and the two secret-value constructors accept only credential-use or server-computation. Free-form strings, structural object literals, copied fields, casts, subclasses, surplus fields, an unknown tuple, or a policy created for another door MUST NOT authorize release. TypeScript's nominal shape is an author-time guardrail; the module-private constructor token, exact runtime registry membership, closed option validation, and exact-door check own the runtime floor.

A policy exposes no public door, purpose, or ownerScope data fields; those facts live only in the module-private record used for admission and audit. The bounded destructive reveal-audit drain is framework-internal. Public callers observe only the exact door-specific constructor and the reveal result inferred from the reveal operation.

The finite compiler IR admits trustedReveal as server.data.declassify only for exact direct named imports of trustedReveal and DeclassifyPolicy from @kovojs/core/security and the inline exact spelling DeclassifyPolicy.forTrustedReveal({ ownerScope: <closed literal> }). The released expression and every finite enclosing enabling condition MUST both have integrity strictly above request input. If either is request-derived, foreign executable, unresolved, or otherwise unknown, the operation MUST fail closed with KV449; an attacker-chosen condition may not select release of an otherwise constant secret. This is a robustness judgment over the existing normalized provenance relation, not a claim to interpret general JavaScript. Independently, declassification is an L1 capability: DeclassifyPolicy, revealSecret, revealUntrusted, and trustedReveal are request-closed public exports. A module reachable from any untrusted-data root, including through a transitive helper or re-export, MUST fail capability closure with KV448 if it imports the constructor or a reveal door. Closing policy construction also closes .reveal(policy) use without prohibiting creation of a poison box. A module with no such root does not gain a trusted root merely by importing the door.

This layer deliberately does not claim general JavaScript interpretation or same-realm isolation. The emitted operation lists are immutable, inspectable audit evidence consumed by component graphs and kovo explain; they are not an opcode sandbox and do not replace the actual C9 sink checks. For an app-scoped declaration, the factory root includes the receiver proof from §6.2.1. The compiler follows ordinary immutable local import/re-export aliases back to the exact defineKovo() result and binds the declaration to that contract's one assemble() closure before authored evaluation. A destructured factory, computed property, wrapper result, structural copy, cast, mutable or ambiguous alias, duplicate package identity, or declaration omitted from assembly is an unresolved root. The facade cannot make a same-named callback or structurally similar object trusted; successful receiver proof only enrolls the existing finite root analysis below.

Every supported factory root MUST resolve from an inline definition object to either an inline function or one exact immutable same-file function. Definition spreads/computed root keys, missing roots, imported/aliased/reassigned roots, and dynamic definition carriers are KV449. This includes query({ load }): query roots appear in the emitted manifest even when the loader is effect-free, and a directly reached managed DB write from a query remains KV449. Value-flow beyond the closed alias/receiver and explicit local-call-edge rules above belongs only to the normalized abstract interpreter defined next; the edge preserves that obligation rather than guessing its downstream verdict.

Normalized helper provenance (normative, narrow abstract interpreter). The compiler MUST discharge every server.helper.call over kovo-security-semantic-graph/v3, a normalized graph whose nodes are enrolled handler roots, exact same-file callables, finite operations, and explicit closed verdicts. This is not a JavaScript evaluator, SSA optimizer, or type-inference engine. Its complete value lattice is: plain local data; request/context authority; managed database, structured-header, storage, response-constructor, response-outcome, principal-scope, and exact module-constant derived() dataset authority; one exact operation:<securityOperationKind> terminal; non-authority governed-data carried by managed database and derived-dataset reads; and absorbing unknown authority. governed-data survives reviewed calls, static member projection, containers, aliases, destructuring, binary/conditional joins, and same-file helper arguments so a persistent non-engine sink cannot erase owner/governed provenance merely by reshaping a value. The scanner is the only raw-syntax boundary; validation, emission, graph, and explain consumers decide from these typed facts (SPEC §5.2 rule 10).

Version 3 is unconditional; consumers MUST reject versions 1 and 2 rather than enter a compatibility posture. Every semantic root carries an exact binding to its full root identity, factory family, callback name, factory-call [start,end) span, and callback-callable [start,end) span in the same UTF-16 source snapshot. Every proved terminal additionally carries the SHA-256 hash of the exact UTF-16LE source slice selected by its JavaScript code-unit [start,end) span. Every helper transfer carries its exact invocation and ordered argument spans, callable name and declaration span, complete ordered root-to-helper transfer prefix, authority-input vector, terminal-operation inventory, and verdict. A consumer may admit a helper summary only when an exact invocation fact has the same callable identity and span, authority-input vector, terminal inventory, and verdict; the invocation span and root binding must also match the authored call and root being classified. The terminal inventory MUST equal the unique finite sink kinds reached by proved traces under that complete transfer prefix. A downstream consumer MUST independently reconstruct the exact root, argument authority, and every terminal-operation family on which its own admission decision depends; self-consistent carrier fields are not authentication. A consumer-specific gate MAY project the complete graph onto only the terminal families it owns, but unverified families confer no authority in that gate. A missing or contradictory relevant fact, a root/trace identity mismatch, an omitted or extra relevant terminal kind, a closed trace, or a closed sibling summary/invocation for the same callable span closes that consumer proof. Source bytes, callable identity, and all-path closure remain mandatory; these carrier checks do not turn semantic facts into app-authored authority.

Transfer semantics are finite. An exact immutable alias preserves its lattice value. Static object destructuring applies the reviewed member transition one property at a time. Results of finite operations are plain data, except the explicit principal-scope acquisition that returns a scoped context and managed database/derived-dataset reads that return governed-data. Passing authority or governed data to an exact immutable same-file helper maps each positional argument to that helper's parameter binding and computes a context-sensitive summary keyed by the complete authority-input vector. Summaries are computed callee-first and merged back into the caller; nested helper operations retain the source root and ordered transfer path. Returning or throwing authority, placing it in an opaque container, mutating an authority alias/member, using a mutable or ambiguous join, capturing it in an unsummarized nested callable, recovering it through arguments or a rest/spread mapping, invoking an operation through call/apply/bind, or using an imported, computed, aliased, reassigned, unresolved, or otherwise foreign callable is unsupported and MUST remain KV449. A query root's no-managed-write posture propagates unchanged through every summary.

Closed analyzable-fragment prohibitions (generated)#

This table is generated from security/analyzable-fragment.json. The classification describes the general prohibition; each fixture is a minimal compiler-verdict witness, not an impossibility proof.

Prohibition Classification KV449 closed reason Witness
Returning authority DELIBERATE unsupported-authority-use fixture
Throwing authority DELIBERATE unsupported-authority-use fixture
Opaque authority container FUNDAMENTAL opaque-transfer fixture
Mutating an authority alias or member FUNDAMENTAL unsupported-authority-use fixture
Mutable or ambiguous join FUNDAMENTAL opaque-transfer fixture
Unsummarized nested callable DELIBERATE opaque-transfer fixture
arguments, rest, or spread recovery DELIBERATE opaque-transfer fixture
call, apply, or bind invocation DELIBERATE opaque-transfer fixture
Imported, computed, aliased, reassigned, unresolved, or foreign callable FUNDAMENTAL opaque-transfer fixture

The ledger also records the current real-root budget-binding measurement. Its reviewed hand argument states the compositionality claim, adequacy claim, and limits.

The resource contract is deterministic and has no app-authored widening knob: at most 16 helper edges on one path, 50,000 interpreted AST nodes, 4,096 finite operations, and 256 helper summaries per root. A repeated active summary key is a recursion cycle, not a fixpoint guess. The only closed reasons are helper-cycle, opaque-transfer, unknown-operation, unsupported-authority-use, and the four named budget-* reasons. A cycle, unsupported construct, or exhausted call-depth/node/operation/summary budget MUST produce KV449 before output, with root, ordered transfers, sink, and verdict=closed:<reason> in the diagnostic. Successful generated server manifests and kovo explain expose the same root-to-transfer-to-sink trace and bottom-up summaries. These artifacts are audit evidence; they neither grant runtime authority nor replace the C9 sink owner.

Authorization-gates-DATA scope (normative honesty boundary). The normalized substrate may contribute to OPP-28 only when the data analyzer has an exact private principal symbol, an exact owner-column identity, and an equality-equivalent predicate (eq or singleton membership) whose accepted guard principal is the same symbol. That structurally proven subset may be reported as owner-scoped. Arbitrary JavaScript guard correctness, semantic equivalence between general predicates, multi-principal policy composition, database policy correctness, and whether an opaque helper actually enforces the intended business rule are not proved by this interpreter. They remain an explicit database-engine/runtime-policy and audit responsibility; unknown correspondence stays scope: unknown and MUST NOT be promoted by naming, types, or a permissive helper summary.

Analyzer-summary proof boundary (normative). kovoAnalyzerSummary is a candidate marker, not an app-authored provenance assertion. A private-scope marker contributes to any invalidation, owner-scope, accepted-guard, write, or diagnostic verdict only when the analyzer independently resolves a bare helper identifier to exactly one declaration in the same source file. The only accepted declaration forms are a direct function declaration or a const binding initialized directly by an arrow or function expression. Object-literal properties and methods, class members, property-access targets, imports, alias bindings presented as the marker target, destructured bindings, let/var bindings, reassigned bindings, and otherwise opaque or multiply declared callables remain unknown. No alias or container may stand between the marker and the proven declaration.

The direct helper MUST have one non-default, non-rest identifier parameter and no generator body. Its body MUST be either an expression-bodied arrow or a block containing exactly one return, and that expression MUST be a literal property chain rooted in the parameter. The first private-scope segment MUST be guard, session, or tenant (for example, parameter.guard.userId or parameter.request.session.id), and the declared kind and path MUST exactly equal that segment and the literal suffix after it. A provenance-bearing invocation MUST call that exact helper identifier or one direct same-file immutable const alias = provenHelper identifier and pass the exact framework request/context parameter (req, request, ctx, or context), proven by its callback/receiver position rather than its spelling, as the sole argument. That one-hop alias may preserve an already-proven identity but cannot be the marker target or widen the proof. Property or element access, destructured/container aliases, alias chains, and imported, opaque, or mutable aliases remain unknown. Multi-statement/general bodies, computed returns, mismatched principals, unresolved symbols, and calls with client input or opaque/container arguments also remain unknown. No server summary kind exists: general server provenance and KV438 cannot be discharged by an app declaration. These restrictions apply uniformly to every consumer of session provenance; a looser invalidation or explain path MUST NOT become a security side door.

Build-preset capability boundary (normative). KovoPreset is an opaque, framework-owned selection token, not a public structural deployment descriptor. node(), vercel(), and cloudflare() mint exact frozen tokens registered by identity in a framework-private WeakMap; kovo build resolves only those exact objects through the matching internal module instance. Copying or spreading a token, reconstructing its symbol-shaped type, or supplying an object with name/emit/inspect/capabilities fields MUST fail closed. Emission callbacks, inspection callbacks, and capability records remain internal build authority and MUST NOT be reflectively reachable from the public token. The module-private unique symbol type is only author-time ergonomics; exact runtime registry membership and config preflight own enforcement.

Classifier-intrinsic lockdown (normative). Rule 6 unconditionally pins the finite global bindings and direct namespace members that the request classifier recognizes, and guards their language/Web intrinsic prototypes before caller-controlled evaluation; the classifier corpus gate MUST keep that runtime inventory exact. A custom runner MUST import @kovojs/server/runtime-bootstrap as its literal first import, while generated runners establish the same order themselves. The public dispatch refusal detects an omitted bootstrap, but cannot authenticate earlier evaluation in the same mutable realm: importing authored/package code first and bootstrapping later is unsupported privileged-host misuse, not a repair path. This unconditional intrinsic lockdown does not freeze or claim provenance for egress/transport instrumentation prototypes such as undici, node:http, net.Socket, Datadog, OTel, or nock hooks. The separate outbound-egress prototype-freezing option below therefore remains off by default.

Operator-environment trust root (normative). Bootstrap MUST pin operator environment names and values before authored evaluation, and later security lookup MUST preserve the host's name semantics. In particular, Windows names are case-insensitive: the pinned authority MUST resolve every case spelling equivalently and fail closed if an injected source contains case-fold-colliding names, while app env-schema snapshots retain the operator's original key spellings.

Application config-secret door (normative). defineKovo({ env: s.object(...) }) is the sole public operator-environment projection. The runtime MUST admit only a genuine framework s.object schema, parse the bootstrap-pinned source once, retain only declared own fields, freeze that parsed record, and expose it only through the precisely inferred read-only declaration/request context. The opaque public KovoApp token has no structural env field. The raw operator snapshot and undeclared keys remain framework-internal. A declared env schema failure refuses boot in every mode: development may warn for a weak framework signing secret, but it cannot return a typed app.env whose value never validated. kovo build is not runtime boot and MUST NOT require or receive the production values for this declared projection. While it evaluates the app only to derive the closed build graph, every declared field MUST instead be a framework-owned, non-coercible unavailable sentinel: the schema shape is still provenance-checked, but no operator value is read, parsed, rendered, serialized, cloned, or copied into an artifact. Observing or exporting a sentinel MUST fail closed. The emitted server then evaluates the app outside that build posture and MUST parse the real bootstrap-pinned source before it can serve a request. s.secret(schema) MUST return a runtime SecretValue, not a type-only Secret<T> cast, so interpolation, template/string coercion, JSON and wire encoding, structured cloning, SSR output, and artifact capture encounter the existing fail-closed confidentiality doors. The box's module-private runtime registration owns this invariant; its type is author-time ergonomics only. A dependency credential should be revealed exactly once inside its boot-time credential factory through revealSecret(value, DeclassifyPolicy.forRevealSecret({ purpose: 'credential-use', ownerScope: 'application' })); the static call site remains an audit-grade row in the existing kovo explain revealed fact graph (and therefore also in its folded capabilities view), while the bounded runtime reveal collector is observational evidence, not a complete process-lifetime proof. The audit collector MUST recognize direct @kovojs/core named imports, bind each reveal to its exact policy tuple, and accept those literal policy fields in any order. A call with dynamic or otherwise unrecordable policy MUST emit error-severity KV426 instead of disappearing from the audit. When the typed query analyzer and runtime audit analyzer observe the same reveal, their facts may be deduplicated only by the exact call span/AST identity; a shared file:line label is insufficient. These audit rules do not relax the request security classifier's stricter exact matcher. This pattern does not claim arbitrary JavaScript string comparison is constant-time; use SecretValue.equals only for fixed token/verifier comparisons whose operands fit that contract.

Authentication request-origin binding (normative). A framework-owned Better Auth binding MUST normalize and pin the configured baseURL origin when the binding is constructed. Before parsing a request cookie, delegating to a Better Auth handler, consuming a credential, revoking a session, or reading or writing auth storage, it MUST require the request's exact normalized scheme, hostname, and effective port to equal that pinned origin. A mismatch fails before cookie parsing, rate-limit storage, credential verification, session lookup/mint/revocation, or response-cookie forwarding; it does not fall back to the request Host, an untrusted forwarded header, or a less-secure cookie name. URL paths do not relax this comparison. Case, Unicode hostname spelling, IPv6 spelling, and default ports are compared only after URL normalization, while non-default ports remain exact. Kovo's fixed SQLite/Postgres binding constructor MUST validate the base URL, construct the complete host-only cookie posture, and privately register the exact Better Auth object with that canonical origin before creating session, credential, or mount operations. Those private consumers admit only an exact registry member; structural compatibility, a dependency $context, and an arbitrary safe-looking cookie configuration are not construction proof. Caller-created betterAuth() objects are unsupported and MUST fail before auth handler/API, cookie parsing/minting, or database access. The ordinary HTTPS __Secure- default is insufficient: a sibling subdomain can plant that name with Domain and browser duplicate-cookie ordering can place the attacker value first. On HTTPS, fixed bindings therefore construct __Host- cookies with Secure, HttpOnly, Path=/, and no Domain, and keep cookie-cache state inside the same fixed posture or disabled. Plaintext is admitted only for an exact loopback origin (localhost, IPv4 127/8, or [::1]) in non-production local development; those bare development cookies have no sibling-domain security guarantee. Production and every non-loopback origin require HTTPS unconditionally.

Better Auth credential-consumer non-egress door (normative). Every supported fixed-binding consumer of Better Auth signing material, submitted or stored credentials, request/session cookies, password hash/verify values, session records, and dependency results MUST be enumerated in one complete package-private contract census and invoked through the same runtime gate. Each contract binds a stable consumer id to the M2 secret paths it may receive and the only result shape that may reach its reviewed next sink. The gate MUST admit only an exact runtime-registered consumer token, validate the result, seal it, and permit exactly one consume by that same token; structural forgeries, unknown consumers, cross-consumer swaps, replayed results, invalid result shapes, and dependency-thrown secret-bearing errors fail closed. Only an exact dependency 400/401/403 verdict from a credential operation may become Kovo's opaque invalid-credential outcome, and that verdict is itself one-shot. A module-private unique symbol or validated secret brand is author-time ergonomics only: the runtime registry, complete path/consumer and source-use censuses, and hostile-value/sink tests own enforcement. The static census proves coverage but is not itself runtime authority (SPEC §10.3 C9-C10).

Every captured external Better Auth credential source—including handler/API callables, constructors, password functions, rate-limit construction, and cookie extraction—MUST be invoked inside that runtime gate after exact consumer/source validation. Passing an owner-supplied callback through a generic gate while the callback itself retains raw dependency call authority is not the sole-door construction; generic callbacks are limited to package-owned transforms such as sanitized session reconstruction. The package source-use census MUST resolve aliases, destructuring, literal computed access, .call/.apply, and local imports/re-exports by symbol/value flow; printed callee spelling or a fixed method-name regex is not coverage evidence.

The fixed credential rate limiter MUST domain-separate each admitted raw Better Auth identity by HMACing the canonical ScopedKey frame under the finite better-auth-rate-limit system posture, not a bespoke delimiter prefix. Its bounded 16-bit bucket is itself a runtime-witnessed key under that same posture, and the SQLite/Postgres consumer MUST authenticate the witness, exact posture, and four-lowercase-hex app-key before persisting the complete canonical frame. Raw IP/path input, bare strings, structural forgeries, public/principal keys, and other system postures fail before the database statement. HMAC collisions deliberately share the same bucket and aggregate attempts, so the fixed 65,536-bucket bound fails closed rather than granting additional credential guesses.

Better Auth redirect mount response boundary (normative). The opaque Better Auth mount is a redirect-protocol adapter, not a public proxy for the dependency router. After the exact request origin check, Kovo MUST admit only status 301, 302, 303, 307, or 308 with exactly one nonempty Location that resolves to the pinned origin over HTTP(S). Missing, duplicate/ambiguous, protocol-relative, credential-bearing, non-HTTP(S), or off-origin locations fail inside the opaque boundary. Kovo MUST canonicalize an admitted location to its same-origin path, query, and fragment, then reconstruct an empty response containing only that Location and reviewed Set-Cookie values plus Kovo's own Cache-Control: no-store floor. It MUST NOT forward the dependency response body, status text, content headers, or arbitrary headers. In particular, dependency routes such as get-session and error pages MUST fail closed rather than exposing session JSON, bearer material, or HTML through the mount (SPEC §6.6/§9.1).

Better Auth lifecycle ownership and non-claims (normative). The fixed SQLite/Postgres bindings own exactly four Kovo-owned identity transitions: the CSRF-protected signIn mutation, the CSRF-protected signOut mutation, development-only seed signUp (which provisions a credential with autoSignIn: false), and the feature-conditional CSRF-protected requestPasswordReset mutation. The fourth transition exists only when the binding receives a constructor-minted, purpose-closed password-reset mail door, an explicit public/pre-auth access decision, and one canonical same-origin reset path. For an accepted provider request, the mutation MUST expose one generic accepted result for account-present and account-absent worlds, MUST discard the provider response and cookies, and MUST invoke the registered sender exactly once with only { to, resetUrl } in either world. Rate-limit or provider failure MUST stop before mail dispatch. The absent world uses a same-shape decoy URL minted before provider work. The real token MUST reach only that mail sender inside the validated same-origin URL; the standalone token, provider user record, request, and other dependency values MUST NOT cross the door. Mail-provider delivery and its attacker-visible behavior are deployment egress outside Kovo's HTTP-equivalence claim. No other direct Better Auth lifecycle API is exposed. The opaque provider mount accepts only GET, so provider lifecycle operations requiring an unsafe method are structurally unreachable through that mount. This is not a claim that every dependency lifecycle route is unreachable: the redirect/callback mount can reach dependency-defined GET handlers that change identity state, including provider or token callback flows. That reachable GET callback lifecycle is delegated and unsupported by Kovo guarantees; only the origin, non-egress, redirect-response, and cookie-posture boundaries above apply. Session expiry, rolling update, freshness, cookie-cache posture, and sign-in rotation behavior are inherited from the exact-pinned provider and MUST remain characterized by the provider-pin conformance test. Reset-token minting, expiry, single use, and reset completion remain exact-pinned Better Auth protocol behavior rather than a Kovo guarantee. kovo explain auth-lifecycle MUST print the inherited values, the four owned transitions (including the feature condition), the structurally unreachable unsafe-method class, and the reachable delegated non-claim.

Outbound egress: the positive framework capability (normative). Untrusted-data-reachable framework code MUST have one supported positive HTTP network door: the exact framework-owned ctx.fetch supplied to durable/scheduled tasks, verified webhooks, and any supported agent-tool callback. A runner or app MUST NOT replace that function. Raw fetch, node:http, node:https, net, datagram, proxy-agent, database-driver, worker, process, native-socket, or dynamically loaded network authority remains unavailable from that graph unless a separately reviewed framework door explicitly owns it. egress.allowDestinations MUST be a dense list of exact HTTP(S) origins. Boot MUST reject an empty, malformed, credential-bearing, path/query/fragment-bearing, non-HTTP(S), or non-string entry instead of warning and widening or silently narrowing posture. Boot canonicalizes scheme, URL-normalized hostname (including Unicode, legacy IPv4, IPv6, case, and a DNS trailing dot), and effective port into one origin identity. The initial request and every redirect or pooled-request origin MUST match that canonical set before DNS, proxy selection, pool reuse, or dial. Every admitted hostname request/hop MUST resolve all candidate addresses and classify all of them; any closed answer closes the whole request. Every new TCP dial MUST classify the exact resolver result that Node may select and pin that immutable result into the dial, so DNS rotation is admitted only when the origin remains declared and every new answer remains safe. A declared private origin additionally needs the ambient allowInternal posture below. A framework-created database socket is a separate, module-private exact-endpoint capability: it may follow DNS rotation for its registered Postgres host/port without opening that endpoint to unrelated sockets. Arbitrary application proxy/dispatcher configuration is unsupported and MUST fail boot or be stripped from ctx.fetch; an operator-controlled transparent proxy remains deployment authority outside this application-level origin proof and does not turn the private-network floor into a sandbox. Future agent-tool APIs MUST supply this same contextual door before they are supported. Same-process deliberately malicious code or intrinsic poisoning is outside this construction proof, as stated by the capability-closure boundary above.

Outbound egress: the private-network deny floor (normative, runtime defense-in-depth — NOT a proof). The threat is the SSRF network position: a reflected or forged inbound request coaxes the server into making an outbound request to an address it must never reach — cloud instance-metadata (169.254.169.254 and the AWS ECS/EKS variants 169.254.170.2/169.254.170.23, the AWS IMDSv6 fd00:ec2::254, Azure's IMDS plus its IDENTITY_ENDPOINT loopback, GCP's metadata.google.internal), localhost sidecars, or internal-only services on RFC1918 / link-local / unique-local / CGNAT ranges. The payoff is managed-identity credential theft off the metadata endpoint, or an internal-service pivot. Kovo installs the floor when app.assemble() closes the defineKovo contract and accepts explicit operator config through defineKovo({ egress: { allowInternal: ['otel:4318', '10.0.5.2:6379'] } }) with the following normative behavior. All public/external egress is UNRESTRICTED at this ambient process floor — the positive framework capability above is the separate control that closes public destinations reached through ctx.fetch. Private / loopback / link-local / unique-local / CGNAT / IANA-special destinations are DENIED by default in production and whenever an explicit egress object is supplied, reachable only when the exact host:port is in the operator's narrow allowInternal allowlist (broad CIDR entries are flagged and warned). In development, an omitted egress option still installs both enforcement layers and still denies cloud metadata, but permits non-metadata private-network destinations so localhost DB/Redis/OTel/Ollama sidecars do not brick ordinary local boot; pass egress: { allowInternal: [] } in development to exercise production empty-allowlist semantics. A blocked connection throws a typed 502-class EgressBlockedError naming the destination and the remediation. The cloud instance-metadata endpoint is DENIED by default and is NEVER reachable via allowInternal — it is reachable only inside a module-private metadataAllowed AsyncLocalStorage frame entered ONLY by the per-cloud credential factories awsCredential() / gcpCredential() / azureCredential(), which wrap the cloud SDK's credential provider so a token refresh re-enters the frame. There is deliberately no generic withMetadataAccess helper. A reflected SSRF never calls a factory, so it never enters the frame, so metadata stays denied at the very same IP — provenance-as-current-frame, unforgeable by SSRF (it survives the await/timer boundaries that destroy stack frames) yet still runtime-DiD, not a proof. DNS64/NAT64 topology is explicit operator authority. Kovo always decodes RFC 6052's well-known 64:ff9b::/96 carrier. A deployment using any Network-Specific Prefix MUST list every active translator prefix in egress.nat64Prefixes; automatic RFC 7050 discovery or A/AAAA correlation is not accepted as the policy root. Only /32, /40, /48, /56, /64, and /96 are valid. Boot MUST reject malformed CIDRs, set host bits, a non-zero /96 u octet, duplicate/overlapping configured prefixes, and any configured prefix that overlaps the implicit well-known decoder. The framework snapshots and canonicalizes the resulting prefix set as process-global posture. At the sink, a matching configured prefix is decoded using RFC 6052 Table 1 before the context-free IPv6 registry verdict: the u octet MUST be zero for layouts shorter than /96, suffix bits are ignored, and the embedded IPv4 destination is classified normally. This explicit topology may expose public IPv4 through RFC 8215's local-use 64:ff9b:1::/48, but embedded metadata remains metadata and can never be reopened by allowInternal. The decision rule runs per request and per redirect hop, at BOTH enforcement layers: resolve the host → normalize (IPv4-mapped ::ffff:, decimal/octal/hex, well-known NAT64, and configured Network-Specific Pref64) → pin the exact validated resolver result from which Node may select a dial address → public IP allow; metadata IP allow iff the metadataAllowed frame is active; other non-public IP allow iff the development omitted-config posture permits it or host:port ∈ allowInternal; anything not confidently classified as public fails closed. Enforcement is dual-layer because a single layer fails open: (a) a custom undici dispatcher at the per-request dispatch() level — pooled-socket reuse skips the per-connection hook, so a connect-only check would pass the second request to an origin; and (b) the node:http/node:https + net.Socket.prototype.connect layer — AWS IMDS via @smithy uses raw node:http and bypasses undici entirely — which also injects a pinning lookup so a TOCTOU DNS-rebind cannot swap a public answer for a private one between check and dial. Bootstrap installs both layers at the assembly chokepoint and runs a loud startup self-probe that warns unmissably when the floor is not installed; production refuses boot when the floor is missing, partial, tampered, or disabled without an audited non-empty opt-out justification. Because monkeypatches do not cross Worker/child_process boundaries, every worker/child bootstrap that serves requests MUST re-install (the self-probe is the safety net). Prototype-freezing is opt-in / off by default (it breaks Datadog/OTel/nock). This control is labeled everywhere as a fail-closed runtime defense-in-depth floor, never a by-construction proof. Residual fail-open holes (enumerated, by design): same-process app code can re-patch net.Socket.prototype.connect or call setGlobalDispatcher after the floor; Worker/child_process/native-socket paths the JS layer never sees; arbitrary raw per-call dispatchers/proxy agents outside the supported capability graph; and provider-shape drift in a future undici/node internal. The floor is redundant on Lambda/PaaS/Workload-Identity-Federation where IMDSv2 / hop-limits already close the metadata path; it earns its keep on long-lived managed-identity VMs and against the internal-service pivot.

Scoped keys for non-database state (normative, fail-closed runtime authority). Every app-addressable key that reaches a non-database stateful sink MUST be a runtime-opaque ScopedKey, not a string or structural brand. Its canonical physical identity is the exact length-prefixed frame (kovo-scoped-key-v1, posture, authority, app-key). Length framing is over JavaScript code units and MUST distinguish delimiter, slash, NUL, and ill-formed-surrogate placements without concatenation ambiguity. posture is exactly one of principal, public, or system: principal authority comes only from the framework-installed request session snapshot (scopedKey(request, key)) or an audited task actAs(id).stateKey(key) scope; public authority comes only from the named publicScopedKey(key) capability; system authority comes only from a finite framework-owned posture registered in the C9 census. App-authored principal ids and free-form system reason strings are not key authority. Authority components and every public/principal app-key are non-empty strings of at most 1,024 code units. The finite mutation-replay system posture alone may carry the already- bounded canonical (scope, idem) subframe as its app-key beyond 1,024 code units; its complete outer ScopedKey frame MUST remain at most 4,096 code units. No app-facing constructor or other system posture inherits that composite-key exception.

The public TypeScript type is ergonomics, not the proof. A module-private runtime witness owns the frame and exact posture facts; storage, signed-URL, stored-file-response, durable-task queue, mutation-replay, bounded rate-limit, and derived-dataset doors MUST authenticate that witness before reading any fields or deriving a namespace. Bare strings, casts, object literals, copied properties, proxies, malformed/non-canonical persisted frames, and unregistered system postures fail KV450. A validated key remains opaque to app code; framework internals may restore a persisted frame only through the same canonical parser and finite-posture check. Storage object results may expose the normalized app-key string as descriptive metadata, but that string carries no authority back into a sink.

The compiler MUST also fail KV450 at every statically visible storage key, signed-URL key, stored-file-response key, and durable-schedule coalescing key unless every finite branch derives the value through the exact scopedKey, publicScopedKey, task stateKey, or task systemStateKey constructor. Casts, structural lookalikes, runtime-selected options, computed properties, and option spreads do not establish provenance. This compile gate is an author-facing early closure only; the module-private runtime witness remains the enforcing authority at the sink.

Derived vector datasets inherit authorization (normative). The only supported transition from managed owner-scoped/governed database data into a persistent non-engine vector/RAG artifact is the exact module-constant derived(adapter, { key: <non-empty static string>, kind: 'vector' }) door. Every query(request, query) and upsert(request, records) operation MUST receive the exact framework request carrier. The runtime re-runs scopedKey(request, 'derived/vector/' + key) for every operation and constructs the physical namespace as kovo-derived-vector-v1/<sha256(complete-canonical-ScopedKey-frame)>; neither an app call site nor a query/write payload can provide or replace that namespace. Query results and upsert arrays are dense, bounded, immutable snapshots at the adapter boundary, and adapter callables are pinned at construction. Equal logical keys under different principals therefore produce different physical artifact identities, while reads under the same principal reconstruct the identity used by writes.

The compiler tracks managed DB and derived reads as governed-data and emits KV452 when that provenance reaches storage put, framework egress, or a durable-task payload outside the exact derived() door; transforms, aliases, containers, conditionals, and exact same-file helpers do not erase the label. A derived read/write with a missing, forged, or non-request first argument is also KV452. A same-spelled local, imported lookalike, alias, dynamic options object, spread, surplus key, unsupported kind, or request-time constructor does not acquire derived-dataset authority and remains inside the ordinary KV449 fail-closed rules.

The adapter is a deployment boundary, not an authorization proof: Kovo guarantees that it supplies only the reconstructed opaque namespace, but the selected adapter/service MUST faithfully isolate that namespace. An adapter that ignores, truncates, aliases, or externally broadens the namespace invalidates the derived-artifact isolation claim and is a retained deployment obligation. Deliberate same-process code that captures the adapter input and performs another raw write remains outside the app-level proof per the capability-closure boundary above.

Memory storage keys by the complete frame. Filesystem storage hashes the complete frame with SHA-256 for its bounded physical slot and atomically records the exact frame in the sidecar, refusing a digest collision. S3-compatible storage uses a framework-owned kovo-storage-v1/<sha256(frame)> namespace. Consequently equal app keys in different principal/public/system postures never address the same physical object. This is an unconditional technical-preview contract: no legacy string-key fallback or compatibility namespace exists.

Capability URLs for storage downloads (normative, by-construction at the verify sink). A download URL for a stored object is signed, short-lived, and scope-bound so the object is un-dereferenceable without a valid token. signCapability mints a token over the canonical, length-prefixed tuple (version, signing-key-id, method, scoped-key-frame, expiry, scope, one-time, nonce) (canonicalize-before-sign, so no field-confusion collision or unsigned replay/key-selection field) using the framework signing secret; the framework-owned download route MUST restore the runtime-witnessed ScopedKey from the request path, then verifyCapability — re-canonicalizing the exact frame/method/scope it derives from the request and comparing the HMAC in constant time — before any storage read. Because the route supplies the expected claims rather than trusting the token's, a token for object a cannot authorize reading object b, or the same app key in a different owner posture, even with a valid signature. Verification is fail-closed and ordered (bounded frame parse → token parse → constant-time signature → expiry → claim match → one-time burn); rejection reasons are never leaked to the client. This is by-construction at the verify sink (an object cannot be read without a verifying token), with one honestly-labeled limit: the URL is a bearer credential whose leakage via Referer/logs/CDN is mitigated (short expiry by default, narrow scope, and an optional one-time token posture) but not proven. The framework storage download route that hosts the sink is shipped: createStorageDownloadEndpoint builds a prefix-mounted GET/HEAD endpoint() whose handler re-derives the expected scoped frame/method/scope from the request and runs verifyCapability before any storage read (a generic, reason-free 404 on any failure), and ctx.signUrl({ key, method?, scope?, expiresIn?, oneTime? }) accepts only a witnessed ScopedKey and mints a URL pointing at that route (canonicalize-before-sign; short-expiry default). Capability signing and verification MUST reject before unbounded decode, parsing, canonicalization, or audit retention: complete scoped-key frames are limited to 4,096 code units, scopes and audiences to 1,024 each, decoded payloads to 12,000 bytes, complete wire tokens to 16,384 code units, and TTL to at most one hour. Production MUST refuse a missing, custom, or volatile download replay store and accept only the opaque durable store exposed by createPostgresAppRuntimeDb().capabilityReplayStore, even when the app currently mints only ordinary tokens; this keeps one-time posture from becoming a deployment-time footgun and makes replica/restart truth mandatory before the sink can serve. Production signing, verification, signer construction, and download-route construction MUST also refuse caller-injected clocks, so expiry is measured only against the framework-owned wall clock (and durable one-time insertion is additionally guarded by the database clock). Every mint records the normalized app key plus exact key posture in a capability fact surfaced by kovo explain capabilities.

Capability token v4 supersedes the base tuple above by signing (version, signing-key-id, method, scoped-key-frame, expiry, scope, principal-epoch, one-time, nonce), with the epoch field empty only for an unscoped token. A principal-scoped mint requires an active authoritative epoch; verification requires the same current epoch after signature, expiry, and request-derived claim matching but before a one-time replay burn or storage read. v3 and all older versions are intentionally rejected rather than accepted through a compatibility path.



Interaction Ladder & Navigation (SPEC §7-§8)

This file is incorporated by reference from ../SPEC.md and is normative for Kovo framework behavior. The root spec remains the entry point and cross-reference index; this module owns the detailed contract below.

7. The Interaction Ladder#

Interactions must use the lowest layer that suffices. The compiler enforces L0 substitutions; lints nudge the rest. Navigation between places is always a real URL and server route (§8); enhanced navigation may intercept eligible clicks only as a progressive enhancement over that same full-document GET. The ladder governs interaction within a place.

Layer Mechanism Example JS shipped
L0 Platform behaviors: invoker commands, Popover API, <details>, <dialog>, :has(), scroll-driven animations Open cart drawer 0
L1 Pure client islands: local state + the update plan (bindings/derives/stamps, §4.8); loaded on interaction or a declared trigger (§4.7) Price-range filter UI, tabs, carousel handler module on first touch
L2 Mutations: real forms + enhanced fetch → fragment/query patch Add to cart loader (already present) + form module
L3 Optimistic: compiler-derived or declared transforms over query values Instant badge tick transform module
L4 Live: SSE pushing the same fragment/query vocabulary; BroadcastChannel tab sync + refetch-on-focus cover common lower-cost cases (§9.3) Order status, presence <kovo-live> subscriber

Cross-island coordination, in order of preference: (1) the URL — filter writes ?max=500, or is a GET form whose fragment response is the grid, both typed against the route's search schema (§6.4); (2) scoped client state — only when the state is local UI intent rather than server/query facts, and still lint-gated with a required justification comment. Typed fire-and-forget cross-island events (emit(...)/on(...)) are not a shipped v1 authoring surface; the repo's internal event-bus experiments do not create public runtime bindings or a registry contract.


8. MPA Spine & Navigation#

  • No client router. Each page has a complete server document; route handlers are server functions declared with route() (§6.4), which carries the path's literal type, param/search schemas, and per-route config. <Link>/href() are compile-time sugar lowering to plain <a href> — typed links whose native URL remains the canonical behavior.
  • Enhanced navigation is a progressive enhancement, not an app mode. The loader may intercept only eligible same-origin, unmodified, GET anchor navigations. It fetches the target document in the structured kovo-document-parts/v1 representation below, validates the envelope's app build token BEFORE constructing any DOM, builds the detached target document from the structured parts, validates compiler-derived segment metadata, updates or validates document-shell state, and morphs only compatible changed segments. The current document's layout chain is an optimization hint only; the target server document decides the route, guard outcome, layout chain, head, and body. A fetched response is eligible for in-document interpretation only when Content-Disposition is absent or one structurally valid, unambiguous inline value; attachment, extension dispositions, combined/duplicate field values, or malformed parameters preserve the browser's native download/navigation boundary through the normal full GET before any response-body read or apply. On non-parts content type (including a declined text/html answer), cross-origin URL, modified click, target/download/hash-only navigation, redirect/guard uncertainty, shell drift, build-token mismatch, envelope/construction failure, or any missing proof, the loader performs the normal full GET.
  • The document-part representation (normative). The enhanced-navigation Accept header is application/vnd.kovo.document-parts+json, text/html, and the negotiated variant is a JSON envelope encoding the exact canonical rendered document — the same full-document oracle, never a server-side diff and never an HTML string the client would have to re-parse. The envelope is {"protocol":"kovo-document-parts/v1","build":<exact §5.2.1 app build token>,"htmlAttrs":Attrs,"bodyAttrs":Attrs,"head":[Part…],"body":[Part…]} where a Part is a JSON string (an entity-decoded text node), ["!", text] (a comment node), [tag, Attrs, [Part…]] (an HTML-namespace element), or [tag, Attrs, [Part…], ns] (a foreign element; ns 1 = SVG, 2 = MathML), and Attrs is [[name] | [name, value], …] ([name] is a valueless attribute). Every 200 document response — parts and ordinary text/html alike — carries Vary: Accept and the same Kovo-Build transport token, and the envelope build MUST equal that transport token. The client validates build against the immutable page-load proof before any DOM is constructed, so a stale build can never be applied to a newer document (§14); the built document's kovo-build meta is then re-validated as the in-document proof.
    • No string→DOM sink. The client applier builds the detached target document exclusively through captured construction controls — createElement/createElementNS/createTextNode/createComment/setAttribute — and then morphs DOM-to-DOM. DOMParser, innerHTML, and every other HTML-string sink are absent from the navigation and lifecycle document paths, so the document CSP's require-trusted-types-for 'script' holds without any navigation parser policy.
    • Inert by construction. A parts document carries no executable content: the only admissible <script> shapes are the framework's JSON data script (type="application/json") and the Speculation Rules data block (type="speculationrules"), never with src; native event-handler (on*) attributes, srcdoc, is, and <base> never encode. The server refuses to encode a document that violates this floor and the client independently aborts construction on the same conditions. The negotiated variant also omits the SPEC §4.4 inline bootstrap — the requesting realm already installed it — and the applier performs no script replay.
    • Fail-closed encoding. The server encoder tokenizes the exact canonical document bytes and MUST refuse — answering the canonical text/html document instead — whenever it cannot prove the built tree matches what the browser parser would produce for those bytes (parser-ambiguous markup, unknown character references, deferred/streaming documents, non-buffered outcomes). A refusal degrades enhanced navigation to a full load; it never degrades to a wrong or unsafe DOM.
  • Navigation partials are not a v1 protocol. Enhanced navigation uses the full target document as its oracle; the parts envelope is an encoding of that complete document, not a partial. Header-selected navigation fragments, target-chain hints, or route-partial responses remain a possible optimization only after no-JS/full-load versus enhanced-navigation render-equivalence is proven over the corpus; app authors cannot opt into or hand-author a navigation partial response.
  • Segment persistence is derived. Only unchanged compiler-stamped layout/region segments may keep DOM identity. Changed layouts, changed route leaves, changed parallel regions, active nav/search/auth/query-dependent chrome, inserted islands, removed islands, and route boundaries are morphed from the target document or applied through wholesale body replacement. Segment stamps only ever ADD preservation: a target document with no segment stamps preserves nothing and is applied wholesale inside the same realm. App TSX never authors navigation segment stamps or persistence policy. If a route's declared regions cannot be matched to compiler-derived segment metadata in the target full document, enhanced navigation performs the normal full GET rather than preserving uncertain DOM.
  • Navigation state emulates the browser. Enhanced navigation owns pushState/replaceState, popstate, scroll restoration, hash scrolling, focus movement, and route-change announcements only for navigations it successfully completes. A newer navigation aborts older fetch/morph work. Pending optimistic state is reconciled from the target server document or discarded by full GET; it must not silently survive into an incompatible document.
  • bfcache and bootstrap budget stay load-bearing. Enhanced navigation must not add unload handlers or global session heaps that block bfcache. Only code that ships in the inline bootstrap (§4.4, emitted only for documents carrying client surface) counts against inlineKovoLoaderGzipByteBudget; the deferred runtime implementation of navigation, morphing, mutation response application, and query hydration is intentionally outside that byte cap. Future increases to the bootstrap budget require comparable first-paint, security-boundary, or first-class-wire-capability evidence.
  • Speculation Rules are opt-in config, never auto-emitted: prefetch: 'conservative' | 'moderate' | false per route, declared on the route() object (§6.4), default off. Auto-prerender has real hazards — analytics firing inside prerendered pages, non-idempotent per-user renders, discarded-render server cost — so apps opt in route-by-route where renders are idempotent and cheap. prefetch: 'moderate' (which prerenders the route's page/meta/queries with the user's credentials on hover) is gated at compile time: it is KV419 (error) to set moderate on a route that is guarded, session-dependent, or whose page/meta/queries are not proven side-effect-free, unless a named justification is supplied at the route (mirroring KV229 export gating and KV320). conservative (no eager render) is unaffected. The feature is one <script type="speculationrules"> tag; the MPA is fast without it.
  • Cross-document View Transitions opt-in per element pair via view-transition-name props; the compiler stamps matching names across route templates.
  • bfcache hygiene is a framework guarantee: no unload handlers, keepalive: true on in-flight mutations at navigation, pending optimistic logs discarded on document teardown (stale-optimism-outliving-its-mutation is structurally impossible).

bfcache hygiene is conditional on cache posture. A bfcache restore is a history traversal that bypasses the loader and the network, so neither the route guard nor sessionProvider (§6.5) re-runs — a persisted authenticated document would otherwise reappear after logout, expiry, or revocation. Posture is computed from the same proof the export path uses (§9.5/KV229): a document is session-dependent when its route carries a guard or its render reads session-dependent query data, and anonymous otherwise. Guarded/session-dependent route documents MUST be emitted with Cache-Control: no-store so disk persistence and shared-cache reuse are forbidden. Independently — because some user agents still keep a no-store page in the in-memory bfcache — the loader MUST register a pageshow handler that, when event.persisted is true and the document was rendered under a guard or session dependence, revalidates by reloading from the server (a full GET, which re-runs sessionProvider and the guard) rather than presenting the restored DOM. Anonymous/exportable documents carry no such posture and remain fully bfcache-eligible; the pageshow handler is a no-op for them. The pageshow revalidation handler does not itself add an unload handler; if it ships in the inline bootstrap it counts against inlineKovoLoaderGzipByteBudget, and if it ships in the deferred runtime it does not. A session-dependent or fingerprinted document always carries client surface under §4.4, so the bootstrap that registers this handler is never omitted from a document that needs it.

  • Out-of-order streaming: App TSX uses the public <Defer> primitive from @kovojs/server to declare a server-rendered region whose fallback is ordinary JSX/text and whose real content streams later in the same response. The framework emits the owned <kovo-defer> placeholder, then streams the real fragment and morphs it in — the fragment protocol reused within first render. Deferred query JSON is guaranteed to arrive before or with its consumers. The internal defer() string helper is not an app-facing JSX child API; app-authored {defer(...)} is lint KV244.
  • Mutation response streaming: chat-style post-submit streams are not SSE and do not reuse <kovo-defer>. They are one enhanced mutation POST response whose chunks use the §9.1 mutation vocabulary plus the narrow <kovo-text> text-source primitive below.
  • Degradation contract: Safari/Firefox get normal navigations and normal forms. The MPA degrades to "a website" — where an SPA shows a blank screen on the same failure.


Wire Protocol (SPEC §9)

This file is incorporated by reference from ../SPEC.md and is normative for Kovo framework behavior. The root spec remains the entry point and cross-reference index; this module owns the detailed contract below.

9. Wire Protocol#

One vocabulary, transport-agnostic: document load, enhanced fetch, and SSE live updates all carry the same fragment/query chunks (§9.3). All payloads are human-readable (Constitution #4).

9.1 Enhanced mutation round-trip#

http
POST /_m/cart/add HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Kovo-Fragment: true
Kovo-Build: <app-build-token>
Kovo-Targets: cart-badge=cart; cart-drawer=cart; recommendations=%21product%21product%3Ap1
Kovo-Live-Targets: cart-badge#cart-badge@<attestation>:{}; recommendations#recommendations@<attestation>:{"productId":"p1"}
Kovo-Idem: 7f3a-…                          ← stamped hidden field; server replays duplicates

productId=p1&quantity=2&kovo-csrf=…
http
HTTP/1.1 200 OK
Content-Type: text/vnd.kovo.fragment+html; charset=utf-8
Kovo-Changes: [{"domain":"cart","keys":["cart"]},{"domain":"product","keys":["p1"]}]

<kovo-query name="cart">{"count": 3, "items": […]}</kovo-query>
<kovo-fragment target="recommendations">
  <!-- server-rendered HTML, produced by Recommendations.render(…) — the SAME
       render function full page loads use; partials cannot drift from pages -->
</kovo-fragment>

Framework wire-input registry (normative). Every framework-owned header, cookie, or URL-search input belongs to one core-owned typed registry that declares its carrier, canonical name, and finite grammar. Kovo's browser encoder and server decoder for a structured wire value MUST be derived from the same exact codec; independent handwritten implementations of the grammar are not conforming. The codec is covered by a seeded decode(encode(value)) ≡ value oracle, including delimiter, escaping, Unicode, size, and malformed-input cases.

Kovo-Build-Skew is a framework-reserved, response-only field. Its sole valid value is the exact ASCII token true, and it is meaningful only on an admitted HTTP 409 response that also carries the selected app's Kovo-Build, the inline fragment media envelope, and the private/no-store posture. An ordinary unmarked application 409 (including a typed stale-version conflict) is not a deploy-skew verdict. Application response headers cannot mint, replace, or override this marker. Intermediaries serving enhanced Kovo traffic MUST preserve it; if they instead alter or remove the coupled build stamp, the unequal/missing-token recovery rule still forces a full reload.

Identity tokens use one canonical percent codec: RFC 3986 unreserved bytes remain literal and every other UTF-8 byte is %HH with uppercase hex. Decoders reject raw delimiters, lowercase/non-minimal escapes, invalid UTF-8, NUL, CR, and lone surrogates; they never trim ordinary data. kovo-deps stores exact query-dependency tokens separated only by ASCII spaces. An unkeyed dependency is the canonical query-name token. A keyed dependency is !<canonical-name>!<canonical-full-key>; ! is reserved as the structural delimiter and is %21 inside an ordinary identity, so {name, key} is injective and can never collide with an unkeyed name equal to the full key. Component identities are encoded like target identities. A present empty DOM identity (for example kovo-key="") remains distinct from an absent attribute, but header/query names and instance identities are non-empty. DOM token decoding is linear and has no HTTP-size ceiling; the 4,096-character ceiling belongs only to each HTTP form/list encoder.

Every target-bearing browser request carries a required fragment-free Kovo-Current-Url of at most 1,536 ASCII characters and the exact immutable document app build token in Kovo-Build. The framework headers together have an exact 9,216-byte HTTP/1 line budget, counting each name, : , value, and trailing CRLF. A present form target is required to fit; target and live-target lists truncate only at complete entries and empty list headers are omitted. Delegated submit performs this preflight before preventDefault() and leaves the native submit untouched when the URL, form target, or required lines cannot fit. Direct programmatic fetch fails explicitly. Mutation, HMR, and lifecycle target refreshes use Referrer-Policy: origin; malformed responses or refresh failures trigger the owning full-document recovery path rather than leaving stale truth.

Every framework-owned read of a registered carrier MUST pass through a named canonical reader and be present in the exact TypeScript-symbol census enforced by check:wire-input-boundary. A literal read binds the exact carrier and canonical name in the registry. Only an explicitly reviewed dynamic door may bind the registry's * name, and that door remains responsible for applying the selected entry's grammar before the value reaches framework behavior. Same-named lookalikes do not satisfy the census. App-owned reads and reviewed third-party adapters are outside this registry unless their value enters a named framework protocol door; at that point the normal registry and reject-by-default rules apply.

  • Kovo-Targets is read off the live DOM (kovo-deps stamps), so islands patched in after page load participate. Its semantic dependencies are exact {name, key?} facts, not colon-split strings. The header encodes the target and each complete dependency token through the canonical identity codec: for query product at full instance key product:p2, the physical entry is product-form%3Ap2=%21product%21product%3Ap2. The server holds no session of what's on screen — it answers a stateless question.
  • Kovo-Live-Targets is the structured reconstruction companion for server-refreshable component targets. Each entry names the live target, its generated component registry key, and the serialized props/key identity the compiler proved sufficient to reconstruct the component instance. Every entry MUST carry a server-minted attestation over that canonical descriptor, the canonical source-document URL (origin, path, and query; never the fragment), the exact §5.2.1 app build token, the CSRF session binding (including the framework-minted anonymous CSRF cookie when there is no app session), the independently resolved framework principal, and a separate app authority audience. A mutation or HMR sink MUST same-origin validate that source URL, match it to one canonical app route, rerun the route's complete layout/route guard chain, and use only the resulting authorized source-route request for response-side query and component rendering; the mutation or HMR endpoint request is never a substitute render context. A typed failure may select only the compiler-owned component renderer that both matches the submitted form target and declares the submitted mutation key. defineKovo({ appId }) supplies the replica-stable app part of the audience when its single assemble() closes the contract; appId MUST be a canonical UUIDv4 generated once per distinct app. create-kovo generates it and migrations preserve it. A production app with live-target renderers MUST declare it, and distinct apps MUST use distinct UUIDs and signing secrets even across processes or isolates. A rendererless production app or development app that omits appId receives only a boot-local audience, never distributed authority. The app build token is deploy-skew identity and MUST NOT be treated as the app security principal merely because two apps can share the same render contract and active module set; both values are signed independently. Dev mode keeps the descriptor explicit and inspectable; prod may replace the JSON with a versioned token only when kovo explain can recover the same value. App authors never construct this header, import target constants, or route mutations to fragments by hand.
  • The synchronizer token, replay scope, and live-target attestation consume one exact CSRF binding. On a framework lifecycle request, CsrfOptions.sessionId MUST return an opaque non-empty string of at most 1,024 characters for a resolved session and undefined only for a genuinely anonymous request. A non-string, missing, empty, or oversized authenticated value, an unresolved framework session, or an anonymous framework posture paired with a defined id fails closed; it never falls back to the anonymous cookie. Standalone CSRF helper inputs without framework lifecycle posture continue to treat the callback as their declared session/anonymous authority. Session ids and anonymous-cookie secrets occupy separately labeled, canonical length-framed domains, and the framework-resolved authorization principal is independently framed into authenticated bindings, so shared or namespace-shaped rotation ids cannot validate or replay across principals. Every authorization principal and source-derived mutation identity entering that replay scope is non-empty and at most 1,024 JavaScript code units. An inbound anonymous-cookie secret is 32..1,024 base64url characters (framework minting produces 43); a present malformed or oversized cookie fails closed instead of being silently replaced.
  • A csrf: false mutation never derives replay authority from a session field or a mutation-wide fallback. With replay storage active it MUST declare machineReplayPrincipal(request), evaluated exactly once after parse/coerce and the successful guard/access decision. The callback receives that pinned post-guard request and MUST return a primitive 1..1,024-code-unit caller/tenant id. Missing, malformed, thrown, or rejected results produce the generic 422 idempotency conflict before replay-store or handler work. Kovo length-frames the value under a versioned machine-replay domain and SHA-256 commits the exact UTF-16LE encoding of every JavaScript code unit, including lone surrogates; raw identity bytes never enter store keys or diagnostics. Protected-CSRF declarations reject this field. Enhanced and no-JavaScript requests use the same claim namespace, while their response classifiers stay closed: a cross-mode retry conflicts and cannot execute under a second namespace. Buffered and streaming enhanced requests likewise share one claim. The endpoint first normalizes the requested mode to what the dispatched mutation can actually produce (a mutation without a stream hook is buffered), then binds the committed replay record and live response to that delivery vocabulary. A streaming record/response carries the exact framework-owned own-data header Kovo-Stream: true; a buffered one omits every casing of that name. The final framework response and replay-settlement seals remove app-authored marker attempts before minting that vocabulary. Replay release requires a stable header-name snapshot, at most one exact-cased marker, the exact string value true, and equality with the current normalized delivery mode. Missing stream markers, injected buffered markers, duplicates, wrong casing/value, accessors, and buffered-to-stream or stream-to-buffer retries produce the same sanitized 422 idempotency conflict before stored bytes are returned and without rerunning the handler. Same-mode streaming replay stores the complete settled body, including <kovo-done>.
  • Principal-derived durable authority carries the §6.6 persistent epoch without making it a caller-selected wire knob. A capability URL payload uses version v4 and includes signed p (the positive integer epoch) whenever signed s (the principal scope) is present; a scoped token missing either field, an unscoped token carrying p, and every older version are malformed. The route checks signature, expiry, request-derived key/method/scope, and authoritative epoch before burning one-time replay truth or reading storage, and exposes only the existing generic 404 on failure. Mutation Kovo-Idem remains the canonical v1 client token: the server-minted durable replay receipt, not this untrusted header/field, appends the current epoch to its principal-bound replay namespace. Response release, handler admission, in-transaction completion, and settlement each recheck current epoch; stale receipts are never released or silently moved into the new namespace.
  • The compiler's enhanced-form completeness check follows HTML successful-control semantics rather than treating every matching JSX element as one simultaneous value. A single checkbox is a supported scalar boolean: checked submits its string value (on when omitted), while absence coerces to false only through a declared s.boolean() schema. Same-name radio controls are one mutually exclusive scalar group, and same-name submit buttons are one mutually exclusive submitter field whose selected button supplies the value. Disabled controls and button/input[type=button|reset] do not participate. Repeated same-name controls that are not one radio group or one submitter group remain KV242 until an authored array/multivalue primitive declares their semantics. input[type=image] remains KV242 because the browser derives coordinate-suffixed names (name.x/name.y), and submitter form/formaction/formmethod/ formenctype/formtarget/formnovalidate controls cannot replace or escape the compiler-owned mutation transport. Direct, reactive, spread, composed, or externally associated overrides are KV242 and must not survive into data-bind:* or emitted update-plan stamps. A control type that is not a static string (or statically absent) is also KV242 because changing it at runtime could change successful-control and submitter-override semantics after compilation. The browser runtime is the fail-closed floor: once data-mutation identifies a typed form, any effective method/action that is not the exact same-origin POST /_m/<mutation-key> transport is prevented, marked INVALID_MUTATION_TRANSPORT, and never allowed to fall through to native submission. In particular, a tampered formmethod="get" cannot serialize CSRF, idempotency, or form-field values into a URL. Ordinary native forms without compiler-owned data-mutation retain native behavior.
  • Browser response-body disposition (normative). Every framework-owned fetch path whose response bytes can become live document, fragment, stream, or query truth MUST snapshot Content-Disposition with the other response facts and admit the body only when the field is absent or is one structurally valid, unambiguous inline value. attachment, extension dispositions, comma-combined or duplicate field values, controls, and malformed parameters fail closed before the body is read or a stream reader is acquired. Enhanced navigation performs the normal full GET so the browser retains download behavior; an already-dispatched mutation performs source-document reconciliation without applying the response; lifecycle recovery reloads; and a background typed-read refetch discards and reports the response. The readable/generated inline loader and modular runtime use the same classifier.
  • Kovo-Changes is the sanitized wire summary of committed writes: each entry is {domain, keys}. It never includes mutation input, user-provided values, failure reasons, stack traces, or internal diagnostic detail; richer typed change records are internal compiler/runtime artifacts.
  • <kovo-query> replaces the client's query value and runs that query's update plan — bindings, named derives, stamps — across every dependent island. No runtime dependency tracking: the plan is the DOM itself (§4.8). Query JSON serialized inline MUST be encoded for the exact context it lands in so attacker-controlled JSON string content cannot end the host element early. A <script type="application/json" kovo-query="…"> initial-page island is HTML script-data (entities are not decoded), so its JSON MUST escape < as the JSON unicode escape \u003c&lt; would not decode there and would corrupt the value. A post-mutation <kovo-query>{…}</kovo-query> element has parsed content, so its JSON MUST HTML-escape (<&lt;, >&gt;, &&amp;). Both neutralize the </script/<!--/<script break-out; JSON quoting alone escapes neither and is insufficient. This is a normative renderer rule with a conformance test (tests/integration/specs/xss-escaping.spec.ts), and it binds every transport that re-emits an island — including the §9.3 BroadcastChannel rebroadcast, which forwards already-encoded bytes and never re-serializes raw values.
  • <kovo-fragment> is DOM-morphed by default (idiomorph-class algorithm): user-agent and DOM-resident state — focus, scroll position, selection, in-flight CSS transitions, and <details>/media element UA state — survives. The morph carries no serialization of island-local kovo-state, so a refreshed parent re-emits any nested island at its render-time default state (§4.5 rule 3 re-renders the full subtree from declared queries ∪ stamped props); island-private local state is therefore not preserved across a fragment morph of an enclosing target. The compiler forbids the position that would silently lose it: an island declaring local state may not render inside another component's server-refreshable fragment target (KV420, §4.5). mode="append" is the explicit append vocabulary for pagination ("load more") and streams; mode="prepend" is its companion for "load older" feeds, inserting the patch at the start of the target. Both are ordered keyed inserts: a row whose kovo-key is already present is deduped (matched/skipped, never re-inserted) per §13.2, so a re-shipped page never duplicates rows. mode="prepend" additionally carries a normative scroll-anchor guarantee — the runtime treats the patched target as the scroll container and adjusts its scrollTop by the inserted height so previously-visible content stays fixed (no viewport jump when older content lands above). This is a framework guarantee, not an app knob. The read-side companion is a keyed-delta <kovo-query … delta> whose lists.<path> upsert merges the page into the SAME held query instance (§9.1.1) — prepend-flagged so new rows accumulate at the front of the held array — so "load more"/"load older" fetch only the new page and never re-ship prior rows. Patched-in islands are inert-until-touched like everything else — a fragment update is a tiny navigation, not a different programming model.
  • A streaming enhanced mutation response may be applied incrementally from a ReadableStream as complete wire elements arrive. User message rows and assistant shells still use <kovo-fragment mode="append">; token text uses <kovo-text target="..." mode="append">escaped text</kovo-text> against a compiler/runtime-declared stream source such as data-stream-text="assistant-message:a1". <kovo-text> appends text, not HTML. mode="checkpoint" replaces the accumulated source text for that target with server-confirmed text so far. A stream source may declare an app-authored sink renderer for presentation, but Kovo owns the escaped source buffer and never inserts model output as raw HTML. The sink-renderer signature is constrained so this guarantee survives app code: a sink renderer is (escaped: string) => string | TrustedHtml — it receives the framework's already-escaped source text (never the raw model bytes) and MUST return either further-escaped text, which Kovo appends as text, or an explicit trustedHtml(…) value (§4.8) whose escaping it has itself discharged. A sink that returns a plain string is treated as text and re-escaped at the append boundary; only a trustedHtml brand is inserted as markup, so a markdown/rich sink reintroducing model-output XSS is an explicit, audit-visible KV236 trust decision rather than a silent default. The streaming text path is governed by the same §5.2 #10 output-safety contract as bindings. The final successful chunk must reconcile the affected assistant message or message list with ordinary <kovo-fragment> or <kovo-query> server truth; streamed text is progressive rendering, not a new authority.
  • Streaming mutations run the same lifecycle before any user-visible assistant chunks are emitted: CSRF, schema parsing, guards, replay/idempotency reservation, and transaction policy. Interruption, abort, validation failure, guard/session failure, renderer failure, missing target, or deploy/build-token skew must either mark the submitted form/message failed or refetch/navigate to server truth. The runtime must not silently present a partial assistant answer as confirmed. Without JS, or when the form is not opted into streaming, the endpoint remains the existing POST-redirect-GET or buffered enhanced mutation path.
  • Without JS: the same endpoint sees no Kovo-Fragment header and answers POST-redirect-GET with errors re-rendered into the full page. One handler, two response modes. A deterministic declared application failure settles its fully rendered response for same-mode replay. Validation, 429/rate-limit, and 409 retryable failures release the reservation. Response-policy or failure-rendering errors also release it, so a retry may render again rather than inheriting incomplete response truth.

Success response selection is deterministic and generated. After commit, the server intersects Kovo-Changes with the submitted live Kovo-Targets. For each affected server-refreshable target, the generated live-target registry supplies the component render function, serializable props, declared queries, and query-arg bindings. The first v1 implementation reloads all declared queries for each selected target in the same request context and returns a complete <kovo-fragment> for that target. Query JSON and prod deltas are optimizations layered on this registry when §4.8 update coverage and change-record scoping prove they are smaller and equivalent; they are not app-authored configuration knobs. If a target cannot be reconstructed from declared queries plus serializable props, the compiler emits KV311/KV303 before the response path can be relied on.

There is no ordinary app-authored mutationResponse switch, fragmentRenderers list, generated target constant import, or render*RegionFromDb hook in the success path. Raw endpoints/webhooks, downloads, auth redirects, and other non-component responses use their own declared framework surfaces rather than a general mutation-response body override. Mutation failure does not run the success selector: it re-renders only the submitted enhanced form target with typed failure state (§9.2), while the no-JS path re-renders the full page with the same state.

The round-trip above is the dev (and no-JS) form: complete <kovo-query> JSON and full self-describing <kovo-fragment> HTML. Prod ships the same vocabulary delta-encoded, described next.

9.1.1 Prod delta encoding (dev ships full)#

Shipping a full subtree re-render or an entire query value on every mutation is content-proportional waste — it does not compress away because it is real content, not repeated symbols. In prod the framework therefore sends the minimal change, automatically. There is no knob: the dev/prod build mode is the only switch (Constitution #2 — no per-call-site configuration), and within prod the runtime picks delta-vs-full per response. Names are never mangled in either mode; #1 is untouched.

The delta is scoped by the change record, not diffed against client state. This is what keeps the server stateless (§9.1 — it holds no session of what's on screen): the server never asks "what does the client currently have?" It emits only what the committed write provably touched — the Kovo-Changes record carries the changed {domain, keys} (§9.1) — and everything outside that scope is, by server truth (#5), unchanged. Every server-truth chunk additionally carries a settlement set: the Kovo-Idem tokens of the commits whose effects that chunk's re-run already reflects (the triggering mutation's own token plus any prior committed mutation whose effect is present in the post-commit query re-run). The client uses the settlement set to drop already-committed transforms before re-applying pending ones (§10.4), so a transform whose write is already folded into arriving truth is never double-counted. A delta is therefore sound by construction, not by reconciling two states the server would have to remember.

  • Delta query JSON. A <kovo-query delta> carries only the change-record-scoped portion of the value, not the whole value. The client deep-merges it into the held query value under the deep-merge semantics (normative) below, then runs the same update plan (§4.8) — bindings, named derives, stamps.

Deep-merge semantics (normative). The merge of a delta Δ into a held base value is defined field-by-field, and the §5.2.1 prod gate is tied to these exact rules:

  • Non-keyed scalar fields (numbers, strings, booleans, null) present in Δ replace the base field wholesale; the delta carries the field's new value verbatim, never a partial.
  • Non-keyed object fields present in Δ replace the whole object subtree wholesale — the merge does not recurse into a non-keyed object to retain base sub-keys. A non-keyed object the change could have touched is sent whole (objects are cheap); an absent non-keyed field leaves the base field unchanged, and the only way to drop a non-keyed field is to send its parent object whole with the field omitted.
  • Keyed collections (arrays bound with data-bind-list + kovo-key, §4.8) are the sole structures that merge by identity, not position: Δ sends only the touched rows (upsert, matched by kovo-key per §13.2) plus an explicit removed-key list. A row absent from both the upsert set and the removed-key list is left unchanged; a row is dropped only by appearing in the removed-key list — never by mere absence. Within an upserted keyed row, each field follows the scalar/object replace rules above against that row's prior value.
  • Deletion vocabulary. The removed-key list is the only deletion primitive. There is no per-field tombstone and no "set to absent" merge: to remove a keyed row, name its key; to drop a non-keyed field, resend its parent object whole without it. This forbids the stale-sub-key hazard where a partially-merged object retains a key the server meant to drop.

A collection is delta-eligible only when its kovo-key corresponds to a domain the change record scopes with explicit keys; otherwise that collection ships whole. JSON stays schema-shaped; a frame reads as "these keyed rows of cart changed."

  • Smaller fragments. The primary fragment win is not sending a server-computed DOM diff (that would require the client state the stateless server refuses to hold). It is: prefer a query delta + the client update plan over full <kovo-fragment> HTML wherever the plan grammar (§4.8) covers the subtree, and for list fragments the change record can bound, send only keyed mode="append"/upsert rows rather than the whole list. A subtree the plan cannot express and the change record cannot bound ships as full fragment HTML — the §9.1 form, unchanged. The morph stays the same client path; it is simply fed query-driven updates or keyed rows instead of a whole subtree.
  • Base-version validation (mandatory). A delta assumes a base — the client's held query value — that is present and was produced by the same build. Two ways it can be unsafe: the client has no base for that query (an island patched in after first paint, or a cold store), or a build skew (a long-open tab or stale prerender against a redeployed server whose query shape or active module set moved). Every page render, every delta response, and every /_q/ read response carries the build's app build token (§5.2.1); the client applies a delta only when the token matches and a base is present, and treats any token-mismatched read or delta as a §14 build-skew event. On either failure it does not guess — it discards the delta and refetches the full value over the typed read endpoint (/_q/<key>, §9.4), a cheap GET. The client may also send its token up on the mutation request so a skew-aware server emits full directly and saves the extra round-trip. Deploy skew goes from silently-wrong to loud-and-recoverable — see §14 for the version-recovery contract and the mandatory prior-version retention window.
  • Automatic full-vs-delta selection. The runtime ships whichever is smaller and sound: a query with no delta-eligible collection, a tiny value, the first render of a patched-in island, or a build-token mismatch all ship full. The rule is deterministic so the fixpoint and render-equivalence gates (§5.2.2) stay sound — the prod gate is apply_delta(base, render_prod(Δ)) ≡ render_dev(full) over the corpus.
  • Reconstruction for debugging. kovo explain/MCP reconstructs the full query value from a prod delta + the held base, so an owner or agent handed a prod frame recovers dev-equivalent legibility. This is a convenience, not load-bearing: names are intact and the partial payload is already named and schema-shaped.

Mutation handlers may attach response headers through a narrow context channel. The channel is for transport metadata such as Set-Cookie and cache headers; it does not let handlers replace the body, status vocabulary, query reruns, fragment rendering, or PRG redirect contract. Header values emitted on the enhanced and no-JS paths are merged with framework headers after CSRF, replay, parsing, guards, and transaction commit complete.

Header-channel transport safety (normative). Structured app response authoring is settable only through typed surfaces; it is not a raw string map. The direct headers bags on respond.file()/respond.stream() and configured error shells accept only Cache-Control, Last-Modified, and Vary, under case-insensitive runtime comparison; any other direct name is rejected with KV415. File/stream Content-Type, ETag, and Content-Disposition travel through the contentType, etag, and filename/disposition options. The shared live/generated filename serializer MUST replace Unicode bidirectional formatting controls before constructing both the ASCII fallback and RFC 8187 filename*, even when earlier upload ingestion already sanitized its metadata. The Content-Disposition filename serializer and raw forwardSetCookie reserializer MUST return only after the same fail-closed whole-value postcondition accepts the output: CR, LF, NUL, a reverse solidus outside a quoted field, an unterminated quote/escape, a quoted-pair other than \" or \\, or a second quote/escape after the field closes is a framework error. The emitted Node filename path MUST execute the exact same five-state transition and terminal-verdict functions as the live runtime. Location is minted by redirect(). The HTTP Refresh response header is forbidden under every casing and for every value: browsers treat it as navigation, so accepting it would bypass the typed Location redirect posture and origin allowlist. Statically visible Response init occurrences close at compile time, and the complete structured/raw response finalizers plus direct and generated Node, Vercel, and Cloudflare boundaries reject any remaining occurrence with KV415 before it becomes browser-visible. This prohibition does not change the existing Location redirect contract. Set-Cookie is built only through the typed mutation cookie builder (context.setCookie(name, value, options)), which percent-encodes the value, validates the name against the cookie-name grammar, forbids CR/LF/NUL/; in name and value, and serializes attributes structurally so a user-supplied value can neither inject a second cookie nor add unintended attributes. Kovo-* names remain framework-only. Raw endpoint Response values and operator-owned static metadata may carry other end-to-end integration headers, but they remain subject to the adapter-owned transport floor below. Every name and every value a structured channel emits MUST be rejected if it contains CR (\r), LF (\n), NUL, or any control character outside the printable header grammar; the channel never strips-and-continues. The same rejection rule applies identically to enhanced and no-JS mutation paths. This is the header-channel analogue of the <kovo-text> and Kovo-Changes injection discipline (§9.1): values flowing out a header are contextually safe by construction, never by author care.

Browser-state cache floor (normative). Set-Cookie does not itself prevent an HTTP shared cache from storing and replaying a response. Final wire reconstruction therefore MUST replace any authored or declared cache policy with Cache-Control: private, no-store and merge Cookie into Vary whenever a structured or raw response carries Set-Cookie. The same floor applies to Clear-Site-Data: replaying a cached clear instruction can destroy an unrelated visitor's browser state without reaching the endpoint verifier. It also applies whenever a document or raw response emits a synchronizer token or live-target attestation derived from the anonymous-CSRF cookie, including when that binding already existed and the response carries no Set-Cookie; those body bytes are cookie-personalized authority and cannot be reused for another visitor. A streaming document with any pending deferred region MUST conservatively select this posture before headers cross the wire, because a callback can first emit mutation-local CSRF authority after the initial shell and that future path is not knowable from the shell. Before committing those headers, the framework MUST pre-mint every registered anonymous-CSRF binding that a deferred framework form or live-target descriptor can use, deliver its cookie, and share that exact binding with the later region render; this preflight MUST NOT invoke app-authored session extractors for unrelated registered mutations. A token whose binding cookie was minted only after header commit is invalid output and MUST fail closed. The same pre-header conservative floor applies to every raw endpoint backed by the framework's private browser-credential delegation and to every route outcome carrying a live ReadableStream: raw Response hides whether its source executes lazily, and a route stream's pull() can first consume request authority or mint anonymous-CSRF bytes after finalization. Credential-neutral raw endpoints and eager route bodies do not select this floor merely because their transport uses a Web Response. This floor is unconditional across typed mutation output, raw endpoint() responses, redirects, errors, direct Node adapter calls, emitted Node/Vercel runtimes, and future response channels; endpoint cache posture remains useful audit metadata but cannot relax it. Static export rejects both Set-Cookie and Clear-Site-Data because a durable artifact has no response-specific browser-state channel.

During the framework-managed response lifecycle, CSRF token-generation calls made with cloned, reconstructed, or otherwise derived Request values use the canonical response request and share its anonymous-CSRF binding/posture state and header-commit boundary. An exact framework-retained request remains a lifecycle receipt when an external callback loses async context. An arbitrary detached derivative is not such a receipt and MUST NOT first mint anonymous authority; without proof that the binding cookie can still reach the browser, the helper fails closed. This response-only canonicalization MUST NOT apply to CSRF validation or replay resolution, which consume the exact ingress request.

Standalone CSRF response capture (normative). A first-anonymous response helper MUST record its exact binding cookie in the private lifecycle before exposing the token. The final route/raw sink synchronously seals that lifecycle, snapshots and injects every pending distinct-name cookie, deduplicates an exact authored copy, and rejects any non-identical plain/__Host-/__Secure- alias under the same logical name. A safe-method or CSRF-exempt raw endpoint may receive that injection only after its exact browser-state auth proof executed; direct runEndpoint() and direct internal renderRoutePageResponse() have no managed cookie sink and reject pending authority. Sealing precedes the snapshot, so queued work either completes before that single boundary and is included or observes committed headers and fails closed. An exact retained request context takes precedence over an ambient outer lifecycle; nested dispatches MUST NOT cross-bind canonical authority, personalization witnesses, pending cookies, or seal state. Each createRequestHandler() call is a distinct response boundary and MUST clear an ambient caller lifecycle before pre-dispatch callbacks run. If a nested handler receives the caller's exact retained Request, it MUST first reconstruct a detached native ingress carrier; the inner success path and every auth, CSRF, access, error, or method early return therefore cannot seal or consume the outer response's cookie channel.

Endpoint dispatch MUST combine raw-response posture verification and an immediate fresh header snapshot in one synchronous choke. App code may continue producing an authorized body stream, but a later microtask cannot add Set-Cookie, Clear-Site-Data, or any other header to the already classified carrier. A privately captured anonymous-CSRF cookie consumes the same executed/private browser-state proof as an authored cookie on every safe-method or CSRF-exempt endpoint.

Transport compression and BREACH posture (normative). Kovo's Node adapter and every generated production runtime MUST negotiate response compression (br preferred over gzip, honoring Accept-Encoding q-values) for compressible content types, and MUST apply it uniformly to cookie-bearing, no-store, and private responses — a realistic logged-in page is exactly the response whose wire size matters most, and refusing to compress it is not a BREACH mitigation. Kovo's compression-oracle (BREACH/CRIME-class) posture is instead: (1) every framework-owned body secret that repeats across responses is masked with fresh per-mint randomness — CSRF tokens are XOR-masked at every mint, so the wire form of the one framework-owned body secret never repeats; and (2) every compressed response carries Kovo-Pad, a per-response uniformly random 1..64 character length-noise header riding the same encrypted stream as the compressed body, so a ciphertext-length observer must average away uniform noise instead of reading a deterministic length signal. The pad is attached unconditionally on compression so its presence never marks a response as sensitive. Cache-Control: no-transform (RFC 9111) is the sole authored opt-out and MUST be honored. App-rendered secrets co-resident with attacker-reflected input remain the app's responsibility; the framework mitigates, and cannot eliminate, length side channels for authored content. Compressed responses MUST add the Accept-Encoding Vary dimension and drop any stale Content-Length. Dynamic render paths MUST use a bounded-latency quality (brotli quality 5 class, not the quality-11 class, which measured ~115x slower per document); expensive qualities are reserved for static bytes cached by content identity.

Static validators and connection reuse (normative). Every statically served client file (/c/*, /assets/*, static route documents) MUST carry a strong content ETag derived from the exact served bytes and MUST answer a matching If-None-Match with 304 so must-revalidate assets revalidate instead of re-downloading. A bodyless GET/HEAD that passed the payload-free ingress gate (§9.5) MUST NOT be marked non-persistent before dispatch: there is no unread request body to guard, and tearing down every document GET exhausts client ephemeral ports under load. A peer that disconnects mid-body is ordinary transport teardown and MUST NOT surface through the adapter's unhandled-error path.

Proved-document caching (normative). A route document may become publicly cacheable only through the compiler-emitted document: cache-influence entry (§9.4) — never through an authored header, an annotation, or an observed execution. When the registered manifest proves a route public-proved AND the current 200 buffered text/html or document-parts representation carries no runtime credential signal — no request Cookie or Authorization, no enforced authorization graph, no resolved or unresolved principal/session state, no CSRF or deferred personalization witness, no forwarded Set-Cookie, no live or deferred body — the server MUST stamp the canonical validator posture: Cache-Control: public, max-age=0, must-revalidate, a strong content ETag derived from the exact body bytes, and Last-Modified; it MUST answer a matching If-None-Match with a 0-byte 304 carrying the same validator metadata. The server MAY serve subsequent equivalent requests from a per-build in-memory document cache keyed by the manifest's cache-key axes (URL path, URL search), each manifest Vary header value, the negotiated representation (§8), and validated against the app build token; admission additionally requires the exact canonical Cache-Control, the stamping witness on the exact response object, and the absence of Set-Cookie. The safety property is that no proof gap can widen: a missing or closed manifest entry, any credential signal above, or any admission-floor failure keeps the credential floor (private, no-store + Vary: Cookie) and bypasses the cache in both directions — a credential-bearing request is never served from the cache and its response is never stored. Authored public Cache-Control on a document remains demoted to the credential floor until an authored document cache declaration exists; max-age=0, must-revalidate is deliberate — freshness rides the strong validator (a 0-byte 304), not a TTL the compiler cannot prove, and the in-process cache invalidates with the build. Time- or randomness-dependent pages are outside the proof by construction (the finite document cache language closes on such calls), so a proved document is byte-stable for its build by proof, not by convention.

Multi-process deployment posture (normative). Kovo's server runtime is single-threaded by design; the supported horizontal model is N identical processes behind one reverse proxy, not an in-process cluster. Rate budgets (requestLimits, framework defaults included) describe the deployment aggregate: each process MUST enforce ceil(max / N) per rate window when KOVO_PROCESSES=N is set, MUST keep maxKeys and windowMs per process, and MUST fail loudly on an unparseable KOVO_PROCESSES value rather than silently multiplying the authored budget by N. Unset means N=1. The division is conservative under IP-affinity load balancing (a pinned client sees a smaller budget, never a larger one). Every store that must outlive one process or cover every replica — the §14 retention window, replay stores, principal-epoch stores — already carries its own replication contract; the in-memory proved-document cache above is per-process by design and needs none. A built-in cluster/worker mode is explicitly out of scope until the proved-document tier has landed and been re-measured (plans/good-perf.md D10).

Buffered document assembly (normative note). Route documents are assembled and sent buffered: status and every header — cache posture, cookies, CSP hashes, redirects, authorization outcomes — are decided after the complete render, and measured TTFB on the buffered path is milliseconds (plans/good-perf.md D11). Streaming an early <head> flush freezes status and headers before render decisions that Kovo's security model makes late (guard failures, notFound(), per-response cookie floors, hash-locked CSP over the final markup), so the document does not stream. Revisit only for routes the compiler proves make no post-render header decisions; §8's framework-owned deferred regions remain the supported progressive path.

Adapter-owned framing and hop-by-hop fields (normative). Application response channels MUST NOT supply Content-Length, Connection, Keep-Alive, Proxy-Connection, TE, Trailer, Transfer-Encoding, Upgrade, Proxy-Authenticate, Proxy-Authorization, or HTTP2-Settings, under any casing. Reject them with KV415 at the complete response-header boundary; never silently strip them. Rejecting Connection rejects the field and every header name it could nominate before any nominated field can acquire hop-by-hop meaning. This floor applies equally to structured framework responses, respond.file()/respond.stream(), raw endpoint Response values, static-export header metadata, direct Node adapter calls, and emitted Node/Vercel runtimes, for HTTP/1.0, HTTP/1.1, and HTTP/2 compatibility paths. Only after that validation may a framework adapter attach or replace its own framing/connection metadata (for example exact static-file Content-Length, compression-derived Content-Encoding, or Connection: close). Thus an app-controlled length or transfer field can never disagree with the bytes Kovo writes or turn a keep-alive response body into a queued-response prefix.

Raw HTTP integrations use declared endpoint() entries, not ad-hoc server escape hatches. An endpoint is registry-visible, receives Request -> Response, requires an explicit HTTP method (there is no implicit any-method endpoint), requires an endpoint-level reason/purpose, and is enrolled in the endpoint and unguarded audits with the same auth metadata as routes, queries, and mutations. Prefix mounts require a mountJustification because they enlarge the routed surface beyond one path. Endpoint declarations also carry raw response posture metadata for the audit row: body class (html, json, text, bytes, stream, or redirect), cache posture, and whether app code owns body encoding plus response-header safety. That app-owned posture never transfers message-framing or hop-by-hop authority: the framework still reconstructs raw response headers and applies the adapter-owned-field KV415 floor above. The closed safe-method set is GET, HEAD, and OPTIONS; every other method, including an extension method unknown to Kovo, is unsafe and receives the default synchronizer-token check unless the endpoint explicitly opts out of CSRF with a named justification. A safe-method endpoint receives only a managed DB Reader from ctx.actAs() and MUST NOT emit Set-Cookie or Clear-Site-Data; an executable non-ambient verifier that actually succeeds for the exact request (or an equivalent private framework-owned self-verifying receipt) may authorize those browser-state effects. The runtime enforces both known capability boundaries even if application types are bypassed. App-owned side effects outside Kovo's capability and response sinks remain the application's responsibility, so authors MUST use an unsafe method for a state-changing operation. Endpoint handlers receive the raw Request before body parsing so signature verification can use wire bytes; exact and prefix mounts are declared; cookies are not interpreted and no ambient req.session is passed. A CSRF exemption is sound only because endpoint/webhook auth does not ride ambient browser authority. OAuth/SAML callbacks and adapter-owned mounts belong here; browser credential forms should still prefer typed mutation() flows so they keep schema validation, no-JS behavior, and the normal response vocabulary.

Runtime response-posture verification compares the parsed media-type essence, not a substring or top-level-type approximation: text admits only text/plain, html only text/html, and json only application/json or a structured +json subtype. bytes and stream deliberately leave the media type unconstrained because a raw Response does not retain their authored representation; declaring either therefore makes that union's media-type branch opaque. A redirect posture is selected by a 3xx status and the separately validated Location contract. These classes are not aliases: in particular, active text/html bytes never satisfy a declaration that names only text.

An endpoint that legitimately streams or long-polls beyond the app deadline MAY declare response.longLived: { deadlineMs, justification }. This is the only request-deadline escape: it selects one endpoint-scoped finite deadline from 1 through 300,000 ms, requires a non-empty audited justification, and is printed as deadline=long-lived:<milliseconds>:<justification> by kovo explain endpoints. It does not disable or enlarge the app's occupancy budget, does not apply to another endpoint or route, and does not exempt the response from adapter write-out cancellation. Omitting the declaration uses the app deadline.

An endpoint auth declaration MAY carry an executable verifier from the webhook verifier kit. When present, the dispatcher MUST verify cloned raw wire bytes { headers, payload } before CSRF validation and before the handler runs; verifier false, malformed input, or thrown verifier errors fail closed with 401 Unauthorized, and the original request body remains readable by the handler after a successful check. Name-only endpoint auth declarations remain audit metadata. webhook() continues to emit name-only endpoint auth because it self-enforces raw-byte verification in its own lifecycle before parsing.

webhook() is the shaped machine-endpoint primitive for third-party POSTs that write Kovo-owned data. Shape: webhook('/provider/path', { verify, input, idempotency, handler }), lowering to a registry-visible endpoint with a source-derived webhook identity (§4.1) and auth=verifier:<resolved scheme> unless an explicitly justified custom/none verifier is used. The first string is the public HTTP receiver path, not the webhook registry name. The lifecycle is fixed: capture one request clock and the raw bytes → verify → parse/coerce a loose input schema (unknown provider fields pass through) → construct and validate an authenticated provider-event replay identity → atomically reserve/replay under the source-derived webhook identity → optional framework transaction wrapper → handler receives a machine-ingress context with no ambient session and dispatches Kovo-owned writes through context.runMutation(mutation, input) → the called mutation owns the audited DB write, touch set, and static diagnostics → commit/store the response and emit the unified change record {domain, keys, input} derived from the called mutation's committed changes.

idempotency(input) MUST return either undefined or the exact opaque value created by webhookReplayIdentity(key, occurredAtMs). key is the non-empty provider event id (1..1,024 visible ASCII characters), and occurredAtMs MUST be the event's own occurrence time from the authenticated provider payload. Local receipt time, Date.now() inside the callback, an HTTP delivery timestamp, and an HMAC freshness timestamp are not event occurrence and MUST NOT be substituted. The constructor derives an immutable expiresAtMs = occurredAtMs + 30 days; its private TypeScript brand is only an authoring guardrail, while module-private runtime provenance rejects casts, structural copies, and forged objects. After verification and parsing but before any replay-store call or handler execution, the runtime validates the canonical identity against the one captured request clock: expiresAtMs <= now is stale, and occurredAtMs > now + 5 minutes is future-dated. Either temporal failure is a sanitized 422. Because asynchronous verification or parsing can cross the horizon after that captured-clock check began, the replay store MUST also reject fresh reservation at its current clock when expiresAtMs <= now; that refusal is a retry/unavailable response and the handler does not run. Settlement that crosses the horizon MUST leave the already-held claim pending and fail closed rather than create immediately removable committed truth. An invalid key, timestamp, unproven return value, callback throw, or otherwise malformed result is an internal posture failure answered with a sanitized 500 at that same boundary. The callback is evaluated exactly once per delivery.

The replay store receives the canonical { key, occurredAtMs, expiresAtMs } facts intact, never a raw string or store-local TTL. A redelivery of the exact live identity replays the stored response and must not re-execute the handler or dispatched mutation. Reuse of a live (webhook scope, key) with different occurrence or expiry facts is an integrity conflict answered with sanitized 422, never an alternate event admitted alongside the first. Committed truth retires only at the exact authenticated expiry under §10.3; pending truth never expires automatically. recordChange() remains a narrow compatibility/manual-change bridge and is checked against declared writes; it is not the primary audit source for arbitrary raw transaction writes. Direct DB writes from webhook handlers remain KV330/KV406. fail() rolls back and answers the declared 4xx/5xx response so provider retry semantics are explicit.

The verifier kit is part of the normative surface for webhook(): hmacSignature({ header, payload, encoding, tolerance, multiSig }) is the generic form, and standardWebhooks({ secret }) is the shared non-vendor preset that resolves to printed generic HMAC configuration. Provider-specific HMAC recipes live in app/example code on top of hmacSignature, not in framework package exports. Verification is over raw bytes, uses constant-time comparison, enforces timestamp tolerance, and supports rotated secrets/multiple signatures. Non-HMAC providers use a custom verify(request) escape that appears as custom auth in the audit; verify: 'none' requires a named justification and appears as unauthenticated machine ingress.

The public webhook task path exports the constructors plus verifier/request contracts. Constructor input vocabulary is contextually typed and may be named by inference (Parameters<typeof hmacSignature>[0]), not by parallel exported HMAC option, secret, payload, tolerance, or resolved-inspection records. Provider signing material and inspection snapshots remain framework-internal.

9.2 Errors#

Rejection equivalence and observation policy (normative)#

Every remotely reachable surface for which account existence, resource existence/ownership, a secret, or a governed value can change the response MUST have an explicit kovo-response-observation/v1 policy. Schema owner:, secret:, and governed facts nominate surfaces for this review; they do not choose a product policy or prove equivalence. A nominated surface without a policy is a build refusal. A policy names the two worlds being compared and one of the canonical classes below. The only canonical world pairs are exists-not-owned versus absent, account-present versus account-absent, and unexpected-cause-a versus unexpected-cause-b; a product-specific pair requires a separately reviewed class rather than an alias to one of these names.

An attacker observation is the tuple (status, redirect, selected end-to-end headers, normalized cookies/tokens, body relation, connection behavior, work-factor class, timing distribution). Header names are compared case-insensitively and order-independently after adapter-owned framing fields are removed. Set-Cookie is compared by cookie name, security attributes, expiry class, and token presence/shape; fresh random token bytes are never required to be equal. The body relation names media type, encoding, length relation, and content relation. Connection behavior distinguishes a complete response, reset/abort, and timeout. Work factor names the finite operation class and count. Timing is a distribution checked against a versioned statistical budget; this is not a claim of constant-time execution. An oracle compares the declared tuple fields and relations, not raw response bytes. Provider delivery and other effects outside the framework-controlled HTTP boundary are excluded unless the policy explicitly includes and measures them.

Canonical class Required worlds Required attacker-visible relation
input-validation validly parsed but rejected inputs selected by the declared error schema HTTP 422; no redirect or credential token; declared typed body vocabulary. Error codes and field paths may differ, so this class does not conceal which submitted field failed.
authentication-needed enhanced versus no-JS transport for the same unauthenticated request The transport difference is intentional: enhanced HTTP 401 plus Kovo-Reauth; no-JS HTTP 303 to the same-origin login route with the same canonical next. Neither response establishes a session.
authorization-denied authenticated principals denied by the same guard HTTP 403 and the declared unauthorized failure body. This class does not conceal resource existence; a surface that must do so selects resource-concealment.
resource-concealment exists-not-owned versus absent Identical 404 status, no redirect, Cache-Control: private, no-store, credential-varying headers, no cookie/token mutation, the same fixed body media type/bytes/length, complete connection behavior, equivalent storage/verification work class, and a timing distribution within the declared budget.
account-creation account-present versus account-absent The same generic accepted status/redirect/body relation; no account-dependent cookie or token. A surface claiming this class cannot auto-establish a session in only one world. Framework-controlled lookup/write/credential work is normalized and its timing stays within budget.
account-recovery account-present versus account-absent The same generic accepted status/redirect/body relation and no account-dependent cookie/token. Framework-controlled token-generation, lookup, queueing/decoy work, and timing are normalized; delivery by an external mail provider is outside the claim unless separately measured.
unexpected-failure unexpected-cause-a versus unexpected-cause-b The surface-specific stable sanitized HTTP 500 tuple defined below; no cause-derived header, cookie/token, body content/length, connection behavior, or work-class difference before the response is committed.

The table is a minimum floor. A surface may declare a stricter relation, but MUST NOT claim a class while omitting one of the tuple axes or silently treating a raw-byte mismatch as acceptable. The versioned policy and its dual-world oracle are release evidence; a passing unit fixture is not a timing guarantee for every deployment.

Validation failures (schema, with field paths) and declared error codes return HTTP 422. The enhanced path infers the submitted form instance from the request's compiler-emitted form target and returns a <kovo-fragment> for that form only; the no-JS path re-renders the full page. Both paths call the same component render function with the same typed failure state in forms.<mutation>.failure, so expected failure UI is normal TSX (<FieldError>, <FormError>, or direct forms reads) rather than a separate response template. ctx.submit's onError receives the same typed union. Expected failure responses never use committed invalidation or Kovo-Targets success selection.

KV430 request-body posture (normative). After successful JSON decoding, Kovo MUST enforce the iterative depth/breadth/node budget before provenance decoration, schema traversal, CSRF-token field extraction, or handler dispatch. URL-encoded segment and multipart-part ceilings enter the same posture. Provenance decoration MUST keep every app-visible scalar read non-coercible and untrusted, including reads through own-property descriptors, Reflect.get, Object.assign, and serialization, but MUST NOT eagerly allocate one persistent poison object per scalar leaf; the validation-only raw container view is module-private and unforgeable. A CSRF-exempt mutation that exceeds one of these ceilings answers 422 with {"code":"VALIDATION","payload":{"reason":"shape-budget"}}. A CSRF-protected mutation or endpoint cannot safely recover its submitted token from an over-budget carrier and therefore fails through the ordinary CSRF response without exposing the body verdict. After webhook authentication, malformed JSON remains 400 Invalid JSON webhook body, while a valid JSON body that exceeds KV430 answers 422 with {"error":{"code":"VALIDATION","payload":{"reason":"shape-budget"}},"ok":false}. None of these expected input refusals calls the app's unexpected-error hook or handler.

Declared fail() payloads are client-bound wire values and MUST satisfy the same JsonValue vocabulary as query values and island state: JSON primitives, arrays, and plain objects only. An error schema may parse richer server-side values for internal use, but context.fail(code, payload) rejects Date, Map, functions, class instances, and other non-JSON payloads at the TypeScript boundary before they can enter forms.<mutation>.failure or the enhanced/no-JS error wire.

An unauthenticated mutation guard failure is not part of this typed validation union (§6.5). It does not render a forms.<mutation>.failure fragment: the enhanced path answers HTTP 401 with a Kovo-Reauth directive (login route + same-origin next) the loader follows to re-authenticate, and the no-JS path answers a 303 redirect to the login route with next. An authenticated-but-unauthorized mutation guard failure answers HTTP 403 and carries an unauthorized code in forms.<mutation>.failure so authorization-denied UI is typed and distinguishable from schema/app validation failures.

Unexpected server failures are not part of the typed union and must not leak internals. The typed query endpoint (§9.4) returns HTTP 500 with JSON {"code":"SERVER_ERROR","payload":{}}. Full-page route rendering returns HTTP 500 with the app's stable error shell or the fallback body Internal Server Error. Enhanced mutation responses that fail while rendering post-commit queries/fragments return a render-error fragment with HTTP 500 and data-error-code="RENDER_ERROR"; any Kovo-Changes header on that response remains sanitized to {domain, keys} for writes that already committed.

9.3 Liveness and Live#

Kovo separates low-cost liveness from explicit live subscriptions:

  • BroadcastChannel rebroadcast — a mutation's <kovo-query> response is rebroadcast to the user's other tabs; same-user multi-tab sync at zero server cost. Because BroadcastChannel is origin-scoped, not principal-scoped, every rebroadcast envelope MUST carry a session/principal fingerprint derived from the sender's req.session identity. A receiving tab MUST discard any message whose fingerprint ≠ its own current req.session identity, and MUST drop the channel on session change — so one user's private query data can never be morphed into a different user's UI on a shared or fast-user-switched device. This receive-side principal check is normative to the same degree as the SSE per-push guard re-check below; rebroadcast must not become a cross-principal disclosure side channel.
  • Refetch on focus/visibility — a loader behavior (per-query opt-out) that re-runs queries (over the typed read endpoint, §9.4) when a stale tab returns; it fakes an embarrassing share of "live" UX for one conditional in the loader.
  • Live queries (roadmap; not shipped in v1 technical preview)<kovo-live query="cart"> will subscribe over SSE to the identical <kovo-query>/<kovo-fragment> chunks; guards must be re-checked at subscription and at each push (a guard that passed at render must pass at patch time — fragments must not become a privilege-escalation side channel); in-process emitter (single node) or Redis pub/sub (multi-node); instance-key routing; live: true opt-in per query. Until this transport ships, live: true is not a valid query() definition field and <kovo-live> is not an implemented authoring primitive; accepting either as a silent no-op would violate the no-op-field contract.

The vocabulary is transport-agnostic by construction, so SSE is an additive transport, not a rearchitecture.

9.4 Typed reads: the query endpoint#

Every query is addressable over GET — one read surface serving refetch-on-focus (§9.3), GET-form fragment responses (§7), async option/search reads, and the future SSE subscription key:

http
GET /_q/product?id=p1 HTTP/1.1
Kovo-Build: <app-build-token>
Kovo-Fragment: true
http
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8

<kovo-query name="product" key="product:f10:k2:ids2:p1" href="/_q/product?id=p1">{ "name": "Mug", "stock": 4 }</kovo-query>

Every enhanced /_q/ request MUST carry its immutable document app build token as Kovo-Build, and every /_q/ response MUST carry the selected build's token (§5.2.1). The current app rejects a missing or unequal enhanced-request token before decoding the query key or entering the query registry. A client compares the response token before generic failure-status handling, so the stamped 409 skew response triggers §14 recovery rather than becoming a silently skipped background failure. An admitted enhanced read whose guard now denies the current principal MUST return an exact, non-redirecting HTTP 401 (unauthenticated) or 403 (forbidden), text/html, and the selected build token; native query navigation retains the ordinary 303 login redirect. The enhanced client uses Fetch redirect: 'error' and treats only that exact-URL, same-build, admitted 401/403 envelope as revocation: it applies none of the fetched batch and performs a full navigation recovery of the current route, whose native guard then redirects or renders forbidden. A Fetch rejection alone is only a transport failure and MUST NOT be inferred to prove revocation. Args arrive as search params through the query's args schema (§10.2) — the same s.* coercion machinery as forms. A query with no declared args schema MUST reject a non-empty search input with 422 before running lifecycle providers, guards, or its loader; an absent schema is not an unvalidated-input mode. The query's guard (§10.2) is checked on every admitted read, and reads are part of the unguarded audit. Query name and optional canonical full instance key are separate exact facts: a raw string containing : is an unkeyed name, never something to split. A present raw empty key is malformed, while name: is a valid canonical instance identity with an empty value. Because canonical instance keys are intentionally not reversible search encodings, every server-emitted query truth that may be refetched MUST also carry its exact canonical href. The browser snapshots that (name, key, href) authority when it accepts server truth and refetches only that same-origin exact URL; it never reconstructs args by splitting or decoding the instance key. The instance key in the response (product:f10:k2:ids2:p1) remains the §10.2 single currency shared across client store, wire, and optimism.

Caching contract (normative). /_q/<key> is a credentialed GET whose body may vary by identity, so a URL that differs only by args is a shared-cache collision waiting to disclose one principal's data to another. Every compiler-emitted app graph with query, raw-endpoint, or route-page roots therefore carries one versioned kovo-cache-influence/v1 manifest. Each query or raw-endpoint root records URL path and search as cache-key axes, each statically named request-header read as a possible Vary axis, and cookies, Authorization, principal/session facts, secrets, framework state, declared external-data versions, and unclassified influence as distinct axes. A declared external-data version is cacheable only when its version has a manifest-visible URL or named-request-header key contribution. Framework state without complete keyed external versions, a dynamic header name, an opaque call, or any influence outside the finite reviewed language closes shared caching. One observed execution is never positive evidence. A named audited escape may retain an explicit operator obligation, but it remains distinguishable from public-proved compiler evidence.

Document cache-influence surface (normative). Every JSX-authored route() page contributes one document:<route path> entry with surface document. Its authored intent is the route's own mandatory access decision — publicAccess(reason) is the authored public posture; a guard chain or machine-auth decision is a principal/authorization influence; a missing decision is unclassified. Influence derivation is the compiler's finite document cache language, and it is fail-closed in both directions: URL path and search are always cache-key axes; queries reached by the page, its regions, or its layouts are framework state; ctx.signUrl and ambient environment bags (process/Deno/Bun) are secret influences; a handler that binds the request identity parameter, awaits or renders asynchronously, performs construction, calls outside the analyzed same-module closure and a small pure allowlist, references imported or mutable module values, or composes layouts is unclassified. A route page proves public-proved only when every per-request handler (page, regions, meta sources) stays inside that language, every reference resolves to same-module function literals, build-constant literal data, or a reviewed pure framework constructor admitted as a direct callee (trustedUrl/trustedHtml), and the route module's own defineKovo(...) declaration proves the app's per-request renderRoute hook is absent or a one-parameter function inside the same language — a route module with no visible defineKovo cannot prove the render hook of whatever app later assembles it, and a renderRoute that binds its context parameter (which carries the raw request) closes every entry in the module. The access decision must be authored public. Entries are emitted for every route — a closed verdict is an explicit fact, never a missing one — the manifest binds to the exact app build that emitted it (§5.2.1/§14), and hand-authoring a manifest is the same KV235 violation as hand-authoring lowered IR (§5.2). The render-hook clause is sound because the only route modules that can prove carry their own assembling defineKovo; the manifest is trusted (KV235) to attest that the defineKovo visible in a route module is the app that assembles that module's routes. This attestation is a compile-time property, not a runtime-observable one, so the §9.5 proved-document runtime floors below deliberately do NOT depend on it: every runtime-observable credential signal (Cookie, Authorization, resolved/unresolved principal or session, CSRF or deferred-personalization witness, forwarded Set-Cookie) is rejected independently of the manifest — a proof gap in any of those fails closed regardless of a public-proved verdict.

The build MUST compare the evaluated cache declaration with the exact compiler manifest and fail closed on a missing public root or any intent, external-version, axis, Vary, or verdict drift. Runtime observations are rejection-only: a missing or closed compiler verdict cannot be widened by an anonymous-looking request. A typed query may emit its declared public Cache-Control only when the registered manifest has the exact root, surface, declaration, and a public-proved or named audited-escape verdict, and the current request has no Cookie, Authorization, resolved principal/session, or opaque request carrier. Otherwise it MUST emit Cache-Control: private, no-store and Vary: Cookie. Document execution applies the same current-response rejection floor for Cookie, Authorization, unresolved/resolved principal state, late executable bodies, and other existing personalization witnesses. These floors hold for every transport that hits /_q/ — loader fetch, refetch-on-focus (§9.3), GET-form fragment responses (§7), and async option/search reads — and for document responses through every adapter.

Vary MUST be derived only from normalized, statically named request-header axes in the manifest. URL path/search already participate in the cache key and MUST NOT be encoded as Vary; principal/session state, Cookie, Authorization, secrets, framework state, and unclassified influence close shared caching rather than becoming attacker-controlled Vary tokens. A guarded or otherwise principal-dependent query may never be served from a shared cache: the guard-at-every-read invariant must not be bypassable by an intermediary.

9.5 Request shell#

The request shell is the server-owned composition point for routing, document assembly, dev serving, and export. Apps call defineKovo() once for provider/config context and call that contract's assemble() once with explicit dense arrays of routes, layouts, mutations, queries, endpoints, and tasks. Assembly closes those declarations together with an optional client-module store, document options, unexpected-error shells, CSRF config, the lazy db provider, the §6.5 session/auth provider, the frozen declared environment projection from §6.6, and the replica-stable appId. The result is an opaque KovoApp, not that structural aggregate; only framework-owned functions may resolve its private state.

The injected VersionedClientModuleStore exposes only retain, readActiveSnapshot, atomic replaceActiveSnapshot, and retained-history resolve; assembly closes those methods behind a framework-owned registry facade that alone derives representation hrefs and the direct app build token (§5.2.1). Generated route IR and live-target registry artifacts are wired by the compiler/build integration, not by app-authored generated/refresh options. The loader MUST establish an app-owned registry scope before evaluating generated modules; concurrent or top-level-await app graphs may not share a process-global pending registry, unscoped late/HMR registration is not runtime authority, and mutation/HMR sinks may not fall back from their closed app inventory to a process registry. Vite/dev integration points at an authored app entry, for example kovo({ app: '/src/app.tsx' }) from @kovojs/server/vite; the entry must default-export the opaque KovoApp and must not point into src/generated/*. Compiler-owned plugins resolve route IR, live-target registries, and generated client modules internally. The public handler currency is web-standard Request -> Response; adapters such as node:http convert at the edge and receive only that token.

The same bootstrap-first generated-registry channel carries the §6.6 browser response posture. Vite/dev and production build scan the project source snapshot, serialize the exact kovo-browser-posture/v1 manifest into a framework-owned virtual/generated module, and execute its registration before the app entry. Document assembly re-witnesses that carrier and derives CSP, Permissions Policy, and optional COOP/COEP/CORP from it. App code cannot register, replace, or release this boot fact, and a second non-identical registration is a boot error. Direct library tests that omit the generated runner retain only the conservative non-isolated response posture; they cannot opt into cross-origin isolation without an explicit compiler manifest.

Dispatch order is normative and printable: /_m/<mutation-key> mutations, /_q/<query-key> typed reads, /c/__v/<representation-digest>/<module> immutable client modules, declared endpoint() exact/prefix mounts, route table, then the 404 shell. There is no user middleware chain in v1. Extension points that can affect control flow are declared surfaces — sessionProvider, guards, endpoint(), webhook() — so audits can print them and no request behavior is registered from a distance.

Generated Node public authority (normative). A generated standalone Node entry MUST ignore forwarded scheme and host headers by default. Behind TLS termination, the operator supplies either KOVO_NODE_ORIGIN as one canonical absolute HTTP(S) origin or the exact opt-in KOVO_NODE_TRUSTED_PROXY=1, never both. The fixed-origin posture reconstructs every Web Request with the pinned scheme, hostname, and effective port and ignores forwarded authority. The trusted-proxy posture accepts only the proxy-provided X-Forwarded-Proto scheme while retaining the validated inbound Host authority; X-Forwarded-Host remains untrusted. These variables MUST be snapshotted and validated before the authored handler graph is imported. Invalid, non-canonical, ambiguous, or combined posture fails process boot. Authentication deployments MUST additionally satisfy §6.6's exact configured-origin binding, so a trusted forwarded scheme paired with the wrong host or port is rejected before auth state is read or changed. The public Node adapter accepts only an absent origin or one fixed origin string. A per-request origin callback is not a reviewed authority door: it could reinterpret Origin, forwarded, or other hostile request fields as the trusted Web origin and is therefore rejected before it can run. The deployment's proxy, TLS-edge, cache, cookie-domain, schema-writer, and bootstrap assumptions are reported through the door-derived kovo check env contract in §11.4; configuring this adapter does not by itself discharge facts the command cannot observe.

Shared request-ingress decision (normative). Transport-source selection and hostile-value grammar are separate steps, and the supported source set is finite. An HTTP/1 Node source MUST snapshot the exact method, authenticated transport encryption bit, normalized Host, and exact raw Host count/value; it admits exactly one raw occurrence whose value is byte-identical to the normalized field and rejects pseudo-headers. An HTTP/2 Node source MUST snapshot the exact method, transport bit, :authority, and :scheme; it rejects ordinary Host, X-Forwarded-Proto, raw HTTP/1 Host evidence, or a non-HTTP/2 version. :scheme is exact lowercase http or https and, outside an explicit trusted-proxy posture, MUST match the authenticated transport bit. A generated Vercel Node source is a distinct HTTP/1 posture: one exact raw/normalized Host plus mandatory, canonical edge-overwritten X-Forwarded-Proto and X-Vercel-Forwarded-For; missing, ambiguous, or non-canonical platform provenance has no fallback identity. Synthetic/custom carriers MUST declare which enrolled source they emulate; a coincidental bag of fields is not source provenance.

A Fetch-native platform source instead consumes the exact method and canonical URL scheme/authority selected by the named platform-owned HTTP-to-Fetch bridge; it cannot recover or make claims about raw bytes that the platform normalized or discarded. Cloudflare's public edge bridge is the supported Worker source; unauthenticated HTTP Service Binding ingress remains unsupported. Unknown and mixed source postures fail closed before static serving or app import.

After source selection, live and emitted adapters MUST invoke the same finite classifier for method token, authority, and scheme grammar. An absent verdict, unknown source posture, ambiguous value, or lossy spelling is closed. The accepted verdict is immutable reconstruction input: one final target object supplies the Web URL authority and app-visible Host, including when an operator-pinned origin replaces the validated remote authority. No later sink may reread raw Host, :authority, or forwarding fields to make a second decision, and a future adapter MUST enroll its source posture and generated/source parity in the request-ingress C13 corpus before dispatching static or app code.

Adapter request-target identity (normative). The same classifier admits only (1) canonical origin-form path[?query] beginning with exactly one /, or (2) a canonical absolute-form HTTP(S) URL whose scheme and authority exactly match the selected ingress verdict. The accepted value is reconstructed as canonical origin form before routing. Scheme-relative, authority-form, non-HTTP(S) scheme-like (javascript:, mailto:), backslash, fragment, encoded dot/separator, WHATWG-normalizing, mismatched-origin, and otherwise lossy targets MUST fail closed. Kovo assigns no server-wide semantics to asterisk-form: OPTIONS * is explicitly unsupported and returns 400 before static or app dispatch. The 65,536-character and 10,000-query-entry ceilings apply before target parsing on raw Node sources and on the platform-preserved absolute URL for Fetch-native sources.

Vercel pre-filesystem ingress (normative). Every generated Vercel Build Output API v3 artifact, including a static-only build, MUST route all paths through Kovo's generated Edge Routing Middleware before handle: filesystem. That middleware applies the shared platform-Fetch method/URL/target classifier and target ceiling, returns a closed 400/414 response on failure, and uses x-middleware-next: 1 only after acceptance. A mixed build then enters the Vercel Node function's distinct platform-provenance posture. The function MUST prepare one immutable request snapshot and accepted verdict before static/app dispatch, and both its pre-static metadata and final Web Request MUST consume that same prepared value; carrier mutation after preparation cannot trigger a second source, scheme, authority, method, or target decision.

Adapter method identity (normative). HTTP method tokens are case-sensitive (RFC 9110 §9.1), while the Fetch Request constructor canonicalizes its standard methods and rejects several others. A raw-capable adapter MUST reject before static serving or app dispatch whenever its raw method token is invalid or cannot cross the Web Request boundary byte-for-byte unchanged. A Fetch-native adapter applies the same verdict to the platform-preserved method and relies on the named platform bridge—not Kovo—to preserve or reject the pre-Fetch raw token. Thus raw post and PoSt are distinct unsupported methods and MUST NOT become POST; exact POST remains valid, and a syntactically valid extension method is admitted only when Fetch preserves that exact case-sensitive identity. Live and generated adapters MUST share one classifier.

Adapter authority identity (normative). A Node or platform adapter MUST accept an inbound Host or HTTP/2 :authority only when it is one canonical serialized host[:port] identity that crosses URL parsing and the Web Request boundary byte-for-byte unchanged under either supported HTTP scheme. Percent-encoded, Unicode-to-IDNA, case-folded DNS, non-canonical IP, non-canonical bracketed IPv6, explicit default-port (:80/:443), user-info, path/query/fragment, duplicate, and otherwise ambiguous spellings MUST be rejected before static serving or app dispatch. Canonical lower-case DNS names, non-default decimal ports, and canonical bracketed IPv6 remain valid. The Web URL authority and app-visible Host MUST therefore expose the same one remote identity. Live and generated Node/Vercel adapters MUST share this rule. A Fetch-native Worker validates the canonical serialized URL authority delivered by its named platform bridge and reconstructs app-visible Host from that verdict; this is not evidence about a raw authority the platform already erased.

Pre-dispatch load shed (normative). Because there is no user middleware chain, the request shell/adapter itself owns a coarse limiter that runs ahead of replay lookup, schema parse/coercion, and the guard chain (§10.3) — guard combinators such as rateLimit({ per: 'session' }) shed load only after CSRF, replay, and parse have already paid out, and per: 'session' cannot distinguish a flood of null-session attackers, so they are insufficient as the only chokepoint. Before any /_m/, /_q/, endpoint(), or route dispatch the shell MUST enforce: (1) a maximum request/body size — a request exceeding it is rejected with 413 before the body is parsed; streamed bodies additionally have a hard 4,096-chunk budget and exceeding it is the same 413-class body-limit failure even when the byte count remains below the configured maximum, so adversarial transfer fragmentation cannot turn the byte limit into unbounded per-chunk work; (2) a serialized request-target ceiling of 65,536 JavaScript string code units and a 10,000-entry URL-query ceiling — Node/Vite/generated adapters MUST scan the raw target before constructing a Web Request, URL, or URLSearchParams, and a direct Web handler MUST scan Request.url before constructing URL/URLSearchParams; either target violation is rejected with 414, including for static and not-found paths; (3) URL-encoded body segments and multipart parts share the same default KV430 breadth ceiling of 10,000 entries, counted before record reconstruction, split, or part adoption, so a compact separator-heavy carrier cannot amplify into an unbounded parser graph; and (4) a coarse per-IP and global request-rate budget — a request over budget is rejected with 429 carrying Retry-After, before replay+parse. The framework-default per-IP budget exempts ordinary document/endpoint GET/HEAD dispatch: behind a CDN, NAT, or load balancer every visitor shares one source IP, so a default per-IP page budget rejects legitimate traffic wholesale; the mandatory global budget still applies to those requests, the /_m/ and /_q/ per-IP budgets are unchanged, and an app-authored requestLimits.perIp is enforced on every surface including document reads. defineKovo({ requestLimits }), its body-size gate, and every base or per-surface rate budget are mandatory finite postures and MUST NOT accept false; author-supplied maxima are bounded to 67,108,864 body bytes, 100,000 query/list result items, 1,000,000 requests per rate window, 100,000 retained rate keys, and an 86,400,000 ms rate window. These limits are normative defaults closed by assemble() (per-IP and global /_m/ and /_q/ request rates, max body size, and a bound on fragment-targets reconstructed per response, §9.1); the coarse limiter is identity-blind on purpose so it survives the anonymous flood the session-scoped limiter cannot. This pre-dispatch posture is enrolled in and printed by the endpoints audit. The fine-grained rateLimit guard combinator still runs in the guard chain for per-principal policy. It admits a per: 'ip' (and global) dimension in addition to per: 'session', so an anonymous or per-IP budget can also be expressed at the guard layer; the coarse shell limiter and the guard combinator compose rather than replace each other.

Node-family bridges MUST also close the Fetch GET/HEAD body-erasure gap before Web Request construction, static routing, Vite SSR/app loading, DB admission, task startup, or authored code. A GET or HEAD carrying a positive Content-Length, any Transfer-Encoding, or HTTP/2 HEADERS without END_STREAM is rejected with 413; Content-Length: 0 remains payload-free. A custom HTTP/2 carrier MUST provide an exact, pinned END_STREAM witness or fail closed for GET/HEAD. This grammar is deliberately finite and synchronous: the bridge does not wait for or indefinitely drain body bytes. When an HTTP/1 request is incomplete, it flushes the 413 response and closes the connection. Live Node/Vite and emitted Node/Vercel adapters MUST apply the same verdict.

The same pre-dispatch door MUST acquire one app-local occupancy slot and mint one framework-owned request deadline before any DB provider, request-body read, guard, transaction, or handler work. requestLimits.deadlineMs defaults to 30,000 ms and is a finite integer from 1 through 300,000; requestLimits.maxInFlight defaults to 256 and is a finite integer from 1 through 10,000. Neither posture accepts false. A request at the occupancy ceiling is rejected with 503 Service Unavailable and Retry-After: 1 before that work starts.

The admitted request's framework-owned AbortSignal MUST be the signal visible to authored request code and consumed by every Kovo-owned request effect door: outbound fetch (including bounded DNS wait), DB admission/provider wait and transaction checkpoints, deferred-region selection, response stream flush, and an adapter-owned final transport where Kovo controls one. The response-mint door MUST discard a handler result that loses the deadline/disconnect race. An unfinished response body MUST error and cancel its source at expiry. The Node adapter MUST additionally destroy an unfinished response transport at expiry and retain the occupancy slot through actual finish or close; its backpressure-aware pipeline therefore cannot turn a slow reader into an unbounded write. A Fetch-native adapter can bound only the Web response stream Kovo owns; the platform's post-handoff client transport is outside Kovo's proof.

Occupancy release is one-shot. It occurs immediately on deadline or ingress disconnect, on an exception before response mint, on direct-Web response-body completion/cancel/error, and for a bodyless direct-Web response at mint because no later transport receipt exists. When an adapter claims the final transport, body completion alone does not release the slot: actual transport finish/close does. Deadline/disconnect release does not assert that arbitrary authored work has stopped; it prevents that abandoned work from retaining admission forever and revokes the framework-owned capabilities it could otherwise continue to use.

This is cooperative cancellation, not JavaScript preemption. Kovo does not claim to terminate an arbitrary Promise, a synchronous loop, native extension work, an uncooperative third-party API, SQL already issued to a driver without cancellation support, or a transaction already committed. Such work may continue after its result becomes ineligible for the response. A hard guarantee over those cases would require app execution in a terminable worker or process.

Trusted-proxy per-IP identity (normative). When trustedProxy enables Kovo's built-in X-Forwarded-For, X-Real-IP, or RFC 7239 Forwarded source, the selected proxy-nearest hop MUST produce one canonical, address-only IPv4 or IPv6 key. A syntactically valid optional transport port is stripped (an IPv6 port requires bracketed address syntax), equivalent IPv6 spellings are canonicalized, and IPv4-mapped IPv6 is keyed as the corresponding IPv4 address. Unknown, obfuscated, malformed, duplicate, or otherwise ambiguous nodes MUST NOT mint a per-IP key; the mandatory global budget still applies when no trustworthy per-IP key exists. This classifier does not reinterpret the app-owned opaque key returned by an explicit requestLimits.clientIp callback. If more than one of the three built-in client-IP header families is present, the ingress is ambiguous and none of them supplies a key; this prevents an unstripped client-authored family from shadowing the family an operator-owned proxy appended.

Route matching is static-first at each path segment, and ambiguity is a compile error KV228 rather than a runtime precedence footnote. Trailing slashes normalize to one canonical path with a 308 redirect before matching. Page routes answer GET and HEAD; other methods on a page path are 405 because mutations own POST via /_m/.

The shell owns document assembly. The default document contains the doctype, <html lang>, route/query meta, page hints (stylesheet links, modulepreloads, optional speculation rules), initial <kovo-query> scripts before consumers, the page body, and — for a document carrying client surface (§4.4) — the inline loader. Apps may provide defineKovo({ document: { template } }), but the template receives assembled parts rather than a blank canvas, so it cannot silently drop loader or hydration contracts. Deferred streams use the same assembled shell parts; partials must not drift from full documents.

Unexpected-error shells are app config with safe defaults: 404, 403, and 500 documents may be supplied by the app, while unexpected failures still use the stable no-internals bodies from §9.2 when no shell is provided. The shell resolves db and sessionProvider once before route, query, or mutation guards; route/query guard failures use the §6.5 unauthenticated redirect and 403 contract.

Static export replays synthetic GET Requests through the same handler. An exportable route writes .html, referenced immutable /c/ modules, and static assets; there is no second render path. Export is L0/L1 only: a route with a guard, unproven session dependence, mutation-only interaction, or a param path without explicit static-path enumeration fails or skips loudly with KV229 according to the configured export policy. Exported documents disable server refetch assumptions; the no-JS document is the artifact.

Static subresource integrity (normative). Once exact bytes are known, static export adds a SHA-384 integrity value to first-party module-script, modulepreload, stylesheet, and style-preload tags. An authored integrity assertion is accepted only when there is exactly one ASCII-case-insensitive integrity attribute and its decoded value exactly equals the computed hash. An empty placeholder, duplicate, malformed value, stale hash, or mismatch aborts export; the build never hides a disagreement by deleting or replacing author text. A tag with no authored assertion receives exactly one computed value before the artifact is published.

9.5.1 Dev HMR#

Hot module reloading is a dev-only request-shell enhancement over Vite transport. It is not a client render graph, hydration mode, or router. Vite's websocket may carry Kovo custom events, but every DOM-changing hot action still asks the app shell for server-owned route, query, or fragment output before morphing. Unsupported or unproven edits delegate to Vite's full reload.

Kovo proves security posture per commit, not per keystroke. Whole-project analysis — the data-plane analyzers of §10.2/§10.3/§11.4 and the project query/mutation fact census of §5.2 rule 10 — MUST stay off the dev hot-update blocking path: the edited module is staged and served against the last-committed fact snapshot, the analysis re-runs asynchronously after edits settle, its findings surface as teaching diagnostics when they land, and a changed fact snapshot invalidates derived modules and publishes a full reload so the served output converges to the proven snapshot. Because a dev-served page may therefore precede its analysis, every dev response MUST carry the explicit Kovo-Dev-Posture: dev-unproven header — a dev page is never a posture proof. kovo check and kovo build are unaffected: they derive every fact synchronously from the current authored source and fail closed (§5.2 rule 9).

The app-facing dev API is a convenience wrapper around the compiler plugin and the app-shell dev plugin. App authors should not hand-wire generated refresh registries, HMR endpoints, or client module maps into defineKovo() or assemble(): the request shell remains the owner of dev serving, diagnostics, and refresh dispatch. The wrapper wires compiler diagnostics into the same dev diagnostic ledger used by page, fragment, and mutation requests, so a failed hot update and a failed direct request render the same teaching document.

The supported kovo dev runner MUST bind the exact canonical HTTP origin of its owned loopback listener after the socket is listening and before it loads the live app graph. A generated Better Auth constructor with no explicit BETTER_AUTH_URL uses that one-shot framework fact, so an ephemeral or conflict-shifted port remains identical to the Local URL printed by the runner. The fact accepts only localhost, IPv4 127/8, or [::1] with the listener's actual effective port. It MUST NOT be derived from Host, Origin, Forwarded, or X-Forwarded-* request fields, and app code cannot replace it after binding. Outside the supported runner, a Better Auth app MUST configure BETTER_AUTH_URL explicitly. An explicit development value remains fixed and validated; every non-loopback value and every production value remains an explicit canonical HTTPS origin.

HMR impact classification is compiler-owned and fact-based. After parsing, impact decisions must use typed lowering facts (§5.2 rule 9), not source-string heuristics. The impact ladder is: server fragment/query refresh for a proven compatible live target; current-route document refresh when the route shell is still compatible; kovo:diagnostics for compiler errors; and kovo:full-reload for route table, app shell, query-plan, app build token, generated-registry, bootstrap, stylesheet topology, pending optimistic work, missing fact, or any other unsafe change.

The stable dev event vocabulary is: kovo:component-render, kovo:route-shell, kovo:diagnostics, and kovo:full-reload. Events carry the source file, old/new client module hrefs when known, the impacted component/live-target ids when proven, diagnostics summary when present, and old/new app build tokens and render-plan fingerprints when available. Stale events whose token does not match the current document are rejected and escalate to full reload.

The dev-only browser entry is served or injected only by the Vite dev stack. It must be absent from production builds and static export artifacts. Dev refresh endpoints are likewise Vite-dev-only and must reuse existing app-shell render, query, live-target renderer, and fragment-wire code; production createRequestHandler() never exposes HMR endpoints. Live-target refresh accepts POST only. Every route or live-target refresh response, including method and authorization failures, MUST carry Cache-Control: private, no-store and Vary: Cookie, because the bytes can depend on the resolved session, route guards, queries, and component render context even in development.

An HMR live-target refresh carries the current document token in Kovo-Build like every other target-bearing request. The Vite-dev-only endpoint may additionally accept the explicit oldBuild URL parameter as the prior-render selector needed while an update is crossing builds. That exception does not exist in production request dispatch. A malformed HMR snapshot, response envelope, missing build token, or response-token mismatch is never partially applied; it escalates to full reload.

9.6 Durable tasks and scheduling#

task() is the durable background-function primitive for non-transactional side effects. A task is a typed registry entry with an input schema and a run(args, ctx) body; no opaque closures cross the boundary. Task code may perform external I/O, but task DB writes must go through ctx.runMutation(...) and reads through ctx.runQuery(...), so every data change still reuses the audited mutation/query surfaces (§10.2, §10.3). A task context may schedule more tasks, use external fetch/storage/secrets capabilities, and receive a stable job id for external idempotency keys; it does not receive the caller mutation's transactional db. The framework also exposes the current one-based claimed attempt as immutable ctx.attempt. It is runner-owned retry metadata, not app authority: authored code may pass the direct scalar through the compiler's finite plain-input grammar to an exact local task/query/mutation declaration, while aliases, writes, computed access, proxies, and retention fail closed.

request.schedule(task, args, opts?) is the only built-in way for a mutation handler to arrange post-commit work. Scheduling writes a durable job row in the same transaction as the mutation's data: commit means the job is ready to run, rollback means the job was never enqueued. The scheduled args are validated by the task's input schema and serialized data, not captured process state. opts.afterMs / opts.at set run_at; opts.key gives a witnessed ScopedKey pending-job identity (§6.6). Principal work derives it from ctx.actAs(id).stateKey(appKey) (or the equivalent framework request authority); deliberately shared work uses the named public posture, and framework recurrence/system work uses only a finite registered posture. Strings, casts, proxies, forged structures, malformed persisted frames, and reason-string system namespaces fail KV450 before the queue is consulted. The queue persists the complete canonical scope frame in logical_key, and the unique ready-job identity is (task_key, scoped-key-frame). The default keyed behavior is debounce: a ready job with the same complete frame has its run_at and args replaced by the latest schedule. coalesce: 'throttle' keeps the earliest ready job and its first args. Equal app keys under different principal/public/system authority never coalesce. A running or already-finished job is never mutated; re-scheduling creates a new ready job. request.schedule returns a typed handle, and request.cancel(handle) transactionally cancels a still-ready job and returns whether cancellation happened.

The default node JobRunner drains the queue from Postgres with FOR UPDATE SKIP LOCKED, leases, retry/backoff, and dead-letter rows. Multiple nodes may run the same drainer; row locks make claims disjoint. Each job persists the declaration's positive maxAttempts ceiling. A lease reaper returns an expired running job to ready only while its claimed-attempt count remains below that ceiling; at the ceiling it moves the job to dead, so a task that repeatedly kills its worker cannot redeliver forever. A false or failed heartbeat means the worker lost its lease: the runner aborts the AbortSignal delivered as ctx.signal, propagates that signal through framework query/mutation ingress, and refuses later framework state operations from that context. Task-authored external I/O must likewise carry ctx.signal; arbitrary JavaScript cannot be forcibly preempted. Completion and failure writes remain fenced by the claim's owner/token, and a rejected markSucceeded or markFailed settlement is reported through task-runner diagnostics rather than silently discarded. Delivery is therefore at-least-once. Exactly-once effects are obtained by idempotency: Kovo derives a stable idempotency key per scheduled job and exposes the job id as the key a task passes to non-idempotent external APIs. A retry must not double-charge, double-send, or otherwise commit an effect without an idempotency key.

The in-process runner's lazy startup is itself pre-dispatch work. It MUST begin only after the triggering request passes the coarse rate, target, and complete streamed-body admission gates from §9.5; a rejected request MUST NOT resolve or provision the task database. Startup receives only a request-free admission signal: the root queue database resolves through the app-root provider with no request carrier. Every background runQuery / runMutation lifecycle and runner diagnostic uses a newly constructed framework-owned, bodyless, credential-neutral Request at the exact non-public URL https://kovo.invalid/_kovo/task; a remote request's scheme, authority, path, headers, body, session, or other ambient browser/machine authority MUST NOT seed it. Task lifecycle MUST NOT call the app's sessionProvider. Its only principal authority is the framework-minted explicit actAs(id) or declared-system posture, attached before the per-operation app.db provider resolves; the app-root queue handle is not substituted for that scoped provider resolution. A transient startup failure may be retried by a later admitted request, but rejected traffic cannot drive that retry loop.

Every preset that supports task() MUST declare a JobRunner capability. The node preset's in-process runner is on by default; a runner-only mode may drain jobs without serving HTTP. A preset with no runner capability MUST fail closed at build time when task()/schedule() is used, with an actionable diagnostic; it must never silently enqueue work that no deployed artifact can run. Runner capacity is bounded by the DB pool, per-task concurrency, priority lanes, and task timeouts/leases. Delayed self-reschedule carries a lineage generation counter with a conservative default ceiling and a delay floor, so polling/saga loops are explicit and runaway loops dead-letter instead of hammering the database.

The capability declaration above is framework-internal build authority. The public value returned by a built-in preset factory is only the opaque selection token defined by §6.6: it exposes neither the JobRunner record nor any inspection/emission callback. Build preflight resolves the exact token and checks the internal capability; app-authored structural or copied preset objects cannot declare a runner and cannot bypass the missing-runner diagnostic.



Data Plane (SPEC §10)

This file is incorporated by reference from ../SPEC.md and is normative for Kovo framework behavior. The root spec remains the entry point and cross-reference index; this module owns the detailed contract below.

10. Data Plane#

10.1 Schema as domain registry (Drizzle-blessed path)#

ts
// schema.ts
export const carts = pgTable('carts', { id: text('id').primaryKey() /*…*/ });
export const cartItems = pgTable(
  'cart_items',
  {
    /*…*/
  },
  kovo(() => ({ domain: 'cart' })),
);
export const products = pgTable(
  'products',
  {
    id: text('id').primaryKey(),
    stock: integer('stock').notNull(),
  },
  kovo((columns) => ({ domain: 'product', key: columns.id })),
); // row-level invalidation key

Tables default to a same-named domain; annotations group tables into logical domains and declare key granularity. App-authored domain()/tag() declarations derive their default stable names from their exported binding plus module path (§4.1); explicit domain annotation strings remain for shared schema vocabulary where several declarations intentionally speak the same invalidation currency. The reverse index (table → domain), the DomainKey type, and key extractors are all generated from this single file. An optional owner: annotation (kovo((columns) => ({ domain: 'cart', owner: columns.userId }))) names the column tying a table's rows to a principal — it powers the unscoped audit (§10.3). An optional governed: annotation (kovo((columns) => ({ domain: 'account', governed: [columns.role, columns.balance] }))) names columns that may only be written from a server-derived value — the declare-once mass-assignment fact (the primary key: and owner: columns are governed automatically); it powers the KV438 write-provenance gate (§10.3). Optional atomic:/version: annotations (kovo((columns) => ({ domain: 'product', atomic: columns.stock, version: columns.lockVersion }))) name a contended value column and an optimistic-concurrency counter — the declare-once lost-update facts that power the KV429 TOCTOU gate (§10.3).

kovo accepts exactly one inline annotation callback. Its parameter is the concrete Drizzle column record for the table being declared; every column-bearing field uses a direct identity from that record, never a string, an unknown selector, or a structurally forged column:

ts
export const orderItems = pgTable(
  'order_items',
  {
    id: text('id').notNull(),
    orderId: text('order_id').notNull(),
    revision: integer('revision').notNull(),
  },
  kovo((columns) => ({
    atomic: columns.revision,
    domain: 'order-item',
    key: [columns.orderId, columns.id], // ordered composite row key
    ownerVia: {
      fk: columns.orderId,
      parent: orders,
      parentKey: orders.id,
    },
    version: columns.revision,
  })),
);

key is one concrete identity or a non-empty ordered list for a composite row key. owner, governed, secret, confidentialAtRest, atomic, version, and each fan-out via use the annotated table's callback identities. ownerVia.fk uses that same child identity, ownerVia.parent is one concrete Drizzle table, and ownerVia.parentKey must be a concrete column of that exact parent. The public types carry module-private witnesses so typo and wrong-table references fail during ordinary TypeScript authoring. Kovo SQL handles likewise use private witnesses over Drizzle's typed SQL<T> bridge; app-authored structural lookalikes are not valid handles.

Those witnesses are ergonomics and defense-in-depth, not the security proof (§6.6). The compiler independently resolves the callback parameter, table binding, column member, and SQL construction from authored source. Runtime metadata extraction independently compares each concrete Drizzle column's owning-table identity and physical name against the exact table being registered, then binds the result to compiler-emitted security facts. Either layer fails closed on unresolved, cross-table, or structurally forged values.

A table may opt out of domain mapping with kovo(() => ({ exempt: true })) (silencing KV404 for writes — append-only logs, outbox tables), but exemption is write-side only. An exempt table has no domain, so no write can ever invalidate a query reading it; a query whose read set includes an exempt table is therefore error KV411 — the silent-staleness bug §10.6 exists to kill, reintroduced through the exemption. The teaching error's fix is to map the table after all: for an append-only log this costs nothing — inserts then invalidate exactly the timelines reading it. exempt is reserved for tables nothing queries.

10.2 Queries#

ts
// cart.queries.ts — `app` owns request/session/DB/env inference
export const cartQuery = app.query({
  access: [app.authenticated],
  load: (_input, { db, request }) =>
    db
      .select({
        count: count(cartItems.id),
        items: jsonAgg(cartItems),
      })
      .from(carts)
      .leftJoin(cartItems, eq(cartItems.cartId, carts.id))
      .leftJoin(products, eq(products.id, cartItems.productId))
      .where(eq(carts.id, request.session.cartId)),
});

// product.queries.ts — parameterized: args declared once, schema-style
export const productQuery = app.query({
  args: s.object({ id: s.string() }), // coerced wherever args arrive: props, route params, /_q/ search params (§9.4)
  access: [app.authenticated], // checked at page render AND at every typed read / live push
  // guards receive the validated instance key (§10.3): app.owns((a) => a.id, products.id)
  load: (args, { db }) =>
    db
      .select({ name: products.name, stock: products.stock })
      .from(products)
      .where(eq(products.id, args.id)),
});

The app-scoped query signature is load(input, context). input is inferred from args and is undefined for an unparameterized query. context exposes the contract's precisely inferred, validated request, session, read-only managed db, read-only env, and request signal. There is no alternate positional (db, args, request) app signature. The named QueryHandle<Input, Result> carries the inferred JSON result without publishing loader callbacks or private registry metadata; binding it in a component supplies that result under the declared query property name.

Derived from this one expression, statically:

  • Registry key from the exported binding plus module path (§4.1) — this key is the generated QueryRegistry identity and the base name for /_q/<key>, <script kovo-query>, <kovo-query name>, kovo-deps, and explain output.
  • Read set {cart, product} — the JOIN is the declaration (forgetting a joined entity's dependency is unrepresentable).
  • Result type from the select shape — drives the client JSON, data-bind paths, derive inputs, and optimistic transform parameters. Query results are JsonValue-bounded client wire payloads; app-authored query().load results may use named interfaces and readonly JSON arrays/objects, but they cannot carry Date, Map, functions, class instances, or other non-JSON values. A column rename in schema.ts propagates through TypeScript static checking to every template. Opaque projections are the read-side raw-SQL seam: Drizzle's sql<T> generic is an unchecked assertion, so any sql/raw projection requires a declared s.* output schema (KV410), and the observed result shape is runtime-verified (§11.2). An opaque projection also hides which tables it reads, so its output schema says nothing about source tables; a KV410 site MUST therefore additionally declare a reads: table set — the exhaustive set of tables/relations the raw read touches. The reads: set is statically checked against exemption (§10.1): a reads: entry naming an exempt table is KV411, exactly as a statically-visible join would be, so an opaque projection cannot smuggle an exempt/outbox read past the static pass. The declared reads: set is folded into the query's read set (§11.1) and drives invalidation; a KV410 projection with no reads: declaration is itself a KV410 error. A query whose opaque projection reads a table absent from reads: is a CI failure under runtime verification (§11.2). The inferred-type chain stays sound or the seam is visible; never both unsound and silent.
  • Nullable result contracts. A query whose successful result may be null declares output: s.nullable(inner). The wrapper admits exactly null or a value accepted by inner, preserves T | null in the query handle, and contributes nullable query-shape metadata so the compiler continues to require optional traversal or explicit null handling (KV227). undefined is not admitted by this wrapper, and a local lookalike cannot mint framework query-shape facts.
  • Instance key from client-visible args.*. The full identity is name:keyValue. For a genuine s.object args schema, Kovo derives keyValue from the parsed args in schema-declared field order with this typed, collision-free grammar (lengths are JavaScript UTF-16 code units): each present field is f<length>:<k<length>:<field><value>>; string, finite-number, and boolean values are s<length>:<text>, n<length>:<text>, and b1:0|b1:1; a dense scalar array is a<length>:<concatenated scalar frames>. Missing and undefined optional fields are omitted, while a declared empty args object produces an empty keyValue, so name: is valid. Thus { id: 'p1' } for query product is product:f10:k2:ids2:p1, not a delimiter join that can alias strings, scalar types, optional positions, or array boundaries. A custom structural schema or unsupported value shape MUST declare instanceKey; compiler-generated keyed optimism accepts only compiler-resolved genuine s.object args using the same core-owned codec and rejects an authored instanceKey it cannot prove equivalent. This full key remains the single currency for the client store (<script kovo-query="product:f10:k2:ids2:p1">), optimistic transform keys (§10.4), and live-push routing. On kovo-deps and Kovo-Targets, it is carried as the key field of the exact {query name, full instance key} dependency encoding from §9.1; it is never recovered by splitting a colon-shaped string. Two instances of one query coexist on a page; data-bind inside an island resolves against that island's instance.

Args bind locally (Constitution #2). A component declares how its args derive from its own props — queries: { product: productQuery.args((p) => ({ id: p.productId })) } — so any page rendering the component satisfies the dependency without call-site knowledge. Route params reach queries as ordinary props through route().page; no call site enumerates query dependencies.

Queries are the UI data contract. A query-backed component's declared queries must contain the data needed to render that component. "Skinny" queries maintained only for optimistic derivation plus separate page/region loaders for presentation are rejected for ordinary app code: they split the server-truth render path from the statically declared dependency graph and force app authors back into manual fragment routing. The compiler may derive optimistic transforms, deltas, or §4.8 update plans for only the fields and query shapes it can prove; unproved presentation fields still travel through the same declared query and refresh via full server fragments.

Default-deny access decisions (normative)#

Authorization is default-deny, by construction. Every request-reachable surface — a query, a mutation, a route page, an endpoint, or a webhook — MUST carry an explicit access decision. A surface's decision is satisfied by any one of:

  • an access guard chain — one or more self-naming executable guards declared in access: [guard("name", fn), …] on the surface (or inherited from a parent layout);
  • a public decision — access: publicAccess("reason"), declaring the surface intentionally reachable without authentication, with a human-readable justification recorded in the ledger;
  • a verified machine-auth decision — access: verifiedAccess, or (for an endpoint/webhook) an auth:/verify: scheme that authenticates a machine caller.

These alternatives are mutually exclusive on one declaration. A query, mutation, route, or layout MUST NOT author both canonical access and the legacy top-level guard; doing so is KV436 at the static gate and MUST also fail closed in the public constructor and app snapshot. In particular, publicAccess(...) or verifiedAccess MUST NOT suppress an authored guard by runtime precedence. Authors compose executable guards in one access: [guard(...), ...] chain (or, while using the legacy field alone, one guards.all(...) guard). Endpoints and webhooks do not accept the legacy guard field at all; their executable guard chain belongs in access.

App-scoped factories accept only the canonical access shape. Their executable guard values come from the contract-bound algebra (app.authenticated, app.role, app.rateLimit, app.owns, and app.all) or another documented exact guard value. The legacy guard spelling exists only while reading pre-contract primitive source during migration and is not a second app-contract behavior.

A surface with none of these is undecided: the static app graph classifies it decision: 'missing' and the build fails with KV436 (§11.3). An existing guard already counts as a decision — guarded surfaces are not forced to re-declare access. The decision is recorded as a static graph fact (graph.access) that the build derives from each surface's source-captured access or legacy-guard posture; kovo explain access renders the full ledger (every surface, its decision, the names of the guards that actually execute, and any public justification), and a reviewer audits the public set before ship.

This is by-construction: the unsafe state (a request-reachable surface with no access decision) is unrepresentable in a passing build, proven by the static graph fact rather than a TypeScript brand (the compiler runs no type checker, §6.6). The audited decision MUST be the enforced decision: access guard names are attached to the executable guard values, runtime dispatch runs those same guards, and audit-only guard labels are not an accepted access decision. The proof is completeness, not correctness: KV436 proves a decision exists, never that it is right — a no-op return true guard satisfies it. Row ownership / IDOR correctness remains KV414's obligation (§10.3), and publicAccess reason strings are greppable, so they MUST NOT carry sensitive operational detail.

SQL statement safety on managed DB handles#

Framework-managed DB handles — req.db, query loaders, mutation domains, endpoint/webhook request handles, and blessed-adapter wrappers — treat executable SQL text as a typed surface, not an arbitrary string channel. The ordinary accepted forms are: Drizzle query builders and native SQL objects that keep text separate from bound parameters, Kovo tagged-template SQL values (sql and staticSql), and the single audited trustedSql(...) escape hatch. KV406/KV410 remain the freshness/read-write proof diagnostics; KV422 is distinct and answers how executable SQL text was constructed before it reached a managed handle.

App-provided driver streaming/cursor values such as node-postgres Submittable objects are not an accepted v1 SQL carrier. Large owner-scoped reads, when needed after v1, must use a framework-owned SQL-level cursor that reconstructs DECLARE/FETCH statements through the same managed carrier boundary rather than blessing a driver-polymorphic app object.

Scalar/runtime values MUST bind as parameters, never by interpolating bytes into SQL text. Identifiers, operators, sort directions, and clause fragments are not scalar values; they MUST come from static schema facts or typed allowlists such as sql.identifier(..., { allow }) / sql.allow(...), never directly from request strings.

For the static analyzer and explain/audit surfaces, the source set is the request-derived boundary: input, req.search, req.params, form bodies, headers, and cookies. The sink set is every framework-managed SQL construction/execution boundary: db.execute(...), db.query(...), db.exec(...), db.prepare(...), sql.raw(x), sql.identifier(x), and untagged template/string assembly routed into SQL execution. A request-derived or otherwise unproven value that can become executable SQL text at one of those sinks is KV422.

Non-goals are explicit. Kovo does not sanitize arbitrary SQL strings into safety; it requires parameterization, a typed allowlist, or an explicit trustedSql(...) brand. It does not prove safety for driver handles captured before the framework wraps them. Second-order injection is out of scope: a value read back from the database and later re-used in another query is governed by the same sql\`/trustedSql(...)` discipline at the second query site, not by request-taint tracking across storage.

10.3 Mutations & writes#

Open design decision: The domain-write declaration shape in this section is the target contract, not the current app-facing root API. @kovojs/server does not ship write() or tag() from its root public surface until the same change also ships the static enforcement and generated routing that make those declarations authoritative. Until then, authored apps use mutation handlers with analyzed Drizzle writes plus explicit registry.tables/registry.touches on opaque write sites.

Declared raw secret reads have one finite app-authored grammar (normative). The only authored spelling that may attach audited secret-read authority to raw SQL is this exact same-block sequence:

ts
import { sql, trustedSql } from '@kovojs/drizzle';
import { declareSecretReadCapability } from '@kovojs/server/secret-reading';

const statement = trustedSql(sql.raw('select id, classified from accounts'), {
  justification: 'reviewed raw secret read',
});
declareSecretReadCapability(statement, {
  columns: ['classified'],
  justification: 'review classified values on the server',
  source: 'accounts.classified',
  table: 'accounts',
});
const rows = await context.db.rawRead(statement, { reads: ['accounts'] });

declareSecretReadCapability MUST be an exact, unaliased named import directly from the semantic @kovojs/server/secret-reading subpath. statement MUST be one immutable const initialized by the exact trustedSql(sql.raw(<string literal>), { justification: <string literal> }) composition. That declaration, exactly one declareSecretReadCapability call, and exactly one directly awaited rawRead call on the handler's compiler-recognized request-scoped DB (context.db or request.db, as supplied by that callback signature) MUST occur in that lexical block and in that order. The rawRead options MUST be an exact inline { reads: [...] } object whose entries are non-empty, duplicate-free string literals; the set MUST include the declared table and exhaust every relation the statement actually reads. The literal SQL MUST reference the declared table. At runtime, the same declaration is rejected if the executed SQL omits that table or references any other secret-bearing table; one declaration cannot widen privileged read authority across secret tables. Aliases, namespace imports, re-exports, lookalikes, statement escapes, computed/optional/extracted members, spreads, dynamic metadata, duplicate uses, and missing or extra arguments do not establish this authority. The legacy app-authored .all(statement) and .execute(statement) spellings are closed; managed rawRead is the sole execution door. This declaration is audit-grade authority, not a proof or declassification (§6.6): runtime enforcement still boxes secret result fields (§11.2), and no result may cross the public wire without an independently named reveal or an owned sink that enforces its own policy.

ts
// cart.domain.ts — ALL writes flow through here (error KV330 bans db access in handlers)
export const cart = domain({
  addItem: write(async (db, cartId: string, productId: string, qty: number) => {
    await db.insert(cartItems).values({ cartId, productId, qty })
      .onConflictDoUpdate({ target: [cartItems.cartId, cartItems.productId],
                            set: { qty: sql`${cartItems.qty} + ${qty}` } });
    await db.update(products)
      .set({ stock: sql`${products.stock} - ${qty}` })
      .where(eq(products.id, productId));
  }),
  // Statically un-analyzable writes REQUIRE declaration, runtime-verified.
  // Raw-SQL writes MUST enumerate every table they touch via `tables:` — a
  // structurally-parsed allowlist the executor enforces (§11.2); `touches:`
  // names the resulting domains. The executor parses each emitted statement
  // and FAILS CLOSED (conservative whole-domain invalidation of `touches`,
  // plus a CI failure) on any production write to a table outside `tables:`.
  merge: write({ tables: ['cart_items', 'carts'], touches: ['cart'] }, async (db, …) => {
    await db.execute(sql`/* gnarly CTE */`);
  }),
});

No touches on addItem, no invalidate() in handlers. The static pass (§11) extracts {cart_items→cart, products→product} from the AST; calling cart.addItem is the invalidation declaration. invalidate() survives only as a linted escape hatch for external-system effects (e.g., a Stripe webhook changing data Kovo should refresh).

touches/tables declarations on opaque writes are statically required, not best-effort. A write site whose touch set is not fully statically resolved — an 'unresolved' runtime-flowing table value (§11.1 step 2.E) or a call into node_modules carrying a db arg (§11.1 step 3) — is error KV406 when it lacks a manual touches; the dev/build/export gate blocks until one is supplied. A raw-SQL write (db.execute(sql) / opaque projection write) MUST additionally declare tables: — the exhaustive set of tables the statement mutates — which the runtime executor parses and enforces (§11.2). On a production write to a table outside the declared tables:, the executor MUST fail closed: invalidate every domain in touches conservatively (whole-domain, ignoring key granularity) so no reader is left silently stale, and record a CI-failing violation; it MUST NOT skip invalidation on the unexpected table. Dev/test instrumentation under-approximates (executed branches only, §11.2), so passing dev/test coverage does not prove KV406 completeness — an unexercised conditional raw-SQL arm that writes an undeclared table is exactly the case the tables: allowlist and the production fail-closed rule exist to catch, since the dev cross-check never observes it.

For managed Postgres/PGlite, declared write scope has an engine backstop for the dangerous cases. The writer role is not blanket-granted over the schema: owner/owner-via tables receive writer privileges only with RLS USING/WITH CHECK policies that bind the row to the current principal, and unclassified/reference tables receive no writer grant. reference: true is reserved for immutable global lookup rows that contain no tenant membership or owner graph data. Team/org membership is tenant data and MUST be modeled as an owner/ownerVia/authzPolicy table so reads are scoped and request-time create/revoke flows stay behind an explicit policy. Therefore an out-of-declaration write to another principal's row, an ownership reassignment, or an unclassified table such as verification is denied by the database. The remaining declared-write wrapper obligation for benign over-declaration among writable owner/authzPolicy tables is coverage and invalidation honesty, not the primary confidentiality/integrity boundary.

Compiler-bound custom policy authority (normative). Generated dev/build/export paths MUST bind each authzPolicy into the compiler-owned table-security manifest as an exact discriminated value. A literal string is an exact guard assertion for SQLite only, where Kovo's bounded authorizer owns the declared guard posture. A Postgres table MUST instead supply an engine predicate that static analysis can canonicalize as zero-parameter literal SQL (currently a no-substitution SQL tagged template or sql.raw(<literal>)). A Postgres string assertion cannot produce row-level security and is KV414 at compile time and KV433_AUTHZ_POLICY_UNSUPPORTED at runtime before any grant or listener. Dynamic or interpolated policy authority also fails closed. Runtime boot MUST compare the live Drizzle callback projection with that exact manifest value once, then derive every RLS/grant/posture sink from the immutable compiler snapshot. It MUST NOT re-read the callback slot after comparison; callback replacement between validation and asynchronous policy installation cannot weaken the emitted engine policy (C9/C15, §6.6).

Finite generated owner-policy correspondence (normative). Framework-generated Postgres owner policies are exactly a two-constructor algebra: ownerColumn(table, column) and ownerVia(table, fk, parentKey, parentTerm). A term contains exactly one owner/principal equality and at most four ownerVia edges; a deeper chain is KV414 before policy installation. Kovo MUST render the owner/owner-via RLS predicate and derive its framework-owned ownsRow evaluator from the same immutable term. Its decision gate enumerates the complete three-valued domain — {true,false,null} for the owner equality and {present,absent,null} for every relation edge — so the exact model count is 3^1 · 3^e (at most 243), not a sample or an SMT approximation. SQL EXISTS admits only a TRUE inner predicate; missing/null edges and NULL/unset owner comparisons therefore deny. Real PGlite evidence MUST materialize every enumerated model under the actually generated policy on a FORCE ROW LEVEL SECURITY table and compare observed visibility with that denotation.

This is deliberately not a broad guard≡RLS claim. The current public guards.owns(keyOf, ownsRow) callback is app-authored and every explain record marks it unproven until a separately reviewed migration binds the framework-derived evaluator. Hand-authored authzPolicy, arbitrary ownership callbacks, the system/admin USING (true) policies, and guards.role() all lie outside the decided fragment. Authorization explain records MUST place the generated RLS predicate and explainGuard facts side by side and name that status. They MUST also warn that the managed transaction frame writes kovo.role while no generated RLS predicate reads it; session-role guard success is not SQL authorization. The engine's FORCE-RLS/effective-privilege closure remains the enforcement boundary.

Finite grant-transition model (normative). Build MUST derive one grant graph from the same compiler-owned table-security manifest and mutation extraction that feed RLS and the touch graph; an app-authored second grant registry is forbidden. The modeled principal kind is the current request principal. Each owner table contributes an exact resource with {delegate, owner, read, write} rights, each ownerVia table contributes {delegated-owner, read, write} plus a read/write delegation edge from its parent resource, and each authzPolicy table contributes {policy, read, write}. public and reference tables do not become authorization resources. Every extracted mutation write, every exact registry.tables declaration, and every declared touch whose domain contains one of those resources MUST contribute a transition row; domain-level declarations conservatively select every authorization-bearing table in that domain.

The decided fragment is deliberately small. An exact Drizzle delete from an owner or ownerVia resource, with no unresolved receiver/helper path for that mutation, has the successor right-set . The checker MUST enumerate the complete local powerset P(R) and verify successor ⊆ predecessor for every state. The bound is 64 states (2^|R|, currently at most 16), and exceeding it fails closed rather than sampling. Exact insert and update operations and an explicitly table-declared raw-SQL transition lie outside that proof and MUST appear as stable named escapes with budget=1 and a retained review obligation. A custom authzPolicy does not establish positive-grant semantics, so even its exact deletion is not in the decided fragment. Any opaque helper/receiver flow, unresolved operation, authorization-bearing domain touch without an exact operation witness, or other unclassified write is ; kovo check MUST fail it with KV414 before production artifacts are emitted. kovo explain grants MUST print the derived principals, resources, delegation edges, checked-state counts, reasons, and named escape budget.

Attenuating delegation (normative). createDelegationAuthority is only a bridge from rights an already-passed guard/RLS door established; it does not grant database authority and MUST NOT be accepted as a substitute for that door. It snapshots an exact nonempty finite kind:resource set, the acting identity, a revocation principal, and that principal's persistent epoch into a module-private framework receipt. onBehalfOf accepts only such a receipt, rechecks the authoritative epoch without a positive cache, and mints a child only when every requested right is a runtime member of the parent set. Structural casts and widened sets fail. The TypeScript subset relation is an authoring guardrail; the private receipt, runtime subset test, and epoch check own enforcement.

The honest claim is only that, in the exact positive owner/ownerVia deletion fragment, the compiler-derived local right-set cannot grow, and that framework-receipted delegation cannot widen its parent set before the bound epoch changes. Kovo does not claim arbitrary authzPolicy meaning, audited escape safety, global HRU safety, or safety against an external schema writer. The live FORCE-RLS/effective-privilege closure remains the authorization enforcement boundary.

Engine-door completeness (normative). Kovo may claim the storage engine is the sole authorization/confidentiality door only when the runtime itself holds no superuser/BYPASSRLS authority and cannot assume a privileged provision/admin role, and when a closure audit over the engine's actual grant graph proves that every object reachable by the app roles is one of: (i) a base table under FORCE ROW LEVEL SECURITY with a live kovo policy; (ii) a proven security_invoker view/function whose reachable base relations are themselves in that safe set; or (iii) a relation declared through the reviewed public escape, declarePublicRelation(...), and surfaced as a publicRelation row in kovo explain capabilities. The audit MUST ask the engine's finest-granularity effective-privilege oracle instead of lossy grant views or direct-grant rows: table and column reachability both count for relations, PUBLIC and role membership count for every privilege decision, sequence reachability is audited separately from relation reachability, and SECURITY DEFINER routines are scanned across all non-system schemas. Reachable objects that cannot enforce RLS, including materialized views, foreign tables, unsupported relation kinds, non-allowlisted sequences, and reachable SECURITY DEFINER routines, MUST fail closed. App roles MUST also hold no unexpected privilege on other ACL-bearing catalog objects or default privileges that would create future reachable objects outside the audited relation/routine/sequence set; such grants are refused rather than ignored. Build-time lints and source enumerations remain defense-in-depth only — never the thing the authorization/confidentiality guarantee rests on.

Production database driver floor (normative). In-process PGlite is a dev/test-only, single-tenant database. Its bootstrap identity is necessarily superuser because PGlite has no connection-authentication boundary; an app-authored raw new PGlite(dataDir) handle therefore sits outside Kovo's owner-scoping and confidentiality guarantee and is only warned by the defense-in-depth raw-driver lint. A runtime whose bootstrap-pinned operator environment reports NODE_ENV=production MUST refuse the PGlite driver before opening a listener or serving static or dynamic traffic. Production requires KOVO_DATABASE_URL to select external Postgres, and the external runtime login MUST pass the non-superuser/NOBYPASSRLS/non-admin-membership boot invariant and the reachable-object closure audit above before the app serves. This relocation preserves zero-dependency local development without claiming that an in-process superuser can be made into a production least-privilege identity.

Every managed Postgres connection string — runtime, admin, system, provisioning, migration, and posture-check alike — MUST explicitly name a nonempty authority login, a nonempty database path, and a decimal port. Query-string login and database overrides are forbidden. Non-local URLs additionally MUST keep host and port in the authority, use a DNS hostname rather than an IP literal, and authenticate both the certificate chain and server hostname before credentials or queries cross the network. Kovo accepts the exact sslmode=verify-full posture and fails closed before pool construction on absent, malformed, duplicate-last-weaker, or weaker modes. The pinned node-postgres transport does not check IP literals against certificate identity even in verify-full mode, and a boot-pinned NODE_TLS_REJECT_UNAUTHORIZED=0 disables Node certificate verification, so Kovo rejects both for non-local transports. Only exact pg-effective 127.0.0.1, exact query-host ::1, and validated Unix-socket carriers may omit TLS; a bracketed IPv6 authority remains [::1] in pinned pg and is not the proven local carrier. Unix sockets use URL form with explicit login, database, and port; the ambient-dependent historical /socket/path database shorthand is forbidden. Connection-string URLs use an exact lowercase postgres:// or postgresql:// envelope with no raw whitespace or control characters, and every raw percent sign MUST be followed by exactly two ASCII hexadecimal digits. Malformed or truncated percent escapes are rejected globally before parsing so Kovo and the pinned Postgres parser cannot disagree about security-relevant query keys; intentional credential and query bytes must be canonically percent-encoded. The node-postgres driver requires an explicit reviewed databaseUrl/KOVO_DATABASE_URL; it does not combine a pinned Kovo security decision with pg's later live reads of ambient PG* destination or identity variables.

The real authorization boundary is split. Privilege belongs to unassumeable Postgres roles such as kovo_admin/kovo_system, and those roles may be assumed only inside framework-owned scoped clients for provisioning, migration, audit, or other system work. App/runtime roles cannot assume them. Principal belongs to the request-scoped GUC that RLS reads, but only the confined app-SQL statement surface may set it on a per-request scrubbed connection before executing app work. A set_config revoke, routine inventory, or source-level wrapper audit is defense-in-depth: no authorization code may rest on that revoke alone.

Capability ownership is framework-owned, not comment-owned. System/auth DB handles, privileged role assumptions, raw driver clients, and secret-readable handles MUST have exactly one framework mint site and MUST cross app-authored or public-package boundaries only as a narrowed, branded facade with an audited consumer path. Generated app source MUST NOT export a raw system DB, AppDb, provision/admin client, or equivalent ordinary value that app modules can import and route around the managed read/write chokes. Better Auth and other framework integration code may consume a module-private or opaque adapter capability, but request-authored code only receives request-scoped read/write facades whose SQL methods are governed by the source/sink, statement-identity, guard, and secret-read boundaries in this section and §11.2. Any public use of such a privileged facade MUST appear in kovo explain capabilities with its reason and consumer surface.

C9 — boundary-crossing doors are reconstructed, boxed, or framework-owned (normative). Any value crossing a security-relevant boundary in Kovo MUST do so through exactly one of three mechanisms:

  1. Reconstructed carrier — the framework snapshots or rebuilds the boundary value from normalized facts before the sink sees it, so caller-owned mutable carriers cannot change meaning between validation and execution.
  2. Boxed value — the framework keeps the runtime value inside a non-coercible box until an explicit reveal/redaction path discharges the sink.
  3. Framework-owned door — the only way across the boundary is a typed or branded framework channel whose implementation owns grammar, normalization, and fail-closed rejection.

Two corollaries are mandatory:

  • Complete engine-door enumeration. Where Kovo claims the engine is the authorization or confidentiality door, the complete boundary-crossing set is the engine's effective privilege graph itself (§10.3 engine-door completeness), not a source scan, proxy wrapper, or comment inventory.
  • Secrets leave only boxed or owned sinks. A secret, governed, or principal-bearing value MUST never rely on a read-only proxy or a best-effort caller convention at egress. It crosses only via a runtime box, a reconstructed carrier, or a framework-owned sink that is named in the proof inventory and hostile-value tests.

Principal-indexed label lattice and bounded non-interference (normative)#

Kovo's data-plane obligations use one product label L = Conf × Integ × Owner. This is a statement about the statically analyzable fragment and the named framework runtime doors in this specification, not arbitrary JavaScript, termination, timing, resource use, or code behind an audited escape.

  • Conf = public | secret, ordered public ⊑ secret; join selects secret when either input is secret.
  • Integ = literal | server | input | unknown, ordered from least to greatest uncertainty in that sequence. Its join is the least upper bound. This is the normative kind-level meaning of joinSymbolProvenance: unknown dominates, then input, then server, then literal. Equal input/server paths remain precise; different paths join to the same kind with an unknown path.
  • Owner = public | principal(p) | framework. public is bottom, framework is top, equal principal labels join to themselves, and labels for two different principals join to framework. The product join is componentwise. framework means that no ordinary principal inherits visibility or write authority merely because differently-owned values were combined.

The following clause IDs are stable machine-readable obligations consumed by check:label-clause-map:

  • NI-C1 — Confidentiality confinement. A secret value cannot influence a public/client/log observation except through an explicit framework-owned reveal or redaction door recorded as an audited declassification.
  • NI-I1 — Integrity confinement. An input or unknown value cannot influence a governed, credential, authorization, or other security-sensitive sink that admits only literal/server provenance unless a framework-owned validator or guard reconstructs and re-witnesses a new fact.
  • NI-I2 — Semantic integrity and freshness. Opaque reads must declare enough source and shape facts to preserve the query's freshness and wire contract; a source that cannot participate in invalidation cannot silently enter a live query read set.
  • NI-O1 — Principal isolation. A value or effect labeled principal(q) is not observable by or writable for principal p when p ≠ q; row selection and engine policy must preserve that owner label at every supported authorization door.
  • NI-E1 — Audited exception visibility. Any deliberate exception to NI-C1, NI-I1, NI-I2, or NI-O1 must pass a named escape with stable provenance, source span, and obligation text and remain visible to kovo explain; missing or unrecordable escape evidence fails closed.

Termination-insensitive statement. For any principal p, take two supported executions that start from equal p-observable state and differ only in values whose confidentiality/owner labels make them unobservable to p. If both executions terminate, their p-observable framework outputs and governed/authorization effects are equal. Likewise, changing only input/unknown values does not change a sink protected by NI-I1 unless a named validator, guard, or audited exception admits the change. This statement excludes termination, timing, allocation, arbitrary app effects, unanalyzed JavaScript, and the truth of author assertions; those are retained obligations, not implied claims.

The diagnostic clause denominator is exactly KV410, KV411, KV414, KV426, KV435, KV438, and KV439. Their mapping is versioned in security/label-clause-map.json; a missing, duplicate, unknown, or class-relabelled row is a root-check failure.

C10 — security sets are closures or allowlists, never subsets or denylists (normative). Any set used for an authorization, confidentiality, or privileged-execution decision MUST be computed from the boundary relation it represents: a reachability set is the complete closure over the relevant engine edges, and a property set is an allowlist of the minimal safe members. The write-reachability audit closes directly writable relations over structural write-propagation edges: FK referential actions, partition/inheritance routing, and rewrite-rule redirects that can route app-role writes. The runtime identity audit checks the runtime login plus the complete pg_has_role(..., MEMBER) assumable-role closure against two allowlists that jointly range the complete escalation surface, which is role ATTRIBUTES ∪ predefined-role MEMBERSHIP: (i) the classified role-attribute allowlist — rolsuper, rolbypassrls, rolreplication, rolcreaterole, and rolcreatedb MUST be false, while benign role metadata is classified explicitly and future unclassified pg_roles role-attribute columns fail closed; and (ii) an allowlist over PostgreSQL predefined-role membership — the login and every assumable role may be a member of only the framework's own roles plus an explicit benign don't-care set, so membership in any pg_* predefined role outside that allowlist (e.g. pg_execute_server_programCOPY … FROM PROGRAM OS command execution, pg_read_all_data/pg_write_all_data, pg_read_server_files/pg_write_server_files, pg_monitor, pg_maintain) is refused and named. The predefined-role allowlist is required because predefined roles carry NONE of the five elevated role attributes and would otherwise pass the attribute allowlist unflagged; predefined roles are detected by the reserved pg_ name prefix and the < FirstNormalObjectId (16384) system-OID range and surfaced through the same MEMBER closure. As an allowlist (member-of-only-known-safe, not a denylist of known-bad roles), a new pg_* predefined role in a future PostgreSQL release fails closed by default. The SECURITY DEFINER routine and attached-code audits use that same login-plus-assumable identity set for execution reachability. The auth non-egress proof enumerates the request-reachable secret-handling surface and boxes or confines each path; it MUST NOT use a named file as a proxy for that surface, and its plaintext-API confinement enumerates every request-reachable auth.api.* plaintext-reading endpoint so a new or unclassified endpoint used outside the trusted module fails closed rather than sliding through a fixed subset regex. The Better Auth plugin secret classifier follows the same rule in the confidentiality direction: a credential-shaped plugin column — one whose final name segment is a credential noun (key, token, secret, password, hash, …) — defaults to secret: unless the author explicitly annotates it non-secret, so the apiKey plugin's key column and custom credential additional fields fail closed to secret rather than being emitted as ordinary readable columns.

The implementation keeps this as one executable inventory in boundaryCrossingSinkInventory(). pnpm run check:c9-sink-inventory MUST compare the union of its censusFamilies with every family in frameworkSourceSinkInventory(), reject duplicate sink rows, require a stable owner and an existing root proof command, and verify that every proof/hostile-value citation names a live file. Adding a source/sink family without discharging it through this table therefore fails pnpm run check.

Every C9 row MUST also classify keyScoping as exactly one of database-principal-policy, runtime-opaque-scoped-key, or not-stateful-keyed. The database driver row owns the first posture through its complete role/RLS privilege graph. Blob/file storage, derived vector/RAG persistence, and durable-task coalescing own the second posture and MUST authenticate or reconstruct from the §6.6 ScopedKey runtime witness before namespace use. All remaining current rows are explicitly not-stateful-keyed. Missing, unknown, or downgraded classifications fail check:c9-sink-inventory; adding a new app-addressable stateful sink therefore cannot omit an owner-provenance decision silently.

The same executable inventory owns the finite compiler security-operation vocabulary from §4.3 and §6.6. Every member of securityOperationKinds MUST occur in the operationKinds of exactly one C9 row. A missing operation, an operation unknown to the canonical union, or duplicate ownership is a gate failure. This assignment ties terminal-effect evidence to the real sink owner and ties server.handler.root/server.helper.call control records to capability closure. Those records keep the root census and unresolved local-call summary edge visible; they do not assert a runtime effect. The generated operation manifest is never itself the runtime door.

Sink Owner Mechanism Key scoping Sole door Root proof gate Hostile-value evidence
DB driver statement @kovojs/server/managed-db reconstruct database-principal-policy Immutable managed-statement snapshot plus engine policy pnpm run check:single-choke packages/server/src/managed-db.test.ts
HTTP response body @kovojs/server/wire-output reconstruct not-stateful-keyed Typed wire/body envelope renderers pnpm run check:wire-output-boundary packages/server/src/wire-html.test.ts
HTTP response headers @kovojs/server/response-finalization own not-stateful-keyed App-header classifiers plus final raw/structured adapter reconstruction pnpm run check:wire-output-boundary packages/server/src/response-posture.test.ts
Redirect URL @kovojs/server/response-posture reconstruct not-stateful-keyed Same-origin path normalization before Location finalization pnpm run check:wire-output-boundary packages/create-kovo/src/index.build.prod-artifact.redirect-capability.test.ts
Set-Cookie @kovojs/server/cookies own not-stateful-keyed Typed cookie builder and serializer pnpm run check:wire-output-boundary packages/server/src/cookies.test.ts
Blob/file and derived-dataset write @kovojs/core/storage own runtime-opaque-scoped-key ScopedKey witness/frame namespace; exact derived() reconstructs and hashes the complete request-principal frame; static-export path containment pnpm run check:filesystem-boundary packages/core/src/scoped-key.test.ts; packages/compiler/src/derived-dataset-security.test.ts; packages/server/src/derived-dataset.test.ts; static-export containment
Durable-task payload @kovojs/server/task-runner own runtime-opaque-scoped-key ScopedKey frame plus queue envelope and redaction-aware observability views pnpm run check:security-test-builds packages/server/src/task-queue.test.ts; packages/server/src/task-observability.test.ts
Request method/authority/scheme/target @kovojs/server/request-ingress reconstruct not-stateful-keyed Explicit transport-source snapshot plus one finite classifier, immutable prepared verdict, and pre-filesystem platform gate pnpm run check:security-classifier-corpus packages/server/src/request-ingress-policy.test.ts; HTTP/1/H2/Vercel/Fetch and generated middleware parity
Webhook payload @kovojs/server/webhook own not-stateful-keyed Verifier-before-parse plus replay-scoped dispatch pnpm run check:security-test-builds packages/server/src/webhook.test.ts
HTML/document/style render output @kovojs/compiler/output-context reconstruct not-stateful-keyed Contextual render pipeline or explicit trusted-output escape pnpm run check:sink-policy packages/browser/src/security-output.test.ts
Log/error output @kovojs/core/secret box not-stateful-keyed Non-coercible secret/redacted boxes plus normalized error emitters pnpm run check:tcb-boundary packages/core/src/secret.test.ts; packages/server/src/task-observability.test.ts
Outbound egress request @kovojs/server/egress own not-stateful-keyed Declared-origin, per-hop DNS/address classification and selected-address transport choke pnpm run check:egress-boundary packages/server/src/egress.test.ts; packages/server/src/egress-redirect.test.ts
Authorization principal/data access @kovojs/server/postgres-authz own not-stateful-keyed Pinned principal plus least-privilege Postgres role/RLS/effective-privilege-graph closure pnpm run test:authz-paranoid packages/server/src/postgres-authz.test.ts; served paranoid production-artifact matrix
Better Auth credential/non-egress @kovojs/better-auth/credential-gate own not-stateful-keyed Exact registered consumer, validated result, and same-consumer one-shot result opening pnpm run check:security-classifier-corpus packages/better-auth/src/internal.trusted-plaintext.test.ts
Dynamic module/process execution @kovojs/compiler/capability-closure own not-stateful-keyed Compiler-owned immutable client-module registry plus reviewed build/runtime capability doors pnpm run check:sink-policy packages/browser/src/handlers.test.ts; packages/compiler/src/conformance-compat.test.ts

The executable inventory retains the stable sink key blob/file write for this C9 row and maps both file.storage.static-export and data.derived.persistence census families to that one storage-operation owner. server.storage.read and server.storage.write therefore still have exactly one finite-operation owner. The derived family adds the KV452 compiler proof plus runtime request-principal namespace reconstruction described in §6.6; it does not create a duplicate operation vocabulary or claim that an external vector service faithfully enforces its namespace.

External Postgres role topology is a manifest, not environment inference. The runtime config MUST resolve reader, writer, admin, and system roles into one topology that records whether Kovo creates or adopts each role, the runtime login, and required runtime-login membership edges. The same topology facts drive provision, posture check, production boot, and kovo db output. Adopting pre-created roles does not relax verification: provision/check/boot MUST verify required role existence and runtime membership edges, fail before partial DDL when a required adopted role is missing, and refuse configurations where the ordinary runtime login can assume privileged admin/system roles outside framework-owned scoped clients. The runtime login and every role it can assume through the MEMBER closure MUST have only the classified minimal-safe role attributes AND must be a member of only framework-owned roles plus an explicit benign don't-care set; unknown role-attribute columns and non-allowlisted pg_* predefined-role memberships (C10/C11) both fail closed until classified.

External Postgres posture lease (normative). The least-privilege result above is not a boot-only fact. Before an external-Postgres runtime becomes ready, Kovo MUST establish an immutable boot baseline from a versioned, deterministic SHA-256 digest of a bounded authoritative witness. The witness MUST cover the runtime identity, classified role attributes, role memberships and the complete assumable-role closure, protected policy/grant facts, the migration-ledger head, and the monotone posture epoch. Canonical facts are sorted by kind, key, and value. One witness accepts at most 2,048 facts, 4 KiB per fact field, and 256 KiB of canonical evidence; a field or total exceeding its bound fails closed. The digest MUST be stable across unchanged boots and MUST change when one covered grant, membership, policy, identity, ledger head, or epoch changes. The random pooler probe and physical backend PID prove the connection property below but are deliberately excluded from the stable digest.

The lease TTL is 120 seconds with zero serve-degraded grace. Kovo renews from the authoritative catalog on a 30-second base interval with one process-stable plus-or-minus 10% jitter and a 10-second witness timeout. PostgreSQL SQLSTATE 42501 requests the same renewal. Scheduled, permission-triggered, and request-admission renewals MUST share one in-flight promise; a caller cannot create one catalog scan per error or request. A failed witness, timeout, expired lease, or digest different from the immutable boot baseline trips KV433 app load-shed immediately. Kovo MUST reject new database-capability admissions, invoke the pool/session drain exactly once for that outage transition, and await that drain before it may mark the lease fresh again. This drain retires pooled sessions; it is not a claim that JavaScript can cancel SQL already issued to PostgreSQL. Failed recovery attempts use exponential backoff starting at 1 second and capped at 30 seconds. Only a successful authoritative witness whose digest exactly matches the boot baseline restores service. An intentional posture change therefore requires the operator to finish migration or provisioning and restart the process to authorize a new baseline; transient failures may recover in place when the old baseline is restored.

Every witness MUST mechanize the pooler assumption inside one transaction. Statement one writes a random transaction-local frame with set_config(..., true) and reads that frame plus pg_backend_pid(), current_database(), current_user, and session_user; statement two reads the same fields again. A changed backend PID, lost frame, database, current user, or session user fails closed. This admits direct pools and transaction-preserving poolers, not statement-mode poolers.

Freshness binds both the checksum-validated migration-ledger head and a monotone posture epoch that kovo db migrate reasserts with the expected ledger state. An epoch may advance but MUST NOT be silently decreased or reused. This detects a restore that regresses relative to an already-running lease or a deployment that re-runs migration; it does not let a brand-new process distinguish a self-consistent stale backup without an external expected head/epoch. kovo explain capabilities MUST print this static external-Postgres contract, including its bounds and recovery rules. Because that command reads a build graph rather than a live process, it MUST label current status, digest, and expiry not-observed and MUST NOT imply that external Postgres is the deployed driver.

Split Postgres authority is bound to one live writable database (normative). A runtime identity witness and a privileged posture audit are one proof only when they address the same logical database on the same current primary. Provisioning MUST mint one random framework-owned database instance identity in kovo_schema_state. Before normalizing the session, the ordinary runtime connection snapshots exactly one live-identity row containing the database name/OID, cluster system identifier, timeline, recovery state, postmaster start time, and server address/port, plus exactly one framework identity row. The selected system authority (preferred) or admin authority MUST independently reproduce the same compound identity before its audit result is accepted. A mismatch, recovery/standby endpoint, missing exact framework identity row, or provider that withholds the read-only pg_control_system() / pg_control_checkpoint() identity oracles fails closed. This supports one writable primary (or a failover where both URLs reconnect to the same promoted primary). Independently writable physical clones and split proxy routes are unsupported even if their logical schema bytes match; operators must point every authority URL at the same live primary and rerun provision after a logical clone.

Runtime Postgres session state is an allowlist (normative). The runtime witness runs before Kovo overwrites search_path. It MUST reject startup SET ROLE/session-authorization skew, require the safe semantic baseline (session_replication_role=origin, row_security=on, UTF-8, standard strings, and the other classified parser/transaction settings), and enumerate every setting whose live source is client, database, role, or database-plus-role. Only the pinned UTF-8 driver negotiation and semantics-neutral application naming are allowed. The privileged posture authority also enumerates persisted settings for the runtime login and its complete assumable-role closure; an unclassified setting fails closed rather than joining a denylist. Every live pg_settings.source category MUST be explicitly classified; an unknown future source category fails closed. Every framework-owned app SQL transaction then starts with the exact local pg_catalog, public, pg_temp search path before it sets the principal, role selector, or app role.

Postgres security metadata currently requires globally unique base table names (normative). Owner chains, secret-column grants, authorization classifications, and policy dependency sets share the Drizzle base table name as their closed-world key. Until all of those keys are schema-qualified end to end, a schema declaring two physical relations with the same base name MUST fail before any database side effect, even when the relations live in different PostgreSQL schemas. This prevents a public or partially secret classification from colliding with a whole-secret relation.

C15 — classify-and-pin or reconstruct after runtime classification (normative). When a security-relevant sink accepts a caller-owned carrier whose bytes or object identity can still change after runtime validation/classification, the framework MUST do one of the following before the sink executes:

  1. Pin the exact accepted value into an immutable framework-owned carrier that the sink then consumes without re-reading the caller-owned source; or
  2. Reconstruct/fix the sink value from normalized facts, discarding the caller-owned carrier and failing closed to a fixed fallback when normalization does not bless the value.

Re-reading or re-stringifying the mutable caller-owned carrier at the sink after an earlier classification decision is forbidden. A sink may be recorded as N/A only when the sink is already framework-owned and structural from construction time, so no caller-owned carrier survives to the decision point. The required audit inventory for this rule includes at least the egress resolved-IP floor, redirect Location, managed SQL statement carriers, sql.identifier(...), and header/cookie serialization; each row must be classified as pinned, fixed, or N-A with hostile-value evidence.

The closure audit is side-effect-inclusive. Attached code is reachable when the app role can reach it by direct EXECUTE, DML trigger, rewrite rule, CHECK/domain constraint function, default/generated expression function, or index/predicate expression function, including structural write propagation through FK referential actions, partition/inheritance routing, and rewrite-rule redirects. Each such attached code path MUST resolve to the same safe object set above or fail closed.

Principal-epoch revocation and mutation completeness (normative). The app's principalEpochStore is the §6.6 authoritative identity-lifecycle capability. Production apps that combine a sessionProvider with mutations MUST use the module-private durable provenance exposed by createPostgresAppRuntimeDb().principalEpochStore; a missing, memory, custom structural, or global-symbol lookalike fails boot. Postgres persists one SHA-256 principal commitment rather than a raw principal id, with strictly monotone epoch/change time, permanent tombstone status, a finite last-reason constraint, and no ordinary reader/writer/admin/runtime-login table privileges. The system role receives only SELECT, INSERT, and UPDATE. The SQLite/memory implementation is development/test-only and preserves the same state semantics for one process lifetime.

An authenticated mutation captures an active authoritative epoch after parse and guards, then rechecks that exact epoch inside the transaction callback after handler work and before the callback may request commit. A concurrent password/role/tenant/admin/provider/deletion transition therefore throws through the rollback path. A mutation that itself changes privilege MUST declare one exact principalEpoch registry entry: { action: 'advance' | 'tombstone', principal(input, request), reason }, where reason belongs to the matching finite union. Kovo executes that declared transition after handler success but before transaction-callback success. Only the exact current request receives a module-private old-to-new receipt permitting its response/replay settlement under the now-retired prior epoch; any different or later transition closes settlement. The mutation registry is a completeness ledger, not semantic inference from table or mutation names. An epoch transition may commit before a later app-transaction COMMIT failure; that conservative case over-revokes, while an epoch failure prevents app transaction commit. Kovo does not claim cross-database atomic commit.

Provider and out-of-band paths use initializePrincipalEpoch, advancePrincipalEpoch, and tombstonePrincipalEpoch. Initialization atomically creates active epoch 1 or returns the existing row unchanged and cannot revive a tombstone. Better Auth's first-party binding invokes it before an authenticated provider identity reaches app code. Password changes, role/tenant changes, administrative actions, external-provider revocation, and deletion MUST advance or tombstone the same row even when no Kovo mutation initiated the event. Every current lookup is authoritative, uncached, and limited to 1,000 ms. Missing/malformed rows, provider errors, timeout, stale epoch, and tombstone all fail closed. This yields zero positive-cache staleness; only one bounded in-flight authoritative read/action race remains.

Principal erasure receipts (normative). erasePrincipal(principal, options) is the public framework door for erasing Kovo-owned residue. It MUST accept only an exact createPostgresAppRuntimeDb() runtime, an exact framework SigningKeyRing, and a non-empty bounded list containing every storage adapter wired by the app. It MUST permanently tombstone the principal epoch before deletion; erase principal-indexed _kovo_jobs rows, mutation replay rows, and objects from every supplied storage adapter; then independently re-enumerate all three sink families. Any residue, malformed index carrier, incomplete pagination, unrecognized filesystem generation, or non-enumerable structural storage lookalike MUST fail without a receipt. A receipt is a signed point-in-time absence proof over precisely the supplied adapters and exact app runtime, not a promise to recall external egress, browser cookies, omitted/derived third-party adapters, or future writes by arbitrary app code. The receipt contains a one-way principal commitment, never the raw principal, and is signed only through the fixed principal-erasure-receipt crypto purpose.

Every durable task scheduled from a proven request MUST persist that principal in a separately indexed _kovo_jobs.principal column and propagate it to child jobs. Every durable mutation replay row admitted from a proven request principal MUST persist a separately indexed, one-way principal_index; mutation rows without a proven principal plus webhook and capability rows MUST carry NULL. This replay column is non-authoritative and additive: it exists only for erasure and reconstructive row validation, MUST NOT enter mutationReplayScope(), the canonical replay ScopedKey, replay authorization, or uniqueness composition, and a row/index mismatch fails closed. A pre-index task ledger containing rows or a pre-index replay ledger containing mutation rows MUST require explicit operator reconciliation and fail provisioning rather than silently claiming that legacy residue is enumerable. Memory, filesystem, and S3-compatible storage constructors retain an internal enumerable authority without adding list methods to the app-facing StorageCapability. Filesystem sidecars and S3 object metadata MUST reconstruct an exact ScopedKey whose digest matches the physical object identity; S3 listing MUST be bounded, dense, duplicate-free, and completely paginated.

The public S3-compatible client accepts only Kovo's stable positional operation vocabulary. The operation object is contextually typed by S3CompatibleObjectClient.create (and may be named by Parameters<typeof S3CompatibleObjectClient.create>[0]); provider request, response, metadata, and parallel named operation records are framework-internal.

The credential-door census is exact: principal-scoped capability URL mint/verify plus mutation replay-receipt reservation, response release, handler admission, in-transaction completion, and settlement are applicable, while the exactly-once adapter continuation is inapplicable because it cannot outlive its call frame. Capability v4 signs the epoch and checks it before replay burn/storage read (§9.1). Mutation reservation appends the captured epoch to the already principal-bound canonical scope. Every listed verifier rechecks authoritative freshness. A Kovo-Idem issued before the current changedAtMs is a conflict before handler work; equality is accepted only for epoch-1 identity initialization and is closed for every later revocation epoch because millisecond ordering is unknowable. Expiry remains defense-in-depth, never a substitute for freshness.

Request lifecycle (normative):

(pre-dispatch shell: max-body-size → 413 · coarse per-IP/global rate → 429
 · app occupancy → 503 · finite deadline capability — §9.5)
CSRF validation → parse+coerce input (schema) → guard chain
→ resolve the protected-CSRF binding or post-guard machine replay principal
→ capture authoritative principal epoch where applicable and reserve by (binding, derived mutation identity, idem-token)
→ bind handler admission to that exact reservation epoch → BEGIN tx → handler (receives a transaction-scoped db whose public type hides raw transaction openers)
→ declared privilege transition, epoch freshness, and deadline checkpoints → COMMIT (recheck epoch, settle reservation, store response) → re-run invalidated queries (post-commit, same request context)
→ render <kovo-query>/<kovo-fragment> → respond
                    ⇘ on fail() or pre-commit deadline: ROLLBACK → abort replay reservation
                    ⇘ deadline after possible COMMIT: preserve replay truth → discard response only

The request deadline is a transaction-door capability, not a claim that JavaScript or a database driver can undo committed work. The framework checks it before opening transaction work and again inside the transaction callback after the mutation handler. Cooperative expiry at either checkpoint throws before callback success, drives the transaction's rollback path, and invokes the replay reservation abort hook because no successful callback requested commit. A timeout while an adapter is committing is outcome-ambiguous: Kovo MUST preserve the pending replay claim. Once commit is known to have succeeded, the durable settlement remains authoritative even if the request deadline then expires; Kovo may discard the late wire response, but MUST NOT call that a rollback, erase replay truth, or execute the mutation again on retry.

This ordering closes the read-your-writes hazard: responses can never render pre-commit data (which would visibly revert the user's optimistic update). A replay hit does not bypass authorization: the runtime MUST re-evaluate the guard chain against the current request before re-serving a stored response, so a replay never re-serves a private response after authorization changed. For CSRF-protected mutations, the replay store is keyed on (principal ∧ principal epoch ∧ CSRF session/rotation binding ∧ source-derived mutation identity ∧ idem-token), using canonical length framing for each identity component, so a replay can only ever return to the same principal and epoch that produced it even when an app supplies a shared rotation id. Session/rotation ids, independently resolved principals, anonymous-CSRF secrets, and mutation identities are each capped at 1,024 JavaScript code units before composition. A framework lifecycle binding embeds its pinned principal exactly once and replay rejects a later mismatch rather than appending duplicate identity. The bounded epoch suffix is appended only after the existing principal-bound scope is established; the complete raw scope MUST remain below the durable store's 4,096-code-unit ceiling. For csrf: false mutations, no session or mutation-wide fallback exists: replay storage requires machineReplayPrincipal(request) after the successful guard/access decision. Its exact primitive 1..1,024-code-unit result is length-framed under kovo-machine-replay-principal/v1, encoded as exact UTF-16LE code units (including lone surrogates), and SHA-256 committed before scope composition. The raw machine identity is absent from replay keys, metadata, diagnostics, and the non-authoritative Postgres principal_index; that erasure index remains derived only from an independently proven framework principal and MUST NOT participate in authorization or uniqueness.

Every mutation replay-store call MUST carry the runtime-witnessed canonical §6.6 system ScopedKey under the finite mutation-replay posture. Its app-key is the exact injective length frame (scope, idem), including NUL and delimiter placement; the accompanying raw scope and token are descriptive/expiry metadata and MUST match that witnessed frame before a store is invoked. The volatile store keys its map by the complete frame. The durable Postgres store hashes the complete frame into its physical replay namespace before SQL; hashing the raw scope and token as independent authority is insufficient. Custom development/test stores receive the witnessed key as their first argument through the same validating snapshot door. This registered composite posture preserves the bounded enhanced scope plus the canonical 49-code-unit idem token and finite epoch suffix while keeping the outer frame within 4,096 code units. Public/principal keys and every other system posture retain the ordinary 1,024-code-unit app-key bound.

The app-scoped mutation signature is handler(input, request, context). input comes from the declared schema; request carries the validated contract request/session and transaction-scoped managed DB posture; context exposes fail, the declared error union, signal, and other framework-owned mutation operations. fail(code, payload) is keyed to the mutation's exact errors object, and both code renames and payload-field renames propagate to form slots and tests. Endpoint method, access, authentication, CSRF, body, cache, and response posture remain explicit even though their request/result types are inferred. A factory facade MUST NOT synthesize an allow/public/CSRF/response decision from a provider type.

The handler request.db type is a defense-in-depth authoring guardrail, not the security proof: it preserves the configured DB provider's read/write surface while hiding public transaction openers such as .transaction(), so nested transaction/open-handle misuse is a TypeScript error on the normal app.mutation() path. TypeScript cannot prove arbitrary object lifetime or reject every closure/module-scope capture of a handler parameter; runtime transaction ownership, rollback-on-throw, SQL provenance gates, and fail-closed sinks remain authoritative. External I/O inside a mutation handler is not a mutation-specific type or KV-gate error; it is governed by the uniform outbound-egress floor (§6.6). Durable tasks (§9.6) are the framework primitive for retryable/idempotent after-commit effects, not the only syntactically legal place to call fetch.

Replay is an atomic reservation, not a lookup (normative). The replay step MUST atomically claim its complete replay identity before executing a write — an INSERT … ON CONFLICT against the replay store (or an equivalent unique-key claim) inside the same serialization boundary that the commit settles. A request that wins the claim proceeds; a concurrent or sequential request carrying the same identity MUST block on the in-flight reservation and then replay the settled response, never re-execute the handler. For browser mutations, the identity is (principal, principal epoch, CSRF session/rotation binding, source-derived mutation identity, idem-token) under canonical length framing. For csrf: false mutations, it is (SHA-256(UTF-16LE(versioned length-frame(machine replay principal))), source-derived mutation identity, idem-token), where UTF-16LE preserves every exact JavaScript code unit. Both claims occur after parse/coerce and the current guard chain but before handler work. The protected store scope follows the current principal epoch and validated CSRF binding (a different req.session identity, credential rotation, or revocation epoch never replays a prior response); the machine scope follows only the explicit post-guard caller identity. Both include the specific mutation, so an idem-token reused across mutations cannot cross-replay. Enhanced and no-JavaScript delivery share this one claim identity. If an existing record belongs to the other response vocabulary, the closed classifier returns a 422 conflict without re-executing. Deterministic declared application failures settle the complete rendered response. Validation, 429, and 409 failures abort their reservation; response-policy or rendering failure while producing a deterministic failure does the same. A failure after a successful transaction may preserve pending truth as required by the outcome-ambiguity rule above. For webhook(), the identity is the source-derived webhook scope plus the canonical authenticated provider-event facts from §9.1; its claim occurs after verify, loose parse, and temporal validation but before the handler. The provider key is the unique lookup key, and a live row for that key whose stored occurredAtMs or expiresAtMs differs from the supplied canonical identity is an integrity conflict, not a replay hit and not a second admissible event. These rules cover the enhanced and no-JS mutation() lifecycle, webhook(), and the streaming path, so concurrency, not merely strictly sequential retries, is deduplicated.

Durable replay storage is bounded and refuse-never-evict (normative). The shipped Postgres store gives mutation and webhook pending truth separate, database-enforced admission pools of 1,000 claims each. Only a pending claim owns a unique numbered slot, so concurrent replicas cannot race above the in-flight ceiling; successful settlement atomically clears its slot while retaining the committed row. At pending capacity, an existing identity still joins its in-flight claim or replays committed truth, while unseen work is refused before its handler runs (the mutation 429 / webhook retry outcome). No pending claim is evicted merely to admit new work. Aborting a pre-commit reservation or the explicit generation-fenced operator reconciliation path releases its slot.

Pending truth never expires or loses its slot automatically, because the application transaction may already have committed. Committed mutation truth has a canonical token-mint horizon of 24 hours; committed webhook truth has the authenticated event horizon from §9.1, expiresAtMs = occurredAtMs + 30 days. At either exact persisted expiry, committed truth becomes eligible for bounded batched deletion; expiry does not slide on lookup or replay. Fresh admission MUST perform eligible committed cleanup before applying its bounded-retention refusal and MUST compare the supplied expiry against the store's current clock atomically with reservation, so request latency cannot admit already-stale work. Once cleanup reclaims committed truth, the store MUST advance a monotonic reclaimedThroughMs high-water mark and reject fresh reserve or settlement with expiresAtMs <= reclaimedThroughMs, even if the wall clock later moves backward; durable storage persists this watermark, while a volatile development/test store guarantees it only for that store lifetime. Settlement performs the same current-clock and watermark checks; if the horizon elapsed after reservation, it leaves the reservation pending/fail-closed with its slot for reconciliation rather than publishing committed truth that the next cleanup could delete and re-execute. The same identity key may be admitted only after its prior committed row has actually been removed; while a pending or unexpired committed row exists, mismatched canonical facts conflict. The request path MUST NOT apply a receipt-time TTL, slide the deadline, evict the oldest row, or retire pending ambiguity. Volatile and durable stores implement the same temporal and conflict lifecycle, although a volatile store may additionally cap its total retained entries.

One committed snapshot is additionally limited to 1,048,576 UTF-16LE body bytes and 65,536 UTF-8 header bytes. Oversized settlement stores no oversized bytes and leaves the already-claimed key pending/fail-closed, so a retry cannot repeat a write whose application transaction may have committed; operator reconciliation must first establish the application outcome. The schema posture audit proves the exact non-deferrable (surface, scope, idem) primary key, nullable pending-slot column, 1..1,000 slot constraint, unique per-surface pending-slot index, canonical mint/occurrence/expiry columns and constraints, the nullable mutation-only principal erasure index and partial index, persisted per-surface reclamation watermark, response-byte constraint, and exact replay-table ACL before production serves. Ordinary app/runtime roles MUST have neither table-level nor column-level replay privileges; only the isolated system role receives the exact SELECT, INSERT, UPDATE, DELETE set. Provisioning repairs missing canonical identity constraints, revokes stray table/column grants, and fails closed if duplicate or temporally ambiguous legacy truth prevents repair.

Bounded replay-model honesty boundary (normative disclosure). The optional ReplayReservation state model explores exactly 2 replicas, 2 admission slots, 2 replay identities, one backward clock step, and one crash point. Its Postgres-CTE atomicity axiom treats each registered transition CTE as one atomic model action. The watermark row's FOR UPDATE lock is the reviewed justification for that abstraction. It remains a human assumption about Postgres transaction and row-lock behavior, not machine-verified evidence about the database implementation. kovo explain model-boundaries MUST print that axiom, these bounds, every registered modeled action, and the exact action complement plus excluded phenomena below.

The checked status is bounded-model-checked: TLC v1.7.4 under the exact-pinned formal/replay/tlc-toolchain.json exhausts the committed formal/ReplayReservation.cfg state space for type safety, no double execution, refuse-never-evict, monotonic reclamation, no resurrection, and bounded admission. The same gate MUST reproduce the committed evict-pending/double-execute and naive-watermark/backward-clock resurrection counterexamples. This bounded evidence validates the declared abstraction and its historical mutants; it does not prove Postgres, its CTE/row-lock implementation, unbounded cardinalities, or any excluded phenomenon below.

The model explicitly does not cover:

  • durable-task queue transitions or scheduling;
  • driver retries, network partitions, or outcomes hidden by connection failure;
  • the implementation correctness of Postgres transactions, CTEs, or row locks;
  • principal erasure and absence-probe interleavings with replay reservation;
  • response serialization, byte limits, or application response correctness;
  • schema provisioning, migration, ACL posture, or catalog-audit queries; and
  • replicas, slots, identities, clock steps, or crash points beyond the printed finite bounds.

Idem-token minting, horizon, and entropy (normative). Kovo-Idem is a per-submit token, not a per-form constant. Its only accepted production grammar is v1_<issued-at-ms>_<nonce>, where issued-at-ms is exactly 13 decimal Unix-epoch-millisecond digits and nonce is exactly 32 lowercase hexadecimal digits produced from 16 cryptographically random bytes. UUID v4 version/variant bits do not count toward this ≥128-bit nonce floor, so browser minting requires crypto.getRandomValues(new Uint8Array(16)); there is no timeless UUID/base64url fallback. A server-rendered/no-JavaScript form stamps server time. Enhanced modular and inline submits preserve that stamped issued-at value while replacing only the nonce, so JavaScript enhancement cannot silently extend the document's retry/deploy horizon. A direct seedless browser API has no server stamp to preserve and uses its boot-captured client clock.

The nominal mutation retry horizon is 24 hours, aligned with the required deploy-skew retention floor (§14): admission requires nowMs < issuedAtMs + 24 hours and issuedAtMs <= nowMs + 5 minutes. Thus the exact expiry millisecond is stale, the exact future-skew boundary is accepted, and one millisecond beyond either boundary is rejected. Malformed, legacy timeless, stale, and farther-future tokens are answered as a 422 idempotency conflict before any replay-store call or handler execution on enhanced, streaming, and no-JavaScript paths. Parsing produces an immutable { token, issuedAtMs, expiresAtMs } fact so durable storage/cleanup consumes the already-snapshotted token rather than re-reading a request carrier. The timestamp is not a MAC or authorization claim: a client may always mint a new logical token, and changing the timestamp also changes the exact replay key. The security invariant is instead that an already-used exact token cannot become admissible again after its row is safely reclaimed.

Kovo-Idem is read from its reserved carrier and validated independently of parsed mutation input. Although parse/coerce and guards precede replay admission so guards see validated args, the token MUST NOT be derived from those args. The fixed 49-character grammar is shared by volatile, custom, and durable replay paths so a client-controlled key cannot become a storage or memory amplifier. A re-submit that edits visible fields therefore mints a distinct token — eliminating the silent lost-update where an unchanged hidden field replayed the first commit. Token collision within one caller binding and source-derived mutation identity is a server-detectable integrity fault answered as a 422 schema-class failure (§9.2), never a silent replay of an unrelated commit.

Guards (arg-aware, normative). A guard is a refinement run before page/load/handler. Beyond req.session, every guard receives the query's or mutation's validated args / resolved instance key — the same s.*-coerced values the loader and handler see (§9.4, §10.2). A guard may therefore express ownership over a client-visible key, not only session-wide roles. Guards run after schema parse/coerce so the args they inspect are already validated (§10.3 lifecycle).

Validated query and mutation args MUST be reconstructed before providers, guards, or final consumers can observe them. The accepted graph is bounded primitives, plain own-data records, dense arrays, and exact framework-witnessed capability/file leaves. A JavaScript Date is not an immutable leaf: Object.freeze() and shadowed instance methods do not prevent an unchanged native mutator such as Date.prototype.setTime.call(value, replacement) from changing its internal slot. The receipt boundary therefore MUST reject Date values and direct authors to an ISO timestamp string or epoch number until Kovo ships an immutable temporal value. A Proxy membrane is not a substitute for this reconstruct-or-reject rule (SPEC §6.6 and C15 above). A stored upload is reconstructed at its schema-owned door: its returned object metadata is pinned to own data and its lastModified field is exposed to guards/handlers as an ISO timestamp string, never as the storage adapter's mutable Date carrier.

owns() ownership combinator. owns((args) => args.id, table.ownerColumn) is the sanctioned ownership guard: it passes only when the principal (req.session, the column declared by the table's owner: annotation, §10.1) owns the row the key selects. owns() is composable with the other combinators (all(authed, owns(...))) and discharges the KV414 IDOR obligation for the key it covers. The shipped runtime contract is guards.owns(keyOf, ownsRow) where ownsRow(req, key) is an app-provided ownership predicate (so @kovojs/server stays decoupled from the data layer); the table.ownerColumn column-form above is the planned compile-time sugar that lowers to it.

ts
export const adminRefund = mutation({ guard: role('admin') /*…*/ });
export const orderQuery = query({
  args: s.object({ id: s.string() }),
  guard: all(
    authed,
    owns((a) => a.id, orders.id),
  ), // args.id ownership — discharges KV414
  load: (db, args) => db.select().from(orders).where(eq(orders.id, args.id)),
});
// composable: guard: all(authed, rateLimit({ per: 'session', max: 10 }))
// rateLimit also admits per: 'ip' and a global dimension; a coarse per-IP/global
// body-size + rate limiter runs PRE-DISPATCH (413/429) ahead of replay+parse (§9.5)
// static audit: `kovo explain unguarded` lists every mutation, route, and query reachable without `authed`
// static audit: `kovo explain unscoped` lists every query/write touching an owner-annotated
// table (§10.1) whose key predicate is not traceable to req.session and not authorized by an
// ownership guard — the IDOR audit; the §11.1 predicate extractor does the tracing

KV414 — IDOR audit is a blocking gate, not advisory. A query or write whose key predicate touches an owner:-annotated table (§10.1) MUST resolve that key to either req.session.* or an owns()-class ownership guard. A site that reaches an owner-table row through a client-visible args.* key with neither is KV414 (error) — runtime-verified by the §11.2 cross-check against the executed read/write predicates and the §11.1 predicate extractor's session-traceability result, so a smuggled or branch-hidden arg-keyed owner read fails CI as loudly as silent staleness (KV407/KV411). The unscoped audit prints the same set; KV414 is its enforced form. A genuinely public read suppresses KV414 only with a recorded justification at the site, which kovo explain unscoped surfaces verbatim.

KV429 — single-row lost-update is by-construction for declared contended columns. A column declared kovo((columns) => ({ atomic: columns.stock })) (a contended value column) or kovo((columns) => ({ version: columns.lockVersion })) (an optimistic-concurrency counter) signals that its single-row read-modify-write MUST fold the check and the act into one statement. A self-referential write to an atomic column — set({ stock: stock - qty }), lowered to a §10.5 SymbolicValue arithmetic over a self-col reference — whose where() carries NO eq-predicate on that atomic column (a compare-and-set guard) NOR on a declared version column is KV429 (error): a lost-update race where two concurrent read-decide-write requests survive auth and validation and overwrite each other (oversell, double-spend, coupon reuse). The fix is a CAS predicate (where(and(eq(t.id, id), eq(t.stock, prevStock)))) or a carried row version, with a typed 409/422 on a stale write; a DB CHECK/unique constraint is the fail-closed backstop. Framework-owned Postgres mutation transactions MUST pin READ COMMITTED before app SQL, and boot posture MUST reject a different default; that stable ceiling does not replace the CAS/version predicate. Honest ceiling (single-row only): a write whose where() is opaque (range/IN/no key) or multi-row is NOT flagged — multi-row/aggregate invariants need forUpdate/SERIALIZABLE + retry and are not by-construction (the mutation transaction's READ COMMITTED alone does not prevent lost-update). The cross-function check-then-act is a false-negative floor until the interprocedural write-summaries land.

KV433 — a read-surface that writes is a confused deputy. A query({ load }) loader is a read surface; reaching a Drizzle write (insert/update/delete/execute/run/batch) from it is a state change on an idempotent GET. A loader whose body directly reaches such a write is KV433 (error) — the static no-write-reachable proof (Stage 2). There is no public GET-write query escape: move user-triggered state changes to mutation()/domain writes, and move explicitly side-effecting machine/API paths to endpoint(). Current scope: Stage 1 is shipped where Kovo owns the handle: the managed loader db is a read-only proxy whose write verbs throw at runtime, and its Reader<Db> type mirror makes those verbs a tsc error. That proxy is defense-in-depth, not the proof (§6.6). Stage 2's broader interprocedural case (a loader calling an imported domain() function that writes through some captured handle) still needs the bottom-up write-summaries that are not yet built and remains documented residue; today's direct static check covers writes directly reachable in the loader body and treats legacy/demoted query-write spellings as read-surface writes, never as escapes.

KV438 — mass-assignment is by-construction write-provenance. Where KV414 governs which row a write touches, KV438 governs which value reaches a governed column. A column is governed when it is the table's primary key:, its principal owner: column (both AUTO-governed), or is named in the kovo((columns) => ({ governed: [columns.role] })) annotation (the declare-once fact for role/balance/isAdmin-class columns, Constitution #2 — no call-site allowlists). A write that lands request input on a governed column — directly, through an alias/destructure, or via a .values(input) / { ...input } spread — is KV438 (error). The gate is fail-closed: a value the static analyzer cannot prove server-derived, literal, or explicitly asserted is rejected on a governed column (over the §11.1 AST symbol-identity provenance engine, never a branded type or runtime taint, §6.6). This is stronger than Rails strong_parameters / Django serializer denylists because it is schema-anchored and provenance-checked rather than an enumerated allowlist.

Two author-assertion escapes (SPEC §6.6: audit-grade, not proofs) route the residue. serverValue(value, reason) discharges only a value the analyzer independently proves literal or private/server-derived (serverValue(input.x,…), serverValue(opaque(input.x),…), and a missing value all fail). The louder trustedAssign(input.x, obligation) is the deliberate privileged-write path. Its second argument MUST be an inline, exact object literal with these three fields: invariant: 'governed-write.authorized-principal'; why, exactly { kind: 'guard-chain', guard: <machine reference> } or { kind: 'policy', policy: <machine reference> }; and evidence, exactly { kind: 'test' | 'policy-review', reference: <machine reference>, digest: 'sha256:<64 lowercase hex>' }. Machine references are 1..256 characters from the closed [A-Za-z0-9._:/#-] alphabet and begin alphanumerically. Variables, spreads, shorthand/computed/accessor properties, substitutions, duplicate/surplus/missing fields, prose strings, and malformed references or digests do not discharge KV438. The runtime chokepoint independently validates stable own-data properties and reconstructs a frozen framework-owned snapshot; the type is ergonomics, not the proof. App-authored analyzer summaries cannot declare general server provenance or discharge KV438; opaque same-package and cross-module helpers fail closed. The exact structured fact and scanner-owned call-span identity are surfaced by kovo explain capabilities.

For every accepted trustedAssign, kovo build emits an unsigned .kovo/escape-obligations.json subject binding the exact call-span identity and structured obligation to the reviewed graph's SHA-256 artifact subject. The build and app-facing execution surfaces MUST NOT acquire or expose the signer. A reviewer outside the build/coding-agent environment may sign the canonical kovo.escape-obligation-review/v1 subject with the existing runtime-posture Ed25519 authority; this is domain-separated but uses the same out-of-band trust-anchor fingerprint, never a second trust anchor. kovo explain attest <url> --artifact <graph.json> --trust-anchor <fingerprint> --escape-reviews <reviews.json> requires exactly one matching valid envelope for every emitted subject and verifies it together with the live runtime attestation. This establishes only the process fact that the pinned key holder approved the exact site, obligation, and artifact. It does not prove the obligation true, identify a human reviewer, or extend the by-construction claim through the escape. Ceiling: by-construction write-provenance for statically analyzable Drizzle writes; the escapes remain author assertions, and only trustedAssign deliberately accepts request input.

10.4 Optimistic updates#

Optimism is keyed to queries (the data), never islands. One transform per (mutation × invalidated query); every island consuming the query updates from it — including islands written after the mutation (Constitution #2).

Hand-written: transforms are authored in the mutation file as pure (data, input) functions against the query's inferred result type. Explicitly deferred: 'await-fragment' documents "considered; 1-RTT latency accepted here."

The public authoring spelling binds by query-handle identity rather than by registry augmentation or an authored query key:

ts
optimistic: [
  cartQuery.optimistic(cartMutationInput, (data, input) => ({
    ...data,
    count: data.count + input.quantity,
  })),
  productQuery.optimistic(cartMutationInput, {
    keys: (input) => [{ id: input.productId }],
    apply: (product, input) => ({ ...product, stock: product.stock - input.quantity }),
  }),
];

The first argument of every hand-written transform is the exact schema object also passed to app.mutation({ input }). This gives TypeScript a concrete inference source for the callback input without an explicit generic and lets runtime assembly reject schema drift by identity; the 'await-fragment' status needs no schema because it consumes no mutation input. The callback form is legal only for a query with no client-visible instance key. A keyed query requires { keys, apply }; keys(input) returns a bounded dense list of that query's exact inferred args shape and each instance receives the same pure transform. Each invalidated query handle occurs exactly once with status derived, one hand-written transform, or await-fragment. Duplicate, unrelated, cross-app, copied, or non-invalidated handles are hard errors, as is a keyed query with missing, empty, or malformed instance keys. The compiler resolves handle symbol identity to the source-derived registry identity and emits the same string-keyed wire/loader IR internally; app source does not author that key or augment InvalidationSets.

Optimistic handle references must be statically initialized without an import cycle. A top-level query-handle read whose module strongly depends back on the declaring mutation module is a teaching diagnostic naming the cycle and the extraction fix; runtime undefined or import-order-dependent registration is not accepted. The transform remains a pure value-returning function. Kovo may use framework-owned bounded copy-on-write internally, but it MUST NOT expose a mutable draft contract or weaken determinism, touched-path bounds, rollback, settlement, or emitted-transform equivalence.

Derived: for writes whose dataflow is closed over {mutation input, schema constants, data the query already ships} and queries within the shape grammar {scalar-from-keyed-row, COUNT, SUM(arith), jsonAgg, filtered-COUNT, membership transitions}, the compiler generates the transform (full derivation algebra in §10.5). Hand-written transforms share the same IR, so an app can override generated transforms pair by pair.

ts
// cart.mutations.ts — app-authored override of a pair the compiler cannot derive
const addToCartInput = s.object({
  productId: s.string(),
  quantity: s.number().int().min(1),
});

function predictCart(cart: Readonly<CartResult>, input: InferSchema<typeof addToCartInput>) {
  const current = cart.items.find((item) => item.productId === input.productId);
  return {
    ...cart,
    count: current ? cart.count : cart.count + 1,
    items: current
      ? cart.items.map((item) =>
          item === current ? { ...item, qty: item.qty + input.quantity } : item,
        )
      : [...cart.items, { productId: input.productId, qty: input.quantity, pending: true }],
  };
}

export const addToCart = app.mutation({
  input: addToCartInput,
  optimistic: [
    cartQuery.optimistic(addToCartInput, predictCart),
    productQuery.optimistic(addToCartInput, {
      keys: (input) => [{ id: input.productId }],
      apply: (product, input) => ({ ...product, stock: product.stock - input.quantity }),
    }),
  ],
  // access, registry, and handler omitted
});

Compiler-derived transforms lower to the same internal plan, but that string-keyed representation is generated ABI/IR. App source authors query-handle bindings and pure predictors, never a standalone optimistic-plan object.

Runtime protocol: snapshot the affected query values → apply transforms to the shared query values and run their update plans (all dependent islands update at once; affected islands get kovo-pending + aria-busy automatically) → on success, <kovo-query>/morph reconciles over the prediction (right guess ⇒ near-no-op; wrong guess ⇒ silent correction) → on error, restore snapshots, render error fragment.

Bounded snapshot (normative). The snapshot MUST cover only the change-record-touched subset of each affected query value — the keyed rows, scalar fields, and aggregate inputs a transform can mutate — under structural sharing (copy-on-write of the touched path), not an unconditional deep structuredClone of the whole value. The JsonValue constraint bounds serializability, not size; cloning a large dataset per mutation or per rebase is forbidden. A transform may mutate only within its declared touch scope, so an untouched subtree is retained by reference and restored by reference on rollback.

Successful enhanced mutation responses should include <kovo-query> chunks for every invalidated query instance the server can derive and rerun in the request (§10.3). When server truth for an optimistic transform is missing, the client MUST emit a visible runtime diagnostic (KV313) and discard the prediction — roll the affected query back to its pre-transform snapshot (§10.4 bounded snapshot) or force a /_q/<key> refetch (§9.4) — never freeze the unconfirmed prediction on screen as authoritative-looking data. "Settle" here means "discard the transform and reconcile against server truth," not "promote the prediction." This is a development escape valve for explicitly fragment-only or temporarily uncovered responses: KV310 exhaustiveness (§10.6) makes a covered mutation that ships no truth for an invalidated query a build failure, so a missing-truth discard never reaches a production end user as the steady-state contract.

Concurrency: a per-query pending-transform log keyed by Kovo-Idem token (§9.1). On each arriving server-truth chunk the runtime first settles the log: it drops every pending transform whose token is in the chunk's settlement set (§9.1.1) — those commits are already reflected in the arriving truth — then morphs the truth in, then re-applies only the not-yet-committed transforms in log order (rebase). Purity gives determinism but not idempotency, so settlement-before-rebase is mandatory: re-applying an already-committed additive transform would double-count the write. A transform whose token is absent from every truth chunk remains pending until its own response settles it. The settlement-matching rule is exact token-set membership; a truth chunk that carries no settlement set is treated as settling its triggering mutation's token only.

Named FIFO queues (queue: 'cart', normative). A queue serializes the mutations declaring the same conceptual group name. Queue names are not mutation registry identities and are intentionally allowed to span declarations (§4.1). Its semantics are pinned so two conforming implementations cannot diverge between "frozen cart" and "dropped actions":

  • Transform-apply timing. A queued mutation applies its optimistic transform on enqueue (immediately, against the current optimistic value including earlier queued-but-unsent transforms), not on dequeue — so the UI reflects the full queued intent without waiting for the head to drain. Its network request is sent only when it reaches the head.
  • Head-of-line timeout/abort. The in-flight head MUST carry a bounded timeout; on timeout or transport error the head is aborted, its transform is rolled back via its bounded snapshot, an error fragment is rendered, and the queue advances to the next entry. A hung head MUST NOT block the tail indefinitely.
  • Failed/hung-head drain. When the head fails or times out, the tail is not silently dropped: each surviving entry is re-validated against the rolled-back optimistic value and either advances or is discarded with a visible KV313 diagnostic; ordering among survivors is preserved.
  • Queued-but-unsent fate on navigation. Entries already in flight complete via keepalive; entries still queued-unsent at navigation are abandoned with the document (their optimistic transforms die with the log), exactly as for un-queued in-flight work — navigation is a reconciliation point, not a delivery guarantee.
  • Queue bound. Each named queue has a bounded depth; enqueue past the bound is refused with a visible diagnostic rather than growing without limit.

Navigation is a free reconciliation point: in-flight requests complete via keepalive, the log dies with the document.

10.5 Derivation algebra#

The compiler may derive an optimistic transform only when the write can be reduced to symbolic row-effects over mutation input, schema constants, and columns already present in the affected query, and the query shape fits the grammar named in §10.4. The derivation is all-or-nothing per affected field: an opaque server computation, non-key match, unsupported aggregation/windowing shape, interprocedural opacity, or untraceable parameter punts that field to await-fragment or a hand-written transform. Every punt is named in kovo explain mutation <name> --optimistic with the exact expression and reason.

Soundness is property-tested: for derivable pairs, generated-state tests assert patch(clientShape(s), i) ≡ clientShape(apply(effect, s, i)) — the commuting diagram is the deriver's test suite. The expanded derivation grammar and examples live in site/content/guides/optimistic.md.

10.6 Exhaustiveness#

Per mutation, coverage = invalidated-query set (derived) × status. Valid statuses are derived, hand-written, and await-fragment:

kovo check optimistic
mutation cart/applyCoupon:
  cartQuery.items      hand-written ✓
  cartQuery.subtotal   hand-written ✓
  cartQuery.discount   UNHANDLED ⚠ KV310
     → hand-write in cart.mutations.ts, or declare 'await-fragment'

Punts report their reasons inline (e.g. PUNTED (Opaque: compute_discount)).

The check runs at two altitudes off the same derived set. Query-handle bindings make each hand-written predictor editor-visible against the exact query result and mutation schema, and app.mutation({ optimistic: [...] }) rejects copied, cross-app, duplicate, or schema-mismatched bindings. The compiler resolves those handles against §6.1 InvalidationSets; kovo check reports KV310 when any invalidated query lacks either a transform or 'await-fragment'. The generated registry remains compiler/runtime IR rather than an app-authored type augmentation.

Forgetting an optimistic update is a visible, suppressible diagnostic with the suppression recorded in source — never a silent UI inconsistency.



Diagnostic Registry (SPEC §11.3)

This file is incorporated by reference from ../SPEC.md and is normative for Kovo framework behavior. The root spec remains the entry point and cross-reference index; this module owns the detailed contract below. This file is the normative owner for the KV### diagnostic table. The generated diagnostics reference compares framework source and diagnosticDefinitions against this table.

11.3 Diagnostic codes (registry)#

Code Severity Enforcement class Meaning
KV201 error compile-error Closure captures unserializable value (shows lowering + fixes)
KV210 lint audited-escape Anonymous handler — name it for stable identity
KV211 lint audited-escape on:load eager trigger — justification comment required (the greppable eager-JS budget)
KV212 lint audited-escape Unknown on:* event or trigger name (DOM event names; the closed trigger set, §4.7)
KV220 error compile-error Literal href/form action matches no declared route (full-origin URLs / external opt out)
KV221 error compile-error IDREF (commandfor, popovertarget, for, aria-*) references an id not present in scope
KV222 error compile-error Hand-written binding stamp disagrees with the typed expression it wraps (§4.8)
KV223 lint audited-escape Redundant hand-written stamp in sugar — the compiler derives it (§4.8)
KV224 error compile-error Static id in a repeatable component / duplicate id in a page composition (§4.5)
KV225 error compile-error JSX nesting violates the HTML content model — the parser would re-parent (§4.2)
KV226 error compile-error kovo-deps/kovo-c names an unknown query instance or component in emitted IR fixpoint validation
KV227 error compile-error Binding path traverses a nullable segment without ?. or a null-handling derive (§4.8)
KV228 error compile-error Ambiguous route table: two routes can match the same canonical request path or duplicate route path (§9.5)
KV229 error compile-error Static export constraint violation: route/session/mutation/param usage cannot be exported as L0/L1 (§9.5)
KV230 error compile-error Fragment-target children not lowerable to a component reference (shows the hoisting + fixes)
KV231 error compile-error Unmergeable attribute conflict in primitive composition (shows both sources + the §4.6 rule)
KV232 lint audited-escape Author override of a primitive-owned ARIA/state attribute
KV233 error compile-error Two writers for one binding target
KV234 error compile-error Package component prefix registration conflict or reservation violation (§6.1.1)
KV235 error compile-error App source hand-authors lowered IR/string-rendered components or derivable runtime stamps; write TSX (queries, key, typed expressions) and let the compiler emit IR (§5.2)
KV236 error compile-error Binding/derive/sink value reaches an unsafe output context (raw-HTML insertion; URL-scheme attribute against the javascript:/data: denylist for href/src/action/formaction/xlink:href/ping/poster/CSS url(); on*; style attribute or <style> text; srcdoc; <script>/application/json island text) without the typed trusted-HTML escape hatch (trustedHtml/trustedUrl, §4.8); contexts and URL-scheme allowlist defined in §4.8, contract in §5.2 #10
KV237 error compile-error Duplicate derived component registry key (§4.2, §4.8, §6.1.1)
KV238 error compile-error Duplicate derived fragment-target registry key (§4.5, §6.2, §9.1)
KV239 error compile-error Duplicate static view-transition name (§8)
KV240 error compile-error Duplicate or ambiguous query-plan identity (§4.8)
KV241 warn audited-escape Derived component registry key changed since the previous emitted graph (§4.2, §4.8)
KV242 error compile-error Enhanced mutation form control names do not match the bound mutation input schema (§6.2, §6.3)
KV243 error compile-error Invalid stream text target; streaming text targets are framework-owned source IDs, not arbitrary selectors or ambiguous DOM queries (§9.1).
KV244 lint audited-escape defer() used directly as a JSX child; use the public <Defer> primitive so fallback/render output follows JSX escaping (§8)
KV245 error compile-error TypeScript/TSX parse failed; later compiler phases cannot operate on a recovery tree (§5.2).
KV246 warn audited-escape Derived mutation registry key changed since the previous emitted graph (§4.1, §10.3)
KV247 warn audited-escape Derived query registry key changed since the previous emitted graph (§4.1, §10.2)
KV301 lint audited-escape Server fact in island-local state
KV302 error compile-error data-bind path is not present in the declared query shape (§4.8)
KV303 error compile-error Inferred refresh-target render input is not declared as query data or serializable stamped props (§4.5)
KV304 error compile-error Reserved query name such as state is not allowed (§4.8 binding roots)
KV310 warn audited-escape Invalidated query lacks optimistic transform (write/defer/derive)
KV311 warn audited-escape Query/state-dependent DOM position with no update status — plan/isomorphic/fragment/renderOnce (§4.9)
KV312 error compile-error Time-dependent rendered position has no declared clock/query refresh cadence (§4.8/§4.9)
KV314 error compile-error renderOnce position reads a query invalidated by a modeled write, so the immutable declaration would hide stale UI (§4.9).
KV315 warn audited-escape Raw Date.now()/new Date() read in a derive has no declared clock cadence; use a declared clocks input (§4.8/§4.9)
KV320 lint audited-escape Event payload overlaps query data — use a transform
KV330 error compile-error Direct db access in a mutation handler — route through domain
KV402 error fail-closed-runtime Write touched a domain not covered by the derived or declared mutation touch set (silent stale UI)
KV403 warn audited-escape Declared domain never observed written (stale claim / untested branch)
KV404 error fail-closed-runtime Write to unmapped table (map it or mark exempt, e.g. append-only logs — write-side only, §10.1)
KV405 error fail-closed-runtime Conditional/un-fully-resolved write site has branches never executed under instrumentation — CI-gating; static touch set is unproven (§11.1/§11.2)
KV406 error compile-error Statically un-analyzable write site (unresolved/node_modules; raw SQL) — manual touches (and tables: for raw SQL) required, executor-enforced + runtime-verified (§10.3/§11.1/§11.2)
KV407 error fail-closed-runtime Query read from a domain not covered by the derived or declared query read set (missed invalidations)
KV408 error fail-closed-runtime Declared row key ≠ observed row predicate
KV409 notice audited-escape Non-eq predicate — degraded to table-level invalidation
KV410 error compile-error Opaque query projection (sql<T>, raw SQL) — declared output schema AND reads: table set required; reads: checked against exemption (KV411) and folded into the read set, shape runtime-verified (§10.2)
KV411 error compile-error Query read set includes an exempt table — exemption is write-side only (§10.1), runtime-verified (§11.2)
KV412 error compile-error Query reads an unmodeled relation (view / materialized view) with no derived or declared domain (§10.1/§11.1)
KV413 error compile-error Database trigger / engine side-effect needs a declared fan-out edge before invalidation can be proven (§10.1/§11.1)
KV313 error fail-closed-runtime Optimistic transform settled with missing server truth — prediction discarded/refetched, not frozen (§10.4); a covered mutation shipping no truth for an invalidated query is also caught by KV310 (§10.6)
KV420 error compile-error Island declaring local state (kovo-state) nested inside another component's server-refreshable fragment target — the morph carries no local-state serialization and would clobber the child's live state on refresh (§4.5/§4.9/§9.1)
KV316 error compile-error isomorphic: true on a children/slot-accepting component whose render cannot be partitioned into self-render positions plus preserved projected-children regions (client self-render has no slot arguments, would drift from server output) (§4.5/§4.8)
KV317 error compile-error Static state-bearing aria-* value contradicts the primitive's render-time state — frozen-vs-clobbered; distinct from the visible-override lint KV232 (§4.6)
KV318 lint audited-escape isomorphic: true lacks an adjacent justification comment (§4.8)
KV414 error compile-error Authorization fails closed: either a query/write reaches an owner:-annotated table (§10.1) through a key predicate not traceable to req.session and not authorized by owns() (IDOR), or an authorization-bearing mutation transition is because the finite grant analyzer cannot classify it (§10.3). IDOR is runtime-verified (§11.2); suppress only a genuinely public read with a recorded justification. Grant transitions are not suppressible.
KV415 error fail-closed-runtime Response header channel: a direct structured app header is outside Cache-Control/Last-Modified/Vary, a dedicated field bypasses its typed option, an app writes browser-navigation (Refresh) or message-framing/hop-by-hop metadata, or a name/value contains CR/LF/NUL/control chars; Set-Cookie must use the typed cookie builder (§9.1.1)
KV416 error compile-error Prod render-equivalence gate failed (§5.2.2): apply_delta(base, render_prod(Δ)) ≢ render_dev(full) over the corpus, or a corpus edit violated §5.2.1 identity separation/monotonicity (shape/grammar changes must move the render-plan fingerprint and app build token without moving an unchanged representation digest). Build-failing.
KV417 error compile-error Configured supported deploy-skew window (§6.6/§14) is below the required 24-hour prior-version retention floor, or the serving layer cannot retain prior immutable modules and per-token /_q reads for the window (§14).
KV418 error compile-error csrf: false mutation references ambient browser authority (reads req.session or runs a session/cookie-derived guard) — the CSRF exemption is unsound; route non-browser writes to endpoint()/webhook() (§6.6, §9.1)
KV419 error compile-error prefetch: 'moderate' set on a guarded, session-dependent, or not-proven-side-effect-free route without a named justification (§8)
KV421 error compile-error Duplicate mutation key: generated mutation registry indexing and server dispatch would disagree (§6.1, §9.5)
KV422 error compile-error Request-derived or otherwise unproven data reaches executable SQL text on a framework-managed DB handle; bind scalar values as parameters and choose identifiers/keywords from typed allowlists or schema facts (§10.2/§10.3)
KV423 error compile-error Raw endpoint() declaration lacks required audit metadata such as explicit method, reason, mount justification, response body posture, cache posture, or app-owned encoding/header-safety posture (§9.1)
KV424 error compile-error App-authored dangerous sink is not registered or behind a safe Kovo helper/trust API; direct raw HTML, URL/navigation, selector, header, file/path, dynamic-code, process sinks must use the matching safe surface or audited escape hatch (§4.8, §5.2, §9.1)
KV425 error compile-error Source/sink drift detection found a framework sink token that is not in the shared registry and has no narrow repo-internal exclusion
KV426 error compile-error Trust escape hatch such as trustedHtml, trustedUrl, raw endpoint, custom/no verifier, static export path override, or future trusted SQL lacks auditable provenance/source-span/justification (§4.8, §9.1)
KV428 error fail-closed-runtime Inline rendering of an unverified-content-type upload; attacker bytes must default to attachment plus nosniff unless verified-safe or audited (§6.6, §9.1).
KV429 error compile-error Read-then-write on a contended column without an atomic/version guard; use compare-and-set or a version guard for lost-update safety (§10.3, §11.1).
KV430 warn audited-escape Schema admits unbounded breadth/depth on an untrusted source; declare explicit bounds while the runtime budget remains the protection (§6.6, §9.5).
KV431 warn audited-escape Referenced client module is absent from the integrity/CSP manifest; the provenance allowlist cannot audit a module it does not list (§6.6).
KV432 error fail-closed-runtime Insecure cookie downgrade without a recorded justification; credential cookies use the typed cookie builder security floor by default (§6.6, §9.1).
KV433 error compile-error query() loader reaches a write; read surfaces must not cause state changes (§6.6, §9.4).
KV434 error compile-error Non-linear-safe pattern literal in a wire string validator; use blessed formats, safe literal patterns, or audited unsafeRegex (§6.6, §9.5).
KV435 error compile-error Secret-classified query result field, or an opaque/unresolved projection from a table carrying secret columns, reaches the client query wire; remove the field or rewrite the projection so the analyzer proves that only public columns cross the wire. Declassification does not suppress this request-boundary error because §6.6 makes the policy constructor and reveal doors request-closed with KV448 (§6.2, §6.6, §10.2, §11.3).
KV436 error compile-error Query, mutation, route/page, endpoint, or webhook has no explicit access decision (default-deny, §10.2); satisfy it with an access guard chain (an existing guard/layout guard counts), publicAccess("reason"), or verifiedAccess/an auth/verify scheme, and review the ledger with kovo explain access. Proves a decision exists, not that it is correct — IDOR correctness stays KV414 (§10.2, §11.3)
KV437 error compile-error Server-only value captured into a client handler reaches the client bundle; client modules may emit only values proven client-safe or audited (§6.2, §6.6).
KV438 error compile-error Request input or opaque provenance reaches a governed column (mass assignment); governed columns must receive structurally proven server/private values, literals, or trustedAssign(input, obligation) with the exact inline invariant/why/evidence grammar. Prose, dynamic/malformed obligations, app analyzer summaries, and serverValue(unknown, ...) do not discharge the gate. Accepted escapes emit artifact-bound unsigned review subjects (§6.6, §10.3, §11.1).
KV439 error compile-error DB table row reaches the client query wire without an explicit projection; row provenance crosses the client wire only through intentional field projection (§6.2, §9.4, §11.3).
KV445 error compile-error Build registers durable tasks but the selected preset declares no JobRunner capability; deploy with a preset/adapter that emits a real drainer, or remove task()/request.schedule() for that target (§9.6).
KV446 error compile-error Node preset durable tasks are registered for a SQLite/better-sqlite3 server bundle even though the default JobRunner persists jobs in Postgres _kovo_jobs; use a Postgres-compatible app database, a supported durable queue adapter, or remove durable tasks from that deployment (§9.6).
KV447 warn audited-escape SQLite owner annotations are advisory only in the experimental SQLite runtime: kovo((columns) => ({ owner: columns.ownerId })) and ownerVia remain available to static audits, but SQLite has no engine role/RLS layer, so multi-principal authorization requires the default PGlite/Postgres runtime (§10.3).
KV448 error compile-error An untrusted-data-reachable root reaches raw network, filesystem, process, VM, worker, database-driver, or unresolved module-loading authority; or a reachable package summary is absent, stale, contradictory, or incomplete for the installed version/conditional exports. Use the reviewed Kovo capability door or repair the exact-version summary; diagnostics include the root-to-capability provenance path (§6.6).
KV449 error compile-error A browser/server effect is outside the finite security IR, or its same-file-helper summary is unsupported, recursive, or over budget. Unknown operations and partial mutation forms close; server diagnostics name root, transfers, sink, and reason. Use typed <form mutation>, a reviewed operation, an exact immutable helper inside the bounded semantics, or a named explain-visible exceptional door (§4.3, §5.2, §6.6, §9.1). It is not a general code evaluator or a same-realm JavaScript sandbox.
KV450 error fail-closed-runtime A non-database stateful sink key has no framework-witnessed owner scope, carries a forged/malformed frame, or names an unregistered system posture. Derive principal keys with scopedKey(request, key) or task actAs(id).stateKey(key); use publicScopedKey(key) only for deliberately shared state. Storage and durable-task coalescing doors reject strings and casts before namespace use (§6.6, §9.6, §10.3).
KV451 error compile-error A compiler-derived value cannot be represented by the shared structural source-emission grammar for its exact role (jsStringLiteral, jsIdentifier, tsPropertyKey, or importSpecifier). Emission fails before writing an artifact instead of interpolating the value as executable sibling syntax (§5.2).
KV452 error compile-error Owner-scoped or governed database data reaches a persistent non-engine sink without the framework-owned derived() door, or a derived vector operation lacks the exact framework request principal binding. derived(adapter, { key, kind: 'vector' }) reconstructs every physical namespace from the complete runtime-witnessed ScopedKey frame, and reads re-derive that same request-principal scope (§6.6, §10.3 C9).

The enforcement class is the code's primary SPEC §2 posture. compile-error means static analysis, build, export, or check rejects before an affected artifact is accepted. fail-closed-runtime means the decisive fact is only available while executing a bounded framework door, which refuses the operation or discards unsafe state. audited-escape is non-blocking, degraded, or explicitly reviewed posture that must remain visible to check/explain. A runtime floor may backstop a compile-error; the static class wins when the same violation is provable before execution.

The shared diagnosticDefinitions registry is the source of each diagnostic's severity; surfaces must not override severity or invent local blocking policies. A diagnostic with error severity blocks the Vite dev transform by throwing a teaching error rendered by Vite's overlay and terminal, blocks build and static export before output is written, and makes dev-mode page, fragment, or mutation requests that depend on the failed module return a server-rendered teaching-error document with HTTP 500. warn, lint, and notice diagnostics are non-blocking on dev transform, build, and static export; they may be summarized or streamed through the surface's non-blocking diagnostic channel, but they do not trigger dev teaching-error documents. MCP tools expose the same structured diagnostics (code, severity, message, help, and position when available) from the compile/check/explain APIs; MCP is a rendering/query surface, not a second diagnostic channel.

Structured diagnostic provenance is runtime-registry identity, not structural shape. That private identity is realm-local: when a supported compiler, build, or export boundary crosses an isolate or bundled SSR module graph, the originating realm MUST first verify exact registry membership and emit a bounded own-data diagnostic wire record; the receiving realm MUST validate that record and reconstruct it through its own generated diagnostic constructor before collection or rendering. Copying a diagnostic object, sharing a public symbol brand, or accepting a wire record without the originating registry check cannot transfer framework diagnostic authority.

11.5 Finite MCP stdio transport#

Kovo's CLI and devtool MCP surfaces use newline-delimited JSON over stdio only. The closed method set is initialize, ping, tools/list, and tools/call; the only lifecycle notification with state is notifications/initialized. The server has three ordered phases: it first accepts one valid initialize request, then waits for notifications/initialized, then serves ping and tool requests. Duplicate initialization, or a request before the ready phase, fails closed. Other notification-shaped messages receive no response and cannot change lifecycle state. Initialization changes phase only after the exact id-bound success response is proven to fit; an oversized success returns a bounded error and leaves the server able to accept a shorter retry.

Every request MUST be a JSON-RPC 2.0 own-data object with the exact request envelope and a string or safe-integer id whose echo fits the bounded error envelope. Each method has a closed top-level parameter grammar: initialize requires protocolVersion, capabilities, and name/version clientInfo; ping permits only optional _meta; list permits only optional string cursor and _meta; call requires name and permits only object arguments and _meta. _meta, capabilities, and arguments remain inert JSON data. Surplus fields, accessor-bearing direct inputs, invalid ids, and malformed params are rejected. Supported protocol versions are 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, and 2024-10-07; an unknown requested version negotiates to 2025-11-25 so the client can decide whether to continue.

Each input or output payload is at most 4 MiB of UTF-8 bytes; the finite engine accepts configured ceilings from 256 bytes through that maximum. LF and CRLF are equivalent delimiters, and their bytes do not consume the payload ceiling. The parser preserves code points split across byte or string chunks, rejects malformed UTF-8, malformed JSON, and duplicate object members (including escaped key aliases), processes a final nonempty EOF segment as one line, discards an oversized line through its next delimiter, and then resumes. Direct and tool-provided JSON is snapshotted through own data descriptors before field reads and is limited to 128 container levels, 65,536 value/member nodes, and an exact non-allocating estimate no larger than the applicable serialized line ceiling.

The CLI adapter has five tools and an exact, non-extensible argument language:

  • compile_component accepts only fileName and inline source. sourceProvenance is implicitly app; package-prefix, query-shape, and registry fact carriers are not protocol inputs.
  • kovo_check accepts only an optional family (all, coverage, or optimistic) and an optional inline graph.
  • kovo_docs accepts a required 1..256-byte task string and an optional result limit from one through eight. It searches only the digest-authenticated local snapshot selected by kovo update-docs; the result includes the exact Kovo version, snapshot digest, file digest, and bounded excerpt. It has no network or caller-selected path input.
  • kovo_explain accepts only an optional inline graph plus one exact options mode: agent, endpoints, access/unguarded/unscoped audit, or a kind/target lookup. Surplus option fields and multiple modes are errors.
  • list_diagnostics accepts the empty object only.

The protocol has no graphPath or other caller-selected filesystem input. Human kovo check and kovo explain argv commands may still read an operator-selected graph file; that authority is not part of MCP. At server construction the CLI canonicalizes the launch working directory once. It retains that directory's device/inode witness; a rename, replacement directory, or symlink at the original canonical path cannot become new authority for a later compile. Compiler fileName is a slash-separated relative path with no absolute, empty, dot, dot-dot, backslash, or NUL segment, and has at most 64 path segments. Both package discovery's root and its hard canonical boundary are the pinned launch directory. Bounded discovery admits at most 128 unique bare packages before sorting or filesystem probes. Directory and manifest symlinks cannot escape that boundary; FIFO and other non-regular manifest candidates are ignored; and a package manifest above 256 KiB is ignored before JSON parsing. Bounded manifests are read from a no-follow, nonblocking descriptor with a fixed-size loop and one-byte growth probe, so a same-inode grow race cannot allocate past the cap. The accepted descriptor and pathname facts must retain device, inode, size, mtime, and ctime through the read, rejecting same-size in-place rewrites as well as path swaps. Changing process cwd after construction cannot move this capability.

One inline compiler source is at most 256 KiB UTF-8. Before TypeScript parsing, the adapter performs a linear scan capped at 32,768 token starts/punctuation units and 512 potential structural opener tokens. After parsing and before lowering, an iterative whole-AST walk admits at most 20,000 nodes and depth 256. Recursive parser exhaustion is normalized to one closed parser-budget error rather than exposing the host runtime failure text. These are transport work limits, not substitutes for the compiler's semantic fail-closed budgets. They keep flat, deeply nested, alias-heavy, and malformed boundary inputs local to one bounded call.

Graph admission is also a pre-verifier resource proof. A linear walk counts every traversed object property and array entry, then charges their conservative pair envelope plus the explicit render-once updateCoverage × queries × (mutations + touchGraph) domain work with overflow-safe saturating arithmetic. The aggregate ceiling is 65,536 comparison units. It applies before the linear graph validator and therefore bounds mutation/query, query/consumer, endpoint/runMutation, scope/ownership, session-authority, event/query, and endpoint-posture joins as well as the named cubic path. A graph string is at most 4,096 bytes; materialized output is preflighted at 2,048 estimated rows and 2 MiB of amplified graph text before the transport's independent 4 MiB response ceiling. One stdio session admits at most 256 tool calls.

Dispatch and output are sequential and backpressure-aware. A false output write MUST wait for a provided drain capability or fail explicitly; it cannot be treated as successful delivery. Protocol failures use JSON-RPC errors; a tool-domain failure is a successful JSON-RPC result with isError: true, with id-aware text truncation when necessary. Static tool descriptors or instructions that cannot fit the output ceiling are rejected at construction. An oversized or unserializable tool result emits one bounded protocol error and leaves the connection usable. The surface provides no HTTP, SSE, OAuth, resources, prompts, sampling, tasks, logging channel, server-to-client request, or extensible method router. MCP diagnostics remain the registry-owned diagnostics above, not a second diagnostic channel.


Static Analysis & Verification (SPEC §11 except §11.3)

This file is incorporated by reference from ../SPEC.md and is normative for Kovo framework behavior. The root spec remains the entry point and cross-reference index; this module owns the detailed contract below.

11. Static Analysis & Verification#

11.1 Touch-set extraction (the static pass)#

Rests on one property: Drizzle's table argument is always an imported identifier with a statically known declaration site.

For each write() body (ts-morph over the program):
  1. Find CallExpressions where callee.name ∈ {insert, update, delete}
     AND receiver's TYPE originates in drizzle-orm        ← type identity, not variable names;
                                                            renames/destructuring irrelevant
  2. Resolve argument 0:
     A. imported identifier        → follow symbol → pgTable declaration   (90%+)
     B. namespace/re-export chains → getAliasedSymbol loop
     C. alias(T, …)                → recurse on T
     D. conditional initializer    → union both branches (over-approximation is safe:
                                     missing = bug, excess = warning)
     E. runtime-flowing value      → 'unresolved' → KV406 (error: manual touches REQUIRED;
                                     dev/build/export gate blocks until supplied — §10.3)
  3. Interprocedural: helpers receiving a Drizzle-typed value are summarized bottom-up
     (memoized fixpoint); calls into node_modules with a db arg → KV406 (error, same gate).
     `update…from(R)` / `insert…select` contribute R to the READ set, not touches.
     Opaque/raw query projections (KV410, §10.2) contribute their declared `reads:`
     table set to the READ set; a `reads:` entry naming an `exempt` table is KV411.
  4. Parameterized keys: extract eq(T.keyCol, expr) from .where(); expr traceable to a
     write param ⇒ key derivation recorded; ranges/IN ⇒ table-level (KV409 notice).
  5. Whenever a write site's touch set is not fully statically resolved (any 'unresolved'
     table at step 2.E, any node_modules db call at step 3, or any raw-SQL statement whose
     mutated tables cannot be read off the AST), it is **KV406 (error)** absent a manual
     `touches`/`tables` declaration, and an unexecuted conditional write on such a site is
     **KV405 (error, CI-gating)** — see §11.2. KV405 is no longer advisory: a write site
     whose touch set is not fully statically resolved and whose branches were not all
     observed under instrumentation blocks build and static export, because the runtime
     cross-check (§11.2) cannot have proven the unexecuted arm's touch set sound.

Output is reproducible on demand through kovo emit / kovo explain and mechanically proven by fixpoint plus render-equivalence gates. The emitted graph is also the runtime authority for derived query reads and mutation touches; manual reads / touches are checked overrides for opaque sites, not the default authoring model. Invalidation-graph changes are inspected through those commands and CI evidence, not by committing app-local generated files:

ts
// emitted generated/touch-graph.ts — DO NOT EDIT
export const touchGraph = {
  'cart.addItem': {
    touches: [
      { domain: 'cart', via: 'cart_items', site: 'cart.domain.ts:8', keys: null },
      { domain: 'product', via: 'products', site: 'cart.domain.ts:12', keys: 'arg:productId' },
    ],
    unresolved: [],
  },
} as const;

11.2 Runtime verification (independent cross-check)#

Dev server and the test harness wrap db; every executed statement is parsed by the configured dialect path (Postgres uses pgsql-ast-parser; SQLite normalizes ? placeholders before the same structural walk) and checked. Static over-approximates (all branches); runtime under-approximates (executed branches). Invariant: observed ⊆ static ∪ KV406-annotated — violation means analyzer bug or smuggled SQL; either is a CI failure. For raw-SQL writes this invariant is enforced structurally: the executor parses each statement with the configured dialect path and checks its mutated-table set against the write's declared tables: allowlist (§10.3). A statement that mutates a table outside tables: is a CI failure under instrumentation and, in production where instrumentation is absent, fails closed — the executor conservatively invalidates every domain in the write's touches and records the violation, never silently dropping the unexpected table's invalidation.

For managed SQL handles, runtime verification is over a framework-owned statement artifact, not the caller-owned JavaScript object. The first managed boundary MUST snapshot every accepted carrier (sql template calls, Drizzle SQL, separated { text, values }, prepared statements, trusted SQL) into an immutable statement value containing the exact SQL text, parameters, dialect, and provenance that the framework validates. Validation, table/function classification, diagnostics, instrumentation, and driver execution MUST consume that same immutable artifact. A mutable object, getter-backed carrier, proxy, or object identity reused across calls cannot present one statement to the verifier and a different statement to the driver; Kovo must either reject it or make a one-time snapshot before any check. Passing the original carrier to the driver after validating a snapshot is a verification bug.

On the Postgres/PGlite managed path, the engine also enforces the dangerous write-scope cases below the framework declared-write wrapper. Owner and owner-via tables are granted to the writer role only with row-level security and WITH CHECK policies that bind writes to the current principal, so cross-owner writes and ownership reassignment are denied by the database. Unclassified/reference tables are not granted to the writer role at all, so attempts to mutate tables such as verification fail with engine permission denial even if an app smuggles a raw statement past the declared-write wrapper. The framework declared-write wrapper remains load-bearing for coverage and invalidation: over-declaring among writable owner/authz-policy tables can still produce stale or excessive invalidation behavior and is a KV406 contract violation, but it is not the confidentiality/integrity boundary for cross-owner or unclassified-table writes on this engine path. Full per-mutation engine roles remain outside v1.

Because instrumentation under-approximates (executed branches only), passing dev/test runs do not establish KV406 completeness; an unexercised raw-SQL arm is proven sound only by its statically-declared tables:/touches, which is why those declarations are KV406-error (not advisory) and an unexecuted such branch is KV405-error (§11.1). Read-side gets identical treatment (query loaders' SELECT/JOIN tables vs. derived read sets, and observed result shapes vs. declared/inferred types — the runtime half of KV410, so an opaque projection's schema claim is tested against what the database actually returns; an opaque projection that reads a table absent from its declared reads: set (§10.2) is a CI failure on the same observed ⊆ static ∪ declared invariant, but the static reads: declaration — not this dev/test-only observation — is what proves an unexercised branch sound). An observed read of an exempt table is the runtime half of KV411 (§10.1) — the same CI failure whether the exempt read was statically visible or smuggled through raw SQL.

Security-decision event completeness (normative). The generated production runtime has one closed, build-checked answerability denominator: the canonical auth, authorization, declassification, egress, storage, task, and replay decision chokes. Every enrolled choke MUST route both allow and deny outcomes through the single securityEvent() journal and emit exactly these no-payload facts: the door, an outcome, a build-stable decision-site identity, an honest principal scope including its epoch (or an explicit unresolved reason when the epoch or principal is unavailable), and an opaque resource scope consisting only of its registered kind and global or a framework-produced SHA-256 identity. Raw credentials, URLs, keys, rows, secret values, task arguments, replay tokens, and other payload data MUST NOT enter the record.

The reviewed decision-site census and production markers are a closed emission-coverage recorder. The root gate MUST fail when a door lacks exactly one enrolled site, a marker or site exists without the other, an enrolled constructor disappears, a constructor bypasses securityEvent() (or the journal-free core-to-server transport that immediately feeds it), a required fact becomes optional, an allow/deny branch disappears, or an extra field is added. The core transport MUST NOT own a second journal, buffer, export surface, or verdict; generated registration installs it before authored app evaluation and the server journal remains the only event authority.

This completeness claim begins when the compiler-generated runtime-posture registry evaluates. Production artifact emission MUST refuse a runtime entry that can evaluate the authored app before that registration. Direct low-level library calls and unit calls made before registration are explicitly outside the claim. Registration arms decision recording only after any configured deployment journal is installed; after arming, an enrolled decision with no journal MUST fail closed before proceeding. This is emission completeness for the seven named chokes, not a claim about arbitrary app or third-party decisions, host compromise, fleet-wide delivery, or infinite retention. The bounded journal's dropped count and every unresolved principal scope remain explicit unanswerable outcomes for retrospective tooling; absence of a matching retained event is only not-observed, never a no-impact proof.

C9 sink-proof inventory (normative). The verification surface MUST keep a single reviewed inventory for the required boundary-crossing sinks named in §10.3 C9. Each row names: the sink, its mechanism (reconstruct, box, or framework-own), the sole door, at least one lint/check/build proof, at least one hostile-value test file or command, and the stable owner responsible for a gap. The machine gate MUST compare its covered-family union with the complete source/sink census and fail on a missing or unknown family, duplicate sink row, missing owner, absent root proof command, or stale evidence path. The inventory is a proof index, not a runtime policy source: if a sink exists without an inventory row, or a row has no hostile-value evidence, the verification surface is incomplete even if the implementation happens to be sound. It MUST also compare the exact runtime securityOperationKinds union with every row's operationKinds, requiring one and only one C9 owner per finite compiler operation and rejecting missing, unknown, or duplicate kinds. Terminal-effect rows name their real sink owner; the server.handler.root and server.helper.call control rows name capability closure and remain explicitly non-semantic until the latter receives a Phase 2C call summary. Component graph facts and kovo explain component render the compiler-derived operation rows in stable order so a review can connect authored handlers to those owners without reading generated files. For engine-door claims the inventory row points at the engine-closure audit; for wire/file/derived/task/log surfaces it points at the single framework-owned choke or box, never at a proxy-only wrapper. The data.derived.persistence family is discharged by the storage-operation row only when its proof evidence includes both KV452 provenance closure and runtime reconstruction of the complete request-principal ScopedKey namespace.

Every source/sink census row MUST also declare one closed residency posture: none, db-owner, ledger, adapter-enumerable, or unerasable:<reason>. A family that combines multiple runtime sinks takes the least erasable posture of any covered sink; it cannot hide retained task, replay, client, adapter, or external-recipient state behind a transient member. The C9 inventory gate MUST fail on a missing or unknown posture, an empty or malformed unerasable reason, or an unerasable count of zero. kovo explain sources-sinks, kovo check sources-sinks, and the versioned inventory artifact MUST publish each row's posture, and the text summaries MUST publish the current unerasable count. This count is an honesty metric and erasure-work denominator, not a claim that the enumerable postures already implement principal erasure.

Finite provenance relation (normative proof boundary). The compiler MUST publish the current server/browser provenance vocabularies and the complete server member-projection relation as the versioned, diffable security-provenance-relation/v1.json artifact. The current denominator is 43 server states (including explicit derived-dataset, derived query/upsert call, governed-data, and scoped-key-call states), 20 browser states, and the quotient member alphabet recorded in that artifact. check:provenance-closure MUST fail when either operation vocabulary gains a state without a relation row, when any table cell differs from the scanner, or when least-fixpoint reachability finds an operation without its C9 door owner. Unknown future states default to authority-bearing; unknown-authority closes under exactly the declared SecuritySemanticClosedReason domain.

This decidability claim is deliberately narrow. Five serverExpressionProvenance arms are compositional over child provenance values; identifier lookup is an environment leaf, the implicit object-protocol check remains syntax-dependent, and the four fallthrough subtree searches (foreign executable, governed data, unsafe wire data, and authority) are one named nondeterministic oracle edge with outcomes local, foreign-executable, governed-data, unsafe-wire-data, and unknown-authority. The table does not decide general JavaScript, dynamic properties, Proxy behavior, imported executable semantics, or the browser classifier's syntax-dependent transfers. The artifact publishes those exclusions, the four semantic-analysis resource bounds, and the remaining extraction gaps beside the relation so a finite proof cannot be mistaken for a whole- JavaScript soundness claim.

Differential analyzer-soundness oracle (normative evidence boundary). The server semantic analyzer MUST consume the versioned kovo-security-abstract-interpreter-census/v1 lattice, resource bounds, and transfer vocabulary. check:analyzer-soundness-oracle MUST bind every census transfer to a production marker and one seeded generator production, fail on an uncensused production transfer, compile a canonical program that actually reaches every transfer marker, and behavior-check every declared lattice element through the production transfer functions against independent expected results. Intended closed and resource-edge programs MUST produce their exact declared closed reason. The generated language is the finite kovo-security-analyzer-language/v1 grammar recorded beside the census; its declared JavaScript exclusions and generation bounds are part of the claim, not implementation notes.

For every accepted generated program, an independent concrete interpreter predicts the reviewed effect-door calls without consuming the analyzer's provenance relation. The compiler-emitted server module is then executed with explicit framework-door stubs (never general Proxy observation), and the oracle requires both concrete/emitted agreement and observed ⊆ abstract-predicted. A counterexample MUST be minimized and persisted as kovo.security-fuzz-counterexample/v1; a scoped canary that weakens the production effect.invoke transfer and a canary that deletes one effect observation MUST fail. A persisted artifact MUST remain unconfirmed and MUST NOT claim replayVerified: true or an unsafe verdict until a program-specific replay reloads the serialized seed, minimized program, and canary (empty for an organic finding), recompiles that program, and reproduces the exact disagreement. The fixed-seed analyzer-soundness family runs in the nightly security campaign. Passing this falsification search is evidence for only the declared finite language. It does not prove soundness for general JavaScript, imported executable semantics, dynamic properties, browser provenance, asynchronous scheduling, implicit protocols beyond the explicit object-literal close production, or behavior beyond the published resource bounds.

Async-context non-interference (normative evidence boundary). Runtime scheduling evidence is separate from the finite abstract-interpreter claim above. check:async-context-confinement MUST derive every deployable authority cell from kovo.async-context-confinement/v1, reject a raw or uncensused AsyncLocalStorage door, and mutation-check the exact lifecycle, close, isolated-root, and verifier-observer revocation obligations. Its seeded runtime oracle MUST exercise distinct principals concurrently through microtasks, stream backpressure, and thenable callbacks and observe no cross-lifecycle cell value. This proves the shared runtime contract over those interleavings; it does not mean the abstract interpreter models arbitrary event-loop scheduling. Only a separately declared finite check→await→use production may enter the analyzer oracle's generated language.

11.4 The verification surface (the Keppo contract)#

For a Kovo app, the following are checkable without executing a browser:

  1. TypeScript static checking — all wiring (handlers, routes & links, forms, targets, bindings, IDREFs, transforms, guards).
  2. kovo check — TypeScript plus a compiler/security graph derived from the current app source, followed by touch-graph consistency, optimistic exhaustiveness (KV310), update coverage (KV311), fixpoint + render-equivalence invariants, capability closure (KV448), and unguarded and unscoped audits.
  3. Graph queries over kovo explain output — intent-level assertions ("every component displaying cart data is refreshed by cart/add") as set operations over printed, stable-format graphs, including each component's finite operation rows and the capabilities root/door/package/closed provenance ledger from §6.6.
  4. Property suite — prediction ⊆ eventual-truth generative tests over hand-written transforms and derivation soundness (commuting diagrams).
  5. HTTP-level integration tests — mutations as request/response assertions against pglite (real Postgres semantics, in-memory, no container).

Source-proof and deployment-proof split (normative). Bare kovo check derives the app from ./src/app.tsx; kovo check source [app-module] selects another authored entry. Both regenerate TypeScript and compiler/security facts from that current source before emitting the stable kovo-check/v1 result. They MUST NOT require, infer, or fabricate a deployment preset, emitted artifact, least-privilege deployment posture, or §14 retention declaration, and they MUST NOT write deploy artifacts. A graph-consuming compatibility form such as kovo check coverage [graph.json] MUST fail non-zero when neither its explicit graph nor a conventional graph exists; absence is never converted into an empty passing verifier input.

kovo build reruns the same current-source proof, then additionally verifies the selected preset, artifact, least-privilege, and deploy-skew/retention obligations before promoting deploy output. Those deployment obligations remain fail-closed, including KV417 under §14. A passing source check therefore means “current authored source satisfies the source verifier,” not “this deployment is ready.”

Foreground incremental source proof (normative). kovo check source [app-module] --watch --format json keeps one project-confined foreground process open and emits one versioned JSONL record for each serialized source revision. It is not a global daemon. Its queue is bounded to the currently executing revision plus the latest pending filesystem state; intermediate edit bursts may coalesce, but checked revisions never overlap or publish out of order. Delete, rename, symlink, or import/config-closure ambiguity fails closed instead of retaining the previous passing result.

One-shot and watch forms MUST execute the same ordered source-proof pipeline. A watch revision still performs fresh command-owned app evaluation, recomputes the security graph, verifier fixpoint, render-equivalence proof, and diagnostics, and emits the exact kovo-diagnostic/v1 command result a fresh one-shot invocation would emit for the same bytes. Reuse is limited to compiler-owned facts whose authenticated cache key binds every source, config, package-closure, and external-version input; app objects, runtime authority, diagnostics, and partially assembled graphs are never cached. The JSONL record binds exact source/config/closure digests and carries the complete ordered phase census. Census v2 distinguishes executed from reused-authenticated, names the input digest for every phase, and retains every phase after a finding as not-reached; it may not omit a phase to claim a latency win. Rejected filesystem states carry an explicit rejected proof record and never invent a digest for missing or ambiguous bytes.

Command result and diagnostic protocol (normative). kovo check, kovo build, kovo explain, kovo doctor, and kovo verify accept exactly --format human | json | github and retain the exit classes declared by the semantic command AST: success is 0, proof/build findings are 1, and invocation/configuration errors are 2. Every finding crosses the framework-owned kovo-diagnostic/v1 record before presentation. Code, severity, help, and source range are producer-owned facts; renderers may escape or lay them out but may not parse prose, consult a second severity table, or manufacture a location. JSON carries the diagnostic envelope plus the command's existing versioned result protocol and exact result text. GitHub output emits escaped workflow annotations from the same records and preserves the same result facts. kovo-check/v1 and kovo-explain/v1 therefore remain byte-for-byte payloads inside the common envelope rather than being silently replaced by an empty diagnostic array.

The semantic command AST is the sole source for argv parsing, semantic request types, root and subcommand help, shell completion, and command-reference data. It includes aliases, argument kind, enum, default, repeatability, category, examples, exit behavior, and result protocol. Programmatic callers consume its discriminated request union and never an argv-shaped bag of flags.

Versioned starter policy (normative). The starter owns a compact declarative kovo.policy.json; versioned CLI implementations own lifecycle allowlist validation, sound-subset analysis, endpoint-posture orchestration, and fail-closed parallel scheduling. Generated apps do not copy those algorithms. The default kovo check runs source proof and the applicable declared policies. Deployment endpoint probes run through kovo verify --artifact <dist>, after a successful build. App package scripts name Kovo, Vitest, and the package manager only; Vite Plus may remain a framework/CI implementation tool but is not app-facing vocabulary.

Daily coherence and copy-in commands (normative). kovo doctor [root] reads bounded local configuration and package facts without evaluating authored modules or contacting the network. It checks the required Node and pnpm versions, duplicate Kovo installations, Kovo peer ranges, config/preset selection, origin posture, database-role posture, migrations, deploy-skew retention, writable framework paths, and cache freshness. Findings are finite producer-owned kovo-diagnostic/v1 records. --fix is restricted to framework-classified derived state: it may create the project-owned .kovo directory or remove a stale real .kovo/cache directory after containment and non-symlink checks; it does not rewrite security posture, credentials, package versions, migrations, or authored source.

kovo add --list is the exact copy-in registry. A component typo suggestion is derived from that same registry rather than a second alias table. kovo add ... --dry-run performs no filesystem or process writes. --install=never copies source and reports the dependency follow-up without editing the manifest; --install=auto stages component files, atomically updates the captured manifest, captures the package-manager lockfile, runs the declared package manager, and promotes source only after install succeeds. An install or promotion failure restores staged manifest/lockfile/source edits and reports completed, planned, rolled-back, and possible package-manager-owned node_modules work distinctly. kovo test is the app-facing, schema-owned one-shot Vitest command; the CLI may delegate to its pinned Vite Plus implementation dependency, but generated app scripts and help do not expose that implementation command.

Safe cost-to-green rewrites (normative). kovo fix accepts exactly one regular, non-symlink app-authored .tsx/.jsx file inside the invocation root, excluding .kovo, dist, generated, and node_modules trees. It MUST NOT synthesize a trust wrapper, justification, allowlist entry, or other escape. A rewrite is available only through this closed compiler-owned recipe set:

  • KV223 may remove one exact data-bind JSX attribute only when the genuine compiler reports that the attribute is redundant with its typed child expression. Because hand-authored lowered IR is already rejected by KV235 and can suppress compiler-owned escaping, this is a security-hardening rewrite, not a claim that the invalid input had accepted behavior to preserve.
  • KV232 may remove one exact author-owned role, aria-*, or data-state override only when the rewritten compiler output has the exact same semantic behavior fingerprint.

The independent post-rewrite pass MUST prove that the candidate differs only by the approved typed AST nodes, that every target obligation is absent, and that the complete genuine compiler analysis is green before a write. Unknown diagnostics, mixed proof classes, overlapping edits, stale source, an analyzer residue, or a changed behavior fingerprint where equality is required MUST fail closed without returning candidate source. --check is read-only and non-zero when a safe rewrite is available.

kovo fix --cost-report emits kovo.cost-to-green/v1 over the versioned agent-authored corpus. Its per-diagnostic metric is safe AST-node edit atoms − escape argv atoms, where deleting one typed AST node costs one atom and --allow-diagnostic CODE costs two. A missing safe recipe has unbounded safe cost. Every row where escape is cheaper, including an unbounded safe cost, MUST be reported as a framework defect with a non-empty owner; the report exits non-zero while any such row exists. This is an ergonomics and routing measurement, not evidence that an escape is safe or should be chosen.

Deployment assume-guarantee contract (normative). Every current SECURITY.md guarantee MUST carry a machine-readable antecedents list. That list is derived, never independently authored: the versioned kovo.deployment-environment-doors/v1 registry binds each environment fact to the exact framework door that consumes it, the door's source anchors, and the affected published or normative conditional guarantee IDs. The security-guarantee gate MUST reject an unknown fact or guarantee, a missing consumer anchor, a current guarantee absent from the registry, or any SECURITY.md antecedent list that differs from the door-derived relation. A prose assumption or an operator-authored success verdict is not evidence.

kovo check env [deployment.json] consumes kovo.deployment-environment/v1, probes only facts observable from its pinned command-entry environment, and prints every remaining fact as a RETAINED obligation with the exact guarantees it suspends. A canonical KOVO_NODE_ORIGIN discharges only the zero-forwarded-hop proxy-chain fact; it does not authenticate the TLS terminator. KOVO_NODE_TRUSTED_PROXY=1 retains the edge-identity/hop obligation, a host preload cannot be disproved from an absent NODE_OPTIONS, and database writers, shared-cache behavior, registrable-domain occupancy, and TLS edge identity remain retained unless a future framework-owned probe owns corresponding evidence. Contradicted, retained, or posture-withheld antecedents produce a non-zero result. The command reports conditional status; it is not a deployment-integrity proof.

The composition domain has exactly three input shapes. single-kovo retains external occupancy facts. shared-registrable-domain requires at least two unique canonical HTTPS Kovo origins under one explicitly declared DNS suffix and contradicts sole-registrable-domain-occupant; the command therefore withholds the CSRF principal-binding claim because another app can compete for browser cookie namespace. This declaration is not a Public Suffix List proof. foreign-host is accepted only with posture mounted and one canonical non-root mount path made of non-empty RFC 3986 unreserved segments, excluding . and ... Mounted posture unconditionally withholds the host-owned CSRF, request-origin, and browser-state-cache claims; an author cannot turn them back on with a flag or asserted verdict. All other composition/posture pairings fail input validation.

Authenticated advisory contract (normative). kovo.security.advisory/v1 is an exact, closed record containing id, one of low | moderate | high | critical, one finite affectedRange, fixedIn, retracts[], tcbChokes[], and graphSchemaVersion. The only range grammar is >=VERSION <VERSION over strict SemVer, with an increasing exclusive upper bound and a fixedIn version at or above it. Applicability predicates, package-name selectors, executable expressions, host facts, and arbitrary extension fields are forbidden. retracts[] names exact guarantee IDs in the normative SECURITY.md register; tcbChokes[] names exact current TCB entry IDs. The canonical kovo.security.advisory-feed/v1 record contains a positive monotone epoch, a canonical issuedAt, maxFeedAgeSeconds, and an ID-sorted advisory array. The repository gate MUST keep the feed schema exact, fresh within a maximum 90-day release window, and equal in advisory IDs and retraction sets to the public guarantee register; unknown guarantee or TCB IDs fail the gate. It MUST compare the checked-in feed to the first-parent feed: a lower epoch, or any canonical feed change without an epoch increase, fails before release signing.

kovo check advisories [graph.json] MUST first read build-owned kovo.artifact.provenance/v1 from the graph and obtain every exact @kovojs/* package version plus the graph schema version. When no graph path is explicit, exactly one conventional graph artifact MUST exist; multiple candidates produce UNKNOWN instead of a precedence-based choice. The command then fetches the feed over HTTPS (or reads an explicit in-root regular file for an offline drill), computes its SHA-256 digest, and obtains an attestation by that digest. The accepted bundle MUST pass Sigstore signature and Fulcio-chain verification, the GitHub Actions OIDC issuer, the exact .github/workflows/release.yml@refs/heads/main certificate identity, and at least one certificate-transparency and one transparency-log check. Kovo MUST independently parse the verified DSSE payload and require exactly one matching feed digest plus the exact Kovo main release-workflow repository, ref, and path. The release job producing this attestation has only checkout/read and attestation OIDC authority: it performs no dependency installation, repository script, build, or long-lived private-key operation. The Fulcio certificate identity is workflow- wide rather than job-ID evidence. Default remote verification therefore also depends on the digest-indexed Kovo repository attestation API, while the workflow grants attestations: write only to the exact two-action attestation job; the package-publish job's npm OIDC authority cannot attach repository attestations. An explicit local bundle is caller-supplied offline evidence and retains only the cryptographically checked workflow-level identity.

After authentication, the command rejects a feed future-dated by more than five minutes, stale beyond its own bounded maxFeedAgeSeconds, below the highest locally accepted epoch, or different from a previously accepted digest at the same epoch. It persists only kovo.security.advisory-state/v1 (highestEpoch, feedDigest) by an atomic regular-file write inside the invocation root. Concurrent processes MUST serialize on an exclusive sidecar lock, re-read and compare state while holding that lock, fsync the state file, atomically rename it, and fsync the parent directory where the platform exposes durable directory handles. A corrupt state file, busy lock, symlink target, symlinked parent component, or unwritable state is failure, not permission to forget rollback history. An advisory matches when its graph schema equals the artifact schema and at least one exact Kovo package version lies in its range. There are only three verdict classes:

  • AFFECTED: print every matching advisory; exit 1 when any match is at or above the configured severity floor, otherwise exit 0 while retaining the AFFECTED label.
  • NOT-AFFECTED: exit 0 only after the complete authentication, freshness, rollback, and matching sequence succeeds with no match.
  • UNKNOWN: exit 2 for every inability to read the artifact, fetch, parse, authenticate, freshness- check, rollback-check, or persist state. UNKNOWN is never treated as an empty advisory set.

Every verdict MUST print a non-claim: this command detects authenticated advisories Kovo has published for the artifact posture; even NOT-AFFECTED is not proof that the artifact has no vulnerability or no impact outside the feed's version-and-schema scope.

kovo explain attest is the deployment-review composition surface. It first recomputes the reviewed graph's artifact subject and posture digest. If the graph contains a trustedAssign capability, --escape-reviews <reviews.json> is mandatory. The detached file has schema kovo.escape-obligation-reviews/v1 and contains one exact signed envelope per graph-derived kovo.escape-obligation-review/v1 subject; missing, duplicate, surplus, malformed, stale-artifact, replacement-key, wrong-anchor, and invalid-signature rows all fail closed. The same out-of-band fingerprint MUST verify both the escape envelopes and the nonce-bound live deployment response. The build also emits .kovo/escape-census-review-subjects.json, schema kovo.escape-census-review-subjects/v1: one unsigned subject for every exact (artifactSubject, door, root) counted by Metric E, with the complete canonically sorted set of producer sites collapsed into that root. Each site is the exact record { encoding: "utf16le", file, sourceHash, sourceLength, sliceHash, span: { start, end } }. file is a canonical invocation-root-relative POSIX path; lengths and spans are JavaScript UTF-16 code units; sourceHash hashes the full UTF-16LE source; and sliceHash hashes exactly source.slice(start, end) as UTF-16LE. Absolute paths, backslashes, empty/./.. components, control characters, and bidirectional controls are invalid. Every source-root door other than csrf:false and ctx.fetch has exactly one site and its root is file:start:end; the two relation-root doors retain every canonically sorted unique producer site. Metric E series v3 re-reads each site as a regular blob from the retained codeSubjectSha and rejects any source, length, span, or slice mismatch before accepting review evidence. When this set is non-empty, --escape-census-reviews <reviews.json> is mandatory. Its kovo.escape-census-reviews/v1 file MUST contain exactly one valid, domain-separated kovo.escape-census-review/v1 envelope per emitted subject under that same trust anchor. A missing, duplicate, surplus, malformed, stale-artifact, wrong-anchor, or invalid-signature envelope fails the whole set; partial signature coverage never reduces the unsigned count. Build, app code, and the app-facing/internal execution graph expose subject construction and verification but no signer. Success reports both verified review counts and explicitly states the non-claim: a signature records only that the pinned key holder approved those exact bytes; it does not prove an obligation true, identify an independent human, or prove executed-code/host integrity. The two build-emitted subject files are unsigned reviewer inputs, not approval evidence.

The canonical v3 series MAY contain zero rounds. In that state both series.comparability and series.reviewAnchor MUST be null: PENDING 0/3 carries neither a source-comparability claim nor review evidence, so initialization does not stamp the repository's source hashes. The first authenticated append MUST create one validated document that adds the round and locks both the full computed comparability record and the externally pinned review anchor. A partial or pre-seeded empty state, or a nonempty state with either lock null or mismatched, fails closed. Later rounds MUST retain both locks unchanged. Every nonempty series MUST be verified against an exact external policy artifact that pins the already-existing kovo-runtime-posture-attestation/v1 fingerprint. node scripts/metric-e-rounds-gate.mjs --init is the sole initializer and MUST refuse to overwrite a nonempty or non-v3 ledger. The verifier MUST derive authority from that supplied policy, never from series.reviewAnchor, a round, or a caller-provided fingerprint; coherently replacing repository evidence and its embedded anchor therefore still fails against the external pin. In addition to the exact root set, each round retains one detached aggregate kovo.metric-e-independent-review/v3 envelope under the same Ed25519 key. Its canonical payload binds the exact code subject, round number and calendar date, report and ceiling digests, signed root-set path/digest/anchor, reviewer identity and UTC review time, explicit accept verdict, and closed assertions that build, review, and signing-key custody were outside the build/coding-agent environment. Missing, duplicate/reused, malformed, surplus, stale-subject, replacement-key, wrong-anchor, and invalid-signature aggregate evidence fails closed. Reusing an identical aggregate or signed root-set digest under another path or round is not a new independent review and MUST fail closed. Only then are verified root signatures counted as reviewed and subtracted from unsigned escapes. The signature authenticates the pinned key holder and those exact bytes; it does not prove the asserted custody or human independence true, identify the reviewer, or establish review correctness.

kovo explain endpoints is the stable machine-ingress audit. Its diffable table lists every declared endpoint and webhook, every mutation(), plus every route that returns respond.file()/respond.stream(): source-derived registry identity where applicable, method, path, mount mode, auth scheme (session+guard, verifier:<resolved scheme>, custom:<name>, or none:<justification>), CSRF/effect posture, and for webhooks the write→domain chain. Endpoint posture is safe:read-only for the closed GET/HEAD/OPTIONS set from §9.1, checked when an unsafe method receives the default synchronizer-token check, or exempt:<justification> when an unsafe endpoint explicitly opts out. Mutation posture remains checked or exempt:<justification>; a csrf: false mutation appears here with the latter posture, and KV418 (§6.6) guarantees it references no ambient session. The pre-dispatch coarse limiter posture (§9.5) is enrolled and printed here too. The command is snapshot-locked with the rest of P8 output so security review can answer "what can reach this app, and what can it touch?" without executing a browser.

Browser tests are a first-class part of the framework's own suite: morph runs on every mutation response, and its survival contract (focus, caret, scroll, transitions) plus L0 platform behaviors are irreducibly browser-bound. The reconciliation suite splits accordingly: a browser-free structural property suite (morph(a, b) ≡ b with keyed-node identity preserved — runs in jsdom-class DOM), and a named browser suite for the survival contract. The claim is bounded: application wiring is proof-carrying, so apps need few or no browser tests of their own — most SPA testing exists to compensate for unverifiable wiring, and Kovo removes that category, not testing itself.



Testing API (SPEC §12)

This file is incorporated by reference from ../SPEC.md and is normative for Kovo framework behavior. The root spec remains the entry point and cross-reference index; this module owns the detailed contract below.

12. Testing API#

The testing surface mirrors the framework proof surface. Mutations execute as functions with touch-checking enabled, pages render to inspectable HTML without a browser, typed error paths expose the declared error union, and generated optimistic transforms have property tests for patch(shape(s), input) ≡ shape(apply(effect, s, input)). Handlers unit-test as (event, ctx) functions; transforms as pure (data, input) functions; the wire as HTTP.

API examples and integration harness guidance live in docs/integration-testing.md and site/content/guides/testing.md.

An app-scoped test harness accepts the opaque KovoApp plus a digest-verified compiler proof graph. Compile-time types flow from the imported contract and declaration handles: query input/result, mutation input/result/error union, route params/search, request/session/DB/env, and task/endpoint types. The graph supplies runtime identities and proof facts; the token and TypeScript types do not substitute for it. A stale, partial, failed-build, digest-mismatched, or wrong-app graph fails before one handler runs. Public app tests do not inspect the token, call private aggregate constructors, or mock framework internals.

Direct query and mutation tests run their declaration handles in the assertion process. Page and raw request assertions exercise HTTP against an explicit, separately bootstrapped app origin; they do not start an app request handler inside Vitest's mutable realm. The harness rejects a missing origin or a request whose origin differs from that configured base URL. This preserves the §6.6 runtime-bootstrap boundary while keeping page assertions browser-free.



Deploy Skew & Version Recovery (SPEC §14)

This file is incorporated by reference from ../SPEC.md and is normative for Kovo framework behavior. The root spec remains the entry point and cross-reference index; this module owns the detailed contract below.

14. Deploy Skew & Version Recovery#

A long-open tab, a stale prerender, or a cached document may outlive the build it was produced by. Kovo makes this loud and recoverable rather than silently wrong (§9.1.1): a payload whose app build token (§5.2.1) does not match the receiver is never merged.

Recovery contract (normative). On a token mismatch the client MUST NOT apply the delta, the /_q/ read, or the fragment merge. It instead refetches the full value over the typed read endpoint (/_q/<key>, §9.4). If the refetch itself returns a token that still differs from the document token, the document is fundamentally skewed: the client performs a full navigation reload of the current route so the document, its modules, and its query bases are all reissued against one build. Optimistic state on a discarded delta is reconciled or rolled back per §10.4; recovery never promotes an unconfirmed prediction. Recovery is idempotent and side-effect-free: it issues GETs and, at most, one reload.

Build-bound request routing (normative). Every enhanced /_q/ read and every enhanced mutation or HMR request MUST carry the immutable document app build token in Kovo-Build. Mutation dispatch recognizes enhanced traffic from the framework fragment, target, live-target, form-target, idempotency, and streaming carriers; a matching build header alone does not classify a native form as enhanced. A serving layer may route build-bound traffic only to the exact retained app/decoder identified by the token. Once dispatched to one app build, a missing or unequal token is rejected before query-key decoding, either target-header decoder, query/component selection, or mutation handler work. That typed 409 carries the current app build token, the framework-reserved Kovo-Build-Skew: true marker, and the inline fragment envelope. The marker has meaning only on that admitted 409; an unmarked application 409 is an ordinary typed conflict. App response channels cannot mint or override the marker. Equality proves compatibility only, never authentication. The sole prior-token override is the explicit oldBuild selector on the Vite-dev-only HMR endpoint (§9.5.1); production dispatch has no exception and no heuristic dual decoder.

Prior-version retention window (required minimum). The serving layer MUST retain prior immutable artifacts so a skewed document can recover without a 404. For the supported deploy-skew window (§6.6) — a deployment-configured duration with a normative floor of 24 hours of wall-clock retention across redeploys, configurable upward but not below the floor — the server MUST keep resolving: (a) every emitted immutable client-module URL /c/__v/<representation-digest>/<module> (§9.5) and its generated-ABI imports, and (b) the /_q/<key> read surface for every prior in-window app build token, returning a token-tagged full value the stale document can recover from. An interaction or refetch from an in-window document MUST NOT 404 (§6.6). The current active-module manifest used to derive a new app build token is distinct from this retained resolver history: history never enters the token merely because it is still resolvable, while simultaneous active representations of one logical path remain distinct exact hrefs. Artifacts older than the window MAY be evicted; a request for an out-of-window digest/token is answered as a build-skew event that triggers the full navigation reload above, never a silent stale patch. Retention MUST survive process restart and cover every serving replica; the default in-memory store does not prove that production property. A deployment that cannot meet the retention floor MUST surface the gap; shipping a window below the floor is KV417.

Proof ownership (normative). Retention is a claim about the selected serving layer across redeploys, not a property of authored app source. kovo build MUST validate that claim and fail KV417 before emitting or promoting a deployment that does not establish it. Source-backed kovo check MUST neither require the claim nor manufacture a passing retention value; its success does not attest deployment skew recovery.

Self-contained artifact filesystem roots (normative). A deploy artifact MUST NOT depend on files outside its own output to boot. A relative rootedFiles() root resolves against the process working directory in dev and while kovo build evaluates the app; the build MUST stage a snapshot of each such root into the artifact (the node preset emits rooted/root-<encoded-spec>/), and the generated server MUST publish the staged directory (via KOVO_ROOTED_FILES_DIR, set before the handler graph is imported) so the same relative spec resolves to its staged copy at serve time — deterministically, regardless of launch working directory. A published staging directory that lacks a requested relative root fails closed at capability construction; it never falls back to the launch cwd. An absolute root is operator intent for a live deploy-host path and is never staged or remapped.