state-hub/docs/retirement-cutover-slice-plan.md
tegwick 93e95f93e7
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 28s
feat(legacy-meter): scale retirement quiet period to call volume
A clean review window only proves an interface was silent for that window.
For a heavily used interface that is weak evidence -- an infrequent caller
can fall outside the window entirely, so the interface reads as retirable
while still having consumers.

_retirement_state now also requires silence since last_seen_at, scaled by
all-time volume (RETIREMENT_QUIET_LADDER): 7d under 100 calls, 30d under
10k, 60d above. Existing guards (retired / manual hold / replacement ref /
replacement verified / in-window traffic) still take precedence.

On the 2026-08-20 capture this makes 15 of 19 legacy interfaces retirable
and holds 4: the three six-figure /workstreams/ read paths, and
GET /tasks/?workstream_id which still has live traffic.

Unblocks the bulk of Wave E2 in the cutover slice plan. 7 new tests.

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

299 lines
13 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 | caller check; no successor runtime |
| E2 | `work-records-workplan-legacy` → archive | 13 | legacy-meter — candidate rule, see below |
| E3 | `dashboard-meta` | 1 | none |
| 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.
### 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 |
E2 can therefore proceed for the 15, which is the bulk of the slice. 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`.