Contract — R93 B2 audit_verify_chain Cross-Task Partition
Round: R93 debug-sweep
Branch: feature/r93-b2-verifier-partition
Audit: docs/audits/r93-b2-verifier-partition-audit.md
β task: 635d0ed2-5ee5-4414-93d4-489a07fee1bc
§1. Behavioural invariants (post-fix)
| ID | Invariant | Verifier |
|---|---|---|
| I-1 | audit_verify_chain({task_id: "X"}) returns {valid, broken_count, first_broken_at?, record_count} unchanged from current behaviour. No by_task field on this path. |
Existing tests at verifier.test.ts Group B; backward-compat assertion. |
| I-2 | audit_verify_chain({}) (no filter) returns the same 4-field shape PLUS a by_task: Array<TaskRollup> field listing each task’s partition. |
New test: 2 intact chains → valid:true, broken_count:0, by_task.length === 2, each row valid:true. |
| I-3 | valid in the unfiltered case is true iff every partition is valid (logical AND across by_task). |
New test: 1 intact + 1 tampered chain → valid:false. |
| I-4 | broken_count in the unfiltered case equals the sum of by_task[i].broken_count. |
New test asserts arithmetic equivalence. |
| I-5 | record_count in the unfiltered case equals the total records across partitions (matches the current behaviour). |
New test. |
| I-6 | first_broken_at in the unfiltered case (when present) is the first_broken_at of the first partition (in insertion / rowid order) that has broken_count > 0. |
New test: tamper task-B record-1; build task-A first with 3 intact records; assert first_broken_at is task-B’s record-1, not the lowest task_id alphabetically. |
| I-7 | Partition iteration order is the order each task_id first appears in listThoughtRecords(db, {}) (i.e. rowid-ASC of the first record per task). |
New test: assert by_task order matches first-appearance order. |
| I-8 | The pure verifyChain function is unchanged. Its 24 existing tests pass byte-identically. |
Diff inspection + test run. |
| I-9 | Empty DB → {valid:true, broken_count:0, record_count:0, by_task:[]}. |
New test. |
| I-10 | Single-task DB with audit_verify_chain({}) → {valid:true, broken_count:0, record_count:N, by_task:[{task_id, valid:true, broken_count:0, record_count:N}]}. |
New test. |
§2. Output type
The handler’s typed return signature widens to:
{
valid: boolean;
broken_count: number;
first_broken_at?: string;
record_count: number;
by_task?: ReadonlyArray<{
task_id: string;
valid: boolean;
broken_count: number;
record_count: number;
first_broken_at?: string;
}>;
}
by_task is undefined (absent) when input.task_id is supplied; present when omitted.
§3. Non-invariants
- N-1. The pure
verifyChainis NOT extended with multi-task awareness. The handler does the partitioning;verifyChaincontinues to walk a homogeneous chain. - N-2. The
VerifyChainResultexported type inverifier.ts:64-68is not modified — it represents the pure function’s return, not the handler’s. - N-3. No DB schema change. No migration. No new table.
- N-4. No output runtime validation via the SDK (B1 already removed outputSchema passthrough, and this handler never declared one).
§4. Acceptance criteria
| AC | Statement |
|---|---|
| AC-1 | git diff origin/main..HEAD -- src/domains/trail/verifier.ts shows only the handler closure changed (lines 174-202 region). The pure verifyChain function (lines 119-153) is untouched. |
| AC-2 | git diff origin/main..HEAD -- src/__tests__/domains/trail/verifier.test.ts adds a new describe block (Group D or extension to Group B) covering the unfiltered handler path. ≥ 5 new tests covering I-2 through I-10. |
| AC-3 | npm run build exits 0. |
| AC-4 | npm run lint exits 0. |
| AC-5 | npm test exits 0 (modulo documented flakes that don’t relate to this slice). The new tests appear in the verifier.test.ts run. |
| AC-6 | PR body documents the false-positive reproduction + the fix + the new by_task field semantics. |
| AC-7 | Writeback per CLAUDE.md §7. |
§5. Risks
- R-1. Existing handler tests assert the old 4-field return shape. The unfiltered handler is invoked very little in tests; any consumer that destructures
resultwill continue to work becauseby_taskis an additive field. Mitigation: grep test code forrecord_countto confirm no test asserts the shape contains only 4 fields. - R-2. A single-task DB calling
audit_verify_chain({})now returnsby_task: [{...}]instead of bare result. Documented as part of the new contract; no client should fail on an additional field. Mitigation: I-10 invariant is the test that documents this.
Proceeding to packet.