Verification — P4.2.3 Axiom Drift Tracker

1. Summary

P4.2.3 ships src/domains/integrity/detectors/drift.ts + its test mirror at src/__tests__/domains/integrity/detectors/drift.test.ts. The implementation is a single pure function plus four helpers and three public types. Total addition: ~430 source LOC + ~520 test LOC, no modifications to existing files (src/domains/integrity/schema.ts is imported but untouched).

1.1. Gate green

npm run build  → clean (tsc + postbuild migration copy)
npm run lint   → clean (eslint src — 0 warnings, 0 errors)
npm test       → 60/60 drift suite, 120/120 integrity domain suite
                 Full suite: 3609-3612 / 3613 (1-4 known-flake failures,
                 all retry-clean; none in drift or integrity)

1.2. Test count delta

Anchor Before (49560518) After (this PR) Δ
Tests 3553 3613 +60
Suites 80 81 +1

The +60 comes from the 60 G-grouped + bigint-extra tests in drift.test.ts. The new suite is src/__tests__/domains/integrity/detectors/drift.test.ts.

2. Acceptance criteria — line by line

From the dispatch packet §ACCEPTANCE CRITERIA:

  • WINDOW/WARN/BLOCK constants exact: 15_552_000_000n, 800n, 1_000n — pinned in G14.
  • Sliding window filter correctness — G4 (timestamp_logical >= now - WINDOW_MS, inclusive lower bound at exact boundary, exclusive at -1n).
  • Two distinct check codes: axiom_drift and axiom_regression — G9 (“two-check-code preservation — checks are NOT collapsed”).
  • All AX-01..AX-07 enumerated — G8 (“proposal triggering all 7 axioms → 7 advisories”) + G13 (“AXIOM_IDS contains AX-01..AX-07 in canonical order, no duplicates”).
  • BPS bigint throughout — every constant and every magnitude computation uses bigint. G12 includes a Math.abs static forbid; the codebase compiles under strict: true, noUncheckedIndexedAccess: true, exactOptionalPropertyTypes: true.
  • now injected — no Date.now(), no new Date(), no performance.now() in drift.ts. G12 static scanner verifies.
  • Deterministic output across identical inputs — G11 ×100 run.
  • Advisory shape matches P4.1.1 envelope — G15 calls AdvisorySchema.parse(...) on every emitted advisory.
  • npm run build && npm run lint && npm test ALL pass.

From the contract §7 acceptance criteria (cross-check):

  • §I1 — Pure function (G12 static scanner).
  • §I2 — Lamport bigint injection (G14 + extra “early-Lamport — now < WINDOW_MS — window floor is negative but filter still works”).
  • §I3 — Deterministic output (G11).
  • §I4 — Deterministic recommendation (subsumed by G11; same input → byte-identical advisory).
  • §I5 — Two-check-code preservation (G9).
  • §I6 — AX-01..AX-07 exhaustive (G13).
  • §I7 — BPS bigint (G12 includes Math.abs forbid; extra test “very large bigint deltas do not silently overflow” demonstrates ×10^18 magnitude correctly summed).
  • §I8 — now invariance (G15 “timestamp_logical equals injected now on every advisory”).
  • §I9 — Bounded loop (no recursion; outer ≤ stagedProposals.length, inner = 7).
  • §I10 — Zero-length input → zero advisories (G1 “empty changes + empty proposals → []”).

3. Test evidence (verbatim Jest output, drift suite isolated)

PASS src/__tests__/domains/integrity/detectors/drift.test.ts (22.148s)
  G14 constants
    √ WINDOW_MS = 15_552_000_000n (6 months × 30 days × 86_400_000 ms)
    √ WARN_THRESHOLD_BPS = 800n (8%)
    √ BLOCK_THRESHOLD_BPS = 1_000n (10% AX-06 cap)
    √ WARN < BLOCK (monotone)
  G13 AXIOM_IDS
    √ contains exactly 7 ids
    √ contains AX-01..AX-07 in canonical order, no duplicates
  G1 empty
    √ empty changes + empty proposals → []
    √ change under WARN + no proposals → []
    √ only out-of-window change + no proposals → []
  G2 WARN boundary
    √ magnitude 799 → no advisory (just under WARN)
    √ magnitude 800 exact → 1 axiom_drift MED/WARN
    √ magnitude 999 → 1 axiom_drift MED/WARN (just under BLOCK)
  G3 BLOCK boundary
    √ magnitude 1000 exact → 1 axiom_drift HIGH/BLOCK
    √ magnitude 1500 → 1 axiom_drift HIGH/BLOCK
    √ magnitude 10000 (very large) → 1 axiom_drift HIGH/BLOCK
  G4 window filter
    √ change at exact lower boundary (now - WINDOW_MS) is INCLUDED
    √ change 1 tick BEFORE lower boundary is EXCLUDED
    √ mixed in-window + out-of-window only counts in-window
    √ change at now (timestamp_logical === now) is INCLUDED
  G5 domain filter
    √ change in other domain is excluded
    √ proposal in other domain is excluded from regression check
    √ mixed-domain changes only count target-domain
  G6 abs-magnitude
    √ single negative delta sums by abs
    √ negative + positive deltas sum by abs
    √ mixed-sign deltas summing to BLOCK
    √ cancelling deltas DO NOT cancel — abs preserves both
  G7 single AX regression
    √ AX-01 regression → 1 axiom_regression HIGH/BLOCK advisory
    √ each axiom in isolation produces 1 advisory
    √ proposal with no triggers → 0 advisories
    √ regression advisory references the correct axiom in evidence
  G8 multi-AX regression
    √ proposal triggering 3 axioms → 3 advisories
    √ proposal triggering all 7 axioms → 7 advisories
    √ multiple proposals each with their own triggers
  G9 combined drift + regression
    √ 1500 bps + AX-03 regression → 2 advisories (drift first, then regression)
    √ two-check-code preservation — checks are NOT collapsed
    √ WARN drift + regression: 1 WARN + N regressions
  G10 12-month synthetic corpus stub
    √ 12-month corpus at +50 bps/month, observed at month 11 → no advisory
    √ 12-month corpus at +150 bps/month, observed at month 11 → BLOCK
    √ 12-month corpus at +120 bps/month, observed at month 11 → WARN
    √ 12-month corpus split across two domains, target counts only its own
  G11 determinism ×100
    √ 100 invocations produce structurally identical results
    √ decision_hash is identical for identical inputs across 100 runs
    √ decision_hash differs across different inputs (drift change vs regression)
    √ decision_hash differs between WARN and BLOCK at same magnitude tier
  G12 static scanner
    √ no Date.now() in drift.ts code
    √ no new Date() in drift.ts code
    √ no performance.now() in drift.ts code
    √ no Math.random() in drift.ts code
    √ no Math.abs() in drift.ts code (bigint-incompatible)
    √ no crypto.randomBytes/randomUUID in drift.ts code
    √ no top-level `let` mutable state in drift.ts code
  G15 AdvisorySchema parse
    √ every emitted drift advisory parses against AdvisorySchema
    √ every emitted regression advisory parses against AdvisorySchema
    √ decision_hash matches DECISION_HASH_REGEX
    √ timestamp_logical equals injected now on every advisory
    √ role is always Sentinel
    √ evidence is a non-empty array on every advisory
  extra bigint behavior
    √ very large bigint deltas do not silently overflow
    √ early-Lamport — now < WINDOW_MS — window floor is negative but filter still works
    √ proposal evidence carries the exact AxiomId literal

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

4. Build + lint evidence

$ npm run build
> colibri@0.0.1 build
> tsc

> colibri@0.0.1 postbuild
> node scripts/copy-migrations.mjs
copy-migrations: copied 9 migration(s) src/db/migrations -> dist/db/migrations
$ npm run lint
> colibri@0.0.1 lint
> eslint src
(0 errors, 0 warnings)

5. Full-suite evidence

Test Suites: 3-4 failed, 77-78 passed, 81 total
Tests:       1-4 failed, 3609-3612 passed, 3613 total

The 1-4 failures across multiple runs are:

Test Status Note
consensus/parity-harness G7.1 large iteration finishes within budget known flake 5000ms perf borderline under CI load (MEMORY.md §test count progression). Retry-clean.
server.test.ts donor-bug regressions › main() IIFE smoke — script invocation boots and writes a startup log known flake Process-spawn startup-log timing under CI load. Retry-clean.
scripts/script-invocation-skills.test.ts spawns dist/server.js, calls skill_list known flake Same family as server.test.ts (process-spawn + skill_list timing). Retry-clean.
reputation/witnesses W1/W2 flake Confirmed retry-clean (21/21 pass in isolation).

None of these are in src/domains/integrity/ and none touch drift.ts, schema.ts, or anything else in the μ axis. They are pre-existing CI-load timing flakes unrelated to this PR. The drift suite itself is 60/60 deterministic.

Per MEMORY.md “Known CI-load flakes (all retry-clean)” listing — parity-harness G7.1 is explicitly named there. The server-spawn flake family is a sibling.

6. Surface delta

Files added:
  src/domains/integrity/detectors/drift.ts                   (~430 LOC)
  src/__tests__/domains/integrity/detectors/drift.test.ts    (~520 LOC)
  docs/audits/p4-2-3-drift-tracker-audit.md
  docs/contracts/p4-2-3-drift-tracker-contract.md
  docs/packets/p4-2-3-drift-tracker-packet.md
  docs/verification/p4-2-3-drift-tracker-verification.md  (this file)

Files modified: none

Files NOT touched (firewall held):
  src/domains/integrity/schema.ts        (P4.1.1, frozen)
  src/domains/integrity/detectors/circular.ts (P4.2.1 owned, not present in this branch)
  src/domains/integrity/detectors/coercion.ts (P4.2.2 owned, not present in this branch)
  All other src/ files                   (consumed via npm test only)

7. Notes for downstream (Wave 3 P4.4.1, P4.7.1, P4.8.1)

7.1. For P4.4.1 (escalation FSM)

  • Import the two detector check codes as TypeScript literal strings. axiom_drift with result === 'BLOCK' routes to π proposal-intake denial; axiom_regression with result === 'BLOCK' routes to α tool-lock (HARD BLOCK).
  • The drift detector emits at most ONE axiom_drift advisory per call (WARN xor BLOCK xor neither). It emits up to proposals × 7 axiom_regression advisories. FSM should expect this fan-out shape.
  • The ParameterChange[] argument is caller-supplied. The escalation FSM is the natural seam to read whichever shipped persistence exists at the time (π governance rule-version history, or a fresh mcp_parameter_changes table introduced by P4.5.1).

7.2. For P4.7.1 (FP corpus)

  • G10 ships a stub 12-month synthetic corpus (constant deltas across months 0..11, observed at month 11). The real FP measurement should use varied delta patterns (bursty, stepwise, oscillating, cancelling) to bound false-positive rates per the integrity.md “false-positive profile: high in the absence of long time-series data” note.
  • The detector is a pure function — FP measurement can run as a trivial Jest harness without any DB / runtime setup.

7.3. For P4.8.1 (fork-hook subscriber)

  • On post-fork events, P4.8.1 may need to reset cumulative drift for the affected domain. The detector itself has no persistent state — the reset is at the caller’s ParameterChange[] adapter (truncate the feed or filter by an after-fork epoch boundary).
  • AXIOM_IDS is exported so P4.8.1 can iterate the same set when resetting per-axiom invariant trackers.

7.4. Surprising finding — λ parameter-change shape

src/domains/reputation/penalties.ts (λ P2.2.2) defines a ReputationHistoryRow with a delta: number (negative on penalty per AX-09), but reputation_history is for REPUTATION events, not for parameter changes. There is no parameter_changes table in the shipped schema at 49560518. The ParameterChange shape used by P4.2.3 ({ domain, delta_bps: bigint, timestamp_logical: bigint }) matches the dispatch-packet shape verbatim — it is a feed adapter, NOT a query against an existing table.

Action for P4.4.1 (Wave 3 escalation FSM): the FSM needs to construct the ParameterChange[] feed from whatever persistence is available. Candidates:

  1. π governance rule-version history (P6.* — Phase 6, not shipped).
  2. A fresh mcp_parameter_changes table introduced by P4.5.1 (advisory persistence slice).
  3. A direct adapter over κ RuleVersion rows (P1.5.1 versioning).

The drift detector itself decouples cleanly — the caller picks the source.

7.5. AX-01..AX-07 listing — note for P4.4.1 + P4.8.1

AXIOM_IDS is sourced from docs/3-world/physics/constitution.md §AX-01..AX-07, identical text mirrored in laws/constraints.md §Constitutional Axioms L583-624. The seven ids:

  • AX-01 — Append-Only Events
  • AX-02 — Reputation is Derived
  • AX-03 — No Absolute Authority
  • AX-04 — Consequence Windows
  • AX-05 — Subjective Finality
  • AX-06 — Right to Exit (the 10% cap that derives BLOCK_THRESHOLD_BPS)
  • AX-07 — Technical Sovereignty

Wave 3 should mirror this exact ordering when implementing per-axiom routing — divergent ordering breaks parity tests downstream.

8. Sign-off

P4.2.3 ships with all acceptance criteria green. The build + lint + drift-suite gates pass cleanly. Full-suite failures are limited to pre-existing known-flake categories (parity-harness G7.1 perf borderline + process-spawn startup-log timing + witnesses CI-load) — none in the integrity domain.

Ready to merge. Wave 3 P4.4.1 can proceed once the parallel P4.2.1 + P4.2.2 detectors also land (file-disjoint, no merge conflict expected).


Back to top

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

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