Skip to content

refactor: scheduled_testing_eligible column + derivation bridge (PR A)#88

Merged
petterlindstrom79 merged 1 commit into
mainfrom
refactor/scheduled-testing-eligible-pr-a
May 11, 2026
Merged

refactor: scheduled_testing_eligible column + derivation bridge (PR A)#88
petterlindstrom79 merged 1 commit into
mainfrom
refactor/scheduled-testing-eligible-pr-a

Conversation

@petterlindstrom79
Copy link
Copy Markdown
Member

Summary

  • Adds test_suites.scheduled_testing_eligible BOOLEAN NOT NULL DEFAULT FALSE and a Drizzle migration that backfills eligible = TRUE where external_cost_cents = 0 (preserves current scheduler behavior exactly).
  • Swaps the 3 scheduling readers in test-scheduler.ts (dispatch + 2 observability) and 1 diagnostic-script reader (investigate-singapore.ts) to filter on the new column. external_cost_cents returns to being billing-only.
  • Adds startup block 0066_reconcileEligibilityFromCost as an interim derivation bridge — re-derives eligibility from cost at every boot so any row inserted by the 12 untouched INSERT call sites lands at the correct value.

Why now

The May 2026 Haiku token spike (PRs #84/#85/#86/#87) was structurally possible because external_cost_cents = 0 did double duty as billing data and scheduling signal. PR #46's cadence flip (24h → 1h) plus PR #49's deferred Anthropic-Haiku cost-bump silently turned billing-data lag into a scheduling regression for 73 LLM caps for 7 days. PR #85 shipped a CI assertion as the interim guard. This PR decouples the underlying coupling so the assertion is defense in depth, not load-bearing.

Scope split

PR A (this PR):

  • Schema + migration + dispatch swap + observability + script + startup block 0066 + Notion writes.
  • INSERT call sites and CI assertion are not touched.

PR B (separately tracked, Notion To-do):

  • Force scheduledTestingEligible: boolean explicit at all 12 INSERT call sites.
  • Remove block 0066.
  • Rewrite CI assertion to check LLM-importer + ALWAYS_LLM_CAPABILITY_COSTS + eligible = FALSE invariant directly.

Notion

Verification

  • Type-check: pre-existing routes/mcp.ts strale-mcp/tools errors unchanged; no new errors.
  • Vitest: 540 passing / 11 skipped / 1 failing (app.classify-error.test.ts is the pre-existing failure noted in the prompt; unrelated to this PR).
  • Updated startup-migrations.test.ts BLOCKS-list assertion (8 → 9 blocks; new block appended in order).
  • Grep confirms no remaining external_cost_cents = 0 / > 0 scheduling readers in src/ outside the legitimate billing-data UPDATE blocks (0062–0065) and block 0066's intentional derivation.

Closing-steps rule walk

  • Rule 4 (source-health integrity): dispatch swap is the carve-out PR fix: bump external_cost_cents on always-llm capabilities to gate hourly testing #85 named.
  • Rule 7 (bug-fix framework): not invoked. Structural prevention, not a bug fix.
  • Rules 8 + 9: hand-written Drizzle migration + same-commit schema.ts sync, per DEC-20260420-A.
  • Rule 11 (DEC supersession): does NOT supersede DEC-20260503-B. Refines. No sweep needed.
  • Rule 12 (audit follow-up tests): dispatch / observability / script swaps covered by backfill parity (same cap set pre/post); block 0066 covered by its post-condition DO block.

After-deploy plan

cd apps/api && npx drizzle-kit migrate
psql $DATABASE_URL -c "\d test_suites" | grep scheduled_testing_eligible
psql $DATABASE_URL -c "SELECT COUNT(*) FROM test_suites WHERE scheduled_testing_eligible = TRUE"
psql $DATABASE_URL -c "SELECT COUNT(*) FROM test_suites WHERE external_cost_cents = 0"

Both counts must be equal. Then monitor the next hourly dispatch tick — cap set should match the pre-deploy set exactly.

Test plan

  • Drizzle migration runs locally with the post-condition DO block passing
  • Backfill row counts match pre-migration set
  • All 3 swapped scheduler queries return identical row sets to the pre-swap version
  • Startup-migrations BLOCKS-list test passes with new 9-block expectation
  • Full vitest run: 540 passing
  • After Railway deploy: \d test_suites shows new column; both WHERE eligible = TRUE and WHERE cost = 0 counts equal
  • After Railway deploy: next hourly dispatch logs same cap set as pre-deploy

🤖 Generated with Claude Code

…idge (pr a)

Decouple billing data (`external_cost_cents`) from the scheduling signal
that drives the hourly test scheduler. PR A introduces the new column
and a derivation bridge; PR B (separately tracked) will force explicit
declarations at INSERT sites and remove the bridge.

The May 2026 Haiku token spike (PRs #84/#85/#86/#87) was structurally
possible because `external_cost_cents = 0` did double duty as billing
data and scheduling signal. A compound-PR pattern (cadence flip + deferred
cost-bump) silently turned billing-data lag into a scheduling regression
for 73 LLM caps for 7 days. Decoupling these concerns makes that class
of failure impossible at the data model.

Changes
- Schema: add `test_suites.scheduled_testing_eligible BOOLEAN NOT NULL
  DEFAULT FALSE` to schema.ts.
- Migration 0063: backfill `eligible = TRUE` where `cost = 0`. Preserves
  current dispatch behavior exactly. Post-condition DO block asserts
  parity between the two filters.
- Scheduler: `findOverdueCapabilities`, `countOverdueCapabilities`, and
  `countPaidSkipped` swap to read the new column.
- Diagnostic script `investigate-singapore.ts` swaps the
  scheduling-pool-proxy reader.
- Startup block 0066 `runMigration0066_reconcileEligibilityFromCost`:
  re-derives eligibility from cost at every boot as the interim
  derivation bridge. Catches any INSERT site that lands a row without
  setting eligibility explicitly. PR B removes this block when INSERT
  sites are forced explicit.

PR A intentionally does NOT touch the 12 INSERT call sites. The default
FALSE + block 0066's `cost = 0 ⇒ eligible = TRUE` derivation means new
free caps land at eligible = TRUE on next boot.

Refines DEC-20260503-B without superseding. See DEC-20260511-B.

Closing-steps rule walk:
- Rule 4 (source-health integrity): the dispatch swap is the named
  carve-out from the structural follow-up (PR #85 explicit reference).
- Rule 7 (bug-fix framework): not invoked. Structural prevention, not
  a bug fix. The May 2026 leak is contained.
- Rules 8 + 9: hand-written Drizzle migration + schema.ts sync in
  the same commit. Per DEC-20260420-A.
- Rule 11 (DEC supersession): does NOT supersede DEC-20260503-B.
  Refines. No sweep needed.
- Rule 12 (audit follow-up tests): new code paths covered — dispatch
  swap by parity (backfill makes pre/post sets identical), block 0066
  by post-condition DO block, observability swap by same parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@petterlindstrom79 petterlindstrom79 merged commit cb4e8c1 into main May 11, 2026
1 check passed
petterlindstrom79 added a commit that referenced this pull request May 11, 2026
…phase 3 harden) (#89)

Codify the in-startup-migrations.ts pattern as the official schema-change
convention for the api service. Retire the dual-track Drizzle SQL surface
that misled PR #88 into a healthcheck failure.

Background. PR #88 (2026-05-11) added apps/api/drizzle/0063_*.sql expecting
the file to apply at deploy time. Nothing in this codebase applies Drizzle
SQL files anywhere in the deploy pipeline — the Dockerfile CMD runs
node dist/index.js, which calls runStartupMigrations() (in-TS blocks),
which never invokes drizzle-kit migrate. The drizzle.__drizzle_migrations
tracking table exists under the `drizzle` schema with 60 historical
entries but its last apply was ~2026-04-04; since then, in-TS blocks have
been doing the schema work. PR #88's healthcheck failed because in-TS
block 0066 referenced the new column before anything created it.

Phase 1 (Contain) and Phase 2 (Understand) shipped 2026-05-11. This is
Phase 3 (Harden) per DEC-20260511-C.

Changes
- apps/api/src/lib/startup-migrations.ts — block 0066 renamed to
  runMigration0066_ensureEligibilityColumnAndReconcile and the SQL now
  starts with ALTER TABLE ADD COLUMN IF NOT EXISTS before the existing
  reconciliation UPDATE. Idempotent on existing prod (column already
  there from Phase 1 manual recovery); functional on fresh DBs.
- apps/api/drizzle/ — directory deleted (63 SQL files + meta/ +
  README.md).
- apps/api/drizzle.config.ts — deleted.
- apps/api/scripts/check-migration-prefixes.mjs — deleted (vestigial
  with the dir gone).
- apps/api/scripts/verify-migration-rename.ts — deleted (forensic script
  for an already-resolved 0046 collision).
- .github/workflows/ci.yml — check-migration-prefixes step removed.
- apps/api/package.json — db:generate/db:migrate/db:push scripts removed,
  drizzle-kit devDependency removed.
- package-lock.json — refreshed.
- apps/api/src/lib/schema-validator.ts — fix hint now points at
  startup-migrations.ts blocks (was: cd apps/api && npx drizzle-kit migrate).
- apps/api/src/db/schema.ts — integrity_hash_status external-managed
  comment reworded (was referencing drizzle-kit generate).
- apps/api/src/lib/startup-migrations.test.ts — BLOCKS-list assertion
  updated to the renamed block.
- handoff/_general/from-code/2026-05-11-in-ts-migrations-convention-pr88-phase3.md
  — session summary + drift inventory + PR B implication note.

Verification
- Type-check: clean (pre-existing routes/mcp.ts strale-mcp/tools errors
  unchanged).
- vitest: 540 passing / 11 skipped / 1 pre-existing failure
  (app.classify-error.test.ts, unrelated).
- BLOCKS-list canonical test passes with renamed block.

DEC-20260420-A (hand-written discipline) and DEC-20260420-B (schema.ts
sync) are preserved — we still hand-write, schema.ts edits still ship
alongside. Neither is superseded by DEC-20260511-C. Rule 11 does not
fire.

Closes Notion P1 To-do
35d67c87082c810bbe04e597c38f6d89 (Harden schema-migration deploy
pipeline). The canonical Rule 8 in cc-prompts skill is amended in
lockstep; the Notion Working rules page header references DEC-20260511-C.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
petterlindstrom79 added a commit that referenced this pull request May 11, 2026
Two reference-content handoffs from earlier 2026-05-11 sessions. Per
Rule G (handoff note hygiene, DEC-20260510-A), notes with substantive
"Landed" / "Outcome" / runbook content are promoted; pure session
narration is deleted.

Promoted:
- 2026-05-11-decouple-scheduled-testing-eligible-pr-a.md (PR #88 PR A
  shipped state — landed-list, decisions locked, follow-ups).
- 2026-05-11-haiku-cost-leak-audit-contain-cleanup.md (4-PR incident
  arc: #84/#85/#86/#87 with outcomes, audits, decisions).

Deleted (not in this commit; rm'd locally) the third 2026-05-11 note:
- 2026-05-11-pr88-deploy-recovery-and-phase3-halt.md
  Pure session-progress narration. Phase 3 halt was resolved by PR #89
  (merged 2026-05-11); content superseded by DEC-20260511-C + PR #89's
  handoff note (2026-05-11-in-ts-migrations-convention-pr88-phase3.md).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant