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:

  1. D1 — Wire RuleRegistry.computeVersionHash() to the real P1.5.1 implementation in src/domains/rules/versioning.ts.
  2. 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 RuleRegistry instances built from rule sources differing only in declaration order MUST return equal hashes. (P1.5.1 sorts by rule.name ASCII 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 location keys recursively.)
  • I3. Engine-version sensitivity. Bumping ENGINE_VERSION in versioning.ts MUST change the hash. (Concatenation separator is the literal '||'.)
  • I4. Determinism corpus self-scan compatibility. registry.ts MUST continue to pass the κ determinism scanner. The new import (computeVersionHash and ENGINE_VERSION from ./versioning.js) introduces no forbidden tokens — versioning.ts itself passes the scan, and a NAMED import from 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 on versioning.ts’s default-engine_version overload (already exists at line 359 — engine_version: string = ENGINE_VERSION).
  • Explicit pass: import ENGINE_VERSION and 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 computeVersionHash from ./versioning.ts as 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 AND result.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 test all 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 ActivationToken interface, with version_hash, target_epoch, issued_at_epoch, parity_pass, scope_signature, issued_old_version AND 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 literal task_id="P1.4.4".
  • AC8. §P1.5.3 prompt body (lines ~2564–2573) shows the same modern signature, no session_id, no literal task_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.md returns 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.ts docstring references — semantically still accurate.
  • engine.ts IRuleRegistry 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 main or force-pushing the feature branch.
  • Skipping npm run lint (CLAUDE.md §5 — three gates, not two).
  • Marking the β task DONE before the writeback thought_record lands (enforceWriteback hard-block at src/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-verify or --amend.

Back to top

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

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