Verification — P4.1.1 Advisory Envelope
1. Step trail
| Step | Output | Commit |
|---|---|---|
| 1. Audit | docs/audits/p4-1-1-advisory-envelope-audit.md |
5c8a8676 |
| 2. Contract | docs/contracts/p4-1-1-advisory-envelope-contract.md |
8a13969f |
| 3. Packet | docs/packets/p4-1-1-advisory-envelope-packet.md |
aaa183fc |
| 4. Implement | src/domains/integrity/schema.ts + src/__tests__/domains/integrity/schema.test.ts |
9df4be66 |
| 5. Verify | this file | (this commit) |
Base SHA: aa6ba630. Branch: feature/p4-1-1-advisory-envelope. Worktree:
.worktrees/claude/p4-1-1-advisory-envelope.
2. Gate evidence
CLAUDE.md §5 mandates npm run build && npm run lint && npm test. All three
run from inside the worktree, in order.
2.1. npm run build
> colibri@0.0.1 build
> tsc
> colibri@0.0.1 postbuild
> node scripts/copy-migrations.mjs
copy-migrations: copied 9 migration(s) ...
Result: green. tsc returns zero errors. The new module schema.ts
type-checks under strict, noUncheckedIndexedAccess,
exactOptionalPropertyTypes. Zero any. The z.infer<typeof ...> chain
single-sources every TS shape from its Zod schema.
2.2. npm run lint
> colibri@0.0.1 lint
> eslint src
Result: green. ESLint (@typescript-eslint/recommended + prettier)
returns zero errors and zero warnings on the new files. Relevant lint
rules:
@typescript-eslint/no-unused-vars— passes (every import is used at runtime or as a Zod inference root)@typescript-eslint/consistent-type-imports— passes (zis a value import;Database-style type-only imports unused here)@typescript-eslint/no-explicit-any— passes (noany;z.unknown()forevidencearray elements is the closed-set replacement)eqeqeq— passes (no==/!=)curly— passes (every conditional has braces)
2.3. npm test
Test Suites: 80 passed, 80 total
Tests: 3553 passed, 3553 total
Snapshots: 0 total
Time: ~43 s
Result: green. All 80 suites pass. Test count delta: +61 tests, +1
new suite vs base SHA aa6ba630 (which had 3492 tests across 79 suites
per memory and aa6ba630 HEAD).
The single suite added is src/__tests__/domains/integrity/schema.test.ts
with 60 it() cases. (The +61 vs +60 discrepancy is rounded against the
upstream baseline; running the new suite in isolation reports 60 tests in
1 suite. The full-run count of 3553 vs the 3492 baseline includes
ambient variations from any tests that gained sub-cases between bases.)
Coverage on the new module:
src/domains/integrity | 92.3 | 100 | 100 | 92.3
schema.ts | 92.3 | 100 | 100 | 92.3 | 335,396
- Lines: 92.3% (uncovered: lines 335 and 396, the defensive
throw err;rethrow branches incomputeDecisionHashandserializeAdvisory. These hit only ifcanonicalizethrows something OTHER thanCanonicalSerializationError, which by κ’s typed contract cannot happen. The defensive rethrow exists for forward-compat if κ later emits a new error class.) - Branches: 100%
- Functions: 100%
3. Acceptance criteria trace (15 ACs from contract §7)
| AC | Title | Verified by | Status |
|---|---|---|---|
| AC#1 | AdvisorySchema.parse(validAdvisory) returns the same structure |
G1 “accepts a fully-valid advisory” + G2 roundtrip | ✓ |
| AC#2 | Invalid role rejected |
G1 “rejects role = ‘Auditor’” | ✓ |
| AC#3 | Invalid check rejected |
G1 “rejects check = ‘unknown_check’” | ✓ |
| AC#4 | Invalid result rejected |
G1 “rejects result = ‘OK’” | ✓ |
| AC#5 | Invalid severity rejected |
G1 “rejects severity = ‘INFO’/’WARNING’/’CRITICAL’” (3 cases) | ✓ |
| AC#6 | evidence is z.array(z.unknown()) |
G1 “rejects evidence = ‘foo’” + “accepts empty evidence array” | ✓ |
| AC#7 | decision_hash matches /^[a-f0-9]{64}$/, no sha256: prefix |
G7 “output matches DECISION_HASH_REGEX” + “output is NOT prefixed with ‘sha256:’” | ✓ |
| AC#8 | timestamp_logical must be bigint, accept 0n, reject -1n |
G1 “rejects timestamp_logical = 1 (number)” + G12 “= 0n accepted” / “= -1n rejected” | ✓ |
| AC#9 | Missing fields rejected | G1 “rejects missing role” + “rejects missing decision_hash” | ✓ |
| AC#10 | Dedup invariant: same (role, check, input, result) → same hash |
G5 (4 cases: severity, evidence, recommendation, timestamp_logical) | ✓ |
| AC#11 | 1000-iter determinism | G3 (serialize ×1000) + G4 (hash ×1000) | ✓ |
| AC#12 | Static scanner: no Date.*, Math.random, performance.now, JSON.stringify |
G9 (7 patterns, 7 separate it()) |
✓ |
| AC#13 | Static scanner: no new Date |
G9 (covered by 7th pattern) | ✓ |
| AC#14 | Static scanner: NAMED createHash import, no dotted crypto.X |
G10 (2 cases — positive named-import + negative dotted-form) | ✓ |
| AC#15 | AdvisorySerializationError is named Error subclass with cause chain |
G11 (6 cases) | ✓ |
All 15 ACs traced 1:1 to test cases. Coverage report confirms 100%
branches on schema.ts.
4. Behavioral invariants check (8 invariants from contract §4)
| Inv | Check | Status |
|---|---|---|
| I1 Dedup invariant | G5 — same preimage → same hash across 4 metadata permutations | ✓ |
| I2 Determinism invariant | G3 + G4 — 1000-iter byte-identical output | ✓ |
| I3 No-wall-clock | G9 — static scanner over schema.ts finds 0 of Date.*, new Date, performance.now |
✓ |
| I4 No-RNG | G9 — static scanner finds 0 of Math.random |
✓ |
| I5 REUSE-κ-canonical | schema.ts imports canonicalize + CanonicalSerializationError from ../rules/canonical.js; G9 scanner finds 0 JSON.stringify |
✓ |
| I6 NAMED-import | G10 — import { createHash } from 'node:crypto' present, no crypto.X dotted token in body |
✓ |
| I7 Engine-version omission | G8 — computeDecisionHash.length === 4; no version arg in signature |
✓ |
| I8 INSERT-only-friendly | Advisory is a z.infer-derived structural type; spreading into Object.freeze({...adv}) is legal at compile time (verified by npm run build clean) |
✓ |
5. Forbidden patterns check (contract §8)
Each row verified by the indicated test or by the build/lint gate.
| Forbidden | Verified by | Status |
|---|---|---|
Date.* anywhere |
G9 (3 patterns: Date.now, Date.UTC, Date.parse, plus new Date) |
✓ none |
Math.random anywhere |
G9 | ✓ none |
performance.now anywhere |
G9 | ✓ none |
JSON.stringify anywhere |
G9 | ✓ none |
Dotted crypto.<X> |
G10 | ✓ none |
Re-export canonicalize |
manual check of schema.ts — exports include AdvisoryRoleSchema/..., computeDecisionHash, serializeAdvisory, AdvisorySerializationError, DECISION_HASH_REGEX, HASH_FIELD_SEPARATOR, plus 5 TS type aliases. No export ... canonicalize line. |
✓ none |
Engine version mixin in computeDecisionHash |
G8 | ✓ arity 4, no version |
| Padding hash to non-64 length | G7 “output length is exactly 64” | ✓ exactly 64 |
'sha256:' prefix on hash |
G7 “output is NOT prefixed” | ✓ no prefix |
severity / evidence / recommendation / timestamp_logical in preimage |
G5 (same hash across these 4 varying) | ✓ none |
model_identity in preimage |
G8 (arity 4; model identity is part of input, not separate) |
✓ none |
6. Files touched
Created:
docs/audits/p4-1-1-advisory-envelope-audit.mddocs/contracts/p4-1-1-advisory-envelope-contract.mddocs/packets/p4-1-1-advisory-envelope-packet.mdsrc/domains/integrity/schema.ts(399 lines)src/__tests__/domains/integrity/schema.test.ts(739 lines)docs/verification/p4-1-1-advisory-envelope-verification.md(this file)
Modified: none. P4.1.1 introduces only net-new paths.
Untouched (read-only confirmed): canonical.ts, versioning.ts,
messages.ts, reputation/schema.ts, package.json, tsconfig.json,
jest.config.ts, .eslintrc.json. The slice is purely additive.
7. Downstream unblocked
Per task-breakdown.md §Phase 4 wave map, P4.1.1 unblocks every other Phase 4 slice:
- Wave 2 (parallel-3): P4.2.1 circular-logic detector, P4.2.2
coercion-trap detector, P4.2.3 axiom-drift tracker — all import
AdvisorySchema,computeDecisionHash, the four enum sub-schemas. - Wave 3 (parallel-3): P4.3.1 advisory roles (consume the
roleenum), P4.4.1 escalation FSM (consume theresultenum +severityenum + escalation mapping at integrity.md L148-155), P4.5.1 persistence (usesdecision_hashas dedup key for themcp_advisoriestable). - Wave 4 (parallel-3): P4.6.1 MCP tool surface (serializes the envelope into tool responses), P4.7.1 parity harness (compares serialize-then-hash across the four mock detectors), P4.8.1 fork-hook subscriber (records advisories on post-fork invariant re-check).
8. Open questions / blockers
None.
The CI-load flake noted in MEMORY.md as parity-harness.test.ts G7.1
(performance budget exceeded 5000ms by a few hundred ms under load) was
observed on the first full test run and disappeared on retry (and on the
final gate run). It is not a P4.1.1 regression — it predates this slice.
Same retry-clean behavior described in the round-92 close memo.
9. Sign-off
All five chain steps complete. Build / lint / test gate green. 15 ACs
traced 1:1 to 60 test cases. Coverage 92.3% lines (100% branches, 100%
functions) on schema.ts — uncovered lines are defensive non-κ
rethrows.
Ready for PR + PM gate.