# Evidence & integrity runward frames the delivery an agent produces; it does not write the code. This page covers how the gate proves that the decisions behind that code are traced to something real: typed evidence pointers that must resolve to non-empty content, a rule-conformance manifest that accounts for every high-impact rule, a SHA-256 seal that reddens when sealed evidence drifts, and the advisory behavioral/redone-task proofs that sit above the gate. Every check here is deterministic and runs with zero LLM and zero network: it reads bytes and compares them, it never executes your code and never asks a model for a verdict. The evidence layer is a short chain: a typed pointer resolves to real content, then the whole thing is sealed. > **Diagram.** Evidence is not a sentence: it is a typed pointer (file:/test:/adr:) that must resolve to real, non-empty content. Once the strict gate is green, the evidence is sealed with a SHA-256 hash; any later drift reddens the gate until re-verified. ## The rule-conformance manifest Each gated deliverable carries a `## Rule conformance` markdown table. The gated (phase → deliverable) pairs are fixed: - `architecture.md` (architect) - `execution-topology.md` (topology) - `floor.md` (floor) - `governance/threat-model.md` (govern) - `handover.md` (handover) Every CRITICAL or HIGH craft rule mapped to a phase must appear as one row, in one of three accounting states: - **`applied`** — needs an evidence pointer. An applied row with an empty Evidence cell is a violation. - **`deviated`** — needs an ADR that actually exists in `runward/adr/`; otherwise it fails. - **`n/a`** — needs a real one-line reason: shorter than 8 characters or a `[bracketed placeholder]` is rejected as trivial. ### How the expected set is derived The expected set is derived from the rule files themselves: a rule is expected at a phase when its frontmatter says `impact: CRITICAL` or `HIGH` and its `phases: [...]` includes that phase. The gate reads the mission's own `runward/rules/` when present, else the packaged rules. `conformance()` then flags: - a rule not accounted for at all - a status that is unset or invalid - an unknown rule slug — with a migration hint if it was renamed/removed - a rule listed more than once ### The non-vacuity floor There is also a non-vacuity floor: if the number of CRITICAL/HIGH rules mapped to a phase drops below a pinned minimum, the gate raises a `(mapping)` violation — you cannot quietly strip the mapping to make the gate pass. ### Scaffolding the manifest form (never the decision) `runward manifest` gives a read-only overview; `runward manifest --sync` scaffolds only the *form*: - it appends missing expected rows with an **empty** status - it rewrites renamed slugs per the migration record - it creates a missing section It never sets a status, never writes evidence, and never deletes a row (it only *reports* duplicates, unknown slugs, and removed slugs for the operator to fix by hand). A scaffolded row has an empty status, and the gate refuses empty-status rows until the operator decides. `manifest` exit codes: `0` = done, `2` = no mission. ## Typed evidence pointers An `applied` row's Evidence cell can carry typed pointers, opt-in per row, several per cell separated by `;`, with surrounding prose left untouched. There are exactly three prefixes: - **`file:PATH[:LINE][#SYMBOL]`** — a project file, optionally a line number and a `#symbol` substring. - **`test:PATH[::NAME]`** — a test file, optionally a `::testName`. - **`adr:NNNN`** — an ADR id. ### What the gate checks for `file:` and `test:` For `file:` and `test:` pointers the gate checks, deterministically: 1. **Resolves** — the path must resolve within one of three project bases (mission parent, mission dir, deliverable's dir). An absolute path, or a `../` that climbs out of every base, is rejected rather than resolved — that is not "evidence in your project" (the containment is a normalized-prefix test with a separator so `/a/project-evil` never counts as under `/a/project`). Failure → `typed pointer does not resolve`. 2. **Is a regular file**, not a directory. 3. **Is non-empty** — a file with no non-whitespace content fails with `an empty file is not evidence`. 4. **Has the line** — if `:LINE` is given and the file has fewer lines, it fails. 5. **Contains the symbol** — if `#SYMBOL` is given and the substring is absent: `symbol "…" not found in the file (moved or renamed? update the pointer)`. 6. **Contains the test name** — for `test:…::NAME`, the name must appear in the file. ### `adr:` pointers For **`adr:NNNN`**, an ADR with that id must exist in `runward/adr/`; the id match is anchored on a digit boundary so `adr:1` is not satisfied by `ADR-10`. ### Bare-path non-vacuity Beyond typed pointers, every applied row is also checked for **non-vacuity** on any bare file-path token it mentions (recognized by known code/doc extensions): if such a token resolves to a real file, that file must be non-empty. ### Evidence signatures A rule file may declare a `signature:` regex in its frontmatter. When a rule is signed, its `applied` evidence must point at a file whose content matches that regex — this catches "cited, not applied": a pointer aimed at a file that does not actually carry the rule's shape. If no pointed file is present, or none matches `/sig/i`, the row fails. Two guards protect the gate itself: - an **unsafe signature** with nested quantifiers (e.g. `(a+)+`) that risks catastrophic-backtracking DoS is rejected up front — a conservative screen, not a promise to catch every pathological regex; - an invalid regex in the rule file is reported rather than thrown. ### Drift `driftReport` handles rows that use bare prose path references (not typed pointers): if an `applied` row names file-path tokens and none of them resolve, that is drift and it fails. Drift is blocking under `--strict`. Rows that carry typed pointers are diagnosed by the evidence layer instead — one diagnosis per row, never two. ## Sealing: `check --freeze` `runward check --freeze` implies `--strict`. Freeze **refuses to seal a red gate** — if there are any deliverable gaps, conformance gaps, or failed hooks, it prints "refusing to seal a red gate" and writes nothing. On a fully green gate it hashes evidence into `runward/evidence-lock.json`. ### What gets sealed `collectSealableEvidence` walks every `applied` row across all gated manifests, collects each resolvable evidence file (from both typed pointers and bare path tokens), and records a **SHA-256** of its bytes, keyed by project-root-relative path, in sorted order. The lock is `{ version: 1, sealedAt, files }`, rendered with stable ordering so it is byte-idempotent on unchanged evidence. Set `RUNWARD_DRY_RUN=1` to preview the seal without writing. ### Verification is opt-in Verification is **opt-in by construction**: no lock file means no seal check (`verifyEvidenceLock` returns `present: false`). When a lock exists, on every subsequent `check --strict` each sealed file is re-hashed and compared. Three ways a seal reddens the gate: - **Changed** — `sha256(file) !== hash` → `sealed evidence changed: … — re-read the pointer, confirm the evidence still holds, then re-seal with runward check --freeze`. - **Missing** — the sealed file is gone or is no longer a regular file. - **Escaping path** — a forged lock whose key is absolute or `../`-escapes the project is rejected before any read, so the verifier can never be turned into an arbitrary-file-read or `/dev/zero` DoS oracle. Invalid JSON in the lock is also reported, not thrown. ### The freeze subtlety One deliberate subtlety: under `--freeze` the *old* seal is not verified, only replaced — otherwise a legitimately changed sealed file would make re-sealing impossible, since the seal violation would redden the very gate freeze requires green. ### Why a seal at all (and why opt-in) Drift and pointer checks catch a pointer that stops resolving; they do not catch a pointer that still resolves to a file whose *contents* silently changed after you crossed. The SHA-256 seal closes that gap — the gate greens on the exact bytes you crossed on, and reddens the day they change. It is opt-in because sealing only makes sense once a gate is genuinely green and you want to freeze that crossing; forcing a lock on every mission would be noise (ADR-0021). ## Behavioral proof and the redone-task proof These are two distinct things; keep them apart. ### The floor's behavioral proof The floor's behavioral proof is advisory and sits *above* the gate. `runward` reads `runward/floor.md` for a `Behavioral proof: ` line and an optional `Proof artifact: `. It reports whether the artifact is present and *fresh* — fresh meaning its mtime is at least as new as the newest source file under `/code/` (and left undefined when there is no `code/` directory). It is **strictly read-only: never executed, never parsed for pass/fail** — running the suite and judging the result stays the code's job and the operator's. The gate prints this only when the documentary gate is already green, with an explicit note that "runward did not run your code — it is not a runtime. The behavioral proof is your test suite". ### The redone-task proof The redone-task proof is different: it is a gated CRITICAL rule, `handover-redone-task-proof`, mapped to the handover phase (`impact: CRITICAL`, `phases: [handover]`). It is accounted for in `handover.md`'s conformance manifest like any other rule, typically with a typed pointer into the handover note's redone-task section (its "The redone task (the proof)" block), and it is verified by the same evidence layer above. The doctrine behind it: a hand-over is proven the day the receiving team redoes a real task end to end, alone, without the departing builder — recorded, dated, pointed at. So the redone-task is *inside* the gate (a rule with evidence); the floor's behavioral proof is *above* it (advisory presence/freshness). ## Also above the gate: verify findings After the documentary gate is green, the check nudges you to run the adversarial cite-vs-apply verify workflow (`runward/workflows/verify.md`). Its output artifact is `runward/governance/verify-findings.md`; runward reports only its presence and freshness, never reads a verdict from it, and it never blocks the gate. Fresh here means newer than every gated manifest — edit a manifest after the pass and the findings go stale. ## How silent erosion is caught "Silent erosion" is the case where the paperwork still says green but the underlying reality moved. runward catches it deterministically on the next `check --strict`: - A pointer's target file is **deleted or moved** → typed-pointer resolution fails or drift fires for prose refs. - A function is **renamed** so a `#symbol` / `::testName` no longer appears → symbol/test-name check fails. - A pointed file is **emptied** → non-vacuity fails. - The rule's shape is **removed** from a cited file → signature mismatch. - A **sealed file's bytes change** while the pointer still resolves → SHA-256 mismatch reddens the gate. - The **rule mapping is stripped** below its floor → `(mapping)` violation. - A **reconstructed decision** is left unratified (DRAFT filename, `Status: hypothesis`, or `why: UNKNOWN`) → the gate fails until it is ratified. - The **behavioral proof or verify findings go stale** (older than the code / a manifest changed since) → flagged as `stale`, advisory. ## What runward does not claim The gate checks the *presence and shape* of a traced decision — that a high-impact rule is accounted for and its pointer resolves to real, non-empty, shape-matching content. It never judges whether the code truly implements the rule; that judgment stays the operator's, at the gate. The output is audit-ready supporting evidence that feeds a [compliance programme](https://runward.dev/docs/en/compliance/evidence/) (`runward compliance `), not a compliance verdict — acceptance is the auditor's call. runward is deterministic and zero-LLM/zero-network at the gate (the LLM lives in the harness that wrote the code). It never runs your code or your tests, and it never installs git hooks or writes to `.git/` — the adapters are inert samples the operator wires on their own say-so (ADR-0012). ## Why this choice ### Why the SHA-256 seal The SHA-256 seal (check --freeze) exists because the cheaper checks miss one failure mode. Drift and typed-pointer resolution catch a pointer that stops resolving, and the signature check catches a pointer aimed at a file lacking the rule's shape — but none of them catch a pointer that still resolves to a file whose contents silently changed after you crossed the gate. The rejected alternative is to trust that a resolving, non-empty pointer stays valid forever; the seal instead freezes the exact bytes crossed on and reddens the gate the day they change. It is opt-in (no lock file means no check) because sealing only has meaning once a gate is genuinely green; a mandatory lock on every mission would be noise. ### Why `manifest --sync` scaffolds only form Separately, manifest --sync deliberately scaffolds only form and leaves every status empty rather than guessing 'applied' — the rejected alternative (auto-filling a plausible status) would let an agent's guess pass as an operator decision, so the gate refuses empty-status rows until a human decides. ## See also - [Concepts: the deterministic gate](https://runward.dev/docs/en/concepts/the-gate/) - [Sealing, rules, and the method](https://runward.dev/docs/en/concepts/six-phases/sealing-and-method/) - [Compliance evidence for the system you deliver](https://runward.dev/docs/en/compliance/evidence/) - [Rule catalog](https://runward.dev/docs/en/reference/rules/)