state-hub/docs/evidence/a5-topic-spine-comparison-cases.json
tegwick 6d53e707e2
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 4s
STATE-WP-0098: A5 comparison packet, A4 owner map, inbox change-feed contract draft
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
2026-10-13 23:18:36 +02:00

124 lines
8.9 KiB
JSON

{
"packet": "STATE-WP-0098-T01",
"receiver_task": "HUB-WP-0006-T06",
"status": "draft",
"captured_at": "2026-10-13",
"legacy_base": "State Hub primary/railiance01 (workstation forward http://127.0.0.1:8000; in-cluster http://10.43.68.154:8000)",
"candidate_base": "UNVERIFIED: hub-core candidate runtime (Deployment core-hub-api-candidate, Helm rev 31). Route prefix /ports/projections/repository-navigation. Auth requirement not verified from State Hub.",
"candidate_contract": "helixforge.repository-navigation 1.0.0",
"legacy_snapshot": {
"topics_total": 16,
"topics_active": 16,
"repos_total": 154,
"repos_active": 149,
"repos_archived": 5,
"repos_with_topic_id": 58,
"repos_without_topic_id": 96,
"repos_without_category": 23,
"domains_total": 14,
"repos_whose_domain_differs_from_their_topic_domain": ["adaptive-pricing", "commerce-canon", "informed-decision", "whynot-design"]
},
"global_normalisation": [
"Join repositories on legacy /repos/ id == candidate repository_id (Repo Manager imports the registrar UUID from State Hub /repos/{slug} id; verify on the deployed registry).",
"Compare list fields as sorted sets; treat null and [] as equal.",
"Ignore ordering, pagination, cursors, ETag/Cache-Control headers, timestamps and candidate provenance/content_hash fields.",
"Follow candidate next_cursor until null before comparing (limit 500).",
"Record candidate projection_status, source_snapshot.source_revision, rebuilt_at and content_hash in the receipt."
],
"cases": [
{
"id": "A5-C01",
"title": "Topic list maps to legacy topic aliases",
"legacy_request": "GET /topics/?status=active",
"candidate_request": "GET /ports/projections/repository-navigation/repositories?limit=500",
"compare": {"legacy": ["id", "slug", "domain_slug"], "candidate": ["legacy_topic_aliases[].legacy_topic_id", "legacy_topic_aliases[].legacy_topic_slug", "legacy_topic_aliases[].facet_kind", "legacy_topic_aliases[].facet_value"]},
"normalisation": ["Set comparison on legacy id == legacy_topic_id", "facet_kind must be primary_domain and facet_value == legacy domain_slug", "every alias must carry expires_at and residual_owner"],
"expected": "expected-equal (16 aliases) once aliases are seeded",
"current_known_state": "BLOCKER: hub-core runtime returns legacy_topic_aliases = [] (hub_core/runtime/repository_navigation.py line 74); no alias seeding path found. Case fails until an alias source exists."
},
{
"id": "A5-C02",
"title": "Single topic by id resolves to one alias",
"legacy_request": "GET /topics/cee7bedf-2b48-46ef-8601-006474f2ad7a",
"candidate_request": "GET /ports/projections/repository-navigation/repositories?limit=500 (select alias with legacy_topic_id = cee7bedf-...)",
"compare": {"legacy": ["id", "slug", "domain_slug"], "candidate": ["legacy_topic_id", "legacy_topic_slug", "facet_value"]},
"normalisation": ["Repeat for all 16 topic ids from A5-C01"],
"expected": "expected-equal: custodian -> primary_domain infotech",
"current_known_state": "Blocked by A5-C01."
},
{
"id": "A5-C03",
"title": "Unknown topic",
"legacy_request": "GET /topics/00000000-0000-0000-0000-000000000000",
"candidate_request": "No alias lookup route; GET /ports/projections/repository-navigation/facets/primary_domain/no-such-domain",
"compare": {"legacy": ["HTTP status"], "candidate": ["HTTP status", "repositories length"]},
"normalisation": [],
"expected": "documented-difference: legacy returns 404; candidate has no per-alias route, an unknown facet value is expected to return 200 with zero repositories (unverified). Expired aliases return 410 per contract."
},
{
"id": "A5-C04",
"title": "Topic membership versus domain facet membership",
"legacy_request": "GET /repos/ then filter topic_id == <topic id> (instances: custodian cee7bedf, helix-forge f39fa2a3, netkingdom a6c6e745)",
"candidate_request": "GET /ports/projections/repository-navigation/facets/primary_domain/<topic domain_slug>",
"compare": {"legacy": ["id set"], "candidate": ["repositories[].repository_id set"]},
"normalisation": ["Exclude repos whose own domain differs from the topic domain (adaptive-pricing, commerce-canon, informed-decision, whynot-design)"],
"expected": "documented-difference: a topic is a grouping label, a facet is the repository's own domain. After exclusion, legacy topic members must be a subset of the candidate facet; candidate may contain more repos. Consumers that need exact topic membership have no counterpart and must keep reading topic_id from A1/A2 until owners drop it."
},
{
"id": "A5-C05",
"title": "Per-repository classification parity (full set)",
"legacy_request": "GET /repos/",
"candidate_request": "GET /ports/projections/repository-navigation/repositories?limit=500 (all pages)",
"compare": {
"pairs": [["domain_slug", "primary_domain"], ["secondary_domains", "secondary_domains"], ["category", "category"], ["capability_tags", "capability_tags"], ["business_stake", "business_stake"], ["business_mechanics", "business_mechanics"], ["status", "lifecycle"], ["canonical_slug", "slug"]]
},
"normalisation": ["Join on id == repository_id", "sorted-set compare for list fields", "Report legacy-only and candidate-only ids separately"],
"expected": "expected-equal on the joined set. Documented-difference on cardinality: legacy 154 rows vs candidate 123 accepted (2026-10-03 receipt); the 31-row gap must be listed and explained (archived, unregistered, private). Note: repository files are the authority, so a value mismatch is a State Hub staleness finding unless the candidate source revision is older."
},
{
"id": "A5-C06",
"title": "Single repository spot check",
"legacy_request": "GET /repos/state-hub",
"candidate_request": "GET /ports/projections/repository-navigation/repositories?limit=500 (select repository_id 8e9ffb56-7abc-47b5-84b2-5ad392b37dcd)",
"compare": {"pairs": [["domain_slug", "primary_domain"], ["secondary_domains", "secondary_domains"], ["category", "category"], ["capability_tags", "capability_tags"], ["business_stake", "business_stake"], ["business_mechanics", "business_mechanics"]]},
"normalisation": ["sorted sets"],
"expected": "expected-equal: infotech / [agents] / tooling / tags [coordination, governance, knowledge, observability, platform] / stake [automation, intelligence, operations, product, technology] / mechanics [adaptation, control, coordination, operation]"
},
{
"id": "A5-C07",
"title": "Domain facet counts",
"legacy_request": "GET /domains/ and GET /repos/ (count by domain_slug, joined set only)",
"candidate_request": "GET /ports/projections/repository-navigation/repositories?limit=500 (facets[] where kind = primary_domain)",
"compare": {"legacy": ["count per domain_slug"], "candidate": ["facets[].repository_count"]},
"normalisation": ["Count only repositories present in both sides (A5-C05 join)", "Legacy domains with zero repos have no candidate facet"],
"expected": "expected-equal on the joined set"
},
{
"id": "A5-C08",
"title": "MCP get_topic(slug)",
"legacy_request": "MCP get_topic(slug='markitect') = GET /topics/ + GET /topics/{id} + GET /progress?topic_id=...&limit=10",
"candidate_request": "MCP get_repository_navigation_facet(kind='primary_domain', value=<topic domain>) plus alias lookup",
"compare": {"legacy": ["topic.id", "topic.slug", "topic.domain_slug"], "candidate": ["alias legacy_topic_id/slug", "facet_value"]},
"normalisation": [],
"expected": "documented-difference: recent_progress and workplans have no navigation counterpart (B3 progress and A2 work records own them). Only the identity/domain part is comparable."
},
{
"id": "A5-C09",
"title": "Legacy topic fields without counterpart",
"legacy_request": "GET /topics/{id}",
"candidate_request": "n/a",
"compare": {"legacy_only": ["title", "description", "status", "created_at", "updated_at", "workstreams"]},
"normalisation": [],
"expected": "documented-difference: no counterpart. workstreams is always [] in the live API (schema field name 'workstreams' does not match model relation 'workplans'), so no consumer can depend on its content. Titles/descriptions must stay in State Hub (or the alias owner) until the alias expires."
},
{
"id": "A5-C10",
"title": "Rollback read",
"legacy_request": "GET /topics/?status=active and GET /topics/{id} after switching back",
"candidate_request": "n/a",
"compare": {"legacy": ["HTTP 200", "row count 16", "id/slug/domain_slug set unchanged from capture"]},
"normalisation": [],
"expected": "expected-equal to the 2026-10-13 capture (ETag may differ only if topics changed)."
}
]
}