vdelta 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -0
  3. package/dist/adapters/vitest/capture.d.ts +50 -0
  4. package/dist/adapters/vitest/capture.js +7 -0
  5. package/dist/adapters/vitest/capture.js.map +1 -0
  6. package/dist/adapters/vitest/recorder.d.ts +31 -0
  7. package/dist/adapters/vitest/recorder.js +214 -0
  8. package/dist/adapters/vitest/recorder.js.map +1 -0
  9. package/dist/adapters/vitest/reporter.d.ts +15 -0
  10. package/dist/adapters/vitest/reporter.js +97 -0
  11. package/dist/adapters/vitest/reporter.js.map +1 -0
  12. package/dist/canonical.d.ts +9 -0
  13. package/dist/canonical.js +45 -0
  14. package/dist/canonical.js.map +1 -0
  15. package/dist/cli.d.ts +2 -0
  16. package/dist/cli.js +264 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/compare.d.ts +37 -0
  19. package/dist/compare.js +476 -0
  20. package/dist/compare.js.map +1 -0
  21. package/dist/digest.d.ts +5 -0
  22. package/dist/digest.js +14 -0
  23. package/dist/digest.js.map +1 -0
  24. package/dist/gate.d.ts +11 -0
  25. package/dist/gate.js +135 -0
  26. package/dist/gate.js.map +1 -0
  27. package/dist/index.d.ts +15 -0
  28. package/dist/index.js +16 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/redact.d.ts +10 -0
  31. package/dist/redact.js +49 -0
  32. package/dist/redact.js.map +1 -0
  33. package/dist/render.d.ts +6 -0
  34. package/dist/render.js +53 -0
  35. package/dist/render.js.map +1 -0
  36. package/dist/run.d.ts +19 -0
  37. package/dist/run.js +158 -0
  38. package/dist/run.js.map +1 -0
  39. package/dist/schema.d.ts +236 -0
  40. package/dist/schema.js +395 -0
  41. package/dist/schema.js.map +1 -0
  42. package/dist/store.d.ts +38 -0
  43. package/dist/store.js +161 -0
  44. package/dist/store.js.map +1 -0
  45. package/dist/tree-digest.d.ts +11 -0
  46. package/dist/tree-digest.js +103 -0
  47. package/dist/tree-digest.js.map +1 -0
  48. package/package.json +53 -0
  49. package/spec/veridelta-1.md +1093 -0
@@ -0,0 +1,1093 @@
1
+ # veridelta/1 — Verification Delta Protocol
2
+
3
+ | | |
4
+ |---|---|
5
+ | **Status** | Draft |
6
+ | **Schema identifier** | `veridelta/1` |
7
+ | **Spec revision** | 0.3.0 — 2026-07-16 |
8
+ | **License** | MIT |
9
+ | **Reference implementation** | `vdelta` (this repository; in development) |
10
+
11
+ ## Abstract
12
+
13
+ veridelta is a protocol for **proof-carrying verification deltas** in coding-agent
14
+ development loops. Given two comparable test runs, a conforming implementation
15
+ reports — deterministically and with evidence — whether a change improved the
16
+ outcome *while maintaining the same verification surface*, whether pre-existing
17
+ failures mutated into different failures, and whether red results disappeared
18
+ because they were fixed or because the verification surface shrank
19
+ (fail→skip, deleted tests, narrowed selectors).
20
+
21
+ veridelta is not a log-compression tool. It is a **trust layer**: a deterministic
22
+ state-transition report between two immutable, content-addressed runs, with an
23
+ explicit comparability judgment. When comparability does not hold, a conforming
24
+ implementation **abstains** rather than guessing. Token savings for agents are a
25
+ side effect; the product is that an agent (or a CI gate reviewing an agent's PR)
26
+ can proceed to the next step safely.
27
+
28
+ ## 1. Introduction
29
+
30
+ ### 1.1. Motivation
31
+
32
+ In an agent's test→fix→test loop, most of a rerun's output is identical to the
33
+ previous run. Tools that summarize only the *current* run can answer "what
34
+ failed now?" but not "what did this change fix, and what did it newly break?" —
35
+ the agent must reconstruct that from history it does not reliably retain.
36
+
37
+ Three failure modes motivate this protocol:
38
+
39
+ 1. **Pre-existing failures.** In repositories with standing red, the child
40
+ process exits `1` every time. The exit code cannot express *improved*,
41
+ *regressed*, or *unchanged*.
42
+ 2. **Red→red mutation.** A test that fails before and after a change may be
43
+ failing *differently*. Status-only diffs bury this, and the new failure mode
44
+ goes uninvestigated.
45
+ 3. **Verification-surface reduction.** A red result can disappear without a fix:
46
+ fail→skip, fail→xfail, test deletion, selector narrowing, or a test rewritten
47
+ to pass. Outcome-only comparison reports these as improvements. In the
48
+ context of agent-generated changes this is precisely the cheating vector
49
+ (reward hacking), so it must be a first-class, separately reported axis.
50
+
51
+ ### 1.2. Position
52
+
53
+ Three separations of concern define the protocol:
54
+
55
+ - The **child process's exit code** and the **semantic judgment "did anything
56
+ regress since the baseline?"** are different concepts and are never encoded in
57
+ the same channel (§10).
58
+ - **Test-ID identity** and **failure-finding identity** are different concepts:
59
+ the same test failing with different evidence is a state transition (§7.3).
60
+ - **Outcome improvement** and **verification-surface maintenance** are different
61
+ axes and are always reported separately (§7.4).
62
+
63
+ ### 1.3. Scope
64
+
65
+ In scope:
66
+
67
+ - Comparing two recorded runs of a test command over the same repository,
68
+ including dirty working trees.
69
+ - Gating agent-generated changes (e.g., pull requests) on regression and
70
+ verification-surface reduction relative to a baseline ref.
71
+ - The canonical data model, run store semantics, baseline selection,
72
+ comparability rules, delta taxonomy, report contract, gate semantics, and
73
+ conformance requirements.
74
+
75
+ Out of scope (non-goals):
76
+
77
+ - General-purpose log compression or summarization.
78
+ - Test selection or execution optimization (e.g., `--lf`-style reruns).
79
+ - Semantic diffing of arbitrary text, or any LLM-based summarization in the
80
+ trust path.
81
+ - Attribution of *cause* ("the agent cheated", "your change broke this").
82
+ Conforming implementations report observations; intent and blame are the
83
+ consumer's judgment (§7.4).
84
+
85
+ ### 1.4. Roles
86
+
87
+ | Role | Responsibility |
88
+ |---|---|
89
+ | **Adapter** | Translates one runner's native output/hooks into the canonical data model, under a declared capability set (§3.4, §12). |
90
+ | **Recorder** | Captures runs into the store. May be a runner plugin, a harness hook, or a CLI wrapper (§4.2). |
91
+ | **Run store** | Immutable, content-addressed persistence of runs (§4). |
92
+ | **Comparator** | Selects a baseline, judges comparability, computes the delta report (§5–§9). |
93
+ | **Gate** | Turns a delta report into a CI/agent-loop verdict under a policy, with integrity and staleness checks (§11). |
94
+ | **Consumer** | Anything that parses `veridelta/1` reports: agents, harnesses, CI, humans via secondary rendering (§9.4). |
95
+
96
+ A single binary may implement several roles. Conformance is claimed per role
97
+ (§13).
98
+
99
+ ## 2. Conventions and terminology
100
+
101
+ The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**,
102
+ **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** are to be
103
+ interpreted as described in RFC 2119 and RFC 8174 when, and only when, they
104
+ appear in all capitals.
105
+
106
+ - **Run**: one recorded execution of a test command (§3.1).
107
+ - **Stream**: the equivalence class of runs that are candidates for implicit
108
+ baseline selection (§5.1).
109
+ - **Baseline**: the run against which the current run is compared.
110
+ - **Verdict**: the canonical per-test outcome assigned by the runner (§7.1).
111
+ - **Red**: a verdict in the failing set (§7.1).
112
+ - **Failure finding**: the evidence attached to a red observation (§3.3).
113
+ - **Verification surface**: the set of checks a run actually performed —
114
+ inventory, selectors, suppression state, test/config sources, adapter
115
+ capabilities (§7.4).
116
+ - **Abstain**: report `inconclusive`/`none` with a reason instead of guessing.
117
+ - **Closed enum**: a field whose value set is fixed by this spec revision;
118
+ consumers MUST treat unknown values as a hard error (§9.4, §14).
119
+
120
+ ## 3. Data model
121
+
122
+ All persisted records and reports are JSON. Field names are `snake_case`.
123
+ Digests are lowercase-hex SHA-256 rendered as `sha256:<hex>` unless an adapter
124
+ documents otherwise.
125
+
126
+ ### 3.1. Run
127
+
128
+ A Run is the immutable record of one execution. Its identity is
129
+ content-addressed: `run_id` is derived from the canonical serialization of the
130
+ record (stable key order, no volatile fields), so identical runs collide and
131
+ mutation is detectable.
132
+
133
+ | Field group | Fields | Notes |
134
+ |---|---|---|
135
+ | `schema_version` | `"veridelta/1"` | REQUIRED. |
136
+ | `repo` | repo identity, worktree path, branch lineage, `cwd` | REQUIRED. |
137
+ | `invocation` | canonicalized command, test selector | REQUIRED. Selector is recorded as normalized by the adapter. |
138
+ | `instrument` | adapter name, adapter version, runner config digest | REQUIRED. Together these identify the *measuring instrument* (§6.2). The runner config digest MUST cover the effective configuration that alters evidence quality or structure — including assertion-introspection mode, traceback style, and diff/message truncation settings — however that configuration is supplied (command line, configuration files, plugins, or environment). |
139
+ | `environment` | runner, runtime, OS, fingerprint of adapter-declared comparison-relevant env vars | REQUIRED. Secret **values** MUST NOT be stored; fingerprints only (§15). |
140
+ | `provenance` | `head` (VCS revision), `dirty_diff_digest`, `tree_digest` | REQUIRED. `tree_digest` MUST identify the exact source content the run executed against, computed per §3.5. Provenance is **evidence of what was compared, never a stream-matching key** (§5.1). |
141
+ | `surface` | observed test inventory digest; digests of test source and test-relevant config files; suppression metadata | REQUIRED to the extent the adapter's declared capabilities allow. |
142
+ | `completeness` | `status`: `complete` \| `partial` \| `crashed`; `child_exit_code` | REQUIRED. |
143
+ | `observations` | array of TestObservation (§3.2) | REQUIRED. |
144
+ | `recording` | recorder kind, timestamps, raw-record references | Timestamps are permitted here and only here; they MUST NOT influence report content (§7.8), only duplicate matching (§4.3). |
145
+
146
+ ### 3.2. TestObservation
147
+
148
+ | Field | Req | Description |
149
+ |---|---|---|
150
+ | `test_id` | MUST | Canonical item ID as provided by the adapter (e.g., pytest nodeid). The protocol does not accept user-supplied key regexes. |
151
+ | `verdict` | MUST | Canonical verdict (§7.1), derived **only** from the runner's verdict channel (INV-3). |
152
+ | `phase` | SHOULD | e.g., `collection` \| `setup` \| `call` \| `teardown`, if the capability is declared. |
153
+ | `suppression` | SHOULD | skip/xfail markers and reasons, if observable. |
154
+ | `source_ref` | MAY | file/line of the test definition, if the capability is declared. |
155
+ | `finding` | MUST when red | FailureFinding (§3.3). |
156
+ | `detail` | MAY | non-trust detail (e.g., duration). Never used for status derivation. |
157
+
158
+ ### 3.3. FailureFinding
159
+
160
+ Each finding carries two fingerprints with strictly different trust roles:
161
+
162
+ | Field | Role |
163
+ |---|---|
164
+ | `evidence_digest` | **Trust path.** Lossless digest of the canonical failure evidence (§3.6) after deterministic secret redaction and canonical encoding, and after nothing else. Any change to canonical evidence changes this digest. Change detection MUST use this and only this. |
165
+ | `structural_fingerprint` | **Auxiliary.** Phase, exception type, assertion location, top project stack frames. Used for clustering and drill-down presentation only. It MUST NOT be used to suppress or merge a change that `evidence_digest` detects (INV-8). |
166
+ | `evidence` | Stored redacted raw evidence, addressable via anchors (§9.3). |
167
+
168
+ ### 3.4. Adapter capability declaration
169
+
170
+ Adapters MUST declare, in schema form, which observables they can produce:
171
+ verdicts, phases, source locations, suppression metadata, failure evidence,
172
+ inventory, rename proof, coverage, etc. A missing capability means *unknown*,
173
+ never *unchanged*: comparators MUST NOT interpret absent data as absence of
174
+ change (§12). Capability values follow the three-valued convention
175
+ `pass`/`fail`/`unsupported` where applicable — "the runner cannot express this"
176
+ and "this broke" are never conflated.
177
+
178
+ ### 3.5. Identifiers and digests
179
+
180
+ - All digest inputs MUST be canonicalized deterministically (encoding, ordering
181
+ of multi-part evidence) and the canonicalization MUST be documented by the
182
+ adapter.
183
+ - Redaction (§15) runs before digesting and MUST itself be deterministic, so
184
+ digests remain stable for identical inputs.
185
+
186
+ **Run identity.** `run_id` is content-derived and immutable: `run_` followed by
187
+ the lowercase-hex SHA-256 of the canonical serialization of the Run record
188
+ excluding the `recording` group. Canonical serialization is JSON with
189
+ lexicographically sorted keys, UTF-8 encoding, no insignificant whitespace, and
190
+ all numbers restricted to integers (durations in microseconds) — sidestepping
191
+ floating-point serialization divergence between implementations. Because
192
+ `recording` is excluded, two recorders that observe the same physical run
193
+ identically produce the *same* `run_id`: content addressing itself collapses
194
+ exact duplicates, and §4.3 matching is needed only for records that differ.
195
+ Tools MAY accept unambiguous `run_id` prefixes as input; identity is always the
196
+ full digest.
197
+
198
+ **Tree identity.** The `tree_digest` of a git worktree MUST be computed as a
199
+ git tree object id over the union of tracked, staged, unstaged, and untracked
200
+ files, excluding paths ignored by committed `.gitignore`/`.gitattributes`
201
+ rules. Implementations MUST compute it against a dedicated, throwaway index
202
+ (`GIT_INDEX_FILE`) seeded from `HEAD` (`git read-tree HEAD`, or `read-tree
203
+ --empty` for an unborn `HEAD`), followed by `git add -A` and `git write-tree`,
204
+ so that the operation never mutates the repository's real index or working
205
+ tree. To keep the digest deterministic and independent of host and time, the
206
+ computation MUST pin `core.autocrlf=false`, `core.eol=lf`, and
207
+ `core.excludesFile=/dev/null`, and MUST NOT depend on file mtimes. For a clean
208
+ checkout the resulting id is identical to `git rev-parse HEAD^{tree}`, which a
209
+ verifier MAY use directly as the canonical form (subject to the conditions of
210
+ §11.3). The digest records submodules solely by their gitlink commit id; it
211
+ does NOT capture uncommitted changes inside a submodule working tree, and it
212
+ cannot represent empty directories — both are explicit, documented limitations
213
+ (see §11.3 for gate-side handling).
214
+
215
+ ### 3.6. Canonical failure evidence
216
+
217
+ The input to `evidence_digest` is the **canonical failure evidence**: a
218
+ deterministic projection of the runner's structured failure representation,
219
+ declared per adapter as a versioned **composition** and satisfying:
220
+
221
+ - **CE-1 (signal completeness)**: it MUST include the exception type, the
222
+ runner's failure message with asserted values intact, the failing source
223
+ region text, and the traceback entry structure, as provided by the runner's
224
+ structured channel.
225
+ - **CE-2 (rerun stability)**: it MUST NOT include durations, wall-clock or
226
+ monotonic time, process- or host-specific values (memory addresses, PIDs,
227
+ absolute filesystem paths), captured program output streams, or any field
228
+ capable of carrying values supplied by run-scoped resources (e.g.,
229
+ function-argument representations, which can embed per-run temporary paths).
230
+ Exclusion under this rule operates on whole declared fields (CE-5): a field
231
+ is excluded for what it *can carry*, never conditionally on what a particular
232
+ value contains.
233
+ - **CE-3 (position stability)**: source position MUST be encoded in a
234
+ line-shift-stable form (enclosing symbol plus symbol-relative offset, and/or
235
+ source line text). Absolute line numbers MUST NOT enter the digest.
236
+ - **CE-4 (structured fields only)**: the digest MUST be computed from
237
+ structured fields, never from rendered display strings that interleave
238
+ position, style, and message.
239
+ - **CE-5 (whole-field granularity)**: composition includes or excludes whole
240
+ declared fields only. Value-level rewriting of evidence content is
241
+ prohibited; the sole exception is deterministic secret redaction (§15).
242
+ Redaction MUST NOT be used to normalize non-secret volatile values.
243
+
244
+ Where the runner's structured channel does not provide a CE-1 component
245
+ (e.g., vitest provides no source region text), the adapter MUST either
246
+ (a) declare the corresponding capability `unsupported` (§3.4), surfacing it
247
+ through `degraded_capabilities` (§9.1), or (b) reconstruct the component
248
+ deterministically from the run's recorded tree at the location named by the
249
+ structured channel, declaring the reconstruction in its composition. A
250
+ composition MUST distinguish channel-provided from tree-reconstructed
251
+ components; silent omission is non-conforming.
252
+
253
+ Material excluded by CE-2/CE-3 MUST be stored in the finding's annex,
254
+ addressable via anchors (§9.3); exclusion from the digest is never exclusion
255
+ from the record (INV-2). An adapter that stores captured-output annex material
256
+ MUST compute a `context_digest` over it; when it differs between compared runs,
257
+ affected `still_fail_unchanged` entries MUST carry `context_changed: true` with
258
+ an anchor. `context_digest` MUST NOT influence transition classification
259
+ (§7.3).
260
+
261
+ The declared composition is part of the measuring instrument: any composition
262
+ change requires an adapter version change (§6.2).
263
+
264
+ **Known limitation (normative acknowledgment):** a failure message that embeds
265
+ values varying across executions at an identical tree (times, run-scoped
266
+ paths) yields differing digests on rerun. This is honest `updated_fail`
267
+ reporting of genuinely differing evidence, not noise to be normalized; such
268
+ transitions carry `failure_mode_changed: false` when the structural
269
+ fingerprint is stable, and gates MAY narrow blocking accordingly (§11.1,
270
+ subject to the blocking-set floor of §11.5) without narrowing reporting.
271
+
272
+ ## 4. Recording and the run store
273
+
274
+ ### 4.1. Immutability and content addressing
275
+
276
+ Stored runs are immutable. Implementations MUST NOT rewrite a stored run;
277
+ corrections are new records. The store is repo-local, MUST be excluded from
278
+ version control (e.g., enforced gitignore), and SHOULD be bounded (LRU or
279
+ equivalent).
280
+
281
+ ### 4.2. Ambient recording
282
+
283
+ Recording is **ambient-first**: the RECOMMENDED deployment records every run
284
+ via runner plugins or harness hooks, with a CLI wrapper (`vdelta run --`) as
285
+ just one recorder implementation. Rationale: a wrapper-only design depends on
286
+ agent discipline, and one forgotten wrap severs the stream — contradicting the
287
+ premise that agents are stateless. **Record always; compare on demand.**
288
+
289
+ Continuous recording also enables the highest-value inner-loop query: comparing
290
+ the current subset run against the widest available proven-superset run over
291
+ their common IDs, under the `subset` comparability rules of §6.1 and the
292
+ `previous-superset` baseline mode of §5.2.
293
+
294
+ ### 4.3. Duplicate-record normalization
295
+
296
+ When multiple recorders coexist (plugin + hook + wrapper), the same physical
297
+ test run may be recorded more than once. Stores MUST normalize duplicates
298
+ deterministically:
299
+
300
+ - Group candidate records by stream key (§5.1) and match by timestamp
301
+ proximity with a fixed, documented window.
302
+ - Matching MUST be deterministic given the same set of records.
303
+ - Joined records and unjoinable residuals MUST both be explicit; raw and
304
+ normalized forms are both retained. Residuals that cannot be normalized MUST
305
+ remain observable — normalization never silently discards a record.
306
+
307
+ ### 4.4. Store hygiene
308
+
309
+ - Writes MUST be atomic.
310
+ - Locks are advisory and **fail-open**: lock contention degrades to raw
311
+ passthrough (INV-5), never to blocking or corruption.
312
+ - Known secret shapes MUST be redacted before persistence (§15).
313
+
314
+ ### 4.5. Execution-cache coherence
315
+
316
+ A recorder MUST ensure the evidence a run reports was produced from the source
317
+ content identified by `provenance.tree_digest`. Runner- or runtime-level
318
+ caches that can serve compiled or rewritten artifacts from a prior source
319
+ state MUST be neutralized for recorded runs; the mechanism MUST be declared by
320
+ the adapter and MUST pass the stale-cache collision fixture (§13.2). For the
321
+ pytest adapter: purge in-scope `__pycache__` directories, or point
322
+ `PYTHONPYCACHEPREFIX` at a run-scoped empty directory.
323
+ `PYTHONDONTWRITEBYTECODE` alone is insufficient: it suppresses writes but not
324
+ reads of pre-existing cached bytecode. (pytest's `-p no:cacheprovider` does
325
+ not address this cache.)
326
+
327
+ Not every runner has such a cache: empirical probing found no stale-source
328
+ path for vitest in run mode (its persistent caches carry no evidence), so its
329
+ adapter declares that no neutralization is required — the §13.2 fixture
330
+ remains mandatory as the arbiter of that declaration.
331
+
332
+ ## 5. Streams and baseline selection
333
+
334
+ Baseline selection is a public protocol, not an internal heuristic. Every
335
+ report MUST state which baseline was chosen and why (`selection_reason`).
336
+
337
+ ### 5.1. Stream key
338
+
339
+ The default comparison stream is:
340
+
341
+ ```
342
+ repo + worktree + branch + cwd + canonical command + selector + instrument
343
+ ```
344
+
345
+ Branch switches, command changes, and selector changes start a new stream.
346
+ `tree_digest`/`head` are provenance, **not** part of the stream key — the tree
347
+ changes on every edit; that is the thing being iterated, not the thing that
348
+ identifies the series.
349
+
350
+ The **series key** is the stream key minus the selector: `repo + worktree +
351
+ branch + cwd + canonical command + instrument`. The canonical command
352
+ component excludes the selector, which §3.1 records as a separate field; two
353
+ invocations differing only in selector share a series. The series key exists
354
+ solely to scope `previous-superset` selection (§5.2); it never widens
355
+ `previous-comparable` matching.
356
+
357
+ ### 5.2. Baseline modes
358
+
359
+ | Mode | Intended use | Meaning |
360
+ |---|---|---|
361
+ | `previous-comparable` | dirty-tree inner loop (default) | Most recent **complete** run in the same stream. |
362
+ | `git-ref` | PR / regression gate | Complete run whose provenance matches the given ref. |
363
+ | `explicit-run-id` | agent/harness control | Caller names an immutable run. |
364
+ | `previous-superset` | inner loop, subset runs | Candidates are **complete** runs in the same series whose selector the adapter proves to be a **proper superset** of the current run's selector via the `selector-relation` capability (§6.4). Among candidates, only those **maximal under the proven-containment partial order** (no other candidate's selector is a proven proper superset of theirs) are eligible; a narrower run MUST never be selected over a wider one. Select the most recent maximal candidate; recency ties break by lexicographic `run_id`. When more than one maximal candidate exists, the report MUST disclose `baseline.superset_candidates` (count) and the baseline's selector. An equal selector belongs to the same stream and is handled by `previous-comparable`, never by this mode. Abstention reasons are distinct (§6.3): when candidate relations are decided but no proper-superset candidate exists, this mode abstains with `baseline-missing`; when `unknown` relations prevent candidacy from being determined, it abstains with `selector-relation-unknown`. An `unknown` relation is absence of proof, never containment; this mode MUST NOT fall back to a weaker match (§5.3). |
365
+
366
+ An implicit "the previous invocation" baseline (ordinal, order-dependent) is
367
+ prohibited: it is contaminated by branch switches and partial runs. Selection
368
+ MUST be content-addressed and explainable.
369
+
370
+ ### 5.3. Selection transparency
371
+
372
+ `baseline.selection_reason` is REQUIRED in every comparison report and MUST be
373
+ sufficient for the consumer to verify the choice (e.g.,
374
+ `same-worktree-command-config-scope`). If no acceptable baseline exists, the
375
+ comparator MUST abstain with comparability `none` (§6.3), never fall back
376
+ silently to a weaker match.
377
+
378
+ ## 6. Comparability
379
+
380
+ ### 6.1. Levels and permitted claims
381
+
382
+ | `comparability` | Condition | Permitted claims |
383
+ |---|---|---|
384
+ | `exact` | Same scope and inventory; both runs `complete`; same instrument (§6.2) | All status transitions, including `unchanged`. |
385
+ | `scope_changed` | Same selector, but tests were added/removed/renamed | Transitions over common IDs, plus added/removed. A removed test MUST NOT be counted as repaired. |
386
+ | `subset` | Baseline selector is a proven proper superset of the current selector; both runs `complete`; same instrument | Transitions over common IDs, plus `test-added` for current-only IDs. Baseline IDs matching the current selector but unobserved are `removed`; baseline IDs outside it are `out_of_scope` — never `repaired`, never conflated with `removed` or `not_observed`. Red `out_of_scope` IDs MUST be listed. Every `subset` comparison MUST emit a `selector-subset` event (§7.4), which carries both selectors. All claims are bounded to the baseline's scope; claims about any wider suite MUST NOT be asserted. |
387
+ | `partial` | Declared scope not fully observed | Only facts observed in the current run. `repaired`, `missing`, and `unchanged` MUST NOT be asserted. |
388
+ | `none` | No comparable baseline | Structured current-run results only. |
389
+
390
+ When the conditions of more than one level hold, the comparator MUST assign
391
+ the highest level in the order `exact` > `scope_changed` > `subset` >
392
+ `partial`.
393
+
394
+ A comparator MUST NOT claim more than its comparability level permits (INV-7).
395
+ `comparability` and its reason appear in every report.
396
+
397
+ ### 6.2. Same-instrument rule
398
+
399
+ If adapter name, adapter version, or runner config digest differ between the
400
+ two runs, the measuring instrument itself changed. The comparator MUST NOT
401
+ claim `exact`; it MUST report comparability `none` with reason
402
+ `instrument-changed`. (Evidence digests are not comparable across instrument
403
+ changes — formatting drift would surface as false `updated` findings.)
404
+
405
+ ### 6.3. Reasons for `none` (closed enum)
406
+
407
+ "We determined the runs are incomparable" and "the tool did not run properly"
408
+ are different statements and get different vocabulary. `comparability_detail`
409
+ is REQUIRED whenever `comparability` is `none`:
410
+
411
+ ```json
412
+ { "reason": "baseline-missing", "kind": "determined" }
413
+ ```
414
+
415
+ | `reason` | `kind` |
416
+ |---|---|
417
+ | `baseline-missing` | `determined` |
418
+ | `stream-mismatch` | `determined` |
419
+ | `instrument-changed` | `determined` |
420
+ | `selector-relation-unknown` | `determined` |
421
+ | `store-corrupt` | `failed` |
422
+ | `adapter-crashed` | `failed` |
423
+ | `record-integrity-failed` | `failed` |
424
+
425
+ `kind: failed` reasons additionally trigger the fail-open behavior of INV-5
426
+ where applicable. This enum is closed (§14).
427
+
428
+ ### 6.4. Selector semantics and containment proof
429
+
430
+ A recorded selector denotes the invocation's **inclusion intent**. Exclusion
431
+ mechanisms (e.g., pytest `--deselect`, `--ignore`, `collect_ignore`,
432
+ deselection hooks) MUST NOT narrow the recorded selector: an ID matching the
433
+ inclusion selector but not observed is in-scope non-observation (`removed` or
434
+ `not_observed`), never `out_of_scope`.
435
+
436
+ Cross-selector comparability (`subset`) requires a deterministic containment
437
+ proof. Adapters MAY declare the `selector-relation` capability, providing
438
+ pure, documented functions
439
+ `selector_relation(a, b) → equal | subset | superset | disjoint | unknown` and
440
+ `selector_matches(selector, test_id) → yes | no | unknown`, evaluated over
441
+ inclusion intent only and covered by conformance fixtures. Where the relation
442
+ is undecidable (e.g., pytest `-k`/`-m` expressions, vitest
443
+ `--testNamePattern` or `--changed`), the adapter MUST return `unknown`;
444
+ comparators MUST treat `unknown` as absence of proof, never as containment.
445
+
446
+ ## 7. Delta taxonomy
447
+
448
+ Deltas are reported on three axes, never collapsed into one array:
449
+ **outcome delta** (§7.2), **failure-mode delta** (§7.3), and
450
+ **verification-surface delta** (§7.4).
451
+
452
+ ### 7.1. Canonical verdicts
453
+
454
+ `pass | fail | error | skip | xfail | xpass | not_run`
455
+
456
+ The **red set** is `{fail, error}`. Adapters MUST map any runner outcome the
457
+ runner itself treats as failing (e.g., strict-mode `xpass` in pytest) into the
458
+ red set. Verdicts derive exclusively from the runner's verdict channel, never
459
+ from output text (INV-3).
460
+
461
+ ### 7.2. Outcome verdict
462
+
463
+ `outcome_verdict`: `regressed | improved | unchanged | inconclusive`
464
+
465
+ If even one new or updated failure exists, `regressed` takes precedence over
466
+ any concurrent repairs; the breakdown is preserved in `transitions`. No blended
467
+ scores, no confidence values.
468
+
469
+ ### 7.3. Failure-mode delta
470
+
471
+ Test-ID identity and failure identity are separated. For tests red in either
472
+ run:
473
+
474
+ | Transition | Meaning |
475
+ |---|---|
476
+ | `new_fail` | Not red at baseline (or newly added and red), red now. |
477
+ | `still_fail_unchanged` | Red in both; `evidence_digest` identical. Listed by ID; detail suppressed but never omitted (INV-1, INV-2). |
478
+ | `updated_fail` | Red in both; `evidence_digest` changed. Carries `evidence_digest_before/after` and `failure_mode_changed: true` when the structural fingerprint also changed. |
479
+ | `repaired_*` | Red at baseline, not red now — decomposed per §7.5. |
480
+
481
+ If the structural fingerprint matches but raw evidence changed, the transition
482
+ is still `updated_fail`. Rounding "similar failures" together is never in the
483
+ trust path (INV-8).
484
+
485
+ ### 7.4. Verification-surface delta
486
+
487
+ "Red disappeared" and "the same check passed" are separated. Deterministically
488
+ observable surface changes are reported as `verification_surface.events`:
489
+
490
+ - inventory `test-added` / `test-removed` / `test-renamed` (rename only when
491
+ the adapter can prove it uniquely)
492
+ - verdict-class moves into suppression: `fail-to-skip`, `fail-to-xfail`
493
+ - `selector-changed`, `runner-config-changed`, `adapter-capability-changed`
494
+ - `selector-subset` — the current selector is a proven proper subset of the
495
+ baseline selector; carries both selectors and the proving capability.
496
+ Distinct from `selector-changed` (unproven or non-containment change)
497
+ - `test-source-changed`, `config-source-changed` (digest changes of test/config
498
+ sources)
499
+
500
+ `verification_surface.status`: `intact | changed | reduced | inconclusive`.
501
+ `changed` covers source/config digest drift alone; `reduced` is REQUIRED
502
+ whenever the observed scope demonstrably shrank *within the compared scope*
503
+ (fail→skip/xfail, deletions, lost observation). Proven selector narrowing is
504
+ carried by the `subset` comparability level and its mandatory
505
+ `selector-subset` event (§6.1, §11.1), never by `status`: under `subset`
506
+ comparability, `verification_surface.status` is computed over the current
507
+ selector's scope, so `reduced` retains its alarm value for in-scope reduction
508
+ rather than firing on every deliberate subset run. An unproven selector change
509
+ precludes cross-selector comparability entirely (§5.2, §6.3). Coverage-based
510
+ surface evidence is handled only by adapters that declare it; otherwise
511
+ `inconclusive` (Appendix A).
512
+
513
+ Implementations MUST NOT claim intent or causation ("test was weakened to
514
+ cheat", "your change caused this"). The observation is the deliverable; the
515
+ judgment belongs to the consumer.
516
+
517
+ ### 7.5. Repaired decomposition
518
+
519
+ `repaired` is never a single bucket:
520
+
521
+ | Class | Meaning |
522
+ |---|---|
523
+ | `repaired_same_surface` | Same test, same scope, fail→pass, test/config digests unchanged. |
524
+ | `repaired_with_test_change` | fail→pass, but test or config source changed. |
525
+ | `fail_to_skip` / `fail_to_xfail` | Transition into suppression. **Not repaired.** |
526
+ | `removed` / `not_observed` | Deleted or unobserved. **Not repaired.** |
527
+ | `out_of_scope` | Red at baseline, provably outside the current run's inclusion selector (§6.4). Occurs **only** under `subset` comparability (§6.1). **Not repaired. Not removed. Not `not_observed`.** Red IDs listed individually; non-red out-of-scope items MAY be reported as counts in `observation_coverage`. |
528
+ | `verification_inconclusive` | Adapter capability or provenance insufficient to classify. |
529
+
530
+ ### 7.6. Normalization and masking constraints
531
+
532
+ - Free-form, caller-supplied masking/keying/watching expressions MUST NOT
533
+ exist in the trust path. (Design history: Appendix B.)
534
+ - Curated maskers, if implemented at all, MUST NOT touch status, test IDs, or
535
+ assertion regions; they are OPTIONAL, default-off, and restricted to detail
536
+ deduplication (e.g., durations).
537
+ - Any suppression a masker performs MUST be loudly accounted: count and at
538
+ least one concrete example per application, in the report.
539
+
540
+ ### 7.7. Flakiness
541
+
542
+ Implementations MUST NOT infer flakiness from a two-run alternation. A
543
+ `flaky`-class label is permitted only from the runner's own retry verdict, or
544
+ from three or more comparable historical runs (`observed-flaky`, post-1.0 —
545
+ Appendix A). Flaky annotations never suppress new/updated failure reporting.
546
+
547
+ ### 7.8. Determinism
548
+
549
+ Same input runs + same configuration → byte-identical report. Reports contain
550
+ no timestamps and use stable sort orders. Runners that execute tests in
551
+ parallel workers deliver results in nondeterministic order; recorders MUST
552
+ order observations canonically (by test ID) so that run records and reports
553
+ are independent of arrival order. This is proven by conformance fixtures over
554
+ adversarial inputs: reordering, partial execution, flakiness, secret-bearing
555
+ output, branch crossing (§13).
556
+
557
+ ## 8. Trust invariants (normative core)
558
+
559
+ Safety takes precedence over budget: if the mandatory failure IDs and
560
+ observation coverage exceed a requested `--budget`, the implementation MUST NOT
561
+ omit them; it exceeds the budget and sets `budget_exceeded_for_safety: true`.
562
+
563
+ Each invariant carries a **justification class** (general rule: any distrust
564
+ mechanism must declare one, plus a sunset path if capability-bound):
565
+
566
+ - **incentive-structural** — guards against actors whose incentive to game the
567
+ mechanism *grows* with capability. Never sunset.
568
+ - **blast-radius** — guards against failure modes whose cost is unbounded
569
+ regardless of actor intent. Permanent.
570
+ - **capability-bound** — compensates for current model/tooling limitations;
571
+ MUST declare a re-evaluation trigger. *(None of INV-1..11 is in this class;
572
+ the class exists so future mechanisms must self-classify.)*
573
+
574
+ | # | Invariant | Class |
575
+ |---|---|---|
576
+ | **INV-1** | Never report green where red exists: failing items are never omitted and never masked into `unchanged`. | blast-radius |
577
+ | **INV-2** | Never omit without accounting: suppressed items' existence and status always appear in observation coverage. Only the *detail* of unchanged/passing items may be suppressed. | blast-radius |
578
+ | **INV-3** | Status derives from the runner's verdict channel, never from any text a masker could touch. Green→red concealment is structurally impossible, not policy-forbidden. | blast-radius |
579
+ | **INV-4** | Completeness gate: incomplete/partial/crashed runs never yield `repaired`/`missing` as fact; they yield "N items unobserved". | blast-radius |
580
+ | **INV-5** | Fail-open / crash-safe: config errors, cache faults, lock contention, and internal errors all degrade to transparent passthrough of the child's raw output and exit code. **veridelta is never worse than its absence.** | blast-radius |
581
+ | **INV-6** | Determinism: same input + same config → same output, proven against adversarial conformance fixtures. | blast-radius |
582
+ | **INV-7** | Claims never exceed comparability: baseline mismatch → abstain from all transitions; scope change → common IDs only; incomplete runs → no `repaired`/`missing`/`unchanged`. Comparability with reason is always returned. | blast-radius |
583
+ | **INV-8** | Red→red mutation is never buried in `still-fail/unchanged`: if the lossless evidence digest changed, the transition is `updated`. Structural fingerprints cluster; they never conceal. | incentive-structural |
584
+ | **INV-9** | Red that vanished by shrinking the verification surface is never called repaired: fail→skip/xfail, deletion, selector narrowing, non-observation, and capability loss are reported as surface changes, separate from `repaired`. | incentive-structural |
585
+ | **INV-10** | The threat model includes the **deceiving** agent, not just the confused one. The agent can shell into the local store, so local outputs are advisory; gate verdicts require tamper-evident records (content addressing + signature verification) or re-verification in a trusted environment (CI). Every report states which threat model each claim holds under. | incentive-structural |
586
+ | **INV-11** | "Honest but stale" records are never gated on (TOCTOU): the gate verifies exact content-hash equality between the run's `tree_digest` and the judged target before judging. Mismatch is `inconclusive` — no proximity or ratio rescue. Staleness is a distinct threat from tampering; both are closed independently. | incentive-structural |
587
+
588
+ ## 9. Report contract
589
+
590
+ ### 9.1. Comparison report
591
+
592
+ JSON is the primary interface. Human-readable text MUST be a secondary
593
+ rendering of this schema with no independent logic.
594
+
595
+ ```json
596
+ {
597
+ "schema_version": "veridelta/1",
598
+ "outcome_verdict": "regressed",
599
+ "comparability": "exact",
600
+ "baseline": {
601
+ "run_id": "run_abc123",
602
+ "mode": "previous-comparable",
603
+ "selection_reason": "same-worktree-command-config-scope"
604
+ },
605
+ "current": {
606
+ "run_id": "run_def456",
607
+ "complete": true,
608
+ "child_exit_code": 1
609
+ },
610
+ "observation_coverage": {
611
+ "baseline": "842/842",
612
+ "current": "842/842"
613
+ },
614
+ "verification_surface": {
615
+ "status": "changed",
616
+ "events": [
617
+ {
618
+ "kind": "test-source-changed",
619
+ "test_id": "tests/api/test_user.py::test_update_user"
620
+ }
621
+ ]
622
+ },
623
+ "transitions": {
624
+ "new_fail": ["tests/api/test_user.py::test_create_user"],
625
+ "still_fail_unchanged": ["tests/legacy/test_import.py::test_v2"],
626
+ "updated_fail": [
627
+ {
628
+ "test_id": "tests/legacy/test_import.py::test_v1",
629
+ "evidence_digest_before": "sha256:111",
630
+ "evidence_digest_after": "sha256:222",
631
+ "failure_mode_changed": true
632
+ }
633
+ ],
634
+ "repaired_same_surface": [],
635
+ "repaired_with_test_change": ["tests/api/test_user.py::test_update_user"],
636
+ "fail_to_skip": [],
637
+ "fail_to_xfail": [],
638
+ "removed": [],
639
+ "not_observed": []
640
+ },
641
+ "failure_evidence": {
642
+ "composition_id": "pytest-native/1",
643
+ "degraded_capabilities": []
644
+ },
645
+ "trust": {
646
+ "record_integrity": "advisory"
647
+ },
648
+ "anchors": {
649
+ "new_fail:tests/api/test_user.py::test_create_user":
650
+ "vdelta show run_def456 --test tests/api/test_user.py::test_create_user",
651
+ "raw": "vdelta show run_def456 --raw"
652
+ }
653
+ }
654
+ ```
655
+
656
+ Additional REQUIRED-when-applicable fields:
657
+
658
+ - `comparability_detail` when `comparability` is `none` (§6.3).
659
+ - `budget_exceeded_for_safety: true` when §8's budget rule fires.
660
+ - `masking_applied` with count and example when §7.6 dedup fired.
661
+ - `trust.record_integrity`: `advisory | tamper-evident | trusted-environment`
662
+ (INV-10).
663
+
664
+ Reports MUST include `failure_evidence.composition_id` (the adapter's declared
665
+ canonical-evidence composition and version, §3.6) and
666
+ `failure_evidence.degraded_capabilities` (signal-bearing evidence capabilities
667
+ declared `unsupported`, §3.4; empty list otherwise). When
668
+ `degraded_capabilities` is non-empty, every `still_fail_unchanged` and
669
+ `updated_fail` claim MUST carry it, consumers MUST relay it with the claim,
670
+ and the gate report MUST surface it under every policy. Gates MAY treat
671
+ degraded `still_fail_unchanged` as gate-relevant; the default gate-relevant
672
+ set is unchanged.
673
+
674
+ Entries in `still_fail_unchanged` are test-ID strings, or objects
675
+ `{"test_id": ..., "context_changed": true}` when §3.6 requires the context
676
+ flag; the corresponding drill-down anchor appears under `anchors`.
677
+
678
+ ### 9.2. Budget
679
+
680
+ `--budget N` (tokens) is a first-class parameter: the implementation returns
681
+ the most informative representation within N tokens, deterministically. Budget
682
+ never overrides safety (§8).
683
+
684
+ ### 9.3. Anchors
685
+
686
+ Every omission leaves a drill-down anchor: a concrete command (or address) that
687
+ retrieves the elided detail (`vdelta show <run> --test <id>`, `--raw`).
688
+ Progressive disclosure is mandatory — a consumer can always reach raw evidence.
689
+
690
+ ### 9.4. Consumer requirements
691
+
692
+ - Consumers MUST treat an unknown value in any closed enum as a hard error
693
+ (throw), never as a default or a silent skip. Backward-compatibility
694
+ scaffolding around unknown enum values is non-conforming.
695
+ - Consumers MUST NOT reinterpret unknown fields; unknown fields SHOULD be
696
+ rejected (§14).
697
+ - Consumers MUST honor `comparability` limits when relaying claims onward.
698
+
699
+ ## 10. Command interface (reference CLI)
700
+
701
+ Command names are illustrative of the reference implementation; the division of
702
+ responsibilities and exit-code semantics are normative for any CLI claiming
703
+ conformance.
704
+
705
+ | Command | Responsibility | Exit code |
706
+ |---|---|---|
707
+ | `vdelta run -- <cmd>` | Execute, record, report | **Transparent child exit.** Internal errors degrade to raw passthrough (INV-5). |
708
+ | `vdelta compare <run-a> <run-b>` | Compare immutable runs | Success of the comparison operation itself (an `inconclusive` *result* is a successful comparison). |
709
+ | `vdelta show <run-id> [--test <id>\|--raw]` | Drill-down retrieval | Retrieval success. |
710
+ | `vdelta gate` | Policy verdict for CI/agent loops | `0` no gate-relevant transitions; `1` regression or surface reduction; `2` inconclusive or error (§11). |
711
+ | `vdelta doctor` *(OPTIONAL)* | Store-health diagnostics: comparable-run density, inconclusive rate, passthrough rate | Diagnostic success. Small samples MUST abstain (`insufficient_data`), not extrapolate. |
712
+
713
+ The child's "tests failed" and "something regressed since baseline" are never
714
+ encoded in the same exit code: `run` owns transparency, `gate` owns semantics.
715
+
716
+ **`run` I/O contract.** In the normal path, `vdelta run` captures the child's
717
+ output into the run record and emits the delta report to stdout (`--report
718
+ json` for the primary JSON contract; the secondary human rendering otherwise)
719
+ — replacing raw output with the report is the point of the tool, and the raw
720
+ stream stays reachable via anchors (`show --raw`). The child's exit code is
721
+ passed through unchanged, including signal-death conventions. In the degraded
722
+ path (INV-5), the child's raw stdout/stderr are passed through verbatim to
723
+ their respective streams and no report is emitted. veridelta's own
724
+ diagnostics go to stderr and MUST NOT interleave with the report on stdout.
725
+ Implementations MAY offer opt-in live passthrough for long-running suites;
726
+ recording and the report are unaffected by it.
727
+
728
+ ## 11. Gate
729
+
730
+ ### 11.1. Policies and the reporting floor
731
+
732
+ `policy` is a closed enum — there is no free-form selection of which findings
733
+ exist:
734
+
735
+ | Policy | Exit behavior | Intended stage |
736
+ |---|---|---|
737
+ | `report-only` | Always `0` (report published, e.g., PR comment); `2` only if the gate itself cannot produce a report | First deployment; builds trust without blocking. |
738
+ | `advisory` | Exit codes as `blocking`, but the report marks `policy: advisory` so harnesses surface it as a warning | Transition stage. |
739
+ | `blocking` | `1` on gate-relevant transitions, `2` on inconclusive | Required check. |
740
+
741
+ **Ungameable floor:** under every policy, `new_fail` findings and
742
+ `verification_surface` reduction events MUST appear in the output, and no flag,
743
+ configuration, or policy choice may remove them. Implementations MAY allow
744
+ narrowing which transitions *block*, but never which are *reported*; a
745
+ `blocking`-policy gate is further constrained by the blocking-set floor of
746
+ §11.5.
747
+
748
+ The default gate-relevant set is `{new_fail, updated_fail,
749
+ verification-surface reduced}`.
750
+
751
+ When comparing against a `git-ref` baseline, the gate MUST treat a
752
+ `selector-subset` event as gate-relevant: proven selector narrowing relative
753
+ to the baseline yields gate verdict `fail`, with common-ID transitions still
754
+ reported. An unproven selector change (`selector-relation-unknown`) yields
755
+ gate verdict `inconclusive`. Exit behavior follows the policy table: under
756
+ `blocking`, `1` and `2` respectively. Neither outcome ever maps to verdict
757
+ `pass` under any policy. `subset` comparability never relaxes §11.2 or §11.3.
758
+
759
+ ### 11.2. Record integrity (INV-10)
760
+
761
+ The agent under audit can execute arbitrary commands, including edits to a
762
+ local run store. Therefore:
763
+
764
+ - Verdicts computed from an unverified local store are `advisory` and MUST be
765
+ labeled as such.
766
+ - A `blocking` or `advisory` gate verdict MUST be based on records whose
767
+ content addresses (and, where configured, signatures) verify, or on
768
+ re-execution in an environment the agent cannot write to (CI).
769
+ - Verification failure is `comparability_detail: {reason:
770
+ "record-integrity-failed", kind: "failed"}` → exit `2`.
771
+
772
+ ### 11.3. Staleness check (INV-11)
773
+
774
+ Before judging, the gate MUST verify that the run's `provenance.tree_digest`
775
+ equals the content digest of the judged target (e.g., PR HEAD) **exactly**.
776
+ Binary equality only: no ratio, proximity, or "close enough" rescue. Mismatch →
777
+ `inconclusive`, exit `2`. Tampering (§11.2) and staleness are independent
778
+ checks; passing one never waives the other.
779
+
780
+ The gate MUST recompute the target's `tree_digest` from the workspace in which
781
+ the head-side run executed, immediately before judging. `git rev-parse
782
+ HEAD^{tree}` MAY be used as the canonical equivalent only when `git status
783
+ --porcelain --ignore-submodules=none` reports a clean workspace AND the judged
784
+ tree contains no gitlink entries. When submodules are present, the gate MUST
785
+ verify each initialized submodule worktree recursively: the submodule
786
+ worktree's `tree_digest` MUST equal the tree of the commit named by the
787
+ superproject's gitlink; any mismatch is `inconclusive`, exit 2. Submodules
788
+ without an initialized worktree cannot be content-verified: the gate report
789
+ MUST list them under `staleness.unverified_submodules`. The superproject
790
+ digest alone does not witness submodule content.
791
+
792
+ ### 11.4. Gate report
793
+
794
+ The gate report extends the comparison report (§9.1) with:
795
+
796
+ ```json
797
+ {
798
+ "gate": {
799
+ "policy": "report-only",
800
+ "verdict": "fail",
801
+ "triggered": ["new_fail", "verification_surface_reduced"],
802
+ "target": {
803
+ "kind": "merge",
804
+ "head_sha": "1a2b3c...",
805
+ "base_sha": "4d5e6f...",
806
+ "merge_sha": "7a8b9c..."
807
+ },
808
+ "staleness": {
809
+ "run_tree_digest": "sha256:aaa",
810
+ "target_tree_digest": "sha256:aaa",
811
+ "match": true,
812
+ "unverified_submodules": []
813
+ },
814
+ "record_integrity": "tamper-evident"
815
+ }
816
+ }
817
+ ```
818
+
819
+ `gate.verdict`: `pass | fail | inconclusive` — reported identically under all
820
+ policies; policy affects only exit behavior.
821
+
822
+ ### 11.5. Gate invocation contract (CI)
823
+
824
+ **Baseline supply.** A gate producing `blocking` or `advisory` verdicts MUST
825
+ support `same-job` mode: the gate job checks out the base tree and the target
826
+ tree and executes and records both runs itself. A gate MAY additionally
827
+ support `trusted-store` mode, substituting a stored base-branch run for
828
+ base-side execution only if all of the following verify: (1) record integrity
829
+ per §11.2; (2) exact `tree_digest` equality with the base tree; (3) exact
830
+ instrument identity per §6.2 and this section. On any failure the gate MUST
831
+ fall back to same-job base execution or return `inconclusive` (exit 2); it
832
+ MUST NOT select a weaker baseline (§5.3). Records produced inside the audited
833
+ agent's writable environment MUST NOT feed `blocking` or `advisory` verdicts
834
+ (INV-10).
835
+
836
+ **Isolation and sealing (INV-10).** Same-job execution qualifies as
837
+ `trusted-environment` only under this ordering contract: (1) the base-side run
838
+ MUST complete before any code from the audited change (tests,
839
+ conftest/build/install hooks, dependency resolution of the head tree) executes
840
+ in the job; (2) before any such code executes, the gate MUST seal the base
841
+ record's content address and a digest of the gate toolchain (gate executable,
842
+ runtime, configuration) by exporting them to an append-only channel outside
843
+ the job workspace (e.g., the CI step log or a check annotation); where the
844
+ channel permits later appends, verifiers MUST bind to the earliest seal entry
845
+ for the evaluation; (3) immediately before judging, the gate MUST re-verify
846
+ the base record and its own toolchain against the sealed values — any mismatch
847
+ is `record-integrity-failed` (§6.3), exit 2; (4) because step (3) executes
848
+ after the audited change's code has run in the job, a `blocking` verdict
849
+ additionally REQUIRES that the judgment's integrity is established outside the
850
+ audited change's write reach: either (a) the judgment runs in a separate job
851
+ or runner that receives only the sealed records and independently verifies
852
+ them against the sealed values, or (b) the party enforcing the verdict
853
+ independently verifies the published report and records against the sealed
854
+ values before acting on it. A gate that judges in-job without (a) or (b) MUST
855
+ cap `trust.record_integrity` at `tamper-evident` and its verdict at
856
+ `advisory`: the seals make in-job tampering detectable by an external auditor,
857
+ but an in-job judgment cannot witness its own integrity (§15). If the gate's
858
+ invocation path (workflow definition, checkout procedure, or gate-binary
859
+ resolution) is modifiable by the change's author for the evaluation at hand,
860
+ the verdict MUST NOT be `blocking`.
861
+
862
+ **Target definition.** The judged target is a tree, not a ref. The head-side
863
+ run MUST execute against the tree being judged (INV-11); testing a synthetic
864
+ merge tree while judging the head commit tree, or vice versa, is
865
+ non-conforming. The gate report MUST bind the judged tree to its provenance:
866
+ `gate.target: {kind: "head" | "merge", head_sha, base_sha, merge_sha?}`. For
867
+ `kind: "merge"` the base tree is the first parent's tree; for `kind: "head"`
868
+ it is the tree of `merge-base(base_branch, head_sha)`. A `pass` certifies the
869
+ judged tree only; re-validation after base movement is the responsibility of
870
+ branch protection, and the report MUST expose `base_sha` to enable it.
871
+
872
+ **Instrument identity.** Same-job execution does not waive §6.2. Each run MUST
873
+ execute in a dependency environment resolved from its own tree, and the run's
874
+ `environment` fingerprint MUST include the resolved runner plugin set and
875
+ adapter-declared runner-affecting environment variables (for pytest: including
876
+ `PYTEST_ADDOPTS`). Implementations SHOULD resolve both environments from
877
+ committed lockfiles where available, so that registry drift between the two
878
+ in-job resolutions cannot manufacture an instrument mismatch. Any instrument
879
+ mismatch between the two runs yields `none`/`instrument-changed`, exit 2, with
880
+ the corresponding `verification_surface` events (§7.4) in the report.
881
+
882
+ **Bytecode-cache hygiene.** Before each run, the recorder MUST ensure the
883
+ interpreter cannot load compilation or assertion-rewrite caches not derived
884
+ from that run's tree (§4.5). For CPython the RECOMMENDED mechanism is a fresh
885
+ `PYTHONPYCACHEPREFIX` per run, which neutralizes both stale and committed
886
+ `__pycache__` content without mutating the worktree; purging caches is
887
+ acceptable only for untracked files (deleting tracked files diverges the
888
+ workspace from the judged tree). `PYTHONDONTWRITEBYTECODE` alone is NOT
889
+ sufficient: it does not prevent reading pre-existing caches. Adapters SHOULD
890
+ report tracked bytecode files in the judged tree as a `verification_surface`
891
+ event.
892
+
893
+ **Blocking-set floor.** For `policy: blocking`, the gate-relevant set MUST
894
+ include `new_fail`, `updated_fail`, and `verification_surface` reduction. The
895
+ narrowing permitted by §11.1 MUST NOT remove `updated_fail` from a blocking
896
+ gate's set; this clause takes precedence over §11.1.
897
+
898
+ **Claim scope.** Gate claims are facts about the two recorded runs: `new_fail`
899
+ asserts "red in the head run and not red in the base run", nothing stronger.
900
+ The gate MUST NOT rerun tests to derive flakiness (§7.7). A base run that is
901
+ not `complete` is not an acceptable baseline (§5.2) and yields `inconclusive`,
902
+ exit 2 — never `pass`.
903
+
904
+ ## 12. Adapters
905
+
906
+ - Adapters are **capability-declared, structured-first** (§3.4). They consume
907
+ runner-native structured channels (plugins, machine-readable reporters), not
908
+ scraped human-oriented text.
909
+ - Fail-closed on ambiguity: duplicate IDs, unknown outcomes, count mismatches,
910
+ and undeclared capabilities yield `inconclusive` (or `kind: failed`
911
+ abstention), never a guess.
912
+ - Unsupported formats pass through raw (INV-5). Line-level fallback parsing is
913
+ excluded from the trust core.
914
+ - Lossy interchange formats (e.g., JUnit XML dialects that drop xfail, phase,
915
+ source, markers) require an explicit supported-subset declaration and a
916
+ capability matrix; anything outside the subset is `unsupported`, not
917
+ approximated.
918
+
919
+ Adapter roadmap for the reference implementation (informative): `vitest`
920
+ native reporter first — agent-authored code skews heavily toward TS/JS, the
921
+ maintainer's own repositories (the cheapest dogfood subjects) are TypeScript,
922
+ and empirical probing confirmed the channel satisfies §3.6 with no
923
+ stale-cache path in run mode. `pytest` native second (its richer structured
924
+ channel is already probed and its composition pre-designed); JUnit XML third,
925
+ for CI breadth, under the constraints above.
926
+
927
+ ## 13. Conformance
928
+
929
+ ### 13.1. Conformance classes
930
+
931
+ Implementations claim conformance per role: **Recorder**, **Store**,
932
+ **Comparator**, **Gate**, **Adapter**, **Consumer**. The badge
933
+ `veridelta/1 compliant` requires passing the published conformance suite for
934
+ every claimed role.
935
+
936
+ ### 13.2. Required fixture classes
937
+
938
+ The conformance suite MUST include, at minimum:
939
+
940
+ 1. **Invariant fixtures** — one or more per INV-1..11 (INV-10 may be limited
941
+ to the gate path in early revisions).
942
+ 2. **Adversarial input fixtures** — reordering, partial execution, flaky
943
+ sequences, secret-bearing output, branch crossing (§7.8) — including:
944
+ (a) rerun stability — two executions of a deterministically failing test at
945
+ an identical `tree_digest` MUST yield identical `evidence_digest`s;
946
+ (b) stale-cache collision — a same-size source revert under coarse mtime
947
+ resolution MUST NOT reproduce evidence from the prior source state (§4.5);
948
+ (c) degraded-capability marking — a run pair recorded with assertion
949
+ introspection disabled MUST emit non-empty `degraded_capabilities` on
950
+ red-in-both claims.
951
+ 3. **Operational pitfall fixtures** — the four classes that recur in practice:
952
+ gate wired to the wrong target (wiring position), instrument drift
953
+ undetected (§6.2), duplicate records double-counted (§4.3), and
954
+ fail-open/fail-closed confusion (§6.3 vs INV-5).
955
+ 4. **Consumer fixtures** — unknown enum value MUST throw; comparability limits
956
+ MUST be honored.
957
+ 5. **Verification-surface recall fixtures** — labeled cheating corpus
958
+ (fail→skip/xfail, test deletion, repaired-with-test-change, selector
959
+ narrowing, and collection-modifier inputs — `--deselect`, `--ignore`,
960
+ `collect_ignore`-style exclusion — that MUST classify as in-scope
961
+ non-observation, never `out_of_scope`): detection recall MUST be 100% on
962
+ comparable runs.
963
+
964
+ ### 13.3. Determinism proof
965
+
966
+ Fixture outputs are byte-compared. Any nondeterminism is a conformance
967
+ failure, not a warning.
968
+
969
+ ## 14. Versioning and extensibility
970
+
971
+ - `schema_version` `"veridelta/1"` names this contract. Enums defined here are
972
+ **closed** for `/1`; new enum values require a new schema version. During
973
+ the draft phase (0.x spec revisions), enum sets may still change between
974
+ revisions; closure binds from the first published revision of `veridelta/1`.
975
+ - Field additions within `/1` occur only through published spec revisions;
976
+ consumers SHOULD reject unknown fields and MUST NOT silently reinterpret
977
+ them.
978
+ - Producers MUST emit exactly one schema version per report.
979
+ - The spec is licensed MIT and intended for independent implementation;
980
+ absorbing the schema into other tools (with or without the reference CLI) is
981
+ the intended success mode.
982
+
983
+ ## 15. Security considerations
984
+
985
+ - **Secrets**: secret values are never persisted. Environment comparison uses
986
+ fingerprints of adapter-declared variables; failure evidence is redacted for
987
+ known secret shapes before storage and digesting, deterministically (§3.5).
988
+ - **Threat model**: three distinct adversarial conditions are handled
989
+ independently — the *confused* agent (wrong baseline, partial runs; INV-1..9),
990
+ the *deceiving* agent (store tampering; INV-10, §11.2), and the *stale honest
991
+ record* (TOCTOU; INV-11, §11.3). A mechanism addressing one MUST NOT be
992
+ presented as addressing another.
993
+ - **Store**: repo-local, ignored by VCS, ephemeral, bounded; atomic writes;
994
+ advisory fail-open locks (§4.4).
995
+ - The gate MUST run outside the audited agent's blast radius (separate
996
+ process, CI environment) for any non-advisory claim.
997
+
998
+ ## Appendix A (informative): Post-1.0 roadmap
999
+
1000
+ | # | Feature | Constraint carried from this spec |
1001
+ |---|---|---|
1002
+ | A.1 | Failure clusters | Cluster by structural fingerprint; keep all IDs, counts, representative evidence, raw anchors; never assert same root cause. |
1003
+ | A.2 | Changed-hunk correlation | `direct / transitive / none / unavailable` intersection of failure locations and changed hunks — prioritization evidence, not causation. |
1004
+ | A.3 | Observed-flaky history | Only from ≥3 comparable runs at the same code state; never suppresses new/updated findings; gate policy stays with the caller. |
1005
+ | A.4 | Coverage surface delta | Per-changed-line/branch execution comparison where a coverage adapter exists; aggregate coverage % alone never proves surface equality. |
1006
+ | A.5 | Causal proof | Opt-in red/green re-verification: stash implementation-only changes → confirm red → restore → confirm green. Proves the test constrains the implementation (no vacuous pass) — a strictly stronger claim than `repaired_same_surface`, kept as a separate proof level. Ambiguous cases (mixed test/impl changes, stash failure, already-green base) stay `inconclusive`. |
1007
+
1008
+ ## Appendix B (informative): Rejected designs
1009
+
1010
+ These were removed during adversarial design review and MUST NOT be
1011
+ reintroduced without revisiting the arguments:
1012
+
1013
+ | Rejected | Why |
1014
+ |---|---|
1015
+ | Caller-supplied `--mask` regex in the trust path | `--mask '\d+'` collapses `expected 200 got 500` into `expected N got N` — a silently hidden regression that no diagnostic layer can recover. |
1016
+ | `suggest` (inducing masks from run history) | "Environmental noise" and "values my fix moved" are indistinguishable as run-to-run diffs; suggesting masks from them is a self-propagating blind spot. |
1017
+ | Caller-supplied `--key` regex | Replaced by adapter-provided canonical test IDs; also fixes parametrized-test merge accidents. |
1018
+ | Implicit "previous invocation" baseline | Order-dependent; contaminated by branch switches and partial runs. Replaced by content addressing + explicit selection (§5). |
1019
+ | Agent-interpreted diagnostic meta-loop | Making the agent reason about the tool's health spends the context the tool exists to save. Replaced by self-check → automatic fail-open (INV-5). |
1020
+
1021
+ Two counterarguments that were withdrawn, preserved for future reviewers:
1022
+
1023
+ - "`pytest --lf` suffices" — `--lf` changes *what runs* and can hide
1024
+ regressions in previously-passing tests; veridelta keeps the full suite
1025
+ semantics and changes only *what is looked at*. Orthogonal axes.
1026
+ - "Let the agent diff the outputs" — both full outputs must enter context
1027
+ (defeating the purpose), and model summaries hallucinate and drop items;
1028
+ a deterministic diff structurally cannot.
1029
+
1030
+ ## Appendix C (informative): Known residual risks
1031
+
1032
+ Accepted limitations surfaced during adversarial review of revision 0.2.0.
1033
+ None violates an invariant; each is disclosed rather than papered over.
1034
+
1035
+ - **Degraded-evidence repositories.** Where both compared runs were recorded
1036
+ with assertion introspection disabled, a genuine red→red value mutation can
1037
+ yield digest-identical `still_fail_unchanged`. The mandatory
1038
+ `degraded_capabilities` surfacing (§9.1) makes the condition visible;
1039
+ escalating it to gate-relevant remains a MAY.
1040
+ - **Flood valve vs. value signal.** Narrowing blocking to
1041
+ `failure_mode_changed: true` (to absorb honest flapping from
1042
+ volatile-in-message values, §3.6) would also exclude genuine value-only
1043
+ changes, whose structural fingerprint is likewise stable. Hence the
1044
+ blocking-set floor (§11.5); below `blocking`, the tradeoff is the
1045
+ operator's.
1046
+ - **Multiple maximal superset baselines.** When maximal `previous-superset`
1047
+ candidates are mutually non-comparable, recency decides which out-of-scope
1048
+ ledger a report shows. Disclosed via `baseline.superset_candidates`; full
1049
+ closure (a per-series red ledger) is post-1.0.
1050
+ - **Undecidable selectors.** For `-k`/`-m`-style expressions,
1051
+ `selector_relation` returns `unknown` and `previous-superset` abstains — a
1052
+ functional limitation, not a trust hole.
1053
+ - **Cross-stream suppression blind spot (inner loop).** A fail→skip between
1054
+ two superset runs with different selectors appears in no local comparison;
1055
+ the final defense is the gate's full-width `git-ref` comparison.
1056
+ - **Seal channel strength is platform-dependent.** The spec can require only
1057
+ append-only export and earliest-entry binding (§11.5); platforms where the
1058
+ audited job's token can rewrite annotations weaken the evidence.
1059
+ - **Registry drift without lockfiles.** Per-tree dependency resolution twice
1060
+ in one job can hit registry drift, producing PR-unrelated
1061
+ `instrument-changed` abstentions; lockfile resolution is SHOULD-level.
1062
+ - **Uninitialized submodules.** Content verification is impossible; the gate
1063
+ can only disclose them (`staleness.unverified_submodules`).
1064
+ - **Two-run factualism.** A flaky base red whose evidence is byte-identical to
1065
+ the head's deterministic failure classifies as `still_fail_unchanged`;
1066
+ reporting floors keep it visible, but 2-run facts cannot distinguish it.
1067
+ - **Head-run self-forgery is out of contract scope.** A hostile change that
1068
+ makes its own tests report `pass` is common to every architecture that
1069
+ executes the author's code; defenses are the surface events (§7.4) and the
1070
+ non-goal boundary (§1.3) — intent attribution stays with the consumer.
1071
+
1072
+ ## Revision history
1073
+
1074
+ - **0.3.0 (2026-07-16)** — First adapter target flipped to vitest after an
1075
+ empirical probe confirmed §3.6 satisfiability (core digest stable across
1076
+ rerun/line-shift/unrelated-edit, sensitive only to genuine signal; no
1077
+ stale-cache path in run mode). Added: channel-absent CE-1 component rule
1078
+ (declare `unsupported` or reconstruct deterministically from the recorded
1079
+ tree, §3.6); runner-cache asymmetry note (§4.5); truncation settings in the
1080
+ instrument digest (§3.1); canonical observation ordering under parallel
1081
+ workers (§7.8); vitest selector examples (§6.4); adapter roadmap reorder
1082
+ (§12); the `run` command I/O contract (§10).
1083
+ - **0.2.0 (2026-07-16)** — Resolved the open implementation questions via
1084
+ empirical probes (git tree-digest behavior; pytest failure-evidence
1085
+ volatility) and a two-round adversarial design review. Added: canonical
1086
+ failure evidence (§3.6), run/tree identity algorithms (§3.5),
1087
+ execution-cache coherence (§4.5), series key and `previous-superset`
1088
+ baseline mode (§5), `subset` comparability and selector semantics (§6.1,
1089
+ §6.4), `out_of_scope` transition and `selector-subset` event (§7),
1090
+ evidence-composition transparency in reports (§9.1), the gate CI invocation
1091
+ contract (§11.5), conformance fixtures for all of the above (§13.2), and
1092
+ this appendix.
1093
+ - **0.1.0 (2026-07-16)** — Initial draft from the design document.