Contract — R88.A κ hygiene sweep
Audit: docs/audits/r88-a-kappa-hygiene-sweep-audit.md
Round: R88 Phase A hygiene
β task ID: 055feba2-afce-4e5e-96a7-b49656bac933
Branch: feature/r88-a-kappa-hygiene-sweep
§1. Scope
Two deliverables under one PR:
- D1 — Wire
RuleRegistry.computeVersionHash()to the real P1.5.1 implementation insrc/domains/rules/versioning.ts. - D2 — Stale-template sweep in
docs/guides/implementation/task-prompts/p1.1-kappa-rule-engine.md, covering three classes of drift identified in the audit:- 2a. §P1.5.3 ActivationToken interface shape (3 → 6 fields)
- 2b. §P1.4.4 + §P1.5.3 writeback templates (modern MCP signature)
- 2c. All §P1.* test path references (
src/domains/rules/__tests__/→src/__tests__/domains/rules/)
The packet (Step 3) sequences the edits; this contract enumerates the invariants and acceptance criteria that any implementation must satisfy.
§2. D1 — Registry wiring contract
§2.1 Public surface
The wiring MUST NOT change the public signature of
RuleRegistry.computeVersionHash:
class RuleRegistry implements IRuleRegistry {
// unchanged
computeVersionHash(): string;
}
Zero-argument, returns string. The IRuleRegistry interface (in engine.ts)
declares this method; downstream consumers (admission.ts:195,
tool-lock-adapter.ts:285, all admission and tool-lock tests) call it
without arguments.
§2.2 Output shape
After wiring, computeVersionHash() MUST satisfy:
| Property | Value |
|---|---|
| Return type | string |
| Total length | exactly 71 characters |
| Prefix | exactly 'sha256:' (7 chars) |
| Tail | exactly 64 lowercase hex characters |
| Format regex | ^sha256:[0-9a-f]{64}$ |
| Stub-format check | result.includes('stub') MUST be false |
This matches src/domains/rules/versioning.ts constants
VERSION_HASH_PREFIX, VERSION_HASH_HEX_LENGTH,
VERSION_HASH_TOTAL_LENGTH.
§2.3 Determinism invariants (preserved verbatim from P1.5.1)
The wiring inherits — and MUST NOT weaken — these P1.5.1 guarantees:
- I1. Order independence. Two
RuleRegistryinstances built from rule sources differing only in declaration order MUST return equal hashes. (P1.5.1 sorts byrule.nameASCII codepoint; the registry’s own specificity-desc reordering is upstream of this and does not affect the hash.) - I2. Location independence. Two registries built from the same
rule shapes at different file positions MUST return equal hashes.
(P1.5.1 strips
locationkeys recursively.) - I3. Engine-version sensitivity. Bumping
ENGINE_VERSIONinversioning.tsMUST change the hash. (Concatenation separator is the literal'||'.) - I4. Determinism corpus self-scan compatibility.
registry.tsMUST continue to pass the κ determinism scanner. The new import (computeVersionHashandENGINE_VERSIONfrom./versioning.js) introduces no forbidden tokens —versioning.tsitself passes the scan, and a NAMEDimportfrom a corpus-clean module is safe.
§2.4 Engine-version source
The wired implementation MUST pass the canonical ENGINE_VERSION
constant ('kappa-engine/1-0-0' from versioning.ts). The packet may
choose between:
- Implicit default: call
computeVersionHash(ruleNodes)and rely onversioning.ts’s default-engine_version overload (already exists at line 359 —engine_version: string = ENGINE_VERSION). - Explicit pass: import
ENGINE_VERSIONand pass it positionally.
Both are acceptable. The packet picks the implicit default for clarity of intent — the registry has no notion of multiple engine versions in Phase 1, and re-routing through the default keeps the engine_version authority single-sited in versioning.ts.
§2.5 Rules-array projection
The registry stores readonly CategorizedRule[] (engine.ts shape:
{ rule: RuleNode, category: Category }). versioning.computeVersionHash
takes readonly RuleNode[]. The wiring MUST project via:
this.allRules.map((c) => c.rule)
— a single allocation per call. The .rule projection is exactly what
P1.5.1 already canonicalizes; no semantic shift.
§2.6 Comment and docstring contract
The class-level docstring at registry.ts:49-52 (which references the
stub) and the method-level docstring at registry.ts:497-509 (which
contains the TODO(P1.5.1) marker and the “stub” prose) MUST be
rewritten to:
- Remove the
TODO(P1.5.1)marker. - Remove the word “stub” from descriptive copy.
- Reference
computeVersionHashfrom./versioning.tsas the implementation source. - Preserve the determinism / order-independence / location-independence documentation.
§2.7 Test contract — registry.test.ts F10 block
The F10 block (src/__tests__/domains/rules/registry.test.ts:572-613)
MUST be rewritten to assert the real-format invariants:
- F10.1 — Format shape. Empty-registry hash matches
^sha256:[0-9a-f]{64}$(71 chars total). - F10.2 — Sentinel.
result.startsWith('sha256:')is true ANDresult.includes('stub')is false (catches future regression to a stub). - F10.3 — Same source determinism. Same source loaded twice → equal hashes. (Preserved from old F10.3.)
- F10.4 — Adding a rule changes the hash. (Preserved from old F10.4 — semantically still true under the new format.)
- F10.5 — Order independence. Two registries built from rule declarations in different order produce equal hashes (NEW assertion; P1.5.1 already guarantees it via the sort-by-name pipeline; this test re-asserts the wiring delivers it via the registry surface).
The dispatch packet calls for “3 assertions per item #1”: (a) format 71-char regex, (b) order independence, (c) sentinel never returns stub. This contract realizes those three as F10.1, F10.5, F10.2 respectively; F10.3 and F10.4 stay valid (and free) as carry-overs from the old block.
§2.8 Test contract — admission.test.ts impact
The audit confirms F3.2 (the only place using 'sha256:stub:99n' in an
admission test) still passes after the wire because the string is
injected as a request-side rule_version, and any non-equal value still
triggers rule_version_mismatch. No edits to admission.test.ts are
required.
If a non-obvious admission test breaks during the gate run, the contract is: pause and report — the right answer might be a multi-line fixture rewrite, not a quick patch. (Per dispatch §IF BLOCKED.)
§3. D2 — Prompt-file sweep contract
§3.1 Item 2a — ActivationToken (§P1.5.3)
The replacement at line 2529 MUST shape-match the canonical
ActivationToken interface in src/domains/rules/migration.ts:136-143:
interface ActivationToken {
readonly version_hash: string; // 'sha256:<64hex>' — recomputed new
readonly target_epoch: bigint; // when this token activates
readonly issued_at_epoch: bigint; // current_epoch from migrateRuleset call
readonly parity_pass: true; // literal-typed sentinel (only emitted on parity pass)
readonly scope_signature: string; // 'sha256:<64hex>' over canonical(normalized_scope)
readonly issued_old_version: string; // 'sha256:<64hex>' — recomputed old, for chain audit
}
A trailing sentence MUST note that this token can only be constructed
inside migrateRuleset() because scope_signature, issued_at_epoch,
parity_pass, and issued_old_version require migration-time data;
consumers (P1.5.3 activation, future π governance) accept already-
constructed tokens.
§3.2 Item 2b — §P1.4.4 + §P1.5.3 writeback templates
Two prompt-body code blocks (lines ~2154–2163 and ~2564–2573) and two YAML appendices (lines ~2185–2196 and ~2595–2606) MUST be replaced with the modern Phase 0 14-tool MCP signature:
Prompt-body code block (§P1.4.4 + §P1.5.3 each):
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 (§P1.4.4 + §P1.5.3 each):
thought_record:
type: reflection
task_id: <UUID assigned by PM at dispatch time>
agent_id: claude-code-t3-<task-slug>
content: <multi-line writeback — branch, commit_sha, tests_run, summary, blockers>
task_update:
id: <UUID>
patch: { status: DONE }
The order (thought_record FIRST, task_update AFTER) is mandatory because
enforceWriteback in src/domains/tasks/writeback.ts:97 returns
ERR_WRITEBACK_REQUIRED if no thought_record exists for the task_id at
the time of task_update(status="DONE").
The YAML drops session_id (donor-era; the Phase 0 thought_record
tool does not accept this parameter) and the literal task_id: P1.4.4 /
task_id: P1.5.3 strings (replaced with a UUID placeholder note). The
progress: 100 field is removed (Phase 0 task_update accepts patch,
not a progress numeric).
§3.3 Item 2c — src/__tests__/ path correction
Every reference to src/domains/rules/__tests__/X.test.ts in the
prompt file MUST be replaced with src/__tests__/domains/rules/X.test.ts.
The audit identified 44 such references across 22 sections. The
replacement is a single-pattern Edit replace_all over the file,
since the prefix is unique:
- Find:
src/domains/rules/__tests__/ - Replace:
src/__tests__/domains/rules/
Acceptance criterion: post-edit grep -n "src/domains/rules/__tests__/" ...
returns zero matches in the prompt file.
This is a global sweep in spirit even though the dispatch packet calls
out only §P1.4.2/3/4 + §P1.5.3 explicitly — the dispatch language
“search the file for __tests__/” plus “R86/R87 sibling slices all
landed under src/__tests__/domains/rules/” makes the global semantics
clear; otherwise the next §P1.* dispatch re-bakes the wrong path.
§4. Acceptance criteria (test-checkable)
- AC1.
registry.computeVersionHash()returns a string of length 71 matching^sha256:[0-9a-f]{64}$(verified by F10.1 in registry.test.ts). - AC2.
registry.computeVersionHash()never returns a string containing'stub'(verified by F10.2). - AC3. Two registries built from rules in different declaration order produce equal hashes (verified by F10.5).
- AC4.
npm run build && npm run lint && npm testall green from the worktree root (gate per CLAUDE.md §5). - AC5. Total test count strictly greater than the pre-change baseline (we add F10.5 minimum; AC1/AC2 may either replace existing F10 tests or stand alongside).
- AC6. §P1.5.3 line ~2529 shows the 6-field
ActivationTokeninterface, withversion_hash,target_epoch,issued_at_epoch,parity_pass,scope_signature,issued_old_versionAND a sentence about migrateRuleset-only construction. - AC7. §P1.4.4 prompt body (lines ~2154–2163) shows the modern
thought_record-first + task_update-after MCP signature, no
session_id, no literaltask_id="P1.4.4". - AC8. §P1.5.3 prompt body (lines ~2564–2573) shows the same
modern signature, no
session_id, no literaltask_id="P1.5.3". - AC9. §P1.4.4 + §P1.5.3 YAML “Writeback template” appendices match §3.2 of this contract.
- AC10.
grep -n "src/domains/rules/__tests__/" docs/guides/implementation/task-prompts/p1.1-kappa-rule-engine.mdreturns zero matches.
§5. Out-of-scope reaffirmation
- Other §P1.* writeback templates beyond §P1.4.4 + §P1.5.3 (18 sections with the same drift) — left for R89+.
- §P1.* “Pre-flight reading” lists — not in scope; correctness unaffected.
tool-lock-adapter.tsdocstring references — semantically still accurate.engine.tsIRuleRegistry interface declaration — unchanged signature.
§6. Forbidden moves
- Skipping any of the 5 chain steps (audit done; contract this; packet next; implement; verify).
- Editing the main checkout (
E:\AMS) directly. - Pushing to
mainor force-pushing the feature branch. - Skipping
npm run lint(CLAUDE.md §5 — three gates, not two). - Marking the β task DONE before the writeback
thought_recordlands (enforceWriteback hard-block atsrc/domains/tasks/writeback.ts:97). - Touching any other κ surface beyond
registry.ts,registry.test.ts, and the prompt file. (admission.test.ts only if a fixture explicitly asserts the registry’s produced hash equals a stub-formatted literal; the audit found no such fixture, so the expectation is zero edits there.) - Inventing tools or APIs not present in the Phase 0 14-tool surface.
--no-verifyor--amend.