{ "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 == (instances: custodian cee7bedf, helix-forge f39fa2a3, netkingdom a6c6e745)", "candidate_request": "GET /ports/projections/repository-navigation/facets/primary_domain/", "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=) 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)." } ] }