0002 — Prompts live in the database, seeded insert-only, never via HasData
Status: accepted
Context
The AI extraction prompts are editable at /Admin/Prompts because tuning them is an operational
act, not a development one: a declarant notices a document type coming out wrong, and the fix is a
sentence in a prompt. Waiting for a build to change a sentence would mean it never gets changed.
The obvious way to ship defaults for a table like this is EF's HasData. It is the wrong tool
here, and the reason is not obvious until it bites. HasData rows are owned by the model:
every time a shipped default changes, the next migration issues an UPDATE that overwrites
whatever is in that row. So an operator's carefully tuned production prompt is silently replaced
by the shipped text on the next deploy. Nothing errors, nothing is logged, and the extraction
quietly reverts.
This is not hypothetical — prompts were seeded by HasData migrations at one point, which is
why some rows still carry CRLF line endings from that era (see
0003).
Decision
Defaults live in DefaultPrompts.cs as
plain C# and are seeded at startup with insert-if-missing semantics (SeedDefaultData).
The seeding never updates an existing row. When a shipped default differs from what is in the database, startup logs a warning naming the keys and stops there:
Prompt(s) differ from the shipped defaults (admin-edited or new default not applied): … Startup seeding never overwrites DB prompts — apply changes via /Admin/Prompts if intended.
Comparison normalises line endings first, so the CRLF left behind by the old HasData migrations
does not register as drift.
Consequences
- Environments diverge, by design. Seeding is INSERT-only, so a changed default reaches an
existing environment only when someone applies it. Expect the drift warning to be true rather
than spurious, and treat it as a to-do list rather than noise. As of 2026-08-06, OCI carries the
pre-2026-08-05 text for
extract_invoiceandextract_packing_listfor exactly this reason. - Shipping a prompt change is a two-part job: edit
DefaultPrompts.csso new environments get it, then apply it to each existing environment through/Admin/Prompts. - Diff against
DefaultPrompts.csbefore editing anything —dotnet run --project scripts/ai-probe -- --dump-promptswrites every DB prompt beside its shipped default for exactly this. /Admin/Promptsdoes not evict the 5-minute prompt cache on save, so a change takes up to five minutes to take effect whichever route is used. That is a wart, not a design decision.