P0.5.1 — δ Intent Scoring — Execution Packet
Step 3 of the 5-step chain. File-by-file plan, test inventory, rollback strategy. Gates Step 4 (implement).
1. File plan
1.1 src/domains/router/scoring.ts — new file
Full file layout:
/** JSDoc header: purpose, ADR-005 citation, Phase 0 invariants, Phase 1.5 promise. */
// --- types ---------------------------------------------------------------
export type ModelId = 'claude';
export interface ScoreContext {
readonly toolCount?: number;
readonly complexity?: string;
readonly [key: string]: unknown;
}
export interface IntentScore {
readonly scores: Readonly<Record<ModelId, number>>;
readonly winner: ModelId;
}
// --- shared constant -----------------------------------------------------
const PHASE_0_CLAUDE_WINNER: IntentScore = Object.freeze({
scores: Object.freeze({ claude: 1.0 }),
winner: 'claude' as const,
});
// --- public API ----------------------------------------------------------
export function scoreIntent(_prompt: string, _context: ScoreContext = {}): IntentScore {
return PHASE_0_CLAUDE_WINNER;
}
Notes on choices:
- The frozen constant
PHASE_0_CLAUDE_WINNERis module-scoped (not per-call). Every call returns the same object identity in Phase 0. This is load-bearing: tests assertscoreIntent(a, b) === scoreIntent(c, d)to prove the stub does no per-call work. _promptand_contextcarry the ESLint-friendly underscore prefix (matchsrc/domains/integrations/notifications.tsconvention).- The
_context: ScoreContext = {}default matches the stub’s forward-compat promise — callers MAY pass no context in Phase 0 without a TypeScript error. - No import from
zod. Validation is unnecessary — the function reads neither argument.
1.2 src/domains/router/index.ts — new barrel
export * from './scoring.js';
One line. P0.5.2 will append a second export line after this PR merges.
1.3 src/__tests__/domains/router/scoring.test.ts — new test file
16 test cases across 8 describe blocks:
describe('scoreIntent — constant output invariants', () => {
1. returns {scores: {claude: 1.0}, winner: 'claude'} for a trivial prompt
2. claude score is exactly 1.0
3. winner is exactly "claude"
4. scores contains exactly one key
});
describe('scoreIntent — prompt-invariance', () => {
5. returns constant for empty prompt
6. returns constant for short prompt
7. returns constant for long prompt (10k chars)
8. returns constant for unicode + emoji prompt
9. returns constant for whitespace-only prompt
});
describe('scoreIntent — context-invariance', () => {
10. ignores empty context
11. ignores toolCount in context
12. ignores complexity in context
13. ignores arbitrary context keys
});
describe('scoreIntent — determinism', () => {
14. same input twice returns deep-equal result
});
describe('scoreIntent — immutability', () => {
15. returned object is frozen (winner and scores both)
16. returned scores object is frozen (direct assignment blocked in strict mode)
});
Implementation notes:
- Tests use
deepEqual(JesttoEqual) for structural assertions andtoBefor identity assertions on the module-scoped constant. - Immutability test uses
'use strict'implicit via ESM + TS"strict": trueintsconfig. Mutation attempts throwTypeError: Cannot assign to read only property; we assert withexpect(() => { … }).toThrow(). - No file system, no network, no env mocking. Tests are 100% pure.
2. What this task does NOT touch
src/server.ts— no change. No tool registered.src/config.ts— no change. No env var added.src/domains/integrations/*— no change. No adapter wiring.src/domains/tasks/,src/domains/trail/,src/domains/proof/,src/domains/skills/— no change.src/db/schema.sql,src/db/migrations/*— no change. δ does not touch SQLite in Phase 0.- Any document outside
docs/{audits,contracts,packets,verification}/p0-5-1-scoring-*.md— no change.
3. Implementation sequence
- Create
src/domains/router/directory. - Write
src/domains/router/scoring.tsper §1.1. - Write
src/domains/router/index.tsper §1.2. - Create
src/__tests__/domains/router/directory. - Write
src/__tests__/domains/router/scoring.test.tsper §1.3. - Run
npm run build— must compile clean. - Run
npm run lint— must be zero warnings. - Run
npm test— all suites pass; test count rises by 16 (1025 → 1041 baseline-passing, or 1026 → 1042 counting the known startup-subprocess smoke flake which is unrelated). - Commit Step 4.
- Write verification doc (Step 5) citing test counts.
- Commit Step 5.
- Push and open PR.
4. Rollback strategy
Worst-case rollback path (if a downstream regression surfaces after merge):
- Revert the
feat(p0-5-1)commit. The module is a leaf (no callers) — reverting leaves no dangling imports. - The barrel
src/domains/router/index.tsbecomes empty; remove the file if orphaned. - No migration was added, so no schema rollback.
- No env var was added, so no
.envcleanup is needed.
Delete-blast-radius is one directory (src/domains/router/) + one test directory. No other file edits exist in this PR.
5. Gate
Per CLAUDE.md §5 and the Wave G feedback_lint_gate_escape.md lesson:
npm run build && npm run lint && npm test
All three must pass. Sub-agents that skip lint get caught by CI; we run the full gate locally to close that escape.
6. Commit plan
| Step | Commit message |
|---|---|
| 1 | audit(p0-5-1-scoring): inventory surface |
| 2 | contract(p0-5-1-scoring): behavioral contract |
| 3 | packet(p0-5-1-scoring): execution plan ← this commit |
| 4 | feat(p0-5-1): δ intent scoring Phase 0 stub per ADR-005 |
| 5 | verify(p0-5-1-scoring): test evidence |
7. Packet approval
Gates Step 4. Implementation must exactly match §1.1, §1.2, §1.3 file layouts. Deviation requires a re-issued packet.