P1.5.3 — Execution Packet

Step 3 of the 5-step executor chain. Operationalizes the behavioral contract (docs/contracts/p1-5-3-activation-contract.md) into a concrete implementation plan and test plan. The packet gates Step 4 — implementation may not begin without packet approval.

1. Layout

src/domains/rules/activation.ts
  §1 — Imports
  §2 — Re-exports (ActivationToken from migration)
  §3 — Type declarations (JournalEntry, JournalCause, GovernanceReviewSnapshot, GovernanceReviewHook)
  §4 — ActivationError
  §5 — Internal validators (shape predicates)
  §6 — ActivationJournal class
  §7 — scheduleActivation (named function)
  §8 — applyActivation (named function)
  §9 — rollback (named function)
  §10 — governance_review_hook (default no-op)

src/__tests__/domains/rules/activation.test.ts
  Group 1  — Construction (F1 — 6 cases)
  Group 2  — append (F2 — 8 cases)
  Group 3  — current (F3 — 2 cases)
  Group 4  — at (F4 — 6 cases)
  Group 5  — all (F5 — 3 cases)
  Group 6  — scheduleActivation happy path (F6 — 2 cases)
  Group 7  — scheduleActivation rejections (F7+F8+F9 — 5 cases)
  Group 8  — applyActivation rejection (F10 — 2 cases)
  Group 9  — applyActivation happy path (F11 — 4 cases)
  Group 10 — ActivationToken consumed verbatim (F12 — 1 case)
  Group 11 — rollback happy path (F13 — 3 cases)
  Group 12 — rollback governance hook (F14+F15+F18+F19 — 4 cases)
  Group 13 — rollback rejections (F16+F17 — 2 cases)
  Group 14 — Determinism corpus self-scan (covered by existing
              src/__tests__/domains/rules/determinism.test.ts §Group 12,
              which scans every src/domains/rules/*.ts; no new test needed,
              we just have to keep activation.ts clean)

The grouping follows the order of migration.test.ts and parity-harness.test.ts for navigability across the κ corpus.

2. Implementation order

The order below minimizes wasted work when an early-stage type goes wrong.

  1. Skeleton activation.ts — module header JSDoc, imports, re-exports, error class. Verify npm run build is green at this point (catches typos early).
  2. Type declarations — JournalCause, JournalEntry, GovernanceReviewSnapshot, GovernanceReviewHook. npm run build again.
  3. Internal validators — validateBigint, validateNonEmptyString, validateBoolean, validateActivationTokenShape, validateJournalEntryShape. (These are private — not exported.)
  4. ActivationJournal class — constructor, append, current, at, all. Run npm run build after each method to surface tsc errors early.
  5. governance_review_hook (default no-op).
  6. scheduleActivation function.
  7. applyActivation function.
  8. rollback function.
  9. Skeleton activation.test.ts — imports + AST builder helpers + reusable token factory (mirrors migration.test.ts:60-100). Verify npm test -- --testPathPattern=activation is green (no test cases yet, but the file should compile and import cleanly).
  10. Test groups in numerical order. Run npm test -- --testPathPattern=activation after each group to catch local regressions before the next.
  11. Final pass: npm run build && npm run lint && npm test. All green required.

3. Detailed implementation notes

3.1. governance_review_hook (the export)

Required to be a const so the type is inferred as GovernanceReviewHook and the binding can be replaced in test fixtures via the explicit hook parameter without redefining the export. The body is a no-op ((_snapshot) => {}); the underscore-prefixed parameter is by lint convention to mark “intentionally unused.”

The export is named with snake_case (governance_review_hook) to mirror the prompt’s literal wording (line 2545: governance_review_hook()). The TypeScript convention for exported variables is camelCase; we override here for prompt fidelity, since this is a stable seam name that will be cited in documents downstream.

3.2. ActivationJournal entries array

The entries field is private and readonly (in TypeScript visibility — the readonly modifier prevents reassignment of the array reference; Object.freeze of individual entries prevents mutation of any single entry). We do not freeze the array itself — it must support push for append. Only individual entries are frozen.

The all() method returns Object.freeze(entries.slice()) — the slice is the immutable shallow copy; the freeze prevents callers from mutating the array (e.g., arr.length = 0).

3.3. at(epoch: bigint) algorithm

Linear scan from the end of the array (most recent first):

for (let i = entries.length - 1; i >= 0; i--) {
  const e = entries[i];
  if (e !== undefined && e.epoch <= epoch) {
    return e;
  }
}
throw new ActivationError(...);

Rationale: typical lookups target recent epochs. Linear-from-end is O(N) worst case but O(1) for the common path. If profiling later shows hot-spot pressure, a binary search is a drop-in replacement (entries are sorted by epoch by construction).

The e !== undefined guard satisfies noUncheckedIndexedAccess (a strict TypeScript option that may or may not be enabled — using the guard is safe regardless).

3.4. applyActivation and the journal’s append guard

applyActivation does not pre-check current_epoch > journal.current().epoch — instead it relies on journal.append to throw 'non-monotonic epoch'. Rationale: pre-checking duplicates the guard, and the journal’s error message is canonical. The trade-off is that the user sees 'non-monotonic epoch' (less specific) rather than 'apply at non-monotonic epoch' (more specific). The contract §5.4 documents this explicitly.

3.5. rollback and the dispute window

Order of operations is load-bearing per the contract:

  1. Validate inputs.
  2. Validate target_version is in a prior entry.
  3. Append the rollback entry to the journal. The journal mutates here.
  4. If dispute_window_open === true, build the snapshot and invoke the hook.

If the hook throws, the journal is already updated. This is by design — the hook is a notification, not a veto. A π verifier that wants to prevent the rollback should run before rollback is called, not inside the hook.

Pre-checking monotonicity inside rollback: we DO pre-check current_epoch > journal.current().epoch before the append, so the journal_length in the snapshot is always “1 more than before this call.” Without the pre-check, we’d discover non-monotonicity inside journal.append, which would throw before the snapshot is built — but then the test for the dispute-window code path becomes harder to read. The pre-check is cleaner.

3.6. Forbidden patterns avoidance

activation.ts will pass the determinism corpus self-scan. Enforced by:

  • No Math.*, no Date.*, no crypto.*, no await, no async, no float literals (\d+\.\d+), no setTimeout, no fetch, no fs imports.
  • All comparisons of bigint use <, >, <=, >=, ===, !==. No conversions.
  • No crypto import — the module does no hashing. version_hash strings are passed through as opaque values.

4. Test plan in detail

4.1. Reusable fixture: makeToken(version_hash: string, target_epoch: bigint, issued_at_epoch: bigint = 0n): ActivationToken

Constructs a fake-but-shape-correct ActivationToken for non-integration tests:

function makeToken(
  version_hash: string,
  target_epoch: bigint,
  issued_at_epoch: bigint = 0n,
): ActivationToken {
  return Object.freeze({
    version_hash,
    target_epoch,
    issued_at_epoch,
    parity_pass: true as const,
    scope_signature: 'sha256:' + '0'.repeat(64),
    issued_old_version: 'sha256:' + 'f'.repeat(64),
  });
}

Used in F6–F11 + F13–F19. The shape is identical to what migrateRuleset returns; the field values are placeholders since these tests are not exercising migration logic, only activation logic.

4.2. Integration fixture: F12 — token consumed verbatim

This fixture imports migrateRuleset from ../../../domains/rules/migration.js and runs an end-to-end accepted migration to obtain a real token. The token is then handed to applyActivation. The assertion: journal.current().version_hash === token.version_hash (string equality).

The migration’s input ruleset is two trivial admit rules (mirrors migration.test.ts §F1). The corpus is DEFAULT_CORPUS (also imported from parity-harness.js). This validates that the field name version_hash (NOT the stale new_version) is the one used end-to-end.

4.3. F4.b — “returns initial for epoch < first migration’s epoch”

This case is subtle. Given a journal with:

  • entries[0] = { epoch: 0n, version_hash: 'A', cause: 'initial' }
  • entries[1] = { epoch: 100n, version_hash: 'B', cause: 'migration' }

journal.at(50n) must return entries[0] (the initial). The algorithm finds the largest e.epoch <= 50n, which is entries[0] with e.epoch === 0n <= 50n.

This is the contract’s “the initial entry is active until the first migration.” Without this property, at could throw on epochs in the gap, which is wrong: the initial version IS active in that range.

4.4. F13 — past events stand

The fixture asserts:

  1. Journal: initial at epoch 0n with version A.
  2. Migration: B activated at epoch 100n.
  3. Rollback: to A at epoch 200n.
  4. journal.at(50n).version_hash === 'A'. (Initial era.)
  5. journal.at(150n).version_hash === 'B'. (Migration era — still ‘B’ even after rollback.)
  6. journal.at(250n).version_hash === 'A'. (Rollback era.)

Step 5 is the crucial one — it proves rollback does not retroactively rewrite the past.

4.5. F14 — governance hook invoked once with correct snapshot

const hook = jest.fn();
rollback(journal, 'A', 200n, true, hook);
expect(hook).toHaveBeenCalledTimes(1);
const arg = hook.mock.calls[0][0];
expect(arg.target_version).toBe('A');
expect(arg.current_epoch).toBe(200n);
expect(arg.prior_current_entry).toEqual(/* entry that was journal.current() pre-rollback */);
expect(arg.journal_length).toBe(/* post-rollback length */);

4.6. F19 — hook errors propagate

const hook = jest.fn(() => { throw new Error('π veto'); });
expect(() => rollback(journal, 'A', 200n, true, hook)).toThrow('π veto');
expect(journal.current().cause).toBe('rollback');  // journal was already updated

4.7. F20 — Determinism corpus self-scan (passive)

The existing src/__tests__/domains/rules/determinism.test.ts §Group 12 scans every *.ts file in src/domains/rules/. As long as activation.ts contains none of the forbidden tokens, this group passes without modification. We do not write a new test for it; we only verify by running npm test -- --testPathPattern=determinism after Step 4.

5. Build/lint/test gate

5.1. Sequential local runs (during implementation)

cd .worktrees/claude/p1-5-3-activation
npm run build                                        # tsc, fast
npm test -- --testPathPattern=activation             # only the new file
npm test -- --testPathPattern=determinism            # corpus self-scan

5.2. Final gate (before push)

cd .worktrees/claude/p1-5-3-activation
npm run build && npm run lint && npm test

All three must pass. Exit code 0 on each. The && chain stops on the first failure — investigate before pushing.

Test count expectation: ~50 new cases on top of the post-Wave-7 baseline.

6. Commit hygiene

One commit per chain step. Step-4 commit message follows the dispatch prompt template.

audit(p1-5-3): inventory surface
contract(p1-5-3): behavioral contract
packet(p1-5-3): execution plan
feat(p1-5-3): activation epoch + rollback
verify(p1-5-3): test evidence

The feat commit body cites the canonical 6-field ActivationToken consumption strategy (accept-already-constructed) and references PR #216 as the upstream of the token shape.

7. PR body skeleton (used by Step 4 push)

Per the dispatch prompt — a ## Summary + ## β task + ## Test plan body with the required co-author line. The PR title is feat(p1-5-3-activation): activation epoch + rollback (R87 κ Wave 8 — Phase 1 close).

8. Risk acknowledgment

Pre-existing flakes documented in memory:

  • startup — subprocess smoke flake under full-suite load.

If hit, retry once. If stable on retry, proceed; the flake is not Wave-8-introduced.

Other risks:

  • noUncheckedIndexedAccess may or may not be enabled. The implementation uses guards regardless.
  • ESM extension .js on TypeScript source imports — the existing convention in the rules domain (./migration.js, ./parity-harness.js). We follow it.

9. Step 3 sign-off

Execution plan locked. Step 4 (implementation) follows the order in §2, the test groups in §1, and uses the reusable fixtures in §4. Step 5 (verification) records the results.


Back to top

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

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