Execution Packet — P4.3.1 Three Advisory Roles
1. Goal
Ship src/domains/integrity/roles.ts + src/__tests__/domains/integrity/roles.test.ts
implementing three read-only advisory roles (Translator / Sentinel / Guide)
per docs/contracts/p4-3-1-advisory-roles-contract.md.
All three gates pass: npm run build && npm run lint && npm test.
2. File plan
src/domains/integrity/roles.ts (~180 lines)
src/__tests__/domains/integrity/roles.test.ts (~430 lines)
docs/audits/p4-3-1-advisory-roles-audit.md (shipped Step 1)
docs/contracts/p4-3-1-advisory-roles-contract.md (shipped Step 2)
docs/packets/p4-3-1-advisory-roles-packet.md (this file)
docs/verification/p4-3-1-advisory-roles-verification.md (Step 5)
3. Implementation sketch — roles.ts
import type { Advisory, AdvisorySeverity } from './schema.js';
// Public types
export type SentinelFlag = {
action: 'escalate_to_pi' | 'log_only';
reason: string;
advisory: Advisory;
};
export type Suggestion = {
headline: string;
advisory_refs: string[];
rationale: string;
};
// Constants
export const SEVERITY_RANK: Readonly<Record<AdvisorySeverity, number>> = Object.freeze({
LOW: 0,
MED: 1,
HIGH: 2,
});
const TRUNCATION_LIMIT = 240;
const TRUNCATION_SUFFIX = '…';
// Helpers (module-private)
function truncate(s: string): string { ... }
function headlineFor(check: Advisory['check']): string { ... }
function rationaleFor(check: Advisory['check'], group: readonly Advisory[]): string { ... }
// Role classes
export class Translator {
readonly role = 'Translator' as const;
summarize(advisory: Advisory): string { ... }
}
export class Sentinel {
readonly role = 'Sentinel' as const;
flag(advisory: Advisory, severityThreshold: AdvisorySeverity): SentinelFlag | null { ... }
}
export class Guide {
readonly role = 'Guide' as const;
suggest(state: unknown, advisories: readonly Advisory[]): Suggestion[] { ... }
}
4. Implementation order
- Skeleton — module header + imports + closed-set helper signatures.
SEVERITY_RANK+truncatehelper.Translator.summarize— template fill + truncation.Sentinel.flag— rank lookup + flag construction.headlineFor+rationaleForhelpers.Guide.suggest— Map-based grouping + insertion-order preservation.- Test file — 14 G-groups per contract §6.
- Build / lint / test — iterate until all gates pass.
5. Test fixture sketch
import {
computeDecisionHash,
type Advisory,
} from '../../../domains/integrity/schema.js';
import {
Guide,
Sentinel,
Translator,
SEVERITY_RANK,
type SentinelFlag,
type Suggestion,
} from '../../../domains/integrity/roles.js';
function makeAdvisory(overrides: Partial<Advisory> = {}): Advisory {
const role = overrides.role ?? 'Sentinel';
const check = overrides.check ?? 'circular_logic';
const result = overrides.result ?? 'WARN';
const input = { fixture: 1 };
return {
role,
check,
result,
severity: 'MED',
evidence: [],
recommendation: 'Investigate the cycle and break it at the weakest edge.',
decision_hash: computeDecisionHash(role, check, input, result),
timestamp_logical: 1n,
...overrides,
};
}
6. Static scanner (G10) implementation pattern
const ROLES_SRC = readFileSync(rolesPath, 'utf8');
const FORBIDDEN_LITERAL_PATTERNS: readonly string[] = [
'db.run',
'db.exec',
'db.prepare',
'UPDATE',
'INSERT',
'DELETE',
'mutate',
'Date.now',
'Math.random',
'performance.now',
];
for (const pattern of FORBIDDEN_LITERAL_PATTERNS) {
expect(ROLES_SRC).not.toContain(pattern);
}
// Verb-method regex (e.g. updateRow, updateState)
expect(ROLES_SRC).not.toMatch(/update[A-Z]/);
7. Module-exports snapshot (G11 — no Mutator)
import * as roles from '../../../domains/integrity/roles.js';
const exportedNames = Object.keys(roles).sort();
expect(exportedNames).toEqual([
'Guide',
'SEVERITY_RANK',
'Sentinel',
'Translator',
// type-only exports are erased at runtime and do not appear here
]);
expect(exportedNames).not.toContain('Mutator');
for (const name of exportedNames) {
expect(name).not.toMatch(/[Mm]utator/);
}
Note: TS type aliases (SentinelFlag, Suggestion) are erased from the
runtime * import object; only class-declared and const-declared
runtime values appear. The snapshot is therefore 4 entries.
8. Sentinel boundary table (G3)
The 9-cell truth table from the contract §6:
const cases: ReadonlyArray<[AdvisorySeverity, AdvisorySeverity, boolean]> = [
['LOW', 'LOW', true],
['LOW', 'MED', false],
['LOW', 'HIGH', false],
['MED', 'LOW', true],
['MED', 'MED', true],
['MED', 'HIGH', false],
['HIGH', 'LOW', true],
['HIGH', 'MED', true],
['HIGH', 'HIGH', true],
];
const sentinel = new Sentinel();
for (const [adv, thr, shouldFlag] of cases) {
const advisory = makeAdvisory({ severity: adv });
const flag = sentinel.flag(advisory, thr);
if (shouldFlag) {
expect(flag).not.toBeNull();
expect(flag!.action).toBe('escalate_to_pi');
expect(flag!.advisory).toBe(advisory);
} else {
expect(flag).toBeNull();
}
}
9. Guide cardinality test (G6)
const guide = new Guide();
const advisories: Advisory[] = [
makeAdvisory({ check: 'circular_logic', recommendation: 'cyc-1' }),
makeAdvisory({ check: 'circular_logic', recommendation: 'cyc-2' }),
makeAdvisory({ check: 'coercion_trap', recommendation: 'coe-1' }),
makeAdvisory({ check: 'axiom_drift', recommendation: 'drf-1' }),
makeAdvisory({ check: 'axiom_regression', recommendation: 'reg-1' }),
];
const out = guide.suggest({}, advisories);
expect(out).toHaveLength(4);
const checks = out.map((s) => s.headline);
expect(checks).toEqual([
'Address circular logic',
'Address coercion trap',
'Address axiom drift',
'Address axiom regression',
]);
// First suggestion (circular_logic) has 2 advisory refs
expect(out[0]!.advisory_refs).toHaveLength(2);
10. Gotchas / pitfalls
10.1. decision_hash collision across fixtures
makeAdvisory calls computeDecisionHash(role, check, input, result). If
two fixtures share (role, check, input, result) they will share
decision_hash. Fixtures that need distinct hashes must vary input or
one of the closed enums. The Guide grouping test (G8) uses distinct
input shapes to ensure advisory_refs contains distinct hashes.
10.2. Test path levels
src/__tests__/domains/integrity/roles.test.ts imports:
../../../domains/integrity/schema.js(3 levels up + domain path)../../../domains/integrity/roles.js(same)
Compare against schema.test.ts line 45 (../../../domains/integrity/schema.js)
— same depth. detectors/drift.test.ts is one level deeper (4 levels up).
10.3. Truncation determinism
truncate(s) must not call Math.random() or use locale-aware string
length. Use s.length (UTF-16 code units) for the cap; this is
deterministic across hosts (mirrors κ canonical’s locale-independence).
10.4. as const on readonly role field
readonly role = 'Translator' as const;
The as const is required to narrow the type from string to the literal
'Translator'. Without it, TS infers the field as string and the
contract §I7 is violated (the field would not be a literal).
10.5. Empty advisories array
Guide.suggest({}, []) MUST return []. The Map-based grouping naturally
handles this (empty iteration produces empty output), but assert
explicitly in G7.
10.6. Object.freeze on SEVERITY_RANK
Belt-and-braces — Object.freeze prevents at-runtime mutation by buggy
callers. The readonly modifier is compile-time only.
11. Risk surface
as constdiscipline — Forgettingas conston thereadonly rolefield drops the literal type. Tests assertinstance.role === 'Translator'(etc.) to catch this.- Map insertion order vs. Object.keys order — Map preserves insertion
order per ES2015 spec; iterating with
for ... of bucketsworks. UsingObject.entries(buckets)on a plain object would also preserve insertion order for string keys per ES2020+, but Map is more idiomatic. - Pure-function discipline — The CI grep gate is the safety net. The
contract requires the test suite to read
roles.tsand assert zero forbidden patterns; this is the runtime invariant.
12. Estimated effort
- Step 4 implementation: ~30 min
- Step 5 tests + verification: ~30 min
- Total: ~1 hour (M task — medium complexity, well-scoped surface).
13. Commit plan
| # | Commit | Files |
|---|---|---|
| 1 | audit(p4-3-1-advisory-roles): inventory surface |
docs/audits/p4-3-1-advisory-roles-audit.md |
| 2 | contract(p4-3-1-advisory-roles): behavioral contract |
docs/contracts/p4-3-1-advisory-roles-contract.md |
| 3 | packet(p4-3-1-advisory-roles): execution plan |
docs/packets/p4-3-1-advisory-roles-packet.md |
| 4 | feat(p4-3-1-advisory-roles): three read-only roles |
src/domains/integrity/roles.ts, src/__tests__/domains/integrity/roles.test.ts |
| 5 | verify(p4-3-1-advisory-roles): test evidence |
docs/verification/p4-3-1-advisory-roles-verification.md |
End of packet. Implementation may proceed.