Audit — R88.A κ hygiene sweep

Round: R88 Phase A hygiene β task ID: 055feba2-afce-4e5e-96a7-b49656bac933 Branch: feature/r88-a-kappa-hygiene-sweep Worktree: .worktrees/claude/r88-a-kappa-hygiene-sweep Base: origin/main @ f327936b Effort: S (1–2 hours)

Charter

Two scoped hygiene fixes against the src/domains/rules/ (κ) surface and its prompt-file artefact:

  1. Wire RuleRegistry.computeVersionHash() to the real P1.5.1 implementation in src/domains/rules/versioning.ts. The registry has shipped a Phase 1 stub since R86 / PR #212 even though versioning.ts (R86 / PR #213, P1.5.1) exposes the canonical SHA-256 implementation. admission.ts calls registry.computeVersionHash() to compute its rule_version field — the stub has been silently undermining rule_version_mismatch detection.
  2. Sweep three classes of stale templates in docs/guides/implementation/task-prompts/p1.1-kappa-rule-engine.md so future R88+ dispatches do not re-bake donor-era and pre-shipped wording into prompts:
    • 2a. §P1.5.3 ActivationToken interface — 3-field stale shape vs. the actual 6-field canonical shape from migration.ts.
    • 2b. §P1.4.4 + §P1.5.3 writeback templates (and their YAML appendices) — donor-era session_id="r81-kappa-phase-1" parameter, literal task_id strings ("P1.4.4", "P1.5.3"), progress=100, no agent_id. None of these match the Phase 0 14-tool MCP signature.
    • 2c. §P1.4.2 / §P1.4.3 / §P1.4.4 / §P1.5.3 (and other §P1.* sections) test paths — all use the nonexistent src/domains/rules/__tests__/X.test.ts layout. The actual project layout is src/__tests__/domains/rules/X.test.ts.

Surface inventory — Item #1 (registry wiring)

Stub site

src/domains/rules/registry.ts:497-512 — the computeVersionHash() method on class RuleRegistry:

/**
 * Phase 1 stub — returns a content-free identifier derived from rule
 * count. The format is `'sha256:stub:' + size + 'n'` (the `n` suffix
 * mimics a bigint literal so future tooling that parses this prefix can
 * tell stubs apart from real hashes by shape).
 *
 * TODO(P1.5.1): replace with a real SHA-256 over canonicalize(ruleset) ||
 * engine_version. The P1.5.1 sibling slice (parallel this wave) ships
 * `wireVersionHash(registry: RuleRegistry): RuleRegistry` which patches
 * `computeVersionHash` to call the real implementation. This stub MUST be
 * kept stable as a fallback so consumers in Phase 1.0 see a deterministic
 * value.
 */
computeVersionHash(): string {
  return 'sha256:stub:' + this.size + 'n';
}

The class-level docstring at lines 49–52 also references this stub.

Real implementation (already shipped)

src/domains/rules/versioning.ts:

  • ENGINE_VERSION constant (line 99): 'kappa-engine/1-0-0'
  • VERSION_HASH_PREFIX (line 106): 'sha256:'
  • VERSION_HASH_HEX_LENGTH (line 109): 64
  • VERSION_HASH_TOTAL_LENGTH (line 112): 71
  • computeVersionHash(ruleset: readonly RuleNode[], engine_version: string = ENGINE_VERSION): string (line 357) — returns 'sha256:' + hex(sha256(canonicalize(stripLocations(ruleset).sort(byName)) || engine_version))
  • Default-engine_version overload IS present (parameter has default value).

Internal accessors on RuleRegistry

The registry stores its rules in three private fields (lines 340–342):

  • private readonly allRules: readonly CategorizedRule[] — the canonical ordered list returned by getAll().
  • private readonly byName: Map<string, RuleNode> — name index.
  • private readonly byType: Map<TransitionType, readonly RuleNode[]> — type index.

computeVersionHash() needs readonly RuleNode[] (parser AST shape). The registry’s allRules is readonly CategorizedRule[] where CategorizedRule = { rule: RuleNode, category: Category } (engine.ts). Wiring projects via this.allRules.map(c => c.rule).

getAll() (line 471) returns the same frozen allRules array. The internal field is the right source — projecting to RuleNode[] is a single .map.

Downstream consumers

registry.computeVersionHash() is called in:

  • src/domains/rules/admission.ts:195const v_actual = registry.computeVersionHash();
  • src/domains/rules/tool-lock-adapter.ts:285 — middleware adapter’s request shaping.
  • src/__tests__/domains/rules/admission.test.ts — F3.1, F3.2, F3.4 expect-paths (lines 328, 343, 372, 375), F2 admit-paths (lines 78, 277, 292, 307), F11+ paths (588, 596, 602, 611).
  • src/__tests__/domains/rules/tool-lock-adapter.test.ts — F8.0, F8.1 monkey-patching tests (line 472, 502, 503, 508, 538).
  • src/__tests__/domains/rules/registry.test.ts — F10 block (lines 575–613) asserts the stub format directly.

Tests asserting stub-format strings (will need updating)

Stub-format hardcoded strings are at:

  • src/__tests__/domains/rules/registry.test.ts:578 ('sha256:stub:0n')
  • src/__tests__/domains/rules/registry.test.ts:589 ('sha256:stub:3n')
  • src/__tests__/domains/rules/registry.test.ts:612 (.startsWith('sha256:stub:'))
  • src/__tests__/domains/rules/admission.test.ts:338 ('sha256:stub:99n' — used as a deliberately-mismatching rule_version injected into a request)
  • src/__tests__/domains/rules/admission.test.ts:344 (asserts the same string was echoed back as actual — still valid after the wire because the request’s rule_version is what’s echoed, not the registry’s).

Of those, the F10 block (registry.test.ts:575–613) is the only place that asserts on the registry’s produced hash. F10.1, F10.2, F10.5 directly encode the stub format — these need rewriting against the real format. F10.3 (same source twice → equal hashes) and F10.4 (adding a rule → hash changes) are format-agnostic and remain valid.

In admission.test.ts, F3.2 uses 'sha256:stub:99n' as an injected request value to test mismatch detection. After wiring, registry.computeVersionHash() will return a real hash that does not equal 'sha256:stub:99n', so the mismatch is preserved — F3.2 stays green without modification. The expected: registry.computeVersionHash() assertion (line 343) compares against whatever the registry produces — also unchanged in semantics.

Surface inventory — Item #2 (prompt-file sweep)

2a. §P1.5.3 ActivationToken stale shape

docs/guides/implementation/task-prompts/p1.1-kappa-rule-engine.md:2529:

* interface ActivationToken { new_version: string, target_epoch: bigint, proposal_id: string }

Three fields: new_version, target_epoch, proposal_id.

The actual canonical shape in src/domains/rules/migration.ts:136-143:

export interface ActivationToken {
  readonly version_hash: string;
  readonly target_epoch: bigint;
  readonly issued_at_epoch: bigint;
  readonly parity_pass: true;
  readonly scope_signature: string;
  readonly issued_old_version: string;
}

Six fields, all readonly. proposal_id does not exist in the shipped shape. new_version was renamed to version_hash. The four added fields (issued_at_epoch, parity_pass, scope_signature, issued_old_version) are all migration-time data that the proposer cannot supply directly, which is why the dispatch packet calls out “this token can only be constructed inside migrateRuleset()”.

2b. §P1.4.4 + §P1.5.3 writeback templates

Two zones per section: the “Ready-to-paste agent prompt” code block and the “Writeback template” YAML appendix.

§P1.4.4 — line 2154–2163 (prompt body) + lines 2185–2196 (YAML appendix). §P1.5.3 — line 2564–2573 (prompt body) + lines 2595–2606 (YAML appendix).

Stale wording in both:

  • task_update(id="P1.4.4", ...) / task_update(id="P1.5.3", ...) — uses a literal task name, not a UUID. β task_update actually expects a UUID id.
  • progress=100 — Phase 0 task_update payload accepts patch: { status }, not a progress numeric field.
  • thought_record(session_id="r81-kappa-phase-1", thought_type="reflection", ...) — the Phase 0 thought_record tool does not accept session_id (the session is a server-side construct); the discriminator parameter is type, not thought_type; agent_id is required and missing here.
  • The YAML appendix carries the same drift in YAML form.

The dispatch packet’s reference template (modern MCP signature):

WRITEBACK (mandatory, ordering enforced by writeback.ts:97):
1. mcp__colibri__thought_record FIRST:
   type:     "reflection"
   task_id:  "<UUID assigned by PM at dispatch time, NOT a literal task name>"
   agent_id: "claude-code-t3-<task-slug>"
   content:  <multi-line writeback>

2. mcp__colibri__task_update AFTER:
   id:    "<UUID>"
   patch: { status: "DONE" }

The reflection MUST land before the DONE transition or task_update returns ERR_WRITEBACK_REQUIRED.

YAML appendix update — drop session_id and the literal task_id: P1.4.4 strings, use modern field names.

2c. §P1.* — src/domains/rules/__tests__/ references

grep -n "__tests__/" docs/guides/implementation/task-prompts/p1.1-kappa-rule-engine.md shows 44 references to the nonexistent layout src/domains/rules/__tests__/X.test.ts. These cluster into 22 sections, each with two references (a “Files to create” line and a “Pre-flight reading” line):

Section “Files to create” “Pre-flight reading”
P1.1.1 integer-math line 74 line 126
P1.1.2 determinism line 210 line 246
P1.1.3 bps-constants line 332 line 384
P1.2.1 lexer line 464 line 527
P1.2.2 parser line 611 line 681
P1.2.3 validator line 767 line 825
P1.2.4 registry line 911 line 971
P1.3.1 engine line 1056 line 1126
P1.3.2 builtins line 1216 line 1282
P1.3.3 state-access line 1366 line 1434
P1.3.4 policy-gate line 1518 line 1576
P1.4.1 admission line 1658 line 1720
P1.4.2 denial-reasons line 1807 line 1859
P1.4.3 budget line 1938 line 1996
P1.4.4 tool-lock line 2079 line 2136
P1.5.1 versioning line 2219 line 2267
P1.5.2 migration line 2349 line 2408
P1.5.3 activation line 2490 line 2547
P1.5.4 canonical line 2630 line 2677
P1.5.5 parity-harness line 2762 line 2829

Actual on-disk layout (verified via find from worktree root) — every test lives at src/__tests__/domains/rules/X.test.ts. Twenty test files, zero in src/domains/rules/__tests__/.

Dispatch packet calls out §P1.4.2/3/4 + §P1.5.3 specifically but says “search the file for __tests__/” — the natural interpretation is “fix every matching reference, not just the four sections”, since otherwise the next dispatch from §P1.* will re-bake the wrong path. Item #2c is therefore a single-pattern global replacement.

Out of scope (deliberate non-changes)

  • Other §P1.* writeback templates beyond §P1.4.4 + §P1.5.3 — the dispatch packet scopes to those two sections (and their YAML appendices). The remaining 18 sections have the same drift but stay unchanged this round to keep the diff bounded. R89+ may sweep them.
  • The §P1.* “Pre-flight reading” lists pointing at task-breakdown.md §P1.X.Y — out of scope for hygiene; correctness untouched.
  • tool-lock-adapter.ts line 264 / 271 / 285 docstring references to registry.computeVersionHash() — semantically still accurate; only the shape of the return value changes, and that’s covered by tests.
  • The R86 §P1.5.1 “stub fallback” wording in registry.ts:506-508 — once the wiring lands, the comment becomes obsolete; the implementation patch removes the entire stub-style block including this prose.
  • F3.2 in admission.test.ts — its 'sha256:stub:99n' injection is a request-side fixture (a deliberately bogus rule_version supplied by the caller). Mismatch behaviour is preserved against any non-equal value, so no change is needed.
  • F8.0/F8.1 monkey-patching tests in tool-lock-adapter.test.ts — these replace computeVersionHash on a registry instance via Object.defineProperty. Their tests are about what happens when the hash function throws, not what value it returns. Untouched.

Acceptance criteria (lifted from dispatch packet, audit-confirmed)

  • registry.computeVersionHash() returns 'sha256:<64hex>' (71 chars), not the stub format
  • Same rules in different declaration order produce identical hash (order independence)
  • All existing tests pass (admission.test.ts adjustments only if needed; registry.test.ts F10 rewritten)
  • §P1.5.3 of p1.1-kappa-rule-engine.md shows the 6-field ActivationToken
  • §P1.4.4 + §P1.5.3 writeback templates use modern MCP signature (no session_id, no literal task_id="P1.X.Y" strings)
  • All src/domains/rules/__tests__/ prompt-file references replaced with src/__tests__/domains/rules/ (grep -n returns zero matches)
  • Determinism scanner clean (registry.computeVersionHash now imports from versioning.ts which is corpus-clean)
  • No new lint warnings; build clean

Risks

  • Test breakage cascade. F10 in registry.test.ts hard-codes the stub format; rewriting is mandatory. F3.2 in admission.test.ts uses the stub as a request-side string, which still behaves correctly post-wire (any non-matching string still mismatches). Other admission tests use expected: registry.computeVersionHash() (semantic equality), which is format-agnostic and stays green.
  • Determinism corpus self-scan. registry.ts importing from versioning.ts is safe — versioning.ts already passes the corpus scan (its createHash is a NAMED import, not crypto.createHash literal). No new forbidden tokens introduced.
  • No code path changes outside registry.ts + 2 test files. The wiring changes the value returned by computeVersionHash, not the call sites.
  • Prompt file is doc-only. Item #2 is editorial; no code under test.

Plan summary (for the contract step)

  1. Audit (this file) — inventory all stale spots and impact sites.
  2. Contract — declare invariants for both deliverables.
  3. Packet — sequence the edits (registry → registry tests → admission tests only if needed → prompt file sweep).
  4. Implement — single feat commit covering all four file edits.
  5. Verify — run the full gate, update verification doc with evidence.

Back to top

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

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