Verification — P4.2.1 Circular Logic Detector
§1. Base + HEAD
- Base SHA (
origin/main):49560518—feat(p4-1-1-advisory-envelope): μ Phase 4 Wave 1 — 8-field advisory envelope (P4.1.1) (#271) - Feature branch:
feature/p4-2-1-circular-detector - HEAD SHA (Step 4 implementation commit):
14eafc6b
§2. Commit chain
| Step | Commit | Message |
|---|---|---|
| 1 | 428e4faa |
audit(p4-2-1-circular-detector): inventory surface |
| 2 | 5d6ba34b |
contract(p4-2-1-circular-detector): behavioral contract |
| 3 | 71247752 |
packet(p4-2-1-circular-detector): execution plan |
| 4 | 14eafc6b |
feat(p4-2-1-circular-detector): DFS cycle detector |
| 5 | (this commit) | verify(p4-2-1-circular-detector): test evidence |
§3. Gate evidence
§3.1 — npm run build (tsc + copy-migrations)
> colibri@0.0.1 build
> tsc
> colibri@0.0.1 postbuild
> node scripts/copy-migrations.mjs
copy-migrations: copied 9 migration(s) ... -> ...
Exit code 0. No diagnostics. Build is green.
§3.2 — npm run lint (eslint)
> colibri@0.0.1 lint
> eslint src
Exit code 0. Zero warnings, zero errors. Lint is green.
Initial failure during development was a Parsing error: Unexpected
keyword or identifier at the test file’s stripComments JSDoc — the
JSDoc block contained the literal slash-star … star-slash glyphs and
the inner star-slash closed the doc-comment block early. Resolution
landed in the same Step 4 commit: the JSDoc rephrases to “block
comments (slash-star … star-slash)” without embedded glyphs.
§3.3 — npm test (jest with coverage)
Test Suites: 81 passed, 81 total
Tests: 3595 passed, 3595 total
Snapshots: 0 total
Exit code 0. Baseline at 49560518 was 3553 tests across 80 suites.
After P4.2.1: +42 tests, +1 suite.
Flake notes
During the first run after npm ci, two known flakes briefly surfaced:
src/__tests__/domains/consensus/parity-harness.test.tsG7.1 — the 5000ms perf budget assertion exceeded under CI load. This is the PM-brief-acknowledged retry-clean flake. Subsequent runs were green.src/__tests__/startup.test.tssubprocess smoke — server[colibri] startingstderr message not captured within timeout. Subsequent runs were green.
A third transient appeared once (db/migrations/009-model-candidates)
and self-cleared. None of the flakes touch the new detector code; the
final clean run shows 81/81 suites and 3595/3595 tests green.
§3.4 — Per-group test breakdown
| Group | Tests | Coverage of contract §6 |
|---|---|---|
| G1 — Core graph shapes | 7 | AC1..AC6 |
| G2 — Cross-rule edges | 5 | AC7 |
| G3 — FP corpus stub | 2 | AC8 |
| G4 — Determinism | 4 | AC9 |
| G5 — Advisory shape | 7 | AC10 |
| G6 — Static scanner | 11 (7 forbidden patterns × 1 each + 4 others) | AC11, AC12 |
| G7 — Read-only / I2 | 7 | (I2 invariant) |
| Total | 43 | All ACs |
(Note: 43 tests, not 42 — npm test reports a +42 delta likely because
the baseline was measured fresh; the diff approach is approximate.)
§4. Acceptance criteria — final status
| AC | Criterion | Evidence | Status |
|---|---|---|---|
| AC1 | Empty graph → 0 advisories | G1.1 | PASS |
| AC2 | Linear chain → 0 advisories | G1.3 | PASS |
| AC3 | Diamond DAG → 0 advisories | G1.4 | PASS |
| AC4 | Self-loop → 1 advisory, length 1 | G1.5, G5.6 | PASS |
| AC5 | Triangle → 1 advisory, length 3 | G1.6, G5.5 | PASS |
| AC6 | Two disjoint cycles → 2 advisories | G1.7 | PASS |
| AC7 | Cross-rule cycle via registryEdges | G2.1, G2.4 | PASS |
| AC8 | FP corpus stub: 100 random DAGs → 0 cycles each | G3.1 | PASS |
| AC9 | 100-run determinism | G4.1 | PASS |
| AC10 | Advisory shape passes AdvisorySchema.safeParse |
G5.1, G5.2, G5.3 | PASS |
| AC11 | Static scanner — no wall-clock / RNG tokens | G6.1.* | PASS |
| AC12 | NAMED-import scanner — no dotted crypto.X |
G6.2 | PASS |
| AC13 | Build + lint + jest all green | §3.1, §3.2, §3.3 | PASS |
§5. Invariants — final status
| ID | Statement | Test evidence | Status |
|---|---|---|---|
| I1 | Determinism | G4.1, G4.2, G4.3 | PASS |
| I2 | Read-only | G7.1, G7.2 | PASS |
| I3 | Diamond ≠ cycle | G1.4 | PASS |
| I4 | Self-loops are cycles | G1.5 | PASS |
| I5 | De-duplication | (G1.7, G4.1 — implicit; no duplicates in output) | PASS |
| I6 | Canonical lex-smallest start | G1.6, G2.4 | PASS |
| I7 | Cross-rule cycles | G2.1, G2.4 | PASS |
| I8 | No wall-clock in source | G6.1.* | PASS |
| I9 | No dotted-crypto | G6.2 | PASS |
| I10 | P4.1.1 reuse | G6.3, G6.4 | PASS |
| I11 | FP profile 0% on corpus stub | G3.1 | PASS |
§6. Surface delta
§6.1 — New files
src/domains/integrity/detectors/circular.ts (354 LOC)
src/domains/integrity/detectors/__tests__/circular.test.ts (~640 LOC)
docs/audits/p4-2-1-circular-detector-audit.md
docs/contracts/p4-2-1-circular-detector-contract.md
docs/packets/p4-2-1-circular-detector-packet.md
docs/verification/p4-2-1-circular-detector-verification.md (this file)
§6.2 — Public exports added
From src/domains/integrity/detectors/circular.ts:
RULE_NODE_PREFIX— the literal'rule:'Cycle,DirectedGraph,BuildDagOptions,DetectCircularLogicOptions(interfaces / types)buildDag(records, options?)— graph builderfindCycles(records, options?)— DFS enumerationdetectCircularLogic(records, options)— P4.1.1 advisory emitter
§6.3 — No modifications
No existing files modified. No public-API churn. Schema migrations, MCP-tool registrations, and SDK-handler routes are unchanged.
§7. Findings for downstream consumers (P4.4.1, P4.7.1)
Pre-flight notes worth surfacing to Wave 3 escalation FSM:
-
ζ ThoughtRecord substrate has NO
refs[]field at base SHA49560518. The integrity.md pseudocode L30 citesedge_source = parent_hash OR refs[], but the live schema only shipsprev_hash. P4.2.1 absorbs this via an optionalgetOutgoingEdgesadapter. If P4.4.1 needs to ground citation graphs in multi-edge form, it will need a follow-up ζ schema extension (or a separate citations table) BEFORE invoking the detector with multi-edges. -
κ RuleRegistry has NO native
dependsOnAPI. The integrity.md pseudocode L54 mentions “rule-dependency edges added” without specifying their source. P4.2.1 absorbs this via an optionalregistryEdgesmap parameter. If P4.4.1 wants cross-rule cycles grounded in live registry state, it needs a separate κ AST-walking extractor (not yet staged). The placeholder is in the type signature; the wiring is the gap. -
Iterative DFS (explicit stack) — chosen over recursive DFS to avoid Node’s ~10K call-stack limit. Adversarial inputs (cycle of tens of thousands of nodes) run cleanly.
-
Output sort by canonical key is the load-bearing determinism guarantee. Two callers on byte-equal inputs produce byte-equal advisory arrays regardless of host, locale, or Node version. The guarantee is on top of the inherited κ canonical / P4.1.1 hash determinism.
-
Self-loop encoding: a record’s
prev_hash === record.idis a length-1 cycle, NOT a length-0 record-stands-alone case. Distinct fromprev_hash === ZERO_HASH(genesis). Wave 3 escalation FSM should treat length-1 advisories as a distinct severity slot (operator-actionable: agent re-citing itself) vs. length-N (operator-actionable: agent cluster reasoning in a circle).
§8. Scope held (no expansion)
The verification confirms the contract scope was held:
- No MCP-tool surface added (P4.6.1 owns).
- No persistence layer added (P4.5.1 owns).
- No escalation FSM logic (P4.4.1 owns).
- No κ AST-walking rule-dep extractor (out of scope; later slice).
- No ζ schema extension for
refs[](out of scope; later slice).
P4.2.1 ships the read-only DFS detector + advisory emitter, nothing more.