DR-131: CI suites that mutate the corpus run against a disposable database
DR-131: CI suites that mutate the corpus run against a disposable database
Section titled “DR-131: CI suites that mutate the corpus run against a disposable database”Context
Section titled “Context”DR-096 moved the E2E lanes onto a runner-local Supabase stack and scoped itself to those lanes. Nothing ruled on the integration lanes, which kept writing to shared Platform staging. The only record of that split is a parenthetical in id-365 — “integration-nightly still targets staging by design — different lane” — which asserts scope, not a reason. No document states a property of the integration suite that requires a shared database.
S538 measured the cost of that gap. promote-corpus.integration.test.ts calls
promoteCorpusExtractions, the authoritative knowledge-admission gate (R2 /
DR-025), through an RPC that takes no arguments and claims every eligible row
DB-wide. Its teardown tracks only the ids it seeded. Fired nightly by cron against
shared staging, it published and embedded 88 mock-tier q_a_pairs over eight
runs — attributed to their runs on timestamp, 8 for 8, each run failing and
publishing anyway.
Those rows were not confined to the lane that made them. They reached 57% of the
q_a_pairs table, polluted q_a_search on every real query (median 10 of 20
results, worst 17 of 20), filled the MCP kb://qa/ resource list 10 of 10, and
formed a complete pairwise dedup clique — C(88,2) = 3828 candidates at cosine
1.0000 — that cost the cocoindex nightly ~300s per walk and pushed it past its
90-minute job cap every night. An assertion in two-step-retrieval had already been
weakened to survive the accumulation.
Decision
Section titled “Decision”A CI suite that mutates the corpus beyond the rows it seeds runs against a
disposable database, never a shared one. Both integration lanes (ci.yml’s
integration job and integration-nightly.yml) provision a per-run local Supabase
stack, using the machinery DR-096 already proved. requireDisposableDatabase()
enforces it at the suite boundary by refusing any non-loopback host — the
TypeScript analogue of the Python tier’s existing require_disposable_dsn().
Alternatives considered
Section titled “Alternatives considered”- Scope the promotion RPC. Rejected for this decision, on the narrow ground that the test’s own assertions are already per-seeded-row, so the corpus-wide sweep buys it nothing. Whether the RPC should be scoped remains open — see Consequences.
- Move only the offending file to its own lane. Rejected: a second vitest
config, workflow and env block for the same ~2 minutes, and the env drift that
follows.
vitest.integration.config.tshas oneinclude. - Extend the nightly sweep to delete mock rows. Rejected on measurement:
q_a_pairsis outside the sweep’s table set on any key, so it could not have removed one of the 88 rows, and extending it there collides with the ratified S511 D1 amendment that showcase/platform content is never sweep-eligible.
Consequences
Section titled “Consequences”- ~2 minutes per run (
supabase start~64s +db reset~28s). No build and no server: the suite imports route handlers in-process, so it needs a database only. - The shared
integration-staging-integrationconcurrency group is deleted from both lanes, asintegration-staging-e2ewas at DR-096. Integration jobs from different PRs stop serialising. environment: Stagingis retained on both lanes — it scopes the secrets, not the database.- Assertions weakened to tolerate shared-DB accumulation can be tightened. At least
one is known (
two-step-retrieval.integration.test.ts:364) and is not yet fixed. - This does not make the lanes green. 10 of 15 failures were a retired
form_typeskey and are fixed separately; the other 5 are promote/eligibility failures whose cause is not established. - Left open, deliberately. Whether
promoteCorpusExtractionsshould accept a scope is unreconciled: a route-file comment (app/api/governance/promotion-candidates/[extractionId]/accept/route.ts:17-23) describesnew/self_healingas promoted wholesale, while DR-099 rules that an unscoped global claim is a defect, not a default. A route-file comment is evidence of what exists, never of what is correct. This decision changes the substrate and does not resolve that question. - Not validated by execution — no stack was started and no workflow run in S538. The first CI run exercises it.