diff --git a/alembic/versions/0089_baseline.py b/alembic/versions/0089_baseline.py
index b9d89d5..f961ee1 100644
--- a/alembic/versions/0089_baseline.py
+++ b/alembic/versions/0089_baseline.py
@@ -39,22 +39,31 @@ Four things still had to be added by hand, because they are not in the models:
Extensions are database objects, not table metadata, so no model can carry
them. `IF NOT EXISTS` because a re-run must not fail.
-2. **The two settings singleton rows.** 0002 and 0003 did not only build
- schema — they each inserted a row:
+2. **Three seed inserts** — the two settings singletons (0002, 0003) and the
+ three hygiene system tags (0075). Some migrations did not only build schema;
+ they inserted rows the product needs in order to function, and nothing in
+ the application ever creates them. Every consumer reads them with
+ `scalar_one()`, which RAISES `NoResultFound` on an empty result rather than
+ returning None, so their absence is a crash and not a degradation.
- INSERT INTO import_settings (id) VALUES (1)
- INSERT INTO ml_settings (id) VALUES (1)
+ Distinguishing these from the other data statements in the chain is the
+ whole trick, and the rule turns out to be mechanical:
- and nothing in the application ever creates them. `ImportSettings.load()`
- and `MLSettings.load()` are `select(...).scalar_one()`, which RAISES
- `NoResultFound` on an empty result rather than returning None. So a baseline
- built from models alone would produce those two tables empty and crash a
- fresh install on its first settings access.
+ * `INSERT ... VALUES (...)` with literal values is a SEED. It creates
+ something the product ships. It must be carried.
+ * `INSERT ... SELECT ... FROM
` is a BACKFILL. It derives rows
+ from rows that already exist, so on an empty database it inserts
+ nothing and carrying it would be pointless. 0034 (artist_visit), 0040
+ and 0047 (series_chapter) are all of this shape and are correctly
+ absent here.
- This is worth dwelling on, because it is invisible to every automated check
- this project has: `baseline.yml` compares SCHEMA, and the schema would have
- been a perfect match. Only running the application against a fresh database
- finds it.
+ This category is invisible to every automated check this project has:
+ `baseline.yml` compares SCHEMA, and a baseline missing all three seeds still
+ produces a byte-identical schema and a perfectly green diff. What caught the
+ system tags was the integration suite — 36 tests failing on
+ `NoResultFound` — after a first version of this file shipped with only the
+ two settings rows. A first-run check against the real application is the
+ only thing that finds this class of defect.
3. **The `pgvector` import.** Autogenerate emits qualified
`pgvector.sqlalchemy.vector.VECTOR(...)` references without importing the
@@ -794,6 +803,26 @@ def upgrade() -> None:
op.execute("INSERT INTO import_settings (id) VALUES (1)")
op.execute("INSERT INTO ml_settings (id) VALUES (1)")
+ # The three hygiene system tags, from 0075. These are PRODUCT data, not
+ # operator configuration — 0075's own docstring says so: "the fix keys on
+ # SYSTEM tags the product ships". The presentation and process auto-apply
+ # sweeps look them up with scalar_one(), so without these rows those
+ # features raise NoResultFound rather than degrading.
+ #
+ # 0075 adopted an existing same-name general tag before inserting, because
+ # an operator might already have tagged `wip` by hand. That cannot happen
+ # on the empty database this file runs against, but the guard is kept: it
+ # costs nothing and makes the statement safe to re-run.
+ for _name in ("wip", "banner", "editor screenshot"):
+ op.execute(
+ sa.text(
+ "INSERT INTO tag (name, kind, is_system) "
+ "SELECT :name, 'general', true WHERE NOT EXISTS ("
+ " SELECT 1 FROM tag WHERE lower(name) = lower(:name)"
+ ")"
+ ).bindparams(name=_name)
+ )
+
def downgrade() -> None:
"""Deliberately not implemented.