state-hub/docs/retirement-cutover-slice-plan.md
tegwick fb363b37d3
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 37s
feat(STATE-WP-0079): retire the suggestion-backlog surface (slice E1)
History is archived fleet-side, so the read surface has no remaining job.

- api/routers/suggestions.py: whole prefix 410s with a pointer to intake and
  to the archive; the router drops from 176 lines to a stub
- mcp_server: the 6 suggestion tools removed outright rather than stubbed --
  a retired tool that still appears in the tool list costs every agent
  session context on every call, which is the opposite of retiring it
- write_idempotency: 5 /suggestions rules dropped
- dashboard: suggestions.md deleted, nav entry removed, reference.md and
  wsjf-triage.md updated; docs/suggestions.md rewritten as archive pointer
- tests: two tests pinned the old read-live behaviour and now pin the
  retirement contract instead

Tables stay: they are retire/archive in SHR-INV-0001 and are captured by the
final dump at T06.

Untouched, and worth knowing during cutover: ui-feedback.md / todo.md
'suggestions' are Shift+click dashboard feedback backed by technical_debt, a
different entity that shares the word. E3 (dashboard-meta) is that page; its
owner is state-hub-until-cutover so it retires at the T06 window, not now.

Full suite 612 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 08:11:14 +02:00

332 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# State Hub retirement — cutover slice plan
**Workplan task:** `STATE-WP-0079-T02`
**Inventory:** `SHR-INV-0001` (`prj-state-hub-retirement/inventory/`), 425 items, v0.1.1
**Created:** 2026-08-19
Groups every dispositioned inventory item into an ordered cutover slice. Each
slice names its adapter flag, the owner-side API that receives the traffic, and
its rollback. Slices are the execution unit for `STATE-WP-0079-T04`.
## Method
Slices are cut on **capability**, not on route or table, because a capability is
the smallest unit with a single owner and a coherent contract. The inventory's
`capability` field yields 51 groups; those are bundled into waves by owner, and
ordered within a wave by dependency and blast radius.
Two rules constrain the ordering:
1. **A slice cannot move before its owner can receive it.** Owner readiness is
the owner repo's own workplan status, not an assertion here.
2. **Read-only projections move before write paths** in the same capability, so
a failed cutover degrades to stale reads rather than lost writes.
## Owner readiness (verified 2026-08-19)
| Owner | Items | Gating workplan | Status | Can receive? |
| --- | --- | --- | --- | --- |
| `repo-manager` | 167 | RMGR-WP-0001/0002/0003 | `finished` | **Yes** |
| `hub-core` | 128 | HUB-WP-0004 runtime + extension contract | `proposed` | No |
| `state-hub-until-cutover` | 55 | — (keep/retire in place) | — | n/a |
| `activity-core` | 18 | ACTIVITY-WP-0029 hub port alignment | `active` | Partial |
| `ops-hub` | 13 | OPS-WP-0003 extension contract alignment | `proposed` | No |
| `fin-hub` | 10 | FIN-WP-0003 fabric authority boundary | `proposed` | No |
| `archive` | 30 | legacy-meter evidence | see Wave E | Conditional |
| `ops-bridge` | 2 | — (`keep`) | — | n/a |
| `the-custodian` | 2 | — | — | Yes |
Only the `repo-manager` lane is fully unblocked today. **Wave A is the whole of
the near-term executable surface**; everything else is correctly waiting on an
owner-side contract, not on State Hub.
## Adapter mechanism
Wave A reuses the dual-run strangler proven in RMGR-WP-0002/0003 rather than
introducing a new mechanism:
- Adapter: `api/services/repo_manager_dual_run.py`
- Existing flags: `RM_WRITEBACK`, `RM_WRITEBACK_PUSH`, `RM_RECONCILE`,
`RM_PILOT_REPOS`, `RM_METER_PATH`, `RM_DUAL_RUN_CONFIG`
- Reference: `repo-manager/docs/dual-run.md`, evidence
`repo-manager/docs/evidence/wp0002-completion.md`
New slices add a **slice-scoped flag** of the form `RM_SLICE_<NAME>` with three
states — `off` (hub only), `dual` (both, hub authoritative, divergence metered),
`owner` (owner authoritative, hub proxies). Rollback for every Wave A slice is
therefore the same operation: set the slice flag back one state. No slice
advances to `owner` until it has run a full consistency-sweep cycle at `dual`
with zero divergence.
Waves BD must not invent hub-side adapters ahead of their owner contract.
Per the T01 freeze rule, building an adapter for an owner that cannot yet
receive establishes new permanent ownership here — the exact thing retirement
is removing.
---
## Wave A — repo-manager (167 items, executable now)
### A1 · Repository registry — 27 items
`repository-registry` (24: 16 route, 7 mcp_tool, 1 dataset) · `repo-reconciliation` (1)
· `repo-doi` (1) · `repo-onboarding` (1)
| | |
| --- | --- |
| Flag | `RM_SLICE_REGISTRY` |
| Owner API | `rmgr` repo registry + classification spine |
| Rollback | flag → `dual`, then `off`; hub tables retain rows throughout |
First because every other repo-manager slice keys off repo identity, and because
`repo-onboarding` is already delegated (`STATE-WP-0080-T02`, `rmgr scaffold`),
so the slice starts partly cut over.
### A2 · Work records — 59 items
`work-records` (20) · `work-records-workplan` (12) · `work-records-intake` (9) ·
`work-records-decision` (8, move+replace) · `work-records-deps` (2) ·
`work-records-task` (8, repo-manager portion)
| | |
| --- | --- |
| Flag | `RM_SLICE_WORKRECORDS` |
| Owner API | `rmgr` file-backed work-record index + PATCH `/tasks` writeback |
| Rollback | flag → `dual`; C-15 writeback already reconciles file↔DB drift |
Highest blast radius in the program — this is the surface every agent session
touches. Split the flag by record kind if divergence appears; do not cut the
whole slice at once.
`work-records-task` is owner-split: the route/dataset portion is repo-manager,
the job portion is `activity-core` (Wave C1). Cut the repo-manager portion first
and leave the job dual-running.
### A3 · Registers — 49 items
`sbom-inventory` (10) · `repo-goals` (9) · `upstream-contributions` (9) ·
`register-technical-debt` (9) · `register-extension-points` (6) · `register-entries` (6)
| | |
| --- | --- |
| Flag | `RM_SLICE_REGISTERS` |
| Owner API | `rmgr` register endpoints |
| Rollback | flag → `off`; these are append-mostly, so replay is cheap |
Low coupling and low write volume — the natural place to prove the
`RM_SLICE_*` pattern before A2's blast radius.
### A4 · Work + repo UI — 22 items
`work-coordination-ui` (14, `replace`) · `repo-ui` (8)
| | |
| --- | --- |
| Flag | `RM_SLICE_UI` |
| Owner API | owner-side dashboard; hub pages redirect |
| Rollback | remove redirect; hub pages are unchanged behind it |
`replace`, not `move`: the contract changes. Must follow A1A3, since the pages
render what those slices serve.
### A5 · Topic spine — 8 items
`topic-spine` (8, `replace`)
| | |
| --- | --- |
| Flag | `RM_SLICE_TOPICSPINE` |
| Owner API | `rmgr` topic/classification contract |
| Rollback | flag → `off` |
Last in Wave A: it is a `replace` under a changed contract, and A1A4 read it.
---
## Wave B — hub-core (128 items) · gated on HUB-WP-0004
HUB-WP-0004 is `proposed`. **No hub-side adapter work until it is at least
`ready`.** Slices are listed to fix scope and order, not to start.
| Slice | Capabilities | Items |
| --- | --- | --- |
| B1 catalogs | `capability-catalog` (18), `cross-domain-registry` (16) | 34 |
| B2 messaging + interface log | `agent-messaging` (11), `interface-change-log` (11) | 22 |
| B3 telemetry | `token-telemetry` (14), `third-party-software-catalog` (10), `progress-telemetry` (8) | 32 |
| B4 projections + policy | `state-projections` (9), `domain-goals` (6), `policy-integration` (2), `write-idempotency` (1), `event-bus` (1), `recently-on-scope` (1) | 20 |
| B5 hub UI | `hub-projection-ui` (14, replace), `agent-integration` (2), `dashboard` (1) | 17 |
Order B1 → B4 → B2 → B3 → B5: catalogs and projections are read-mostly and
carry the contract; messaging and telemetry are write paths; UI last.
## Wave C — activity-core (18 items) · ACTIVITY-WP-0029 `active`
| Slice | Capabilities | Items |
| --- | --- | --- |
| C1 execution | `workplan-execution-queue` (7, replace), `task-flow-engine` (6, replace) | 13 |
| C2 jobs | `ops-run-projection` (1), `repo-consistency` (3), `work-records-task` activity-core portion (1) | 5 |
Both are `replace` — schedulers move to activity-core under a different
contract. C2 must follow A2, since the consistency job writes work records.
## Wave D — remaining owners (25 items) · all gated
| Slice | Owner | Capabilities | Items | Gate |
| --- | --- | --- | --- | --- |
| D1 | `ops-hub` | `service-catalog` (9), `service-catalog-ui` (2) | 11 | OPS-WP-0003 `proposed` |
| D2 | `fin-hub` | `fabric-graph` (10) | 10 | FIN-WP-0003 `proposed` |
| D3 | `the-custodian` | `kaizen-agents` (2) | 2 | none — can go anytime |
D3 is two MCP tools with no dependants; take it opportunistically.
## Wave E — retire (42 items)
| Slice | Capability | Items | Gate |
| --- | --- | --- | --- |
| E1 | `suggestion-backlog` → archive | 17 | history archived 2026-08-20; read surface + dashboard remain |
| E2 | `work-records-workplan-legacy` → archive | 13 | legacy-meter — candidate rule, see below |
| E3 | `dashboard-meta` (`/ui-feedback`) | 1 | T06 freeze window — owner is `state-hub-until-cutover` |
| E4 | `legacy-meter` itself | 9 | **last** — it is the instrument for E2 |
E4 retires only after every other legacy surface is closed; retiring the meter
first destroys the evidence for the retirements it gates.
### E1 status (2026-08-20)
Already half-retired before this plan: mutations have returned HTTP 410 since
2026-07-21 (`CUST-WP-0061-T06`), pointing at `POST /intakes/`. Reads were kept
live so the historical record stayed reachable.
**History is now archived** at `the-custodian/docs/archived-suggestion-backlog.md`
— all 10 suggestions, 10 notes, and 5 relevance bumps, with each entry's
migration target. Every entry was `declined` in the intake migration and none
was promoted, so nothing forward-looking is lost by removing the surface.
**Executed 2026-08-20.** The 8 read routes now 410 with a pointer to intake and
to the archive, the 6 MCP tools are removed (not stubbed — a retired tool that
still appears in the tool list costs every agent session context on every call),
the 5 `/suggestions` write-idempotency rules are gone, and the dashboard page is
deleted with its nav entry, `reference.md` row, and `wsjf-triage.md` link
updated. `dashboard/src/docs/suggestions.md` is rewritten as the archive
pointer. Tables stay for the T06 dump.
One naming trap worth recording: **`ui-feedback.md` and `todo.md` "suggestions"
are a different entity** — Shift+click dashboard feedback backed by
`technical_debt`, not the suggestion backlog. They are untouched. Anything
grepping for `suggestion` during cutover will hit them.
### E3 status (2026-08-20)
`dashboard-meta` is the `/ui-feedback` page — the feedback scratch surface just
mentioned. Its owner is `state-hub-until-cutover` and it is still in active use,
so it retires **at the T06 freeze window**, not before. Removing it now would
delete a working feedback loop during the retirement it serves. No action.
### E2 evidence — defect found and partly fixed (2026-08-20)
The window discrepancy flagged on 2026-08-19 was a real defect in
`scripts/capture_legacy_meter_evidence.py`, not a labelling nit.
`_review_query` fell back to `hours=8` whenever `--days` was omitted, and
`--hours` defaulted to `8`. Every unattended capture therefore sampled an
8-hour band (06:00Z14:00Z) while writing a file named `weekly-review` with
`cadence: weekly` in the payload. **39 of 40 captures ran this way** — only the
first, 2026-07-08, used a true 7-day window.
Calls in the other 16 hours of each day were never sampled, so quiet-in-band
interfaces were reported as retirement candidates while still having live
callers. On the 2026-08-19 capture three candidates had recent traffic:
| Interface | All-time calls | Last seen |
| --- | --- | --- |
| `GET /tasks/?workstream_id` | 594 | 2026-08-18 |
| `GET /progress/?workstream_id` | 2 | 2026-08-12 |
| `GET /workstreams/` | 144 086 | 2026-08-10 |
**Fixed:** the script now defaults to `days=7`, matching the endpoint name and
the cadence label. `--hours` is retained for spot checks and documented as not
retirement evidence. Corrected capture:
`docs/evidence/legacy-meter-weekly-review-20260820.json` — 18 candidates of 20
interfaces, `GET /tasks/?workstream_id` correctly excluded.
**Candidate rule tightened (2026-08-20).** `_retirement_state` now requires a
quiet period scaled to all-time call volume, because a clean window only proves
silence for that window — for a heavily used interface an infrequent caller can
fall outside it entirely. `RETIREMENT_QUIET_LADDER` in
`api/services/legacy_meter.py`:
| All-time calls | Days since `last_seen_at` required |
| --- | --- |
| 0 | none — never used |
| 1 99 | 7 |
| 100 9 999 | 30 |
| 10 000+ | 60 |
Applied to the 2026-08-20 capture this splits the 19 legacy interfaces cleanly:
**15 retirable** — all four `workstream` MCP tools, the `state://` resource, and
the low-volume REST aliases, plus `PATCH /workstreams/{id}` (571 calls, quiet
42d), `POST /workstreams/` (824, quiet 42d) and
`POST /workstreams/{id}/dependencies/` (4 971, quiet 49d).
**4 held:**
| Interface | All-time calls | Reason |
| --- | --- | --- |
| `GET /tasks/?workstream_id` | 595 | traffic inside the window |
| `GET /workstreams/` | 144 086 | quiet 9d of 60d |
| `GET /workstreams/{id}` | 511 406 | quiet 19d of 60d |
| `GET /workstreams/{id}/dependencies/` | 255 865 | quiet 42d of 60d |
**Executed 2026-08-20:** the 15 are retired; the meter reads 16 retired / 4
legacy with 0 outstanding candidates. Journalled at
`the-custodian/docs/retired-legacy-interfaces.md`. The four
held are the three six-figure read paths and the one with live traffic — exactly
the set where a false retirement would hurt most. They clear on their own as the
quiet counter runs, provided no new traffic arrives; the earliest,
`GET /workstreams/{id}/dependencies/`, needs 18 more silent days.
Re-check before executing E2: the ladder is evaluated at review time, so a
capture older than the retirement decision is not evidence for it.
## Keep — 43 items
`schema-history` (43 migrations, `keep` until cutover) plus `ops-automation`,
`connectivity` (ops-bridge). Not slices: they persist until the T06 freeze
window and are dispositioned by the final dump and archive.
## Coverage
Computed against `state-hub-disposition.yaml`, not hand-counted. Every item is
assigned by `capability`; `work-records-task` splits on `owner`.
| Wave | Slices | Items |
| --- | --- | --- |
| A repo-manager | A1 27 · A2 59 · A3 49 · A4 22 · A5 8 | 165 |
| B hub-core | B1 34 · B2 22 · B3 32 · B4 20 · B5 17 | 125 |
| C activity-core | C1 13 · C2 5 | 18 |
| D ops-hub / fin-hub / the-custodian | D1 11 · D2 10 · D3 2 | 23 |
| E retire | E1 17 · E2 13 · E3 1 · E4 9 | 40 |
| Keep | `schema-history` 43, `ops-automation` 1, `connectivity` 1 | 45 |
| Ops dependencies | see below | 9 |
| **Total** | | **425** |
Wave A totals 165 rather than the owner table's 167, and Wave B 125 rather than
128: the difference is `ops_dependency` items, which carry no `capability` and
so attach to no capability slice.
### Ops dependencies (9 items, no capability)
| Owner | Disposition | Items | Handling |
| --- | --- | --- | --- |
| `hub-core` | move / replace / keep | 3 | ride with Wave B; confirm at B4 |
| `ops-hub` | replace | 2 | ride with D1 |
| `state-hub-until-cutover` | retire | 2 | T06 freeze window |
| `activity-core` | keep | 1 | keep to cutover |
| `ops-bridge` | keep | 1 | keep — external dependency, survives retirement |
These are infrastructure couplings (DB, scheduler, tunnel), not API surface.
They cannot be strangled behind a flag and are dispositioned at the freeze
window instead. Worth an explicit owner confirmation during T04 so none is
discovered live at cutover.
## Next actions
1. Resolve the legacy-meter window discrepancy above — it gates E2 and T05.
2. Implement `RM_SLICE_REGISTERS` (A3) first as the pattern proof.
3. Do not open Wave B until HUB-WP-0004 reaches `ready`.