state-hub/workplans/STATE-WP-0083-forge-derived-projection-reset.md
tegwick fd0d0d537b
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 23s
feat(projection): derive a repository's projection from the forge
Implements ADR-012 decisions 1 and 2 (STATE-WP-0083 T01, T02 partial). Central
clones the default branch from Forgejo and derives its own projection: 69
workplans and 459 tasks from the-custodian at d5013ae, identical across runs,
with the commit recorded as provenance.

Identifiers are derived in the ADR-007 namespace and verified against live
records, so a forge-derived projection and a preliminary overlay agree on
identity without reconciliation.

The diff first matched hub records by UUID and was badly wrong: most hub records
carry pre-ADR-007 random identifiers, so nearly everything appeared
simultaneously missing and stale, and a reset built on it would have destroyed
and recreated the entire projection. It now matches canonical record id, falling
back to the backing file. whitehat-security — bootstrapped straight from files —
now reports clean, which is the control.

Task-level comparison is deliberately not trusted: hub tasks carry no canonical
record id, only a title, so matching is by title. Recorded as T06; T03 is
limited to workplans until it lands.

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
2026-08-25 23:34:25 +02:00

8.3 KiB

id type title domain repo status owner topic_slug created updated related
STATE-WP-0083 workplan Forge-derived projection reset, per repository infotech state-hub active codex infotech 2026-08-25 2026-08-25
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

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

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

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

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

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

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.