Packet — P1.5.9 Model Candidates Table Population

Task: P1.5.9 — Phase 1.5 graduation Wave 1 (foundation). Branch: feature/p1-5-9-candidates. Audit: p1-5-9-candidates-audit.md. Contract: p1-5-9-candidates-contract.md.

1. Plan summary

Two file creates inside the worktree:

  1. src/db/migrations/009_model_candidates.sql — creates the table and seeds 8 rows.
  2. src/__tests__/db/migrations/009-model-candidates.test.ts — verifies the migration’s contract (11 assertions per Contract §5).

Plus the four 5-step chain docs:

  • docs/audits/p1-5-9-candidates-audit.md (already shipped, Step 1).
  • docs/contracts/p1-5-9-candidates-contract.md (already shipped, Step 2).
  • docs/packets/p1-5-9-candidates-packet.md (this file, Step 3).
  • docs/verification/p1-5-9-candidates-verification.md (Step 5).

No source file outside src/db/migrations/ or src/__tests__/db/ is edited. No MCP tool is registered. No router code is changed.

2. File-by-file plan

2.1. src/db/migrations/009_model_candidates.sql (new)

Content outline (the verbatim file follows the same comment-header pattern as 007 / 008):

-- 009_model_candidates — δ Model Router candidate table + seed (P1.5.9).
--
-- (35-line header explaining purpose, columns, bitmask, idempotency,
--  reversibility, Phase 1.5 posture, canonical references.)
--
-- Idempotency: CREATE TABLE IF NOT EXISTS + INSERT OR IGNORE makes the
-- migration body self-contained — the PRAGMA user_version shield is
-- belt-and-braces, not the only line of defence.
--
-- DOWN block (commented; runner is forward-only).

CREATE TABLE IF NOT EXISTS mcp_model_candidates (
  model_id               TEXT PRIMARY KEY,
  provider               TEXT NOT NULL,
  context_window_tokens  INTEGER NOT NULL,
  latency_tier           TEXT NOT NULL CHECK (latency_tier IN ('fast','balanced','slow')),
  cost_bps_per_kilotoken INTEGER NOT NULL CHECK (cost_bps_per_kilotoken >= 0),
  domain_fit_profile     INTEGER NOT NULL CHECK (domain_fit_profile >= 0 AND domain_fit_profile <= 255),
  enabled                INTEGER NOT NULL DEFAULT 0 CHECK (enabled IN (0, 1))
);

-- 1. Claude 3.5 Sonnet — the Phase 0 active model (only row with enabled = 1).
INSERT OR IGNORE INTO mcp_model_candidates
  (model_id, provider, context_window_tokens, latency_tier, cost_bps_per_kilotoken, domain_fit_profile, enabled)
VALUES
  ('claude-sonnet-3-5', 'anthropic', 200000, 'balanced', 300, 139, 1);

-- 2..8. Phase 1.5 candidates — shipped enabled = 0; each adapter flips its own row.
INSERT OR IGNORE INTO mcp_model_candidates
  (model_id, provider, context_window_tokens, latency_tier, cost_bps_per_kilotoken, domain_fit_profile, enabled)
VALUES
  ('claude-haiku-3-5', 'anthropic',  200000,  'fast',     80,  66, 0),
  ('gpt-4o',           'openai',     128000,  'balanced', 250, 35, 0),
  ('gpt-4o-mini',      'openai',     128000,  'fast',     15,  65, 0),
  ('gemini-1-5-pro',   'google',    1000000,  'slow',     125, 162, 0),
  ('llama-3-3-70b',    'meta',       128000,  'balanced', 50,  145, 0),
  ('mixtral-8x22b',    'mistral',     64000,  'fast',     60,  5,  0),
  ('kimi-k2',          'moonshot',   200000,  'balanced', 120, 73, 0);

-- DOWN (manual; runner is forward-only):
--   DELETE FROM mcp_model_candidates WHERE model_id IN (
--     'claude-sonnet-3-5', 'claude-haiku-3-5', 'gpt-4o', 'gpt-4o-mini',
--     'gemini-1-5-pro', 'llama-3-3-70b', 'mixtral-8x22b', 'kimi-k2'
--   );
--   DROP TABLE IF EXISTS mcp_model_candidates;
--   PRAGMA user_version = 8;

The header block lives above the executable SQL and the runner strips line comments before db.exec (see index.ts §stripSqlComments), so the commented DOWN block has zero runtime cost.

2.2. src/__tests__/db/migrations/009-model-candidates.test.ts (new)

Test file mirroring the conventions in src/__tests__/db-init.test.ts (temp DB per test via initDb(path), afterEach cleanup, no env mutation, no global state).

Structure:

describe('009_model_candidates — P1.5.9', () => {
  describe('schema', () => {
    it('creates the mcp_model_candidates table', () => { /* I1 */ });
    it('rejects latency_tier outside {fast, balanced, slow}', () => { /* I5 */ });
    it('rejects enabled values outside {0, 1}', () => { /* invariants */ });
    it('rejects domain_fit_profile outside [0, 255]', () => { /* I4 */ });
  });
  describe('seed', () => {
    it('seeds exactly 8 rows', () => { /* I1 */ });
    it('seeds all 8 expected model_ids', () => { /* contract §2.1 */ });
    it('enables exactly claude-sonnet-3-5', () => { /* I2 */ });
    it('disables the 7 Phase-1.5 candidates', () => { /* I2 */ });
    it('matches the contract row values byte-for-byte', () => { /* contract §2.1 */ });
    it('has pairwise distinct domain_fit_profile bitmasks', () => { /* contract §2.3 */ });
  });
  describe('idempotency + tracking', () => {
    it('bumps PRAGMA user_version to 9 (or the highest version on disk)', () => {});
    it('is idempotent — re-running the migration body keeps 8 rows', () => { /* I3 */ });
  });
});

The idempotency test reads the migration file from disk, strips ---line comments (mirroring stripSqlComments in index.ts), and calls db.exec against an already-migrated DB. It then asserts the row count remains 8.

Total: 12 tests; the count delta vs. main c44e709f is +12.

2.3. docs/verification/p1-5-9-candidates-verification.md (new, Step 5)

Will land after Step 4 implementation. Records the npm run build, npm run lint, npm test results plus the verification checklist from the staging file slice.

3. Implementation order

  1. Write src/db/migrations/009_model_candidates.sql.
  2. Write src/__tests__/db/migrations/009-model-candidates.test.ts.
  3. Run npm run build. If it fails (it shouldn’t — no TS changes), fix.
  4. Run npm run lint. Fix any new violations.
  5. Run npm test. All previously-green suites stay green; the new suite is green.
  6. Commit as feat(p1-5-9-candidates): seed mcp_model_candidates with real cohort (no stubs).
  7. Write the verification doc (Step 5) and commit it separately.

4. Tooling notes

  • npm run build: TypeScript-only compile; the new .sql file is moved to dist/db/migrations/ by the postbuild scripts/copy-migrations.mjs (see index.ts header).
  • npm run lint: ESLint on .ts files. The new test file must comply with the existing .eslintrc for src/__tests__/ (which is a touch more permissive about unused vars than src/).
  • npm test: Jest ESM. Per db-init.test.ts (line 169), the baseline test bumps PRAGMA user_version to reflect all applied migrations uses countRealMigrations() which auto-tracks the new file. No fixture updates needed.

5. Risk register

Risk Mitigation
Build fails because tsc copies the .sql file scripts/copy-migrations.mjs copies the whole directory; no per-file enumeration
Test count assertions elsewhere break (e.g. fixed-number expectation) Search confirmed only db-init.test.ts:166 has a migration-count assertion, and it’s dynamic
Pre-existing flake on startup — subprocess smoke If hit, retry per the dispatch packet; the test is documented as a flake in MEMORY.md
The migration file’s row order matters for non-idempotency-safe SQL drivers better-sqlite3 is single-threaded; row order is deterministic and the test is order-agnostic
jest worker WAL-file cleanup on Windows Per db-init.test.ts:80 the existing pattern swallows ENOENT / EBUSY; we follow it

6. Acceptance check (mapped from staging-file §”Acceptance criteria”)

Criterion (staging file) Where verified
Migration named with the next sequential number Step 4 file: 009_model_candidates.sql
8 total rows post-migration Test “seeds exactly 8 rows”
7 new rows ship enabled = false Test “disables the 7 Phase-1.5 candidates”
Column values per the cohort table Test “matches the contract row values byte-for-byte”
Idempotent (INSERT OR IGNORE) Test “is idempotent — re-running the migration body keeps 8 rows”
Reversible (DOWN section) Migration file’s DOWN comment block (see §2.1)
npm run build && lint && test green Verification doc records the tail

The acceptance criterion “Claude 3.5 Sonnet (already present)” is satisfied differently than the slice anticipated — the audit found the table absent from any prior migration, so this migration creates the table AND seeds the Sonnet row. The post-condition (8 rows, Sonnet enabled, 7 others disabled) is identical.

7. Out-of-scope (re-stated from contract)

P1.5.9 does not modify:

  • src/domains/router/scoring.ts or fallback.ts.
  • src/db/schema.sql (the schema-snapshot is comment-only; not load-bearing).
  • src/db/index.ts (migration runner; no changes needed).
  • src/server.ts or any MCP tool registration.
  • Any documentation outside the four chain docs.

This is the foundation slice for Phase 1.5 graduation — every later slice (P1.5.1 scoring, P1.5.2–4 adapters, P1.5.5 cost, P1.5.7 MCP tools) reads from this table.

8. Gate sequence

  1. git add → staged.
  2. npm run build → green.
  3. npm run lint → green.
  4. npm test → green; new tests pass; no pre-existing regression.
  5. git commit → Step 4 commit (the chain step “implement”).
  6. Step 5 verification doc written + committed.
  7. git push → remote feature branch.
  8. gh pr create → PR opened against main.

Back to top

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

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