Packet — R88.A κ hygiene sweep
Audit: docs/audits/r88-a-kappa-hygiene-sweep-audit.md
Contract: docs/contracts/r88-a-kappa-hygiene-sweep-contract.md
Round: R88 Phase A hygiene
β task ID: 055feba2-afce-4e5e-96a7-b49656bac933
Branch: feature/r88-a-kappa-hygiene-sweep
§1. Edit sequence
Single feat commit covering all four file edits. Order within the commit:
src/domains/rules/registry.ts— wirecomputeVersionHashtoversioning.computeVersionHash. Update class-level docstring (lines 49–52) and method-level docstring (lines 497–509).src/__tests__/domains/rules/registry.test.ts— rewrite F10 block (lines 572–613).docs/guides/implementation/task-prompts/p1.1-kappa-rule-engine.md— sweep all three classes of stale templates.
src/__tests__/domains/rules/admission.test.ts is NOT touched — the
audit confirms F3.2 still passes after the wire (its 'sha256:stub:99n'
is a request-side injection, not a comparison against the registry’s
produced hash). If the gate fails on admission.test.ts in unexpected
ways, pause and report per dispatch §IF BLOCKED.
§2. registry.ts edits
§2.1 Imports (top of file, after existing imports)
Add the new import next to the existing parser.js and validator.js
imports (lines 65–73):
import { parse } from './parser.js';
import type { Expression, ParseError, RuleNode } from './parser.js';
import { validate } from './validator.js';
import type { ValidationError } from './validator.js';
import { computeVersionHash } from './versioning.js';
import type {
Category,
CategorizedRule,
RuleRegistry as IRuleRegistry,
} from './engine.js';
The new line: import { computeVersionHash } from './versioning.js';
A NAMED import only — versioning.ts exposes computeVersionHash as a
named export. The default-engine_version overload is the second
parameter (engine_version: string = ENGINE_VERSION); the registry
calls without a second argument and inherits the default. No
ENGINE_VERSION import needed.
§2.2 Class-level docstring (lines 49–52)
Replace:
* `computeVersionHash` is a Phase 1 stub returning a content-free identifier
* derived from rule count. P1.5.1 (parallel sibling slice this wave) will
* patch the body via `wireVersionHash(registry)` to produce a real SHA-256
* over the canonicalized ruleset.
With:
* `computeVersionHash` produces the canonical SHA-256 fingerprint over the
* registry's ruleset, mixed with `ENGINE_VERSION`. Implementation lives in
* `./versioning.ts` (P1.5.1); the registry projects `allRules.map(c => c.rule)`
* and forwards. Output shape: `'sha256:<64hex>'` (71 chars). Determinism,
* order independence, and location independence are inherited from P1.5.1.
§2.3 Method body and docstring (lines 497–512)
Replace the whole block (docstring + method):
/**
* 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';
}
With:
/**
* Canonical SHA-256 fingerprint over the registry's ruleset, mixed with
* the κ engine version. Implementation: `./versioning.ts §7
* computeVersionHash`. Output shape: `'sha256:<64hex>'` (71 chars).
*
* Inherits from P1.5.1:
* - Order independence — the hash sorts rules by `rule.name` before
* canonicalization, so two registries built from sources differing
* only in declaration order produce equal hashes.
* - Location independence — `location` keys are recursively stripped
* before canonicalization.
* - Engine-version sensitivity — bumping `ENGINE_VERSION` in
* `versioning.ts` changes the hash. The default-engine_version
* overload is consumed here (no override).
*
* Determinism corpus self-scan: this method imports `computeVersionHash`
* as a NAMED import from a corpus-clean module; no forbidden tokens
* are introduced.
*/
computeVersionHash(): string {
return computeVersionHash(this.allRules.map((c) => c.rule));
}
§3. registry.test.ts edits — F10 block
Replace lines 571–614 (the F10 block including its /* */ banner) with:
/* ========================================================================== */
/* F10 — computeVersionHash (P1.5.1 wired) */
/* ========================================================================== */
describe('F10 — computeVersionHash (P1.5.1 wired)', () => {
test('F10.1 output shape is sha256:<64hex> (71 chars)', () => {
const reg = RuleRegistry.loadRuleset('');
const out = reg.computeVersionHash();
expect(out).toMatch(/^sha256:[0-9a-f]{64}$/);
expect(out.length).toBe(71);
});
test('F10.2 sentinel — never returns the stub format', () => {
const reg = RuleRegistry.loadRuleset(
concat(
singleRule('COMMITMENT_ACCEPT_A', '$event.a == 1'),
singleRule('Yield', 'else'),
),
);
const out = reg.computeVersionHash();
expect(out.startsWith('sha256:')).toBe(true);
expect(out.includes('stub')).toBe(false);
});
test('F10.3 same source loaded twice → equal hashes', () => {
const src = singleRule('R', '$event.a == 1');
const r1 = RuleRegistry.loadRuleset(src);
const r2 = RuleRegistry.loadRuleset(src);
expect(r1.computeVersionHash()).toBe(r2.computeVersionHash());
});
test('F10.4 adding a rule changes the hash', () => {
const r1 = RuleRegistry.loadRuleset(singleRule('R', '$event.a == 1'));
const r2 = RuleRegistry.loadRuleset(
concat(
singleRule('R', '$event.a == 1'),
singleRule('S', '$event.b == 2'),
),
);
expect(r1.computeVersionHash()).not.toBe(r2.computeVersionHash());
});
test('F10.5 order independence — same rules in different declaration order produce equal hashes', () => {
// P1.5.1 sorts by rule.name ASCII codepoint before canonicalization,
// so the registry surface inherits the same invariant. The registry's
// own specificity-desc reordering is upstream of this and does not
// affect the hash, even when both rules carry equal specificity but
// distinct transition types (so loadRuleset doesn't throw).
const a = singleRule('COMMITMENT_ACCEPT_A', '$event.a == 1');
const b = singleRule('SETTLEMENT_COMPLETE_B', '$event.b == 2');
const r1 = RuleRegistry.loadRuleset(concat(a, b));
const r2 = RuleRegistry.loadRuleset(concat(b, a));
expect(r1.computeVersionHash()).toBe(r2.computeVersionHash());
});
});
Five tests. F10.1 and F10.2 are the new format / sentinel assertions (dispatch packet ACs); F10.5 is the new order-independence assertion. F10.3 and F10.4 are preserved verbatim (semantically still correct under the new format).
The singleRule and concat helpers exist at the top of the file; the
new tests use them in the same way as the old block. The block keeps
its position between F9 and F11 to preserve neighborhood.
§4. p1.1-kappa-rule-engine.md edits
§4.1 Item 2c — global path replacement (do this FIRST)
Replace all 44 occurrences of src/domains/rules/__tests__/ with
src/__tests__/domains/rules/ via a single Edit replace_all.
The pattern is unique and short — only one direction of substitution
applies. After this edit, post-replace grep MUST return zero matches.
§4.2 Item 2a — §P1.5.3 ActivationToken (line 2529)
Replace the single line:
* interface ActivationToken { new_version: string, target_epoch: bigint, proposal_id: string }
With a multi-line block (preserving the leading ` * ` JSDoc-list prefix to match neighboring lines):
* 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
* }
* NOTE: this token can only be constructed inside migrateRuleset() — scope_signature,
* issued_at_epoch, parity_pass, and issued_old_version require migration-time data;
* consumers (this P1.5.3 activation, future π governance) accept already-constructed.
The neighbor at line 2530 keeps its JournalEntry definition on its own
line; no other rewrites needed in this range.
§4.3 Item 2b — §P1.4.4 writeback template (lines ~2154–2163)
Replace the prompt-body code block:
WRITEBACK (after success):
task_update(id="P1.4.4", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
content="task_id: P1.4.4
branch: feature/p1-4-4-tool-lock-adapter
worktree: .worktrees/claude/p1-4-4-tool-lock-adapter
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: createToolLockAdapter factory. Converts evaluateAdmission into a stage-1 middleware. Denial short-circuits stages 2-5; emits audit event; throws ToolAdmissionDeniedError. NOT wired into server.ts (deferred to R81+ α integration task).
blockers: none")
With:
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-p1-4-4-tool-lock-adapter"
content: task_id: <UUID>
branch: feature/p1-4-4-tool-lock-adapter
worktree: .worktrees/claude/p1-4-4-tool-lock-adapter
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: createToolLockAdapter factory. Converts evaluateAdmission into a stage-1 middleware. Denial short-circuits stages 2-5; emits audit event; throws ToolAdmissionDeniedError. NOT wired into server.ts (deferred to R81+ α integration task).
blockers: none
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.
§4.4 Item 2b — §P1.4.4 YAML “Writeback template” appendix (lines ~2185–2196)
Replace the YAML block:
task_update:
task_id: P1.4.4
status: done
progress: 100
thought_record:
session_id: r81-kappa-phase-1
task_id: P1.4.4
branch: feature/p1-4-4-tool-lock-adapter
commit_sha: <sha>
tests_run: ["npm run build", "npm run lint", "npm test"]
summary: "createToolLockAdapter(registry, options) factory. Converts P1.4.1 evaluateAdmission into an α stage-1 middleware function. On admit: calls next() and propagates. On deny: emits structured audit event, invokes on_deny callback, throws ToolAdmissionDeniedError. Stages 2-5 never reached on denial. Adapter is not auto-registered with src/server.ts; α wiring is a separate R81+ task."
blockers: []
With:
thought_record:
type: reflection
task_id: <UUID assigned by PM at dispatch time>
agent_id: claude-code-t3-p1-4-4-tool-lock-adapter
content: |
task_id: <UUID>
branch: feature/p1-4-4-tool-lock-adapter
commit_sha: <sha>
tests_run: ["npm run build", "npm run lint", "npm test"]
summary: createToolLockAdapter(registry, options) factory. Converts P1.4.1 evaluateAdmission into an α stage-1 middleware function. On admit: calls next() and propagates. On deny: emits structured audit event, invokes on_deny callback, throws ToolAdmissionDeniedError. Stages 2-5 never reached on denial. Adapter is not auto-registered with src/server.ts; α wiring is a separate R81+ task.
blockers: []
task_update:
id: <UUID>
patch: { status: DONE }
(thought_record FIRST, task_update AFTER — order matters for enforceWriteback at writeback.ts:97.)
§4.5 Item 2b — §P1.5.3 writeback template (lines ~2564–2573)
Replace the prompt-body block:
WRITEBACK (after success):
task_update(id="P1.5.3", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
content="task_id: P1.5.3
branch: feature/p1-5-3-activation
worktree: .worktrees/claude/p1-5-3-activation
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: Activation journal (append-only, monotonic epochs) + scheduleActivation / applyActivation / rollback flows. Rollback does not retroactively invalidate events; dispute-window rollbacks trigger governance_review_hook (stubbed).
blockers: none")
With:
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-p1-5-3-activation"
content: task_id: <UUID>
branch: feature/p1-5-3-activation
worktree: .worktrees/claude/p1-5-3-activation
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: Activation journal (append-only, monotonic epochs) + scheduleActivation / applyActivation / rollback flows. Rollback does not retroactively invalidate events; dispute-window rollbacks trigger governance_review_hook (stubbed).
blockers: none
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.
§4.6 Item 2b — §P1.5.3 YAML “Writeback template” appendix (lines ~2595–2606)
Replace the YAML block:
task_update:
task_id: P1.5.3
status: done
progress: 100
thought_record:
session_id: r81-kappa-phase-1
task_id: P1.5.3
branch: feature/p1-5-3-activation
commit_sha: <sha>
tests_run: ["npm run build", "npm run lint", "npm test"]
summary: "ActivationJournal (append-only log of epoch, version_hash, cause tuples) + scheduleActivation (target epoch strictly greater than current) + applyActivation (runs when current epoch reaches target) + rollback (reinstates prior version, past events stand). Dispute-window rollbacks trigger governance_review_hook stub for future π integration."
blockers: []
With:
thought_record:
type: reflection
task_id: <UUID assigned by PM at dispatch time>
agent_id: claude-code-t3-p1-5-3-activation
content: |
task_id: <UUID>
branch: feature/p1-5-3-activation
commit_sha: <sha>
tests_run: ["npm run build", "npm run lint", "npm test"]
summary: ActivationJournal (append-only log of epoch, version_hash, cause tuples) + scheduleActivation (target epoch strictly greater than current) + applyActivation (runs when current epoch reaches target) + rollback (reinstates prior version, past events stand). Dispute-window rollbacks trigger governance_review_hook stub for future π integration.
blockers: []
task_update:
id: <UUID>
patch: { status: DONE }
§5. Commit message
Single feat commit:
feat(r88-a): wire registry.computeVersionHash + sweep stale prompt templates
R88 Phase A hygiene sweep — two scoped items:
1. RuleRegistry.computeVersionHash now calls versioning.computeVersionHash
(P1.5.1) instead of returning the Phase 1 stub `'sha256:stub:<size>n'`.
Output shape becomes `'sha256:<64hex>'` (71 chars). Order independence,
location independence, and engine-version sensitivity are inherited
from P1.5.1's pipeline. admission.ts and tool-lock-adapter.ts pick up
the wire automatically (call-site signature unchanged).
2. p1.1-kappa-rule-engine.md sweep:
- §P1.5.3 ActivationToken interface updated from the 3-field stale
shape to the 6-field canonical shape from migration.ts.
- §P1.4.4 + §P1.5.3 writeback templates (prompt body + YAML appendix)
replaced with the modern Phase 0 14-tool MCP signature
(thought_record FIRST → task_update AFTER, no session_id, no literal
task_id strings).
- 44 occurrences of src/domains/rules/__tests__/ replaced with the
actual project layout src/__tests__/domains/rules/.
Test deltas:
- registry.test.ts F10 block rewritten to assert the real format
(^sha256:[0-9a-f]{64}$, 71 chars), order independence, and a sentinel
that never returns 'stub'. Five tests total (was five).
- admission.test.ts unchanged — F3.2's `'sha256:stub:99n'` injection is
request-side and still triggers mismatch behavior.
§6. Gate
cd .worktrees/claude/r88-a-kappa-hygiene-sweep
npm run build && npm run lint && npm test
All three must be green per CLAUDE.md §5. The packet expects no new lint warnings; the build should pass with no TypeScript errors; the test suite delta is the F10 block rewrite + nothing else.
§7. Verify (Step 5)
After the gate passes, write docs/verification/r88-a-kappa-hygiene-sweep-verification.md
with:
- Build / lint / test outputs (counts before vs after).
- Confirmation each acceptance criterion (AC1–AC10) is met.
- The grep result for
src/domains/rules/__tests__/(zero). - The new F10 test outcomes.