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:
- Wire
RuleRegistry.computeVersionHash()to the real P1.5.1 implementation insrc/domains/rules/versioning.ts. The registry has shipped a Phase 1 stub since R86 / PR #212 even thoughversioning.ts(R86 / PR #213, P1.5.1) exposes the canonical SHA-256 implementation.admission.tscallsregistry.computeVersionHash()to compute itsrule_versionfield — the stub has been silently underminingrule_version_mismatchdetection. - Sweep three classes of stale templates in
docs/guides/implementation/task-prompts/p1.1-kappa-rule-engine.mdso future R88+ dispatches do not re-bake donor-era and pre-shipped wording into prompts:- 2a. §P1.5.3
ActivationTokeninterface — 3-field stale shape vs. the actual 6-field canonical shape frommigration.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, noagent_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.tslayout. The actual project layout issrc/__tests__/domains/rules/X.test.ts.
- 2a. §P1.5.3
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_VERSIONconstant (line 99):'kappa-engine/1-0-0'VERSION_HASH_PREFIX(line 106):'sha256:'VERSION_HASH_HEX_LENGTH(line 109):64VERSION_HASH_TOTAL_LENGTH(line 112):71computeVersionHash(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 bygetAll().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:195—const 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-mismatchingrule_versioninjected into a request)src/__tests__/domains/rules/admission.test.ts:344(asserts the same string was echoed back asactual— still valid after the wire because the request’srule_versionis 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_updateactually expects a UUIDid.progress=100— Phase 0task_updatepayload acceptspatch: { status }, not aprogressnumeric field.thought_record(session_id="r81-kappa-phase-1", thought_type="reflection", ...)— the Phase 0thought_recordtool does not acceptsession_id(the session is a server-side construct); the discriminator parameter istype, notthought_type;agent_idis 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.tsline 264 / 271 / 285 docstring references toregistry.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
computeVersionHashon a registry instance viaObject.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.mdshows the 6-field ActivationToken - §P1.4.4 + §P1.5.3 writeback templates use modern MCP signature (no
session_id, no literaltask_id="P1.X.Y"strings) - All
src/domains/rules/__tests__/prompt-file references replaced withsrc/__tests__/domains/rules/(grep -nreturns 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
createHashis a NAMED import, notcrypto.createHashliteral). 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)
- Audit (this file) — inventory all stale spots and impact sites.
- Contract — declare invariants for both deliverables.
- Packet — sequence the edits (registry → registry tests → admission tests only if needed → prompt file sweep).
- Implement — single feat commit covering all four file edits.
- Verify — run the full gate, update verification doc with evidence.