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
252 lines
9.9 KiB
Markdown
252 lines
9.9 KiB
Markdown
---
|
|
id: STATE-WP-0083
|
|
type: workplan
|
|
title: "Forge-derived projection reset, per repository"
|
|
domain: infotech
|
|
repo: state-hub
|
|
status: active
|
|
owner: codex
|
|
topic_slug: infotech
|
|
created: "2026-08-25"
|
|
updated: "2026-08-25"
|
|
related:
|
|
- CUST-ADR-012
|
|
- CUST-WP-0068
|
|
---
|
|
|
|
# Forge-derived projection reset, per repository
|
|
|
|
## Goal
|
|
|
|
Implement `ADR-012` decision 7: discard a repository's projection and rebuild it
|
|
from what Forgejo holds. Per repository, routine, idempotent, and verifiable.
|
|
|
|
This is the only sanctioned way to remove a hub record. The hub has no
|
|
hard-delete for work records — `DELETE /tasks/{id}` is `cancel_task`,
|
|
`DELETE /workstreams/{id}` is `410 Gone`, and no `session.delete` exists for
|
|
either. Re-derivation is not one option among several; it is the mechanism.
|
|
|
|
## Why here and not in Repo Manager
|
|
|
|
`ADR-012` decision 1 says central derives from the forge, and the pod can reach
|
|
it: `git ls-remote https://forgejo.coulomb.social/...` succeeds anonymously from
|
|
inside the cluster, and `git` is already in the image.
|
|
|
|
Driving the reset from Repo Manager would mean computing a projection on a
|
|
workstation and pushing it into central — precisely the injection of derived
|
|
state `ADR-010` decision 5 forbids. Central must do its own reading. The cost is
|
|
that workplan parsing exists in two places; `scripts/consistency_check.py`
|
|
already parses frontmatter and task blocks here, so the capability is present
|
|
and should be extracted rather than rewritten.
|
|
|
|
## What it must not do
|
|
|
|
- **Not restore preliminary overlay records.** They exist because the forge does
|
|
not hold them; a rebuild from the forge cannot reproduce them and must not
|
|
pretend to.
|
|
- **Not destroy hub-native records.** Progress events, decisions and inbox
|
|
messages originate in the hub (`ADR-010` decision 4). They are not
|
|
forge-derived and must survive a rebuild of forge-derived state.
|
|
- **Not proceed silently when records would be lost.** Stop, name them, and
|
|
require an explicit acknowledgement.
|
|
|
|
## Derive a repository's projection from the forge
|
|
|
|
```task
|
|
id: STATE-WP-0083-T01
|
|
status: done
|
|
priority: high
|
|
```
|
|
|
|
Given a repository slug, clone or fetch its default branch from Forgejo into a
|
|
temporary location, parse the workplan files, and compute the projection that
|
|
state would produce: workplans, tasks, their identifiers, statuses and backing
|
|
paths.
|
|
|
|
Read-only and side-effect free. Record the commit the derivation came from —
|
|
`ADR-012` decision 2 requires provenance, and the existing `git_fingerprint` is
|
|
worthless precisely because nothing ever wrote it correctly.
|
|
|
|
Acceptance: deriving `the-custodian` twice from the same commit yields identical
|
|
output, and the commit is reported.
|
|
|
|
**Done (2026-08-25).** `api/services/forge_projection.py`. Central clones the
|
|
default branch from Forgejo and derives 69 workplans and 459 tasks from
|
|
`the-custodian` at commit `d5013ae`, identically across runs. Identifiers are
|
|
derived from the canonical record id in the `ADR-007` namespace, verified against
|
|
live records — so a forge-derived projection and a preliminary overlay agree on
|
|
identity without reconciliation. Eight tests, including that identifiers are
|
|
derived rather than inherited from whatever a file happens to carry.
|
|
|
|
A fresh shallow clone each time is deliberate: reusing a working copy is how a
|
|
projection ends up reflecting someone's local state instead of the forge.
|
|
|
|
## Report the difference before changing anything
|
|
|
|
```task
|
|
id: STATE-WP-0083-T02
|
|
status: progress
|
|
priority: high
|
|
```
|
|
|
|
Compare the derived projection against what the hub currently holds for that
|
|
repository, and report: records the forge has that the hub lacks, records the
|
|
hub has that the forge does not, and records whose fields differ.
|
|
|
|
This is the half that is immediately useful without any destructive capability,
|
|
and it is what makes decision 7's "verifiable" real. It also answers the
|
|
question `CUST-WP-0068-T09` is waiting on — which stale rows would actually
|
|
clear — before anyone commits to clearing them.
|
|
|
|
Acceptance: a dry-run diff for a repository with known drift matches what
|
|
manual inspection shows.
|
|
|
|
**Workplan-level diff done; task-level blocked (2026-08-25).**
|
|
|
|
The first implementation matched hub records to derived ones *by UUID* and was
|
|
badly wrong: most hub records still carry pre-`ADR-007` random identifiers, so
|
|
nearly every record appeared simultaneously missing and stale. A reset built on
|
|
that comparison would have destroyed and recreated the whole projection. It now
|
|
matches on canonical record id, falling back to the backing file.
|
|
|
|
Workplan-level results are trustworthy and inspectable:
|
|
|
|
| Repository | missing | stale | differing |
|
|
|---|---|---|---|
|
|
| `whitehat-security` | 0 | 0 | 0 |
|
|
| `the-custodian` | 3 | 4 | 52 |
|
|
| `railiance-platform` | 18 | 25 | 2 |
|
|
|
|
`whitehat-security` reporting clean is the control: it was bootstrapped directly
|
|
from its files, so a forge derivation must agree with it exactly. The four stale
|
|
workplans on `the-custodian` were checked by hand and are real —
|
|
`CUST-WP-0023`/`0024` have no file in the repository at all, and
|
|
`state-hub-v0.1`/`v0.2` carry slugs that were never canonical ids. All four are
|
|
genuine hub-first records, the class `ADR-010` says to disposition.
|
|
|
|
**Blocked: hub tasks carry no canonical record id.** The task schema is
|
|
`id, workplan_id, title, status, priority, …` with nothing holding
|
|
`CUST-WP-0067-T01`. A file task and a hub task can therefore only be matched by
|
|
title, which is why the task diff reports 149 missing and 131 stale for
|
|
`the-custodian` where the workplan diff reports 3 and 4. That is the matching
|
|
failing, not drift.
|
|
|
|
Task-level reset must not be built on title matching — renaming a task heading
|
|
would silently destroy and recreate its record. `T03` is limited to workplans
|
|
until tasks carry their canonical id, which is `T06`.
|
|
|
|
## Apply the reset transactionally
|
|
|
|
```task
|
|
id: STATE-WP-0083-T03
|
|
status: todo
|
|
priority: high
|
|
```
|
|
|
|
Replace the repository's forge-derived records with the derived projection in
|
|
one transaction: create what is missing, update what differs, remove what no
|
|
longer derives. Hub-native records are untouched.
|
|
|
|
Refuse by default when removal would destroy a record with no counterpart in the
|
|
forge; report what would be lost and require an explicit acknowledgement to
|
|
proceed. That refusal is evaluated per repository, so one unresolved repository
|
|
never blocks the rest.
|
|
|
|
Acceptance: reset twice produces the same projection; a repository holding
|
|
records the forge lacks is refused with those records named; hub-native record
|
|
counts are unchanged across a reset.
|
|
|
|
## Fleet form as a loop over the repository form
|
|
|
|
```task
|
|
id: STATE-WP-0083-T04
|
|
status: todo
|
|
priority: medium
|
|
```
|
|
|
|
The fleet-wide reset iterates the per-repository reset and shares its
|
|
implementation, so the rarely-used dangerous path is exercised by the frequently
|
|
used safe one. Repositories that refuse are skipped and reported, never aborting
|
|
the pass.
|
|
|
|
Acceptance: a fleet dry-run reports per-repository outcomes including refusals,
|
|
and completes despite them.
|
|
|
|
## Retire what the reset replaces
|
|
|
|
```task
|
|
id: STATE-WP-0083-T05
|
|
status: wait
|
|
priority: medium
|
|
```
|
|
|
|
Once reset is trusted, `CUST-WP-0068-T09` can clear its stale rows — 4 workplan
|
|
rows from the prefix migration and 305 task rows across 44 workplans.
|
|
|
|
`ADR-003` decision 2's `mtime`-based fingerprints should also be replaced by the
|
|
source commit at this point; `ADR-012` invalidated that composition and the
|
|
replacement belongs with the provenance work in `T01`.
|
|
|
|
Acceptance: `CUST-WP-0068-T09` closes; no fingerprint input depends on a local
|
|
filesystem.
|
|
|
|
|
|
## Give hub tasks their canonical record id
|
|
|
|
```task
|
|
id: STATE-WP-0083-T06
|
|
status: todo
|
|
priority: high
|
|
```
|
|
|
|
Hub task rows hold no canonical identifier — only a title — so nothing reliably
|
|
connects `CUST-WP-0067-T01` in a file to its row. Every other record type has a
|
|
stable identity; tasks do not, and that gap is what stops the reset from covering
|
|
them.
|
|
|
|
Add the canonical id to the task record and populate it during derivation and
|
|
registration. Once present, task matching becomes identity-based like workplans,
|
|
and `T03` can extend to tasks safely.
|
|
|
|
Until then a task's identity is its title, which changes whenever someone edits a
|
|
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.
|