P1.5.3 — Audit
Step 1 of the 5-step executor chain. Inventories the surface this slice creates, the integration points it consumes, and the doc/spec drift it must respect.
1. Mandate
Per docs/guides/implementation/task-prompts/p1.1-kappa-rule-engine.md §P1.5.3 (lines 2478–2617):
Implement activation scheduling, apply, and rollback flows with an append-only journal.
P1.5.3 is the operational counterpart to P1.5.2. P1.5.2 produces ActivationTokens (proof-grade migration receipts); P1.5.3 consumes them by registering them in a journal and swapping rule versions when the runtime epoch crosses target_epoch. P1.5.3 also adds rollback — a way to restore a prior version at the current epoch — and a stub seam for π governance review during dispute windows.
2. Files this slice creates
| Path | Purpose |
|---|---|
src/domains/rules/activation.ts |
Public surface: ActivationJournal class, JournalEntry interface, scheduleActivation, applyActivation, rollback, governance_review_hook stub, ActivationError. |
src/__tests__/domains/rules/activation.test.ts |
Test fixtures covering all six prompt-required scenarios + ActivationToken consumption parity + governance hook routing. |
docs/audits/p1-5-3-activation-audit.md |
This file. |
docs/contracts/p1-5-3-activation-contract.md |
Behavioral contract. |
docs/packets/p1-5-3-activation-packet.md |
Execution plan. |
docs/verification/p1-5-3-activation-verification.md |
Post-implementation evidence. |
No file is mutated outside this list. The 5-step chain artefacts live in docs/{audits,contracts,packets,verification}/. The runtime surface is one new module in src/domains/rules/ colocated with peers (engine.ts, migration.ts, versioning.ts, etc.).
3. Integration points (read-only consumers)
3.1. P1.5.2 migration.ts — ActivationToken (CRITICAL)
The dispatch prompt’s source template at p1.1-kappa-rule-engine.md line 2529 declares a stale 3-field shape:
interface ActivationToken { new_version: string, target_epoch: bigint, proposal_id: string }
This is WRONG. The dispatch prompt’s CRITICAL OVERRIDE block establishes that P1.5.2 (Wave 7, merged at 1a3ea59b) shipped the canonical 6-field shape exported from src/domains/rules/migration.ts. The shape, verified by direct read of migration.ts:136-143:
export interface ActivationToken {
readonly version_hash: string; // 'sha256:<64hex>' — recomputed new ruleset
readonly target_epoch: bigint; // when this token activates
readonly issued_at_epoch: bigint; // current_epoch at issue time
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
}
Properties verified by reading migration.ts:617-624:
- Token is
Object.frozenat construction. - Exactly six own enumerable keys (no extras).
parity_pass === trueliteral — sentinel proving parity-pass provenance.- All three hash fields are 71 chars total (7-char
'sha256:'prefix + 64 lowercase hex).
P1.5.3 must consume this canonical shape verbatim. The journal entry’s version_hash field maps to token.version_hash (NOT the stale token.new_version). The audit ledger’s chain audit lookup, when added later, will use token.issued_old_version for old→new continuity verification.
P1.5.3 does not re-construct an ActivationToken. The token is produced exclusively by migrateRuleset (which has the parity-harness output, the recomputed hashes, and the scope-signature inputs). scheduleActivation accepts an already-constructed token and registers it for later application, which keeps the parity-pass invariant intact: a token cannot exist without parity having passed.
Re-export: P1.5.3 re-exports ActivationToken from ./migration.js so consumers can import it from one place (./activation.js). This avoids cross-module import noise downstream.
3.2. P1.5.1 versioning.ts
Used indirectly. ActivationToken.version_hash and JournalEntry.version_hash are the strings versioning.ts produces (71-char 'sha256:<64hex>'). No direct call from activation.ts to versioning.ts — the migration module already routes through it before issuing a token.
3.3. P1.5.4 canonical.ts
Not consumed directly. Indirect dependency through ActivationToken.scope_signature (computed at migration time using computeScopeSignature which uses canonicalize).
4. Spec & contract anchors
4.1. Acceptance criteria from dispatch prompt
The dispatch prompt enumerates eight acceptance items. They are restated here for traceability with the contract that follows. Quoting items 1–6 verbatim:
class ActivationJournal— append-only;entriesprivate; methods:append(entry: JournalEntry): void(rejects non-monotonic epoch),current(): JournalEntry,at(epoch: bigint): JournalEntry,all(): readonly JournalEntry[].interface JournalEntry { epoch: bigint, version_hash: string, cause: 'initial' | 'migration' | 'rollback' }.scheduleActivation(journal, new_version, target_epoch, current_epoch): ActivationToken—target_epochMUST be strictly greater thancurrent_epoch; throws on violation.scheduleActivationreturns the canonical 6-fieldActivationTokenfrom P1.5.2 (import { ActivationToken } from './migration.js').applyActivation(token, journal, current_epoch): void— applies only whencurrent_epoch >= token.target_epoch; appendsJournalEntrywithcause='migration'andversion_hash = token.version_hash.rollback(journal, target_version, current_epoch, dispute_window_open): void— looks uptarget_versionin journal (must exist prior to current entry); appendsJournalEntry { epoch: current_epoch, version_hash: target_version, cause: 'rollback' }; ifdispute_window_open === trueinvokesgovernance_review_hookstub for π.- Rollback does NOT retroactively invalidate events admitted under rolled-back version.
- Test fixtures cover all six scenarios + non-monotonic + ActivationToken verbatim consumption.
4.2. Spec sources
docs/spec/s11-rule-engine.md— referencesEpoch number, rule version hashin §1 context. No direct activation flow text; activation is operational, not part of the law-library spec.docs/3-world/physics/laws/rule-engine.md§Rule versioning (lines 139–156) — describesrule_version_hash, θ consensus signing, ι fork-id derivation, and the parity-run prerequisite. P1.5.3’s journal embodies the post-parity activation lifecycle: aftermigrateRulesetsays “yes,” activation happens here.docs/reference/extractions/kappa-rule-engine-extraction.md§5 — referenced by dispatch prompt; describes the activation lifecycle at extraction time.
4.3. Constitutional considerations
- Append-only journal (AX-04 informally — “no retroactive change”). Past
JournalEntryrecords are never rewritten or removed; rollback adds a new entry rather than replacing the offending one. The consequence:journal.at(historical_epoch)returns the version that was actually active at that epoch, regardless of later rollbacks. This preserves event history integrity. - Synchronous operations — no
asyncin any public method. Per the prompt’s “Common gotchas” section: “an async append leaves a window where two rollbacks race.” - Determinism — same inputs to
scheduleActivationproduce same outputs (the input itself comes from migration.ts which is fully deterministic).Object.freezeon every emitted token.
5. Determinism corpus self-scan compatibility
activation.ts will be scanned by src/__tests__/domains/rules/determinism.test.ts §Group 12. The scanner forbids these tokens (post-comment-strip):
| Pattern | Avoidance strategy |
|---|---|
Math.* |
None used; the body has no math (epoch comparisons via > < operators on bigint). |
Date.*, new Date |
None. No timestamps. |
setTimeout, setInterval, setImmediate |
None. Synchronous only. |
fetch, XMLHttpRequest |
None. Pure module. |
require\(['"]fs['"]\), from 'fs' |
None. No file I/O. |
crypto.<member> |
None. No hashing in this module — token already carries hashes. (Pre-existing peers like versioning.ts:72 use a NAMED import { createHash } from 'node:crypto' to avoid the pattern; this module imports nothing from crypto.) |
process.hrtime, process.nextTick |
None. |
await, async function |
None. Synchronous only. |
\d+\.\d+ float literal |
None. No float math; only bigint. |
JSDoc comments are stripped before the scan, so verbatim references to Math.floor-style tokens in prose comments are tolerated; the body itself stays clean.
6. Public surface inventory
| Export | Kind | Purpose |
|---|---|---|
ActivationToken |
re-export | The 6-field ActivationToken from ./migration.js, re-exported for ergonomics. |
JournalEntry |
interface | Single tuple in the journal: { epoch: bigint, version_hash: string, cause: 'initial' \| 'migration' \| 'rollback' }. |
JournalCause |
type alias | 'initial' \| 'migration' \| 'rollback'. |
ActivationJournal |
class | Append-only log with monotonic-epoch invariant. |
ActivationError |
error class | Thrown for invariant violations (non-monotonic epoch, target_epoch <= current_epoch, apply-too-early, rollback-version-not-found, rollback at non-monotonic epoch, governance-hook violation). |
GovernanceReviewHook |
type alias | Function shape for the π governance seam ((snapshot: GovernanceReviewSnapshot) => void). |
GovernanceReviewSnapshot |
interface | Read-only payload passed to the governance hook (target_version, current_epoch, prior_current_entry, journal_length). |
governance_review_hook |
const function | Default no-op stub implementation; replaceable per call. |
scheduleActivation(journal, token, current_epoch) |
function | Validates token.target_epoch > current_epoch; returns the same token (or throws). Token shape is left unchanged — the function is a guard, not a constructor. |
applyActivation(token, journal, current_epoch) |
function | Validates current_epoch >= token.target_epoch; appends JournalEntry { epoch: current_epoch, version_hash: token.version_hash, cause: 'migration' }. |
rollback(journal, target_version, current_epoch, dispute_window_open, hook?) |
function | Validates target_version is present in a prior entry; appends rollback entry; invokes hook on dispute window. |
A note on the prompt signature for scheduleActivation. The source prompt says:
scheduleActivation(journal, new_version, target_epoch, current_epoch): ActivationToken
Per the CRITICAL OVERRIDE in the dispatch prompt, the canonical 6-field token has 5 of 6 fields that depend on data only migrateRuleset has access to (scope_signature requires the declared scope; issued_old_version requires a recomputed old hash; parity_pass is the migration-pass sentinel). It is therefore impossible for scheduleActivation to construct a valid token from a string + epoch + journal alone.
The contract resolves this by having scheduleActivation accept a pre-constructed ActivationToken (already issued by migrateRuleset) and validate its target_epoch > current_epoch invariant. This is documented in §4 of the contract.
7. Out of scope
- Persisting the journal to disk — Phase 1.5+.
- Wiring the governance hook to a real π verifier — Phase 5.
- Concurrent application coordination across replicas — θ governance concern; Phase 1.5+.
- Multi-version coexistence (canary deployments) — not part of the κ contract.
- Engine-side actually swapping the active ruleset at the runtime layer — handled by ι; this slice owns only the journal.
8. Risks & mitigations
| Risk | Mitigation |
|---|---|
Activation applied at exact target_epoch — >= boundary off by one. |
Test fixture apply at target_epoch === target_epoch (boundary-equal case). The implementation uses current_epoch >= token.target_epoch. |
| Mutating frozen ActivationToken after issuance. | Don’t write to the token. Re-export (typeof unchanged); pass by reference; trust Object.freeze enforcement from migration.ts. |
Rollback to a version that was activated at the same epoch as current_epoch. |
The contract requires “prior to current entry,” interpreted as: target_version must appear in a JournalEntry whose epoch < current_epoch (or in the unique entry that is < journal.current().epoch if current_epoch === journal.current().epoch). Test fixture covers both cases. |
| Hook throwing under dispute window halts rollback partway. | The hook is invoked after the rollback entry is appended. If the hook throws, the journal is already updated. This matches the prompt’s “log that it was called” framing — the hook is a notification seam, not a veto. |
Non-monotonic epoch from rollback at exact current_epoch == journal.current().epoch. |
The journal’s append rejects non-monotonic. Rollback at an epoch equal to the current journal head must be rejected by rollback before reaching append. |
at(epoch) lookup before any entry exists. |
at throws ActivationError. The journal must have at least one 'initial' entry before any other operation runs; this is part of the construction contract. |
9. Greek-letter alignment
- κ (kappa) — primary axis. Activation lifecycle is a κ runtime concern.
- π (pi) — governance review hook is the seam to π. Stub-only in Phase 1; full wiring is later.
- θ (theta) — consumes
version_hashfor consensus signing; the journal is the canonical source of “what version was active at epoch E.” - ι (iota) — fork-id derivation reads
rule_version_hashfrom the journal at fork time. Not part of this slice.
10. Step 1 sign-off
Surface inventoried. Integration with P1.5.2 confirmed (canonical 6-field ActivationToken consumed verbatim, NOT the stale 3-field template at p1.1-kappa-rule-engine.md:2529). Determinism scan compatible. No external module is mutated. Step 2 (contract) follows.