feat(projection): add workplan retirement columns, and record the migration gap
Adds projection_retired_at, projection_retired_reason and derived_from_commit. Retirement cannot be expressed by deletion — hub-native records reference workplans with RESTRICT — nor by `status`, since an archived workplan was closed by its owner while a retired one is simply no longer derived by the forge. Those are different facts and must not share a field. Discovered while applying this: central's schema is two revisions behind the code it runs. review_contracts does not exist there although its migration ships in the serving image, and there is no migration mechanism at all — bare uvicorn CMD, no chart-declared job. Recorded as STATE-WP-0083-T07, which now blocks T03. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 2583210@bnt-lap001 Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
This commit is contained in:
parent
fd0d0d537b
commit
89ff2b2ea3
3 changed files with 95 additions and 0 deletions
|
|
@ -53,6 +53,16 @@ class Workplan(Base, TimestampMixin):
|
|||
index=True,
|
||||
)
|
||||
backing_filename: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
# Projection provenance and retirement (ADR-012 decisions 2 and 7).
|
||||
# A retired record is one the forge no longer derives. It is not deleted:
|
||||
# hub-native records reference it with RESTRICT and must survive. It is not
|
||||
# `archived` either — that says the owner closed the work, which is a
|
||||
# different fact.
|
||||
projection_retired_at: Mapped["datetime | None"] = mapped_column( # noqa: F821
|
||||
DateTime(timezone=True), nullable=True, index=True
|
||||
)
|
||||
projection_retired_reason: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
derived_from_commit: Mapped[str | None] = mapped_column(String(40), nullable=True)
|
||||
backing_relative_path: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
backing_archived: Mapped[bool | None] = mapped_column(nullable=True)
|
||||
backing_synced_at: Mapped[datetime | None] = mapped_column(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
"""workplan projection retirement (STATE-WP-0083-T03)
|
||||
|
||||
A forge-derived projection must be able to say that a record no longer derives
|
||||
from the forge. It cannot say so by deleting the row: progress events, tasks,
|
||||
decisions and review contracts reference workplans with ON DELETE RESTRICT, and
|
||||
those hub-native records must survive (ADR-010 decision 4, ADR-012 decision 7 as
|
||||
amended 2026-08-25).
|
||||
|
||||
Nor can it say so through `status`. A workplan that is `archived` was closed by
|
||||
its owner; one that no longer derives is a different fact, and overloading the
|
||||
same field would destroy the distinction exactly where it matters.
|
||||
|
||||
Revision ID: d1a2b3c4e5f6
|
||||
Revises: c9e5a1b3d7f2
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "d1a2b3c4e5f6"
|
||||
down_revision = "c9e5a1b3d7f2"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"workplans",
|
||||
sa.Column("projection_retired_at", sa.DateTime(timezone=True), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"workplans",
|
||||
sa.Column("projection_retired_reason", sa.String(length=255), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"workplans",
|
||||
sa.Column("derived_from_commit", sa.String(length=40), nullable=True),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_workplans_projection_retired_at",
|
||||
"workplans",
|
||||
["projection_retired_at"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_workplans_projection_retired_at", table_name="workplans")
|
||||
op.drop_column("workplans", "derived_from_commit")
|
||||
op.drop_column("workplans", "projection_retired_reason")
|
||||
op.drop_column("workplans", "projection_retired_at")
|
||||
|
|
@ -214,3 +214,39 @@ heading. That is not a foundation for deletion.
|
|||
Acceptance: task rows carry their canonical record id; the task diff for
|
||||
`whitehat-security` reports clean; `the-custodian`'s task diff falls to something
|
||||
manual inspection confirms.
|
||||
|
||||
|
||||
## Restore a migration mechanism for central
|
||||
|
||||
```task
|
||||
id: STATE-WP-0083-T07
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
**Central's schema is behind the code it is running, and nothing detects it.**
|
||||
Discovered 2026-08-25 while preparing the retirement columns.
|
||||
|
||||
Central is at revision `b8d4f0a2c6e1`; the chain is
|
||||
`b8d4f0a2c6e1 → c9e5a1b3d7f2 → d1a2b3c4e5f6`. The `review_contracts` table does
|
||||
not exist on central even though the migration creating it ships inside the image
|
||||
currently serving traffic. The code and the database disagree silently.
|
||||
|
||||
There is no migration mechanism. The image `CMD` is bare `uvicorn`, the Helm
|
||||
chart declares no migration job, and the only evidence migrations ever ran was an
|
||||
ad-hoc `state-hub-alembic-init` Job created outside the chart — deleted earlier
|
||||
the same day as a retired-forge leftover. Deleting a completed Job removed a
|
||||
historical record rather than a working mechanism, since Jobs do not re-run, but
|
||||
it also removed the last visible sign that this gap existed.
|
||||
|
||||
This blocks `T03`: the retirement columns cannot reach central without it.
|
||||
|
||||
Needed: migrations as a declared part of the release — a chart-managed job or
|
||||
init container that runs `alembic upgrade head` before the API serves — and a
|
||||
check that surfaces a schema/code mismatch instead of leaving it silent. A
|
||||
deployment that can run against a schema it was not built for is the same class
|
||||
of defect as a projection that cannot name its source commit.
|
||||
|
||||
Acceptance: central reaches `head`; `review_contracts` exists; the mechanism is
|
||||
declared in the chart; a mismatch between code and schema is reported rather than
|
||||
tolerated.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue