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 (z is a value import; Database-style type-only imports unused here)
  • @typescript-eslint/no-explicit-any — passes (no any; z.unknown() for evidence array 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 in computeDecisionHash and serializeAdvisory. These hit only if canonicalize throws something OTHER than CanonicalSerializationError, 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.md
  • docs/contracts/p4-1-1-advisory-envelope-contract.md
  • docs/packets/p4-1-1-advisory-envelope-packet.md
  • src/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 role enum), P4.4.1 escalation FSM (consume the result enum + severity enum + escalation mapping at integrity.md L148-155), P4.5.1 persistence (uses decision_hash as dedup key for the mcp_advisories table).
  • 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.


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.