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_WINNER is module-scoped (not per-call). Every call returns the same object identity in Phase 0. This is load-bearing: tests assert scoreIntent(a, b) === scoreIntent(c, d) to prove the stub does no per-call work.
  • _prompt and _context carry the ESLint-friendly underscore prefix (match src/domains/integrations/notifications.ts convention).
  • 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 (Jest toEqual) for structural assertions and toBe for identity assertions on the module-scoped constant.
  • Immutability test uses 'use strict' implicit via ESM + TS "strict": true in tsconfig. Mutation attempts throw TypeError: Cannot assign to read only property; we assert with expect(() => { … }).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

  1. Create src/domains/router/ directory.
  2. Write src/domains/router/scoring.ts per §1.1.
  3. Write src/domains/router/index.ts per §1.2.
  4. Create src/__tests__/domains/router/ directory.
  5. Write src/__tests__/domains/router/scoring.test.ts per §1.3.
  6. Run npm run build — must compile clean.
  7. Run npm run lint — must be zero warnings.
  8. 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).
  9. Commit Step 4.
  10. Write verification doc (Step 5) citing test counts.
  11. Commit Step 5.
  12. Push and open PR.

4. Rollback strategy

Worst-case rollback path (if a downstream regression surfaces after merge):

  1. Revert the feat(p0-5-1) commit. The module is a leaf (no callers) — reverting leaves no dangling imports.
  2. The barrel src/domains/router/index.ts becomes empty; remove the file if orphaned.
  3. No migration was added, so no schema rollback.
  4. No env var was added, so no .env cleanup 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.


Back to top

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

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