Audit — P4.2.3 Axiom Drift Tracker (μ Phase 4 Wave 2 detector)
Purpose
Inventory the surface that P4.2.3 consumes before writing one line of detector
code: (1) the P4.1.1 envelope shape + helpers, (2) the κ P1.1.1 bigint BPS
primitives, (3) the λ P2.2.2 parameter-change event surface (or its absence),
(4) the AX-01..AX-07 enumeration from the constitution, (5) the
integrity.md §3 algorithm pseudocode, and (6) the sibling-detector scope
boundary (P4.2.1 / P4.2.2 own circular.ts and coercion.ts in parallel —
P4.2.3 owns ONLY drift.ts).
Base SHA
49560518 — feat(p4-1-1-advisory-envelope): μ Phase 4 Wave 1 — 8-field
advisory envelope (P4.1.1) (#271). Branched from origin/main.
What exists upstream (consumed via REUSE, never duplicated)
P4.1.1 envelope — src/domains/integrity/schema.ts
Public surface confirmed live at 49560518:
AdvisoryRoleSchema—z.enum(['Translator','Sentinel','Guide'])AdvisoryCheckSchema—z.enum(['circular_logic','coercion_trap','axiom_drift','axiom_regression'])— both P4.2.3 check codes (axiom_driftandaxiom_regression) are pre-declared in the closed enum. No schema change is required by this slice.AdvisoryResultSchema—z.enum(['PASS','WARN','BLOCK'])(3-valued; the HARD-vs-soft distinction is carried bycheckper design invariant 9).AdvisorySeveritySchema—z.enum(['LOW','MED','HIGH'])(R91 audit Q6, λ-aligned).AdvisorySchema— z.object with 8 fields includingevidence: z.array(z.unknown()),decision_hash: z.string().regex(/^[a-f0-9]{64}$/),timestamp_logical: z.bigint().nonnegative().Advisory—z.infer<typeof AdvisorySchema>(TS interface).computeDecisionHash(role, check, input, result): string—sha256(role || '||' || check || '||' || canonical(input) || '||' || result).DECISION_HASH_REGEX—/^[a-f0-9]{64}$/.
P4.2.3 will call computeDecisionHash exactly once per advisory (one for each
axiom_drift WARN/BLOCK, one for each axiom_regression BLOCK). The input
preimage is the canonical-JSON-encodable subset of the call inputs that
identifies the advisory semantically; for axiom_drift it is
{ domain, magnitude_bps, window_ms, threshold_bps }; for
axiom_regression it is { proposal_id, axiom }. This keeps the dedup key
discriminating without re-encoding the full envelope (κ canonical handles
the rest at serialize time).
κ P1.1.1 bigint BPS primitives — src/domains/rules/integer-math.ts + bps-constants.ts
BPS_MAX = 10_000n,BPS_MIN = 0n(the integer-math range for BPS values).OverflowError,UnderflowError— thrown bybps(...)validator outside[BPS_MIN, BPS_MAX]. P4.2.3 does NOT use thebps(...)validator —delta_bpsin a parameter-change row may be negative (parameter was reduced) or positive (parameter increased), and the magnitude calculation is the absolute value, sum-bounded by the 10% AX-06 cap. The detector operates on rawbigintand uses the existing(x < 0n ? -x : x)idiom to take absolute value — JavaScriptMath.absdoes NOT accept bigint (gotcha §Common gotchas in p4.1-mu-integrity.md L843).
No new κ primitive is required. P4.2.3 will not import bps-constants.ts
because the WARN/BLOCK thresholds are domain-specific to μ axiom drift
(separate from the λ damage-tier constants like DAMAGE_MINOR); they live
as module-local constants in drift.ts.
Constitution — docs/3-world/physics/constitution.md §AX-01..AX-07
The seven immutable axioms μ guards (file confirmed live at 49560518,
identical text in docs/3-world/physics/laws/constraints.md §Constitutional
Axioms L583-624):
- AX-01 — Append-Only Events: events are never deleted; corrections are new events.
- AX-02 — Reputation is Derived: reputation is computed from history, never assigned administratively.
- AX-03 — No Absolute Authority: no role (admin/owner/arbitrator) can bypass consequences or reset reputation.
- AX-04 — Consequence Windows: subjects get an admission window to contest a proposed sanction.
- AX-05 — Subjective Finality: events become fact when valid + signed + accepted by counterparties.
- AX-06 — Right to Exit: participants may fork with reputation penalty ≤ 10% (this is where the 10% / 1000 bps drift cap derives — exit cost ceiling).
- AX-07 — Technical Sovereignty: each node validates independently; no centralized arbiter can force states.
P4.2.3’s AxiomId union must enumerate exactly these seven values. The
would_reduce_invariant(ax) callback on a StagedProposal is provided by
the caller (governance / β scheduler) — μ does NOT simulate axioms itself
in this slice. P4.2.3 enumerates the seven axiom IDs and queries the
callback once per (proposal × axiom) pair.
The 10% / 1000 bps BLOCK threshold and 8% / 800 bps WARN threshold are
derived from AX-06 per integrity.md L102-105 (“AX-06-derived cap:
cumulative change must stay within 1000 bps (10%)”). These numbers are
governance parameters that may be tuned in Phase 6 π governance — for
Phase 4 they are module-local constants per the dispatch packet.
integrity.md §3 — docs/3-world/physics/enforcement/integrity.md L87-115
The algorithm reference (confirmed live at 49560518):
fn check_axiom_drift(domain, now):
window = 6_months
changes = parameter_changes(domain, since=now - window)
magnitude = sum(abs(c.delta_bps) for c in changes)
if magnitude >= 800: # 8% warning threshold
emit ADVISORY(check="axiom_drift", severity="MEDIUM", domain=domain)
if magnitude >= 1000: # 10% = cap
emit ADVISORY(check="axiom_drift", severity="HIGH", domain=domain)
return BLOCK_NEW_PROPOSALS(domain)
for proposal in staged_proposals(domain):
if proposal.would_reduce_invariant(AX_01 through AX_07):
emit ADVISORY(check="axiom_regression", severity="HIGH")
return HARD_BLOCK(proposal)
The pseudocode says MEDIUM and the envelope says MED — P4.2.3 emits
MED per the P4.1.1 closed enum (R91 audit Q6 λ-aligned). The Phase 4
narrative MEDIUM is a doc-only label; the code uses MED.
The pseudocode’s return BLOCK_NEW_PROPOSALS / return HARD_BLOCK are
escalation-FSM actions consumed by P4.4.1 (Wave 3). In our 3-valued envelope
both surface as result: 'BLOCK'. The HARD-vs-soft distinction is carried by
the check field — axiom_regression routes (in P4.4.1) to α tool-lock,
axiom_drift BLOCK routes to π proposal intake. P4.2.3’s job is to emit
the correct check; routing is downstream.
λ P2.2.2 parameter-change events — src/domains/reputation/penalties.ts
Live audit at 49560518: P2.2.2 defines ReputationHistoryRow and
apply_penalty(...), which writes delta (a NEGATIVE number per AX-09
“history_event.delta <= 0”) into reputation_history. reputation_history
is for REPUTATION decay/penalty events on node_id × domain — it is NOT a
parameter-change feed.
There is no parameter_changes table in the shipped schema at 49560518.
The integrity.md pseudocode’s parameter_changes(domain, since=...) is a
future surface — a governance-rule-change ledger fed by π proposals
ENACTED into rule version bumps. The shape used by P4.2.3 is the
dispatch-packet shape verbatim:
type ParameterChange = {
domain: string;
delta_bps: bigint;
timestamp_logical: bigint;
};
This is a feed adapter, injected by the caller (β scheduler / P4.4.1 escalation FSM in Wave 3). The detector is pure with respect to its inputs — no DB read, no clock read. The caller is responsible for filtering / paginating / persisting the events.
This is the key finding for Wave 3 P4.4.1 — the escalation FSM must
construct the ParameterChange[] feed from whatever shipped persistence
exists at the time (π governance rule-version history, or a fresh
mcp_parameter_changes table created by P4.5.1’s persistence slice). The
detector contract decouples from the source.
θ P3.1.1 messaging — src/domains/consensus/messages.ts (pattern reference only)
Pattern reference only — P4.2.3 does NOT import from θ. The Lamport
bigint timestamp_logical injection idiom is borrowed; the
createHash + canonicalize pattern is borrowed by way of computeDecisionHash
already shipped in P4.1.1.
What does NOT exist (P4.2.3 ships these — only its files)
src/domains/integrity/detectors/ directory
Confirmed absent at 49560518. src/domains/integrity/ contains only
schema.ts from P4.1.1. P4.2.3 creates the detectors/ subdirectory
and its single file drift.ts. Two sibling files
(circular.ts, coercion.ts) are owned by parallel agents P4.2.1 /
P4.2.2 in different worktrees / different branches — P4.2.3 MUST NOT
touch them (Wave 2 file-disjoint pattern, mirrors R92 W3 swarm).
Test directory layout
The repo convention (per jest.config.ts roots: ['<rootDir>/src'] +
testMatch: ['**/__tests__/**/*.test.ts', ...]) places tests under
src/__tests__/domains/<axis>/. P4.1.1’s test sits at
src/__tests__/domains/integrity/schema.test.ts. P4.2.3 will place its
test at src/__tests__/domains/integrity/detectors/drift.test.ts
mirroring the source path under __tests__.
(The dispatch prompt §FILES TO CREATE suggests an in-tree
src/domains/integrity/detectors/__tests__/drift.test.ts layout. Both
locations satisfy the Jest testMatch glob; we follow the repo’s
established src/__tests__/domains/... layout to keep colocation
consistent with the P4.1.1 envelope test.)
Boundary recap — what P4.2.3 OWNS
src/domains/integrity/detectors/drift.ts— exportsWINDOW_MS,WARN_THRESHOLD_BPS,BLOCK_THRESHOLD_BPS,AXIOM_IDS,type AxiomId,type ParameterChange,type StagedProposal,function checkAxiomDrift(...).src/__tests__/domains/integrity/detectors/drift.test.ts— Jest suite covering 13+ acceptance cases plus 100× determinism sweep plus static-scanner forbids.
Boundary recap — what P4.2.3 DOES NOT TOUCH
src/domains/integrity/schema.ts— frozen at49560518. P4.2.3 consumes its exports; it does not edit them.src/domains/integrity/detectors/circular.ts— owned by P4.2.1 (parallel).src/domains/integrity/detectors/coercion.ts— owned by P4.2.2 (parallel).- κ / λ / θ / β / α / ζ / η / ε / δ / ν / γ source — read-only references.
docs/3-world/physics/enforcement/integrity.md— spec source, read-only.docs/3-world/physics/constitution.md— axiom source, read-only.- Any file outside
src/domains/integrity/detectors/drift.ts+src/__tests__/domains/integrity/detectors/drift.test.ts+ the four 5-step doc files underdocs/{audits,contracts,packets,verification}/.
Unresolved questions for the contract step
- Severity for the WARN advisory: pseudocode says
MEDIUM, envelope enum hasMED. Resolution: emitMED(P4.1.1 schema is the truth). recommendationtext contract: the envelope requires arecommendation: z.string(). The detector emits a deterministic fixed-string recommendation per check code so two callers with identical inputs produce byte-identical advisories — non-determinism in recommendation text would break the dedup invariant (P4.1.1 contract §I1).evidenceshape: must be canonical-encodable. The detector packs[{ kind: 'parameter_change', domain, timestamp_logical, delta_bps_abs }, ...]for axiom_drift, and[{ kind: 'staged_proposal', proposal_id, axiom }]for axiom_regression. Bigints are canonical-encodable (κ P1.5.4 emits them as decimal strings).AXIOM_IDSexport: needed by P4.4.1 (escalation FSM iterates the same set) and P4.8.1 (fork-hook subscriber may reset cumulative drift on FINAL fork). Exported as areadonlytuple indrift.ts.
The contract step pins each of these into invariants.
Phase 4 Wave 2 swarm context
P4.2.3 is one of three parallel detectors in Wave 2:
- P4.2.1 →
circular.ts(cycles in evidence-chain DAG) - P4.2.2 →
coercion.ts(no-escape decision traps) - P4.2.3 →
drift.ts(this task — sliding-window cumulative + per-AX regression)
File-disjoint. No shared imports between detectors. Each agent owns
exactly one .ts under detectors/ plus its test mirror. Wave 3 P4.4.1
will compose the three into an escalation FSM; until then, each is a
pure standalone library.
Conclusion
P4.2.3 ships a single pure-function module + its test. The envelope, the
hashing, the canonical serializer, the closed enums, and the AX-01..AX-07
list are all live upstream — P4.2.3 consumes them. The only new vocabulary
is ParameterChange, StagedProposal, AxiomId, AXIOM_IDS,
WINDOW_MS, WARN_THRESHOLD_BPS, BLOCK_THRESHOLD_BPS, and the
checkAxiomDrift function. Wave 3 P4.4.1 will consume these without
modification.