Verification — P4.3.1 Three Advisory Roles (Translator / Sentinel / Guide)
1. Build / lint / test status
All three gates per CLAUDE.md §5 pass on this worktree
(feature/p4-3-1-advisory-roles, base 41226615):
npm run build → ok (TypeScript compile clean, migrations copied)
npm run lint → ok (eslint src — 0 errors)
npm test → ok (3745 / 3745 tests, 84 / 84 suites)
Base anchor: main 41226615 (after PR #274 — P4.2.1 circular detector
merged). Pre-existing test count baseline at this SHA: ~3650 tests (per the
dispatch packet). Post-P4.3.1 count: 3745 tests (Δ = +95 across the new
roles suite, including a 9-cell sentinel boundary table via test.each).
Flake note: initial full-suite run surfaced known retry-clean flakes (per memory):
parity-harness G7.1(5000ms perf borderline) — passes on retry aloneserver.test.ts main() IIFE smoke— startup-load flake, passes on retryinteger-math.test.ts decay 1e9 adversarial epochs— load-induced timeout
A second full-suite run at --maxWorkers=4 returned 3745 / 3745 pass,
84 / 84 suites green. None of the failures touched roles.ts or its tests.
2. Acceptance criteria — sign-off
| AC# | Description | Test group | Status |
|---|---|---|---|
| AC#1 | Translator class exported with readonly role = 'Translator' |
G1 (2 tests), G13 (4 tests) | PASS |
| AC#2 | Sentinel class exported with readonly role = 'Sentinel' |
G3 (9 cells), G4 (3 tests), G13 | PASS |
| AC#3 | Guide class exported with readonly role = 'Guide' |
G6 (3 tests), G7 (1 test), G8 (3 tests), G13 | PASS |
| AC#4 | Output types distinct: string / SentinelFlag \| null / Suggestion[] |
G1, G3, G6, G12 (4 tests) | PASS |
| AC#5 | No “Mutator” role exported | G11 (2 tests) | PASS |
| AC#6 | Sentinel emits flag, never calls π/α/κ/λ directly | G4, G5 (2 tests), G10 (forbidden imports) | PASS |
| AC#7 | Guide suggests, never executes (no mutation API) | G7, G8, G10 | PASS |
| AC#8 | Static-analysis gate passes (no mutation tokens in roles.ts) | G10 (10 literal-substring × test.each + regex follow-ups) |
PASS |
| AC#9 | npm run build && npm run lint && npm test ALL pass |
gate | PASS |
| AC#10 | No Date.now(), Math.random(), performance.now() in roles.ts |
G10 (regex + literal) | PASS |
| AC#11 | Severity-rank semantics inclusive at boundary | G3 (9-cell truth table) | PASS |
| AC#12 | Guide cardinality ≤ 4 (one per check) | G6 (3 cardinality tests) | PASS |
3. Invariants — sign-off
| § | Invariant | Enforcement | Status |
|---|---|---|---|
| §I1 | No mutator role | G11 — module-exports snapshot + comment-stripped Mutator scan | PASS |
| §I2 | Roles are pure presentation | G5 (sentinel no-mutation) + G10 (no forbidden imports) | PASS |
| §I3 | Static-analysis gate enforced at test time | G10 — 14 patterns scanned over comment-stripped source | PASS |
| §I4 | Determinism | G2 (×100 translator) + G5 (×100 sentinel) — byte-identical outputs | PASS |
| §I5 | No fourth role | G11 — 4 exported runtime names exactly (Guide, SEVERITY_RANK, Sentinel, Translator) | PASS |
| §I6 | Output types are distinct | G12 (4 tests on type discrimination) | PASS |
| §I7 | readonly role field on each class |
G13 (4 tests covering all three classes + idempotency) | PASS |
4. Test inventory
4.1. New file — src/__tests__/domains/integrity/roles.test.ts
| Group | Subject | Tests |
|---|---|---|
| G1 | Translator output substring containment | 2 |
| G2 | Translator determinism + truncation | 3 |
| G3 | Sentinel boundary table (9 cells × test.each) |
9 |
| G4 | Sentinel flag shape | 3 |
| G5 | Sentinel never mutates input | 2 |
| G6 | Guide cardinality bound | 3 |
| G7 | Guide empty input | 1 |
| G8 | Guide grouping correctness | 3 |
| G9 | Guide ignores state parameter |
1 |
| G10 | Static-analysis scanner (code-only) — 10 literals × test.each + 3 follow-ups |
14 |
| G11 | No Mutator role exported | 2 |
| G12 | Output types structurally distinct | 4 |
| G13 | readonly role field on each class |
4 |
| G14 | SEVERITY_RANK exported | 3 |
| Total | 54 |
All 54 tests pass; total suite count is now 3745 (was ~3650 at base).
5. Static-analysis sweep — roles.ts
Comment-stripped source (block + line comments removed) scanned for the following forbidden tokens — all 0 hits:
db.run 0 hits
db.exec 0 hits
db.prepare 0 hits
UPDATE 0 hits
INSERT 0 hits
DELETE 0 hits
mutate 0 hits
Date.now 0 hits
Math.random 0 hits
performance.now 0 hits
update<Capital> 0 hits (regex /update[A-Z]/)
new Date 0 hits (regex /\bnew\s+Date\b/)
[Mm]utator 0 hits (in code; doc comments mention "no mutator role" per integrity.md L127)
Forbidden imports scanned in the import-declaration text:
from 'node:fs' 0 hits
from 'node:crypto' 0 hits
from 'better-sqlite3' 0 hits
from '../tasks/' 0 hits
from '../trail/' 0 hits
from '../proof/' 0 hits
from '../reputation/' 0 hits
from '../consensus/' 0 hits
from '../skills/' 0 hits
from '../router/' 0 hits
from '../rules/' 0 hits
The only import in roles.ts is:
import type { Advisory, AdvisorySeverity } from './schema.js';
A type-only import; erased at runtime; zero runtime coupling.
6. Determinism evidence
- Translator: G2 ×100 same-advisory invocation → byte-identical strings.
- Sentinel: G5 ×100 same-advisory + same-threshold → byte-identical flag (action, reason, advisory all identical).
- Guide: G8 verifies
advisory_refspreserves input order; G9 verifies identical output across 5 differentstatevalues. - No clock reads: G10 confirms no
Date.now, noperformance.now, nonew Datein code. - No randomness: G10 confirms no
Math.randomin code.
7. Sentinel boundary verification — 9-cell table
| Advisory severity | Threshold | Expected | Observed (test) |
|---|---|---|---|
| LOW | LOW | flag emitted | PASS |
| LOW | MED | null | PASS |
| LOW | HIGH | null | PASS |
| MED | LOW | flag emitted | PASS |
| MED | MED | flag emitted | PASS |
| MED | HIGH | null | PASS |
| HIGH | LOW | flag emitted | PASS |
| HIGH | MED | flag emitted | PASS |
| HIGH | HIGH | flag emitted | PASS |
All cells via test.each parameterized form. The boundary is inclusive at
equality (rank-adv >= rank-thr).
8. Guide grouping verification — example
Input (G6 test #1):
[
{ check: 'circular_logic', hashInput: { i: 1 } },
{ check: 'circular_logic', hashInput: { i: 2 } },
{ check: 'coercion_trap', hashInput: { i: 3 } },
{ check: 'axiom_drift', hashInput: { i: 4 } },
{ check: 'axiom_regression', hashInput: { i: 5 } },
]
Output (verified):
[
{ headline: 'Address circular logic', advisory_refs: [hash1, hash2], rationale: '2 advisory record(s)...' },
{ headline: 'Address coercion trap', advisory_refs: [hash3], rationale: '1 advisory record(s)...' },
{ headline: 'Address axiom drift', advisory_refs: [hash4], rationale: '1 advisory record(s)...' },
{ headline: 'Address axiom regression', advisory_refs: [hash5], rationale: '1 advisory record(s)...' },
]
Cardinality bound: 4 suggestions on 4 distinct checks (one per group). Order: insertion order of distinct check values (Map iteration).
9. Module-exports snapshot
Runtime exports (G11):
Guide, SEVERITY_RANK, Sentinel, Translator
Four entries exactly. Zero entries match /[Mm]utator/. Type-only exports
(SentinelFlag, Suggestion) are TypeScript erasure — not present at
runtime.
10. Notes for Wave-4 P4.6.1 (MCP tool wrapper)
The roles module is structured for cheap MCP-tool wrapping. Each role method has a single typed parameter (Translator: 1, Sentinel: 2, Guide: 2) and a typed return shape. Wave-4 wrappers should:
- Translator → wrap
summarize(advisory)asmu_translator_summarize.- Input schema:
AdvisorySchema(P4.1.1). - Output: text/plain content with the summary string.
- Stateless — instantiate the class per-call or cache module-level.
- Input schema:
- Sentinel → wrap
flag(advisory, threshold)asmu_sentinel_flag.- Input schema:
{ advisory: AdvisorySchema, severityThreshold: AdvisorySeveritySchema }. - Output: structured JSON with either
SentinelFlagshape ornull. - The
actionliteral is always'escalate_to_pi'in P4.3.1; if Wave-3 P4.4.1 changes this in the FSM layer, MCP schema may need an update.
- Input schema:
- Guide → wrap
suggest(state, advisories)asmu_guide_suggest.- Input schema:
{ state: z.unknown(), advisories: z.array(AdvisorySchema) }. - Output: structured JSON
Suggestion[]. - The
stateparameter is forward-compat slack; v1 ignores it.
- Input schema:
The role classes themselves carry no state across calls, so MCP wrappers
can hold a single module-level instance (const guide = new Guide())
without contention.
11. Notes for Wave-3 P4.4.1 (Escalation FSM)
The escalation FSM consumes SentinelFlag objects. The action field’s
'log_only' literal is reserved for FSM use when the flag fires on a
result: 'PASS' advisory (which P4.3.1’s Sentinel.flag does not emit —
P4.3.1 always emits 'escalate_to_pi').
The FSM should NOT re-call Sentinel.flag; instead it should consume the
flag object directly and dispatch based on advisory.check,
advisory.result, and advisory.severity.
12. Acceptance — final sign-off
All 12 acceptance criteria + 7 invariants pass. The implementation is ready for PR review. Five commits on the feature branch:
audit(p4-3-1-advisory-roles): inventory surfacecontract(p4-3-1-advisory-roles): behavioral contractpacket(p4-3-1-advisory-roles): execution planfeat(p4-3-1-advisory-roles): three read-only rolesverify(p4-3-1-advisory-roles): test evidence(this file)
End of verification.