Skip to content

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_invoice and extract_packing_list for exactly this reason.
  • Shipping a prompt change is a two-part job: edit DefaultPrompts.cs so new environments get it, then apply it to each existing environment through /Admin/Prompts.
  • Diff against DefaultPrompts.cs before editing anything — dotnet run --project scripts/ai-probe -- --dump-prompts writes every DB prompt beside its shipped default for exactly this.
  • /Admin/Prompts does 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.