Story 1877: the label and the identity are two different things — annexure columns + freeing exhibit_label

Why this exists (the catastrophe it prevents). A filing has two kinds of names, and an early schema fused them into one column. The first is the annexure label a drafter writes on a copy that rides a parent's filing — “Annexure A”. The second is the exhibit identity a court assigns to a standalone document — Ex. P-1. Migration 0108 created a single exhibit_label column and bolted on a CHECK constraint, documents_exhibit_label_requires_parent, that says: you may only carry a label if you ride a parent. That welds the door shut for lead documents: a lead document that is itself an exhibit can never be labelled, because the constraint forbids any label on a row without a parent. As long as that constraint stands, the exhibit feature is impossible to build — the schema actively rejects it. This Story does the expand half of an expand/contract: it adds the real annexure columns, copies the existing values onto the right one, and removes the constraint so lead documents are free, while keeping the old column alive and clearly marked dead so nothing in flight breaks.

The analogy to hold the whole way down: a physical court bundle. The annexure tab ("Annexure A", "Annexure B-3") is a sticker the drafter puts on a page that is stapled behind a main document — it belongs to that page, one sticker per page, and you can re-order the tabs. The exhibit number is what the court stamps on a document it admits into evidence in its own right — that identity belongs to the proceeding's exhibit register, not to the page. Story-1877 stops storing the court's stamp in the slot meant for the drafter's sticker. The sticker (annexure label) stays on the page; the stamp (exhibit identity) moves to a register later (Story T1b).

🛰 Satellite view: add the right columns, free the lead docs

Migration 0140_document_annexure_columns (revises the current head a3f7c2e9b105; the panel’s "use 0138" was overtaken by two 0138/0139 render migrations landing first) is an expand/contract-safe step — additive columns, a backfill, and a constraint drop. Nothing is deleted.

MoveWhatWhat it buys
ADD filing_label VARCHAR(64)The annexure label, drafter-set ("Annexure A"). 1:1 with the document instance, so it stays on the row.A correctly-named home for the sticker, independent of the deprecated column.
ADD annexure_position INTEGERA counsel-reorderable index sequence within a filing.Tabs can be re-ordered without renaming.
ADD filing_copy_type VARCHAR(16)Copy provenance: original / certified / notarised / attested. App-validated, no DB CHECK.Provenance is captured; the enum stays app-owned and free to grow (RFD-DOC-003 D10).
BACKFILL filing_label ← exhibit_labelPer pramaan-docs ADR-125 D3, every existing exhibit_label value is in fact an annexure label.No data is lost — the existing labels land on the correctly-named column.
DROP CHECK documents_exhibit_label_requires_parentConstraint only. No data touched.A lead document can finally carry a label — the welded door is open (verified end-to-end after T1b).
RETAIN exhibit_label, marked DEPRECATEDColumn kept; its comment now reads "DEPRECATED (Story-1877): use filing_label… do not write."Anything still reading the old column keeps working through the contract phase; the deprecation is self-documenting in the catalog.

✈️ Airplane view: copy, don’t move — so the round-trip is lossless

The upgrade copies exhibit_label into filing_label; it does not move it. The old values stay exactly where they were. That single decision is what makes the downgrade safe and the whole step reversible on a staging clone:

  upgrade()                          downgrade()
  ─────────                          ──────────
  + filing_label, annexure_position,   COMMENT exhibit_label ← original (0108)
    filing_copy_type                   UPDATE exhibit_label = NULL
  filing_label ← exhibit_label (copy)    WHERE parent_document_id IS NULL  ← clean first
  DROP CHECK requires_parent           RECREATE CHECK requires_parent
  COMMENT the 3 new cols               DROP the 3 new cols
  COMMENT exhibit_label = DEPRECATED   (exhibit_label values never left — intact)

Because the upgrade copied rather than moved, downgrade has nothing to restore into exhibit_label — it simply drops the three new columns and the originals are untouched.

🔬 Microscopic view: the decisions that matter

1. The downgrade must clean before it can re-constrain (RFD-DOC-003 §10 A2)

Here is the trap. Once the upgrade drops documents_exhibit_label_requires_parent, the database permits a lead document (parent_document_id IS NULL) to acquire an exhibit_label. If a downgrade naively re-creates the old CHECK, Postgres validates it against existing rows and the migration fails the moment any such lead-doc label exists. So downgrade nulls out exactly those offending values first:

  UPDATE documents.documents
     SET exhibit_label = NULL
   WHERE parent_document_id IS NULL AND exhibit_label IS NOT NULL;
  -- only then:
  CREATE CHECK documents_exhibit_label_requires_parent
    (exhibit_label IS NULL OR parent_document_id IS NOT NULL)

This is a deliberate, narrow data change on the reverse path — it touches only lead-doc legacy labels that the dropped constraint made possible, and the real value already lives safely on filing_label.

2. No DB CHECK on filing_copy_type — on purpose

The other two enums on this table (processing_status, availability) carry DB CHECKs because their value sets are closed and load-bearing for query correctness. filing_copy_type is different: the copy-provenance vocabulary (original / certified / notarised / attested) is expected to grow as jurisdictions add copy types, and a DB CHECK would force a migration for every new value. RFD-DOC-003 D10 puts validation in the app, where the enum is owned, so the column is a plain VARCHAR(16).

3. The entity comment and the migration comment must be byte-identical

CI runs alembic check, which compares the live schema against the SQLModel Document entity. Column comments are part of that comparison. The comment= on each new Field in entity.py and the COMMENT ON COLUMN string in the migration are therefore kept character-for-character equal — including the deprecation notice now on exhibit_label. A drift between the two is a CI failure, not a cosmetic nit; the migration carries a comment reminding the next editor of this coupling.

4. The entity loses one CHECK, keeps the column

In __table_args__ the documents_exhibit_label_requires_parent CheckConstraint is removed (mirroring the DROP) while documents_parent_not_self stays. The exhibit_label field remains in the model — deprecated, not deleted — alongside the three new fields, so existing read paths compile and run unchanged through the contract phase.

How it was proven

One-sentence recall: the drafter’s annexure label now lives on its own filing_label column (with position and copy-type beside it), the existing values are copied there, and the constraint that forbade lead documents from carrying a label is gone — the mis-named exhibit_label stays only as a deprecated, do-not-write tombstone until the exhibit register lands in T1b.