Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 356991@bnt-lap001 Assistant-Session: 5d99ea69-ed55-4a1e-8d97-f4f312db247f
203 lines
12 KiB
Markdown
203 lines
12 KiB
Markdown
# A5 topic-spine comparison packet (draft)
|
|
|
|
**Task:** STATE-WP-0098-T01 (A5) and STATE-WP-0098-T02 (A4 section)
|
|
**Receiver:** hub-core `HUB-WP-0006-T06`
|
|
**Captured:** 2026-10-13, read-only GETs against State Hub `primary/railiance01`
|
|
**Cases:** `docs/evidence/a5-topic-spine-comparison-cases.json` (10 cases)
|
|
**Status:** draft. Not yet sent to hub-core. No route, flag or deployment was changed.
|
|
|
|
## 1. Where the A5 and A4 item lists come from
|
|
|
|
The counts "A4 / 22" and "A5 / 8" in `docs/retirement-cutover-slice-plan.md`
|
|
come from the original slice plan (commit `613477b`). That plan grouped
|
|
inventory items by capability: A5 is `topic-spine` (8); A4 is
|
|
`work-coordination-ui` (14) plus `repo-ui` (8). The item list is
|
|
`prj-state-hub-retirement/inventory/state-hub-disposition.yaml`
|
|
(SHR-INV-0001). Items were matched on their `capability` field; none of them
|
|
were guessed.
|
|
|
|
**Correction to the framing.** The 8 A5 items are not 8 read endpoints:
|
|
|
|
| Inventory id | Kind | Read? |
|
|
| --- | --- | --- |
|
|
| `route:GET:/topics/` | REST | read |
|
|
| `route:GET:/topics/{topic_id}` | REST | read |
|
|
| `mcp:get_topic` | MCP tool (wraps the two GETs plus `/progress`) | read |
|
|
| `route:POST:/topics/` | REST | write |
|
|
| `route:PATCH:/topics/{topic_id}` | REST | write |
|
|
| `route:DELETE:/topics/{topic_id}` | REST | write |
|
|
| `mcp:create_topic` | MCP tool | write |
|
|
| `table:topics` | dataset | data |
|
|
|
|
That leaves three read surfaces to compare. One more read surface is not in
|
|
the inventory: the MCP resource `state://topics`. The three writes are out of
|
|
scope for a read switch. They should be retired or frozen, not redirected,
|
|
because the receiver accepts no classification writes. Classification fields
|
|
on `/repos/` (`domain_slug`, `category`, tags, stake, mechanics, `topic_id`)
|
|
belong to A1 in the inventory. They are still the real counterpart of the
|
|
hub-core projection, so A5-C05 to A5-C07 compare them.
|
|
|
|
## 2. Callers
|
|
|
|
**Legacy meter.** None of the topic routes is metered. `GET /legacy-meter/interfaces`
|
|
returns 16 interfaces, all for workstreams, SBOM or messages. The weekly reviews
|
|
up to `legacy-meter-weekly-review-20261013.json` do not mention topics. State
|
|
Hub has no other per-route usage telemetry. **So there are no volume or
|
|
last-seen figures.** To get them, register `rest_api:GET /topics/` and
|
|
`rest_api:GET /topics/{topic_id}` in the meter before the switch window.
|
|
That is a State Hub write and needs approval.
|
|
|
|
**Code callers** (ripgrep over `/home/worsch/*`, excluding archives and
|
|
workplans). Every direct caller is inside `state-hub`:
|
|
|
|
| Caller | Route | Use |
|
|
| --- | --- | --- |
|
|
| `custodian_cli.py` (`statehub`) l.178, 481 | `GET /topics/?status=active` (+ `POST` l.184) | resolve topic for register/brief |
|
|
| `statehub_register.py` l.851, 900 | `GET /topics/?status=active`, `POST /topics/` | repo registration |
|
|
| `scripts/consistency_check.py` l.3398, 3784, 3969 | `GET /topics/{id}`, `GET /topics` | fix-consistency (C-06 etc.) |
|
|
| `scripts/promote_intake.py`, `validate_repo_adr.py`, `normalize_attached_repo_workplans.py`, `update_agent_instruction_files.py`, `register_project.sh` | `GET /topics[/?status=active]` | slug/id/domain lookup |
|
|
| `mcp_server/server.py` (`state://topics`, `get_topic`, l.208, 263, 2402, 2757) | `GET /topics`, `GET /topics/{id}` | MCP clients (agents) |
|
|
| Dashboard: `tasks`, `decisions`, `dependencies`, `extensions`, `interventions`, `techdept`, `todo`, `workstreams` pages | `GET /topics/` | topic label lookup |
|
|
|
|
The only hit outside `state-hub` is `kontextual-engine/core/cmis.py`. It builds
|
|
a CMIS path, not a State Hub call, so it is not a caller.
|
|
|
|
**Indirect alias consumers.** 126 repos carry a `topic_id` in `AGENTS.md` or
|
|
`.claude/rules/repo-identity.md`. They use it in
|
|
`/workplans/?topic_id=` (A2) and in progress events (B3). They never call
|
|
`/topics/`, but they need the topic UUIDs to stay stable until those slices
|
|
drop `topic_id`.
|
|
|
|
## 3. Legacy response shapes (captured live)
|
|
|
|
- `GET /topics/?status=active` → 200, a list of 16 rows. Each row has
|
|
`{id: uuid, slug, title, description, domain_slug, status, created_at, updated_at}`.
|
|
All 16 are `active`. Headers: `Cache-Control: max-age=60`, `ETag`.
|
|
- `GET /topics/{id}` → 200, the same fields plus `workstreams: [WorkstreamStub]`.
|
|
An unknown id → 404. **Defect:** `workstreams` is always `[]`. The model
|
|
relation was renamed `workplans`, but the schema still reads `workstreams`.
|
|
No consumer can depend on its content.
|
|
- MCP `get_topic(slug)` → `{topic: <GET /topics/{id}>, recent_progress: [<progress>]}`.
|
|
- Repo classification (`GET /repos/`, 154 rows: 149 active, 5 archived) includes
|
|
`id, slug, canonical_slug, domain_slug, secondary_domains, category,
|
|
capability_tags, business_stake, business_mechanics, topic_id, classified_at,
|
|
classified_by`. 58 repos have a `topic_id` and 96 do not; 23 have no
|
|
`category`.
|
|
- The topic ids and slugs are listed in the JSON case file (A5-C01/C02 use all 16).
|
|
|
|
## 4. Hub-core side and alias expectations
|
|
|
|
- **Routes** (`hub_core/runtime/repository_navigation_routes.py`, GET only):
|
|
`GET /ports/projections/repository-navigation/repositories` (filters
|
|
`primary_domain`, `secondary_domain`, `category`, `capability_tag`,
|
|
`business_stake`, `business_mechanic`; `cursor`; `limit` ≤ 500) and
|
|
`GET /ports/projections/repository-navigation/facets/{kind}/{value}`.
|
|
MCP: `query_repository_navigation`, `get_repository_navigation_facet`.
|
|
503 means no accepted projection; 409 means a stale cursor.
|
|
- **Contract:** `helixforge.repository-navigation` 1.0.0
|
|
(`hub_core/contracts/repository_navigation/v1_0_0/`, `docs/repository-navigation-contract.md`).
|
|
The output carries `repositories[]` keyed by registrar `repository_id`,
|
|
`facets[]`, `legacy_topic_aliases[]`, provenance and `content_hash`.
|
|
- **Identity.** Join on legacy repo `id` == `repository_id`. Repo Manager's
|
|
registry import takes the UUID from State Hub `/repos/{slug}`
|
|
(`repo_manager/repository_publisher.py`). That this holds for all 123 rows
|
|
in the deployed registry is **unverified**. Slugs are labels, not identity.
|
|
- **Topic aliases.** Each alias maps one State Hub topic UUID/slug to exactly
|
|
one facet and must carry `expires_at` and a `residual_owner`. The expected
|
|
mapping is `facet_kind = primary_domain` and `facet_value = topic.domain_slug`,
|
|
with residual owner `STATE-WP-0079`. This mapping is lossy:
|
|
- several topics share a domain (7 topics are `infotech`);
|
|
- 4 repos sit under a topic of a different domain (`adaptive-pricing`,
|
|
`commerce-canon`, `informed-decision`, `whynot-design`);
|
|
- 96 repos have no topic.
|
|
|
|
A topic is a grouping label, not a facet, so exact topic membership has no
|
|
hub-core counterpart (A5-C04).
|
|
- **Legacy fields with no counterpart:** topic `title`, `description`,
|
|
`status`, `created_at`, `updated_at`, `workstreams`; in `get_topic`,
|
|
`recent_progress` (B3) and workplans (A2); in repos, `classified_at` and
|
|
`classified_by` (provenance replaces them).
|
|
|
|
## 5. Comparison cases
|
|
|
|
Ten cases are defined in the JSON file:
|
|
|
|
| Id | What it compares | Expected result |
|
|
| --- | --- | --- |
|
|
| C01/C02 | Topic list and topic-by-id against the aliases | equal once aliases exist |
|
|
| C03 | Unknown topic | 404 vs empty facet, a documented difference |
|
|
| C04 | Topic membership vs domain facet | subset, a documented difference |
|
|
| C05 | Full per-repo classification parity on the id join | equal; cardinality gap 154 vs 123 to be listed |
|
|
| C06 | `state-hub` spot check | equal |
|
|
| C07 | Domain facet counts | equal |
|
|
| C08 | MCP `get_topic` | partial |
|
|
| C09 | Fields with no counterpart | none to compare |
|
|
| C10 | Rollback read | equal to the 2026-10-13 capture |
|
|
|
|
## 6. Fallback and rollback
|
|
|
|
The fallback is the current State Hub read routes, which stay unchanged
|
|
throughout: `GET /topics/`, `GET /topics/{topic_id}`, and MCP
|
|
`get_topic`/`state://topics`. Rollback means setting `RM_SLICE_TOPICSPINE`
|
|
back to `off` and confirming with A5-C10.
|
|
|
|
**Caveat:** `RM_SLICE_TOPICSPINE` does not exist in any code in State Hub,
|
|
hub-core, rapp-core-hub or repo-manager. The slice plan calls the `RM_SLICE_*`
|
|
names "proposed controls, not proof of deployed flags". So today the rollback
|
|
is "callers keep or restore the legacy base URL". Someone must implement the
|
|
flag, or name a per-caller switch instead, before the switch window.
|
|
|
|
## 7. Gaps blocking hub-core from running the comparison
|
|
|
|
1. **No alias source.** The hub-core runtime always emits
|
|
`legacy_topic_aliases: []` (`repository_navigation.py` l.74). The only
|
|
alias in the fixtures expired on 2026-09-30. C01, C02 and C08 cannot pass
|
|
until someone owns alias seeding: hub-core config, or a State Hub export of
|
|
the 16 topics.
|
|
2. **No flag.** `RM_SLICE_TOPICSPINE` is not implemented (see §6).
|
|
3. **No usage figures.** The topic routes are unmetered, so there is no
|
|
volume or last-seen data.
|
|
4. **Candidate access not verified.** The base URL and auth for the
|
|
candidate routes have not been checked from State Hub. The only source is
|
|
the 2026-10-03 receipt (Helm rev 31, 123/123 accepted). Nobody has made a
|
|
live candidate GET this session.
|
|
5. **Cardinality gap.** 154 legacy repos vs 123 published. The 31-row
|
|
difference needs an explanation before C05 can be judged.
|
|
6. **The real switch targets are State Hub's own tools.** All direct callers
|
|
are State Hub tools (CLI, consistency check, MCP, dashboard). They use
|
|
topics for id/slug/domain lookup and for writes. A read switch to
|
|
navigation facets only covers the domain lookup.
|
|
|
|
## A4 work/repo UI callers
|
|
|
|
The 22 A4 items are Observable dashboard views in `state-hub/dashboard`, not
|
|
API endpoints. They are served by `make dashboard` on the workstation
|
|
(`:3000`) and viewed by the operator (Bernd). The inventory lists
|
|
`repo-manager` as their owner, but RMGR-ADR-003 excludes a Repo Manager
|
|
dashboard. The slice plan names "hub-core projection + UI clients" as the
|
|
receiver. No hub-core page equivalents were checked (unverified). The backing
|
|
reads below come from each page's `apiFetch`/`fetch`/`FileAttachment` calls.
|
|
|
|
| View | Backing reads | Proposed owner to ask |
|
|
| --- | --- | --- |
|
|
| `/tasks`, `/tasks/*`, `/todo`, `/interventions`, `/workstreams`, `/workstreams/*`, `/dependencies` | `/tasks/`, `/workplans/`, `/workplans/index`, `/state/deps`, `/repos/`, `/topics/`, `/contributions/`, `/technical-debt/` | hub-core (work-record projection, A2) with repo-manager (file authority) |
|
|
| `/decisions` | `/decisions/`, `/topics/` | hub-core; external link from **railiance-platform** (`docs/credential-change-approval.md`, CCR yaml `decision_dashboard_url`) |
|
|
| `/goals` | `/domain-goals/`, `/repo-goals/`, `/domains/`, `/repos/` | hub-core / repo-manager (unverified owner) |
|
|
| `/wsjf-triage` | `/progress/`, `/workplans/index` | hub-core (B3 progress) |
|
|
| `/workplan-queue` | `/execution/*` | activity-core (C1) |
|
|
| `/repos`, `/repos/*`, `/repo-sync`, `/contributions`, `/extensions`, `/techdept` | `/repos/`, `/domains/`, `/extension-points/`, `/technical-debt/`, `/contributions/`, `/workplans/`, build-time `data/repos.json`, `gitea-inventory.json` | repo-manager (A1 registry) via hub-core navigation |
|
|
| `/sbom` | `/sbom/*` (Nexus-backed) | sbom-nexus (rapp-sbom-nexus); archived railiance-cluster/bootstrap docs link it |
|
|
| `/policy/intake-doc`, `/policy/workstream-dod`, `/policy/repo-doi` | static policy text | policy-nexus (B4) |
|
|
| `/suggestions` | page already removed (E1) | none; already retired |
|
|
|
|
That makes 21 live views plus `/suggestions`, which is already gone. Pending
|
|
approval, ask each owner for two things:
|
|
|
|
- **A candidate read smoke.** The owner's endpoint returns the same rows for
|
|
the backing reads above, compared on ids and counts.
|
|
- **A reversible switch plan.** The page reads the candidate behind a toggle.
|
|
Rollback re-points the page, or removes the redirect, to the retained State
|
|
Hub UI, which stays unchanged.
|
|
|
|
railiance-platform should also be asked where `decision_dashboard_url` should
|
|
point after cutover. No messages have been sent.
|