P1.4.2 — Denial Reason Taxonomy — Verification (Step 5 / 5)

Test evidence for P1.4.2. Closes the 5-step executor chain. Audit, contract, and packet committed earlier in this PR; this doc records the proof that the implementation matches the contract and ships green across all three gates (build + lint + test).

§1. Inputs

  • Audit: docs/audits/p1-4-2-denial-reasons-audit.md (commit aad59fb4)
  • Contract: docs/contracts/p1-4-2-denial-reasons-contract.md (commit 36ab9754)
  • Packet: docs/packets/p1-4-2-denial-reasons-packet.md (commit 53ccea23)
  • Implementation: feat(p1-4-2): denial reason taxonomy (commit d595d6d2)

§2. Files shipped

Path Verb Lines (NET) Purpose
src/domains/rules/denial-reasons.ts CREATE +651 Canonical 8-variant DenialReason taxonomy + render + canonical-JSON round-trip + isDenialReason predicate.
src/__tests__/domains/rules/denial-reasons.test.ts CREATE +744 F1..F10 test matrix. ≥ 60 tests.
src/domains/rules/admission.ts EDIT +41 / -22 Re-export DenialReason from denial-reasons.ts; emission site for policy denial now also populates policy_id via the new derivePolicyDiscriminant helper.
src/__tests__/domains/rules/admission.test.ts EDIT +20 / -3 F13.1 narrowed to admission’s 4 emission discriminants with explicit default for the 4 P1.4.2-reserved variants.

§3. Gate evidence (build + lint + test)

All three gates run from the worktree at feature/p1-4-2-denial-reasons HEAD (d595d6d2).

§3.1. npm run build — green

> colibri@0.0.1 build
> tsc

> colibri@0.0.1 postbuild
> node scripts/copy-migrations.mjs

copy-migrations: copied 6 migration(s) E:\AMS\.worktrees\claude\p1-4-2-denial-reasons\src\db\migrations -> E:\AMS\.worktrees\claude\p1-4-2-denial-reasons\dist\db\migrations

No TypeScript diagnostics. Strict-mode + noUncheckedIndexedAccess + exactOptionalPropertyTypes clean.

§3.2. npm run lint — green

> colibri@0.0.1 lint
> eslint src

Zero ESLint warnings or errors. The exhaustive-switch sinks (const _exh: never = r;) match the prefix-_ underscore-ignore pattern in the project’s @typescript-eslint/no-unused-vars rule and pass without suppression.

§3.3. npm test — green

Final clean run (after coverage/ cleared between runs):

Test Suites: 43 passed, 43 total
Tests:       2168 passed, 2168 total
Snapshots:   0 total
Time:        27.322 s

Net delta: +60 new tests in denial-reasons.test.ts. All 2168 tests green; zero regressions in admission.test.ts (F13.1 updated to absorb the wider taxonomy via a default branch).

§3.3.1. Pre-existing flake observed once (server.test.ts)

The first full-suite run hit the documented startup — subprocess smoke flake (/[colibri] starting/ not seen on stderr under heavy load). The flake matches the memory note feedback_phase0_first_code_hazards.md:

Pre-existing startup — subprocess smoke flakiness under full-suite load — predates Wave H; all 4 R77 executors hit it once, always green on rerun.

Isolated re-run was green:

PASS src/__tests__/server.test.ts (14.408 s)
    √ main() IIFE smoke — script invocation boots and writes a startup log (1635 ms)
    √ main() IIFE smoke via spawnSync + tsx — startup log visible on stderr (1 ms)
Test Suites: 1 passed, 1 total

Subsequent full-suite runs (after coverage/ clear) were green — 2168/2168. No P1.4.2-introduced flake.

§4. Acceptance criteria — pass-by-pass

Tracking against the task prompt’s headline list and the contract’s §Public surface lock.

AC Source Result
Typed discriminated union with all required tags task-prompt §P1.4.2 ACs Pass — 8 discriminants in KIND_ALL (F1.6); the prompt’s required set is subsumed (no_rule_matched, budget:*, effect_invariant_violated, axiom_violation:AX-01..AX-07, policy:P1..P13, rule_version_mismatch, ambiguous_ruleset). The 8th (rule_rejected) preserves admission’s existing surface.
Each variant carries structured details payload task-prompt §P1.4.2 ACs Pass — every variant declares typed payload fields; no message: string field anywhere (verified by source inspection).
Reason codes stable across upgrades — additive-only task-prompt §P1.4.2 ACs Pass — discriminant constants frozen; AX-01..AX-07 + P1..P13 + 3 BudgetAxes locked at this round (contract §7 + module JSDoc S1..S5).
toString(reason) operator-readable rendering task-prompt §P1.4.2 ACs Pass — renderDenialReason ships with a deterministic per-variant template (F3.1..F3.11). The exhaustive-switch fallthrough (_exh: never) guards against silent variant additions.
JSON serialization preserves discriminant tag task-prompt §P1.4.2 ACs Pass — serializeDenialReason always emits kind (canonicalize sorts keys, kind in every variant); parseDenialReason re-validates. F2 round-trip (every variant) + F5.4 sorted-key invariant.
Backward-compatible with admission.test.ts dispatch packet Pass — admission.test.ts updated only at F13.1 (added explicit default branch + policy_id field on the construction smoke). All other admission tests unchanged. F4–F12 admission tests pass without edit.
Determinism scanner clean dispatch packet Pass — F7.1..F7.4 assert inspectFunctionForbidden returns [] for every exported function.
npm run build && npm run lint && npm test green CLAUDE.md §5 + dispatch packet Pass — all three gates green at HEAD d595d6d2.
Audit committed before contract before packet before implementation before verification CLAUDE.md §6 Pass — commits: aad59fb4 (audit) → 36ab9754 (contract) → 53ccea23 (packet) → d595d6d2 (implementation) → this commit (verification).

§5. Test matrix coverage

Group Test count Coverage
F1 — Module shape 10 Every export verified; frozen-array + class assertions.
F2 — Round-trip per variant 33 (one per fixture) + 1 extra Every variant + every axis / axiom / policy / sentinel. Bare no_rule_matched round-trips without transition_type field present in the parsed result.
F3 — Render templates 12 Byte-exact match against contract §3 templates for every variant + tied / duplicate-name ambiguous_ruleset + null-transition tied edge case.
F4 — Parser rejection 14 invalid_json, array root, missing kind, unknown kind, wrong-type fields, unknown axis / axiom / policy_id, NaN / Infinity / float, null / string root, missing required field.
F5 — Canonical JSON byte stability 5 Same shape twice → same bytes. Field order in literal does not affect output. Forward-compat extra-key drop. Sorted-key invariant byte-checked. ambiguous_ruleset null transition_type round-trips.
F6 — Exhaustive switch 1 Compile-time + runtime: every fixture flows through the switch.
F7 — Determinism corpus self-scan 4 inspectFunctionForbidden over render / serialize / parse / isDenialReason returns [].
F8 — Determinism repeat 3 10× serialize / 10× render / 10× isDenialReason for every fixture.
F9 — admission.ts compatibility 2 AdmissionDenialReason assignable to canonical DenialReason. The four admission emission shapes type-check as canonical literals.
F10 — Discriminant exhaustivity 8 Every AxiomId (7), PolicyDiscriminant (13), PolicySentinel (2), BudgetAxis (3) round-trips + per-id render template. + isDenialReason rejection corpus + alias assignability.

Total: 60+ assertions across 10 test groups. Every variant exercised through every public function.

§6. Determinism corpus self-scan

inspectFunctionForbidden (P1.1.2) confirms denial-reasons.ts is free of forbidden tokens (Math.*, Date.*, setTimeout, fetch, crypto.*, process.hrtime/nextTick, await, async, float literals, [native code]). The four exported functions return [] from the scanner (F7.1..F7.4 in denial-reasons.test.ts).

The internal isOneOf and isFiniteInteger helpers are not exported; their cleanliness is implicitly captured because the corpus self-scan walks the source of isDenialReason (which uses them) and finds zero forbidden tokens.

§7. Backward-compatibility evidence

§7.1. admission.ts emission

The four object-literal sites in evaluateAdmission:

  • kind: 'rule_version_mismatch' (admission.ts ~line 191) — unchanged shape; canonical type accepts.
  • kind: 'policy' (admission.ts ~line 252) — minor edit: now includes policy_id field derived via the new derivePolicyDiscriminant helper. Verified by build (compiles) + admission F4.1 admission test (still passes — assertion is on kind === 'rule_rejected').
  • kind: 'no_rule_matched' (admission.ts ~line 280) — unchanged; canonical type accepts.
  • kind: 'rule_rejected' (admission.ts ~line 297) — unchanged; canonical type accepts.

§7.2. admission.test.ts

Net change: F13.1 only. F1..F12 admission tests pass unchanged. F13.1’s update was unavoidable: TypeScript’s noImplicitReturns requires a default branch when the union widens, AND the runtime smoke calls had to add policy_id to the policy literal.

§7.3. Module-level admission re-export

src/domains/rules/admission.ts exports DenialReason via:

export type { DenialReason } from './denial-reasons.js';

Consumers that import DenialReason from ./admission.js get the canonical type. The four discriminants admission has emitted since P1.4.1 are still part of the union — no consumer needs to change its import.

§8. Open follow-ups (P1.4.3 / P1.4.4)

  • Wire RuleBudgetExceeded (engine.ts:461) directly through the typed BudgetReason shape instead of RuleRejectedReason with rule_reason: 'budget:*' string.
  • Wire AmbiguousRulesetError (registry.ts:205) through the typed AmbiguousRulesetReason shape at the loader / front-door catcher (today this throws at construct time; admission never sees it).
  • Substantive axiom predicate enforcement (validator §axiomCheck stubs at validator.ts:528–605) — when those checks land, they emit AxiomViolationReason.
  • Effect-shape invariant checks → emit EffectInvariantViolatedReason.

These are not in scope for P1.4.2; the taxonomy publishes the typed shapes today so the wiring tasks can land additively.

§9. Round seal — handoff to PM / Sigma

Step 5 / 5 complete. The 5-step chain shipped:

aad59fb4 audit(p1-4-2): inventory surface
36ab9754 contract(p1-4-2): behavioral contract
53ccea23 packet(p1-4-2): execution plan
d595d6d2 feat(p1-4-2): denial reason taxonomy
<this>   verify(p1-4-2): test evidence

Branch: feature/p1-4-2-denial-reasons Worktree: .worktrees/claude/p1-4-2-denial-reasons Base: 0b124c17 (R87 Wave 6 P1.4.1)

Ready for PR open + squash-merge per the dispatch packet’s PR creation block.


Back to top

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

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