Verification — P1.4.4 Tool-Lock Integration Spec

Round: R87 κ Wave 8 (FINAL — closes Phase 1 at 20/20) Branch: feature/p1-4-4-tool-lock-adapter Worktree: .worktrees/claude/p1-4-4-tool-lock-adapter Base SHA: 69bc4714 (origin/main, R87 Wave 7 close) β task ID: a9b63b19-6781-4f1d-9a76-a7f8eb66ed27 Step: 5 of 5 — test evidence

This doc records the test evidence that gates merge of feat(p1-4-4-tool-lock-adapter). All 10 acceptance criteria from the contract (§9) map onto passing tests below.


1. Gate run — npm run build && npm run lint && npm test

1.1. npm run build (TypeScript compile + migration copy)

> colibri@0.0.1 build
> tsc

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

copy-migrations: copied 6 migration(s) ... -> .../dist/db/migrations

Exit 0. No type errors. tool-lock-adapter.ts compiles clean against tsconfig.json strict + exactOptionalPropertyTypes.

1.2. npm run lint (eslint)

> colibri@0.0.1 lint
> eslint src

Exit 0. Zero errors, zero warnings.

1.3. npm test (jest with coverage)

Test Suites: 46 passed, 46 total
Tests:       2350 passed, 2350 total
Snapshots:   0 total
Time:        26.225 s
Ran all test suites.

Final: 2350/2350 passing across 46 suites. Pre-merge (R87 Wave 7 close at base) the count was 2305; this PR adds 45 new tests for a delta of +45.


2. New file evidence

2.1. src/domains/rules/tool-lock-adapter.ts

  • LOC: ~ 330 (package + types + class + factory)
  • Coverage: 100.00% lines, 94.11% branches, 100.00% functions
  • Determinism corpus self-scan: clean (rule-engine corpus self-scan in determinism.test.ts:833 walks every src/domains/rules/*.ts including this new file; no forbidden-token hits)
  • inspectFunctionForbidden(createToolLockAdapter) returns [] (test F9.1 verified)
  • inspectFunctionForbidden(stage) returns [] (test F9.2 verified — the returned closure body is also clean)

2.2. src/__tests__/domains/rules/tool-lock-adapter.test.ts

  • LOC: ~ 620
  • Total tests: 45
  • Test families: F1–F15 (per packet §4.3) + 2 end-to-end integration fixtures
  • Coverage: 100% of contract invariants exercised at least once

3. Acceptance criteria mapping

AC# Statement Tests Status
AC1 createToolLockAdapter factory exported and constructible F1.1, F1.3 PASS
AC2 MiddlewareStage signature compiles against α reality (forward-compat) F1.3 (compile-time + runtime arity) PASS
AC3 Admit path: next() invoked, result/throw propagated faithfully F2.1, F2.2, F12.1, F12.2 PASS
AC4 Deny path: next() NOT invoked F3.1, F3.3, E2E-2 PASS
AC5 Deny path: throws ToolAdmissionDeniedError carrying {reason: DenialReason} F3.2, F6.1–F6.6 PASS
AC6 Deny path: structured audit event emitted before throw F5.1, F5.2, F5.3 PASS
AC7 on_deny callback invoked exactly once on every deny; never on admit F4.1, F4.2, F4.3 PASS
AC8 Adapter is pure factory output — no global state, no auto-registration F14.1 (instance independence), §4 below (git diff evidence) PASS
AC9 Determinism scanner clean F9.1, F9.2 PASS
AC10 ZERO modifications to src/server.ts §4 below (git diff evidence) PASS

4. ZERO src/server.ts modifications — git diff evidence

$ git diff origin/main..HEAD -- src/server.ts
$ echo $?
0

Empty diff. The PR adds files only; it does not touch src/server.ts at any line.

The full git diff origin/main..HEAD --stat for this branch is:

docs/audits/p1-4-4-tool-lock-adapter-audit.md           | +295
docs/contracts/p1-4-4-tool-lock-adapter-contract.md     | +318
docs/packets/p1-4-4-tool-lock-adapter-packet.md         | +459
docs/verification/p1-4-4-tool-lock-adapter-verification.md | +<this file>
src/domains/rules/tool-lock-adapter.ts                  | +332
src/__tests__/domains/rules/tool-lock-adapter.test.ts   | +618
6 files changed, 0 deletions

Phase 1.5+ α wiring task (separate PR, separate round, separate β task) will add the src/server.ts registration line. This PR is library-only.


5. Contract invariants — verification status

ID Statement Test Status
I1 The adapter never modifies src/server.ts git diff empty VERIFIED
I2 Pure factory — no module-load side effects, no globals, no input mutation F14.1 (independent instances) + manual code review VERIFIED
I3 Admit invokes next() exactly once; both fulfilment + rejection propagate F2.1, F12.1, F12.2 VERIFIED
I4 Deny: next() NEVER invoked F3.1, F3.3 VERIFIED
I5 Deny: on_event (if present) called exactly once with frozen event F5.1 (event count + Object.isFrozen) VERIFIED
I6 Deny: on_deny (if present) called exactly once with result.reason F4.1, F4.2 VERIFIED
I7 Deny: emission order on_event → on_deny → reject. Listener exceptions DO NOT skip later steps F5.3 (ordering journal), F15.1, F15.2, F15.3 VERIFIED
I8 Admit: neither on_event nor on_deny is called F2.3, F4.3, F5.5 VERIFIED
I9 Rejected Promise carries ToolAdmissionDeniedError whose reason is referentially identical to result.reason F6.3 (toBe equality) VERIFIED
I10 at counter monotonic per-instance; starts at 1n; never decrements F10.1 VERIFIED
I11 No async / await keywords in adapter source corpus self-scan in determinism.test.ts (regex matches \basync\s+(?:function|\() and \bawait\b) VERIFIED
I12 No Math/Date/crypto/timers/network/process.time/float-literals in adapter source corpus self-scan VERIFIED
I13 inspectFunctionForbidden(createToolLockAdapter) and (stage) both [] F9.1, F9.2 VERIFIED
I14 evaluateAdmission invoked exactly once per stage call code review (single call site at line ~291) VERIFIED
I15 registry.computeVersionHash() invoked at most once per stage call code review (single call site, behind req.rule_version === undefined guard) VERIFIED
I16 If evaluateAdmission throws, adapter produces clean ToolAdmissionDeniedError deny path; never lets a non-ToolAdmissionDeniedError Error escape F8.1, F8.2 VERIFIED

All 16 invariants verified.


6. Test family detail

PASS  src/__tests__/domains/rules/tool-lock-adapter.test.ts (11.583 s)
  F1 — Module shape (tool-lock-adapter.ts public surface)
    ✓ F1.1 createToolLockAdapter is a function
    ✓ F1.2 ToolAdmissionDeniedError is a class extending Error
    ✓ F1.3 returned stage is a function with arity 2
  F2 — Admit path
    ✓ F2.1 admit invokes next() exactly once
    ✓ F2.2 admit returns the Promise from next() (resolved value propagates)
    ✓ F2.3 admit does NOT invoke on_deny or on_event
  F3 — Deny path: stages 2-5 short-circuited
    ✓ F3.1 deny does NOT invoke next()
    ✓ F3.2 deny rejects with ToolAdmissionDeniedError
    ✓ F3.3 no_rule_matched (false guard) also denies and skips next
  F4 — on_deny callback semantics
    ✓ F4.1 on_deny invoked exactly once on every deny
    ✓ F4.2 on_deny receives the typed DenialReason
    ✓ F4.3 on_deny is NOT invoked on admit
    ✓ F4.4 on_deny exception is swallowed; rejection still fires
  F5 — on_event callback semantics + ordering
    ✓ F5.1 on_event invoked exactly once with frozen event
    ✓ F5.2 on_event event has type, caller, tool, reason, at fields
    ✓ F5.3 on_event fires BEFORE on_deny (assert via shared journal)
    ✓ F5.4 on_event exception is swallowed
    ✓ F5.5 on_event NOT invoked on admit
  F6 — ToolAdmissionDeniedError shape
    ✓ F6.1 name === "ToolAdmissionDeniedError"
    ✓ F6.2 instanceof Error AND instanceof ToolAdmissionDeniedError
    ✓ F6.3 reason field referentially identical to evaluated result.reason
    ✓ F6.4 caller and tool populated from request
    ✓ F6.5 http_status === 403
    ✓ F6.6 message matches renderDenialReason(reason)
  F7 — rule_version_mismatch denial path
    ✓ F7.1 stale rule_version produces typed rule_version_mismatch reason
    ✓ F7.2 expected/actual fields populated correctly
  F8 — Defensive synthesized denial
    ✓ F8.1 mock registry whose computeVersionHash throws → rule_rejected synth denial
    ✓ F8.2 no exception escapes adapter boundary on synth-deny path
  F9 — Determinism scanner clean
    ✓ F9.1 inspectFunctionForbidden(createToolLockAdapter) returns []
    ✓ F9.2 inspectFunctionForbidden(stage) returns []
  F10 — at-counter monotonicity
    ✓ F10.1 three sequential denies see at = 1n, 2n, 3n
    ✓ F10.2 admits in between do NOT increment at-counter
  F11 — Default mode handling
    ✓ F11.1 req.mode undefined defaults to "normal"
    ✓ F11.2 options.default_mode = "admin" overrides default
    ✓ F11.3 req.mode = "readonly" wins over options.default_mode
  F12 — next() Promise propagation
    ✓ F12.1 next() rejects → adapter returns rejection with same error
    ✓ F12.2 next() resolves → adapter returns same resolution
  F13 — Promise return shape
    ✓ F13.1 admit branch returns a Promise (await-able)
    ✓ F13.2 deny branch returns a Promise (await-able rejection)
  F14 — Adapter-instance independence
    ✓ F14.1 two adapter instances have independent at counters
  F15 — Listener exception isolation
    ✓ F15.1 throwing on_event does NOT skip on_deny
    ✓ F15.2 throwing on_deny does NOT block rejection
    ✓ F15.3 both throwing → rejection still fires
  Integration — end-to-end wiring through a self-contained harness
    ✓ E2E-1 admit → next() chain executes with collected events
    ✓ E2E-2 deny → stages 2-5 never reached; on_event + on_deny both fire

45/45 passing.


7. Cross-suite stability

The full npm test run also passes:

  • All 18 κ rule-engine sibling test suites (lexer, parser, validator, canonical, integer-math, bps-constants, determinism, builtins, policy-gate, registry, state-access, versioning, parity-harness, admission, budget, denial-reasons, engine, migration)
  • All 19 prior Phase 0 + Phase 1 test suites (config, tasks, trail, proof, integrations, skills, etc.)
  • The rule-engine corpus self-scan test in determinism.test.ts:833 — which automatically picks up the new tool-lock-adapter.ts and validates it has zero forbidden tokens

No regression. No cross-suite leakage.


8. Coverage detail (jest –coverage)

File                    | % Stmts | % Branch | % Funcs | % Lines |
------------------------|---------|----------|---------|---------|
tool-lock-adapter.ts    |     100 |    94.11 |     100 |     100 |

The single uncovered branch is line 290 — the e instanceof Error ? e.message : String(e) ternary’s String(e) arm (the catch arm fires only when the thrown value is NOT an Error). All callers in current code throw Error instances, so this branch is defensive-only. Same idiom as engine.ts:455 and other κ modules.


9. Out-of-scope (future work)

These are intentionally NOT in this PR (per contract §8):

  • α wiring — registering the adapter into src/server.ts’s wrappedHandler chain. Phase 1.5+ task per ADR-005.
  • Effect-mutation propagation — admit branch currently drops effect_mutations. Phase 1.5+ commit-time integration consumes it.
  • ν audit-layer integration — wiring on_event → η Proof Store. Separate observability wiring task.
  • Pre-deny rate limiting / circuit breaking — Phase 1.5+ ξ observability.
  • Per-mode admin-bypass shortcuts — admission.ts §1 leaves admin handling to policy/rule layer.

10. Closing — green light for PR + writeback

All gates green. All 16 contract invariants verified. All 10 acceptance criteria met. ZERO modifications to src/server.ts. Determinism scanner clean. 100% line coverage on the new module.

Ready to push, PR, merge, writeback.

R87 κ Wave 8 — closes Phase 1 at 20/20.


Back to top

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

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