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:833walks everysrc/domains/rules/*.tsincluding 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-scantest indeterminism.test.ts:833— which automatically picks up the newtool-lock-adapter.tsand 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’swrappedHandlerchain. 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 — wiringon_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.