state-hub/docs/retirement-cutover-slice-plan.md
tegwick be7f2632c3
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
fix(legacy-meter): capture evidence over 7 days, not an 8-hour band
capture_legacy_meter_evidence.py fell back to hours=8 whenever --days was
omitted, and --hours itself defaulted to 8. Every unattended capture sampled
06:00Z-14:00Z while writing a file named weekly-review with cadence: weekly.
39 of 40 captures ran this way; only 2026-07-08 used a true 7-day window.

Calls outside the band were never sampled, so interfaces with live callers
reported as retirement candidates -- GET /tasks/?workstream_id was flagged on
2026-08-19 despite traffic on 2026-08-18.

Default the script to days=7; keep --hours for spot checks, documented as not
retirement evidence. Adds corrected capture for 2026-08-20 and records the
residual gap (candidate rule ignores last_seen_at) against STATE-WP-0079-T05.

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

277 lines
12 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.
**Still open — E2 remains gated.** A 7-day window is not sufficient for
high-volume interfaces, and two false positives survive it:
- `GET /progress/?workstream_id` — last seen 2026-08-12T08:55Z, roughly 14 hours
before the window opened
- `GET /workstreams/` — last seen 2026-08-10, 9 days quiet, 144 086 all-time calls
`last_seen_at` is unbounded by the window and is the stronger signal; the
candidate rule currently ignores it. Before E2 proceeds, the rule should require
a minimum quiet period scaled to call volume — an interface with six figures of
traffic needs materially more than seven silent days. That is a service-side
change in `api/services/legacy_meter.py` and belongs to T05, not here.
Everything else on the list was last seen 2026-07-31 or earlier, most in early
July, so the bulk of E2 is well-evidenced once the rule is tightened.
## 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`.