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:
src/db/migrations/009_model_candidates.sql— creates the table and seeds 8 rows.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
- Write
src/db/migrations/009_model_candidates.sql. - Write
src/__tests__/db/migrations/009-model-candidates.test.ts. - Run
npm run build. If it fails (it shouldn’t — no TS changes), fix. - Run
npm run lint. Fix any new violations. - Run
npm test. All previously-green suites stay green; the new suite is green. - Commit as
feat(p1-5-9-candidates): seed mcp_model_candidates with real cohort (no stubs). - Write the verification doc (Step 5) and commit it separately.
4. Tooling notes
npm run build: TypeScript-only compile; the new.sqlfile is moved todist/db/migrations/by thepostbuildscripts/copy-migrations.mjs(seeindex.tsheader).npm run lint: ESLint on.tsfiles. The new test file must comply with the existing.eslintrcforsrc/__tests__/(which is a touch more permissive about unused vars thansrc/).npm test: Jest ESM. Perdb-init.test.ts(line 169), the baseline testbumps PRAGMA user_version to reflect all applied migrationsusescountRealMigrations()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.tsorfallback.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.tsor 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
git add→ staged.npm run build→ green.npm run lint→ green.npm test→ green; new tests pass; no pre-existing regression.git commit→ Step 4 commit (the chain step “implement”).- Step 5 verification doc written + committed.
git push→ remote feature branch.gh pr create→ PR opened againstmain.