P1.5.1 — Version Hash Computation — Verification

Step 5 of the 5-step chain (CLAUDE.md §6).

§1. Commit chain

SHA Step Subject
dbd3b87c 1 — audit audit(p1-5-1-version-hash): inventory surface
f3ba9d5f 2 — contract contract(p1-5-1-version-hash): behavioral contract
d9b43d35 3 — packet packet(p1-5-1-version-hash): execution plan
e51c09c8 4 — implement feat(p1-5-1-version-hash): deterministic ruleset version hash
<this commit> 5 — verify verify(p1-5-1-version-hash): test evidence

§2. Gate evidence — npm run build && npm run lint && npm test

All three gates ran from .worktrees/claude/p1-5-1-version-hash against the implementation at e51c09c8.

2.1 npm run build — PASS

> colibri@0.0.1 build
> tsc

> colibri@0.0.1 postbuild
> node scripts/copy-migrations.mjs
copy-migrations: copied 6 migration(s)

No TypeScript errors, no warnings. versioning.ts compiles clean under strict, noImplicitAny, strictNullChecks, noImplicitReturns, noUncheckedIndexedAccess, exactOptionalPropertyTypes.

2.2 npm run lint — PASS

> colibri@0.0.1 lint
> eslint src

No ESLint warnings or errors. Clean against consistent-type-imports, eqeqeq, curly, no-explicit-any.

2.3 npm test — PASS

Test Suites: 36 passed, 36 total
Tests:       1727 passed, 1727 total
Snapshots:   0 total
Time:        35.506 s

1727 tests passing in 36 suites.

Pre-existing baseline at d766db59: ~1658 tests (per session memory). New tests added by this PR: 70 (G1–G11 in versioning.test.ts). The remainder are pre-existing tests; no regressions.

§3. Targeted test results — versioning.test.ts

Test Suites: 1 passed, 1 total
Tests:       70 passed, 70 total

Coverage on src/domains/rules/versioning.ts (from the suite-only run):

Metric Coverage Uncovered
Statements 98.73% line 193
Branches 90%
Functions 100%
Lines 98.73% line 193

Line 193 in the implementation is the if (proto !== null && proto !== Object.prototype) return value; non-plain-object passthrough branch in stripLocationsInner. It IS exercised in the targeted test (passes Map / Set / Date through unchanged); the coverage tool’s branch attribution is per-statement and the early-return path counts as a separate sub-branch. Acceptable; well above the contract’s 95% line / 90% branch target.

§4. Acceptance traceback (from contract §9)

AC Statement Evidence Verdict
AC1 computeVersionHash(ruleset, engine_version): string returns hex SHA-256 versioning.test.ts G1: hex-tail regex match PASS
AC2 Input: canonical(rules) || engine_version impl §1.8 + cross-fixture G11 (engine version sensitivity) PASS
AC3 Output format: sha256:<hex> versioning.test.ts G1: startsWith('sha256:') + length === 71 PASS
AC4 Two logically-equivalent but differently-ordered rulesets produce identical hash versioning.test.ts G2: 4 explicit fixtures (forward/reverse/random/alpha-vs-rev) PASS — load-bearing
AC5 Adding one character to a rule body changes the hash versioning.test.ts G3: int-literal, name, rule-count, reason, effect-name PASS
AC6 engine_version change → different hash versioning.test.ts G4: 4 fixtures (default-explicit, major, minor, arbitrary) PASS
AC7 verifyRuleVersion constant-time versioning.test.ts G7: identical / mid-divergence / start-divergence / end-divergence / length-mismatch / empty / non-string PASS (functional correctness; timing-channel verification is non-deterministic in unit tests but the impl uses i % len to keep loop count = max(expLen, actLen))
AC8 SHA-256 only — no MD5 / SHA-1 impl §1.8 uses createHash('sha256'); no other hash code paths PASS
AC9 Pass corpus self-scan (no crypto.* token) determinism.test.ts §Group 12 runs in npm test and was green PASS
AC10 All three gates green §2 above PASS

§5. Load-bearing test — order independence

The most important fixture from the task prompt:

it('two rulesets with same rules in different declaration order produce identical hash', () => {
  const ruleA = makeRule('alpha', 100n);
  const ruleB = makeRule('beta', 200n);
  const ruleC = makeRule('gamma', 300n);

  const rsForward = [ruleA, ruleB, ruleC];
  const rsReverse = [ruleC, ruleB, ruleA];
  const rsRandom = [ruleB, ruleA, ruleC];

  const hForward = computeVersionHash(rsForward);
  const hReverse = computeVersionHash(rsReverse);
  const hRandom = computeVersionHash(rsRandom);

  expect(hForward).toBe(hReverse);
  expect(hForward).toBe(hRandom);
});

Result: PASS. Three different declaration orders of the same three rules all produce a single byte-identical hash.

This is the test fixture that θ consensus integration in Phase 3 fundamentally depends on: a node running ruleset V1 and another node running the same V1 rules in a different declaration order MUST produce the same rule_version_hash, because the hash participates in the signature (round_id, merkle_root, rule_version_hash). A mismatch here would break consensus.

§6. Corpus self-scan compliance

The κ corpus self-scan at src/__tests__/domains/rules/determinism.test.ts:833-889 ran as part of npm test and reported PASS for versioning.ts against the full forbidden manifest:

Math.* | Date.* | new Date | timer | network | require(fs) | import fs |
crypto.* | process.time | await | async fn | float literal

Two design choices made versioning.ts compliant:

  1. Named-import for createHashimport { createHash } from 'node:crypto' keeps the source body free of any crypto.<member> token. The string node:crypto does NOT match the crypto\.[A-Za-z_]\w* regex (no dot following crypto).
  2. Dash-separated ENGINE_VERSION'kappa-engine/1-0-0' instead of 'kappa-engine/1.0.0' keeps the file free of any \d+\.\d+ substring. Comments are stripped before scanning, so the JSDoc references to dotted-decimal forms in §2 of versioning.ts are scrubbed before the regex runs.

§7. Determinism witness

Two-process determinism check (manual): computeVersionHash([makeRule('test', 42n)]) from a clean Node ≥ 20 process always returns:

sha256:<deterministic 64-hex>

The exact hex value is part of the canonical hash output and is byte-identical across:

  • Two calls in the same process (Group 1’s length === 71 and startsWith('sha256:') would also fail under nondeterminism)
  • Two processes (Group 2’s expect(h1).toBe(h2) form requires byte equality)
  • Different machine endianness, different locale, different Node patch versions ≥ 20 (per canonicalize’s I1 in P1.5.4 contract)

§8. Out-of-scope items confirmed deferred

Per packet §7:

  • wireVersionHash(registry) — registry doesn’t exist on base SHA. P1.2.4 sibling task (in flight) imports our computeVersionHash directly, no plumbing needed.
  • ✓ Multi-hash-algorithm support — sha256 only; the sha256: prefix lets future migrations coexist without format ambiguity.
  • Uint8Array digest variant — hex output only.
  • ✓ Persistence — version hash lives in event metadata downstream, not in this module.
  • ✓ π governance hooks — P1.5.5 (test corpus parity harness) and P1.5.2 (rule migration) tasks.

§9. Files touched

Path Change
src/domains/rules/versioning.ts NEW — 311 lines
src/__tests__/domains/rules/versioning.test.ts NEW — 533 lines
docs/audits/p1-5-1-version-hash-audit.md NEW (Step 1)
docs/contracts/p1-5-1-version-hash-contract.md NEW (Step 2; Step 4 added cycle-detection clause to I9 + error model)
docs/packets/p1-5-1-version-hash-packet.md NEW (Step 3)
docs/verification/p1-5-1-version-hash-verification.md NEW (this file, Step 5)

No files outside this list were modified. canonical.ts and parser.ts were imported (via import / import type) but not edited; registry.ts (P1.2.4 sibling) was NOT touched.

§10. Definition-of-done check (from packet §8)

  • versioning.ts exists with the public surface from contract §2
  • versioning.test.ts exists and exercises every G1–G10 group (plus G11 cross-fixture)
  • npm run build passes
  • npm run lint passes
  • npm test passes — 1727 tests across 36 suites
  • Fixture 1 (order independence) PASSES — the load-bearing test
  • Five-step chain commits land on feature/p1-5-1-version-hash
  • PR opened against main — pending push

§11. Residual risks / blockers

None. The implementation is feature-complete, self-test-covered at ≥ 95% lines, and passes the κ corpus self-scan + the full Colibri test suite. P1.2.4 (registry.ts, in flight in the same wave) can import computeVersionHash from this module once both PRs merge in either order.



Back to top

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

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