--- id: IHUB-WP-0008 type: workplan title: "IHF Phase 8 — Federated Hub Maturity" domain: inter_hub repo: inter-hub status: done owner: custodian topic_slug: inter_hub created: "2026-03-29" updated: "2026-03-29" state_hub_workstream_id: "ea47db6f-9a3c-43bc-a3ea-bec17b6b01e7" --- # IHF Phase 8 — Federated Hub Maturity ## Goal Support mature multi-hub deployment in an AI factory context. Phase 7 established cross-hub observability. Phase 8 introduces the governance structures needed when multiple teams, hubs, and policies must coexist: delegated widget ownership, requirement routing across hub boundaries, org-wide federated policy overlays, named stewardship roles, and long-term archival with full lineage inspection. ## Background Phases 1–7 are complete. The IHF core is stable across widget governance, interaction capture, structured feedback, decision ledger, outcome observation, agent assistance, cross-framework adapters, and operational observability. The spec (§Phase 8) calls for: - Delegated ownership - Multi-team governance - Inter-hub requirement routing - Federated policy overlays - Mature reporting and stewardship roles - Long-term archival and lineage inspection Artifacts introduced: `WidgetOwnership`, `HubRoutingRule`, `FederatedPolicyOverlay`, `StewardshipRole`, `ArchiveRecord`. Reference: `specs/InteractionHubFrameworkSpecification_v0.1.md` §Phase 8, `docs/phase7-summary.md`, `docs/ihp-controllers-views-forms.md`. ## Phase 8 Exit Criteria (from IHF spec §Phase 8) - The framework supports organisational scale - Ownership and governance remain clear across hub boundaries - Long-term platform memory is preserved ## Data Artifacts Introduced (Phase 8) `WidgetOwnership`, `HubRoutingRule`, `FederatedPolicyOverlay`, `StewardshipRole`, `ArchiveRecord` --- ## Tasks ### T01 — Schema: WidgetOwnership, HubRoutingRule, FederatedPolicyOverlay, StewardshipRole, ArchiveRecord ```task id: IHUB-WP-0008-T01 status: done priority: high state_hub_task_id: "5c5315b7-98ff-45dc-8eef-a5df83e18ea2" ``` Add Phase 8 tables to `Application/Schema.sql` and write migration: ```sql -- Explicit ownership record for a widget — who owns and who stewards it. CREATE TABLE widget_ownerships ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, widget_id UUID NOT NULL REFERENCES widgets(id), owner_hub_id UUID NOT NULL REFERENCES hubs(id), steward_hub_id UUID REFERENCES hubs(id), -- null = same as owner hub ownership_type TEXT NOT NULL DEFAULT 'local', -- 'local' | 'delegated' | 'global' effective_from TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), effective_until TIMESTAMP WITH TIME ZONE, notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE INDEX widget_ownerships_widget_id_idx ON widget_ownerships (widget_id); CREATE INDEX widget_ownerships_owner_hub_idx ON widget_ownerships (owner_hub_id); CREATE INDEX widget_ownerships_steward_hub_idx ON widget_ownerships (steward_hub_id); -- Rule that automatically routes a RequirementCandidate to another hub. CREATE TABLE hub_routing_rules ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, source_hub_id UUID NOT NULL REFERENCES hubs(id), target_hub_id UUID NOT NULL REFERENCES hubs(id), match_category TEXT, -- null = match any category match_widget_type TEXT, -- null = match any widget type priority INTEGER NOT NULL DEFAULT 0, -- higher = evaluated first status TEXT NOT NULL DEFAULT 'inactive', -- 'active' | 'inactive' notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE INDEX hub_routing_rules_source_idx ON hub_routing_rules (source_hub_id); CREATE INDEX hub_routing_rules_status_idx ON hub_routing_rules (status); -- Add routing destination to requirement candidates. ALTER TABLE requirement_candidates ADD COLUMN routed_to_hub_id UUID REFERENCES hubs(id); CREATE INDEX requirement_candidates_routed_hub_idx ON requirement_candidates (routed_to_hub_id) WHERE routed_to_hub_id IS NOT NULL; -- Org-wide policy overlay applied across selected hubs. CREATE TABLE federated_policy_overlays ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, title TEXT NOT NULL, policy_text TEXT NOT NULL, applies_to_hubs JSONB NOT NULL DEFAULT '[]', -- [] means all hubs enforced_from TIMESTAMP WITH TIME ZONE, status TEXT NOT NULL DEFAULT 'draft', -- 'draft' | 'active' | 'retired' notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE INDEX federated_policy_overlays_status_idx ON federated_policy_overlays (status); -- Named governance role assigned to a hub. CREATE TABLE stewardship_roles ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, hub_id UUID NOT NULL REFERENCES hubs(id), role_name TEXT NOT NULL, -- e.g. "Hub Lead", "Policy Steward", "Triage Owner" assigned_to TEXT NOT NULL, -- person name or identifier granted_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, revoked_at TIMESTAMP WITH TIME ZONE, notes TEXT ); CREATE INDEX stewardship_roles_hub_id_idx ON stewardship_roles (hub_id); CREATE INDEX stewardship_roles_active_idx ON stewardship_roles (revoked_at) WHERE revoked_at IS NULL; -- Long-term archival entry for any IHF artifact. CREATE TABLE archive_records ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, subject_type TEXT NOT NULL, -- 'Widget' | 'Requirement' | 'DecisionRecord' | 'DeploymentRecord' | etc. subject_id UUID NOT NULL, archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, reason TEXT NOT NULL, archived_by TEXT NOT NULL, lineage_ref TEXT -- e.g. external doc URL, git SHA, or ADR reference ); CREATE INDEX archive_records_subject_type_idx ON archive_records (subject_type); CREATE INDEX archive_records_subject_id_idx ON archive_records (subject_id); -- Soft-archive flag on widgets. ALTER TABLE widgets ADD COLUMN is_archived BOOLEAN NOT NULL DEFAULT FALSE; CREATE INDEX widgets_is_archived_idx ON widgets (is_archived) WHERE is_archived = TRUE; ``` **Exit criteria:** `migrate` runs cleanly; all Phase 8 types available in GHCi. --- ### T02 — Delegated Ownership: WidgetOwnership registry and assignment UI ```task id: IHUB-WP-0008-T02 status: done priority: high state_hub_task_id: "4d12c8e2-7b8a-4da7-a37d-0663453a3e43" ``` 1. Scaffold `WidgetOwnershipController`: - `index`: table of all ownership records — widget name, owner hub, steward hub (if different), type badge, effective range - `new` / `create`: assign ownership to a widget - `show`: full detail - `edit` / `update`: change steward hub, ownership type, or effective_until; no delete (audit artifact) 2. Validation: - `widget_id`, `owner_hub_id`, `ownership_type` required - `ownership_type` must be `local | delegated | global` - `effective_until` must be after `effective_from` if set - `delegated` type requires `steward_hub_id ≠ owner_hub_id` 3. Widget show page: ownership badge (owner hub name + type, colour-coded: local=gray, delegated=blue, global=purple) 4. Hub show page: "Owned Widgets" and "Stewarded Widgets" sections (collapsed by default, expandable) 5. Link `WidgetOwnershipController` from global nav as "Ownership" **Exit criteria:** Ownership records can be created and viewed; widget show page renders the badge; hub show page lists owned/stewarded widgets. --- ### T03 — Inter-Hub Requirement Routing: routing rules and cross-hub candidate forwarding ```task id: IHUB-WP-0008-T03 status: done priority: high state_hub_task_id: "54597bea-bd1f-41ab-bb50-f2f19dc45c01" ``` 1. Scaffold `HubRoutingRulesController`: - `index`: table of all routing rules — source → target, match criteria, status - `new` / `create` - `edit` / `update`: notes and match criteria - `ActivateRoutingRuleAction` / `DeactivateRoutingRuleAction` - No delete 2. Add `Application/Helper/RoutingEngine.hs` with `applyRoutingRules`: - For a given `RequirementCandidate`, find active rules for its source hub - Sort by `priority DESC` - First matching rule (by `match_category` and/or `match_widget_type`) sets `routed_to_hub_id` on the candidate - Match is `null`-inclusive: a null `match_category` matches any category 3. Call `applyRoutingRules` in `RequirementCandidatesController`: - After `createRecord` in `CreateRequirementCandidateAction` - In `RouteNowAction { requirementCandidateId }` (manual re-evaluation) 4. Add `RoutedCandidatesAction { hubId }`: candidates with `routed_to_hub_id = hubId`, from any source hub — shown in a dedicated view 5. Candidate show and triage views: show a "Routed to: HubName" badge if `routed_to_hub_id` is set; show "Routed from: HubName" if the candidate originated in a different hub 6. Link "Routing Rules" from global nav; link "Routed In" from hub Show page **Exit criteria:** A candidate created in a hub with a matching active rule receives `routed_to_hub_id`; `RoutedCandidatesAction` shows it; manual `RouteNowAction` re-evaluates. --- ### T04 — Federated Policy Overlays: org-wide policies applied across all hubs ```task id: IHUB-WP-0008-T04 status: done priority: high state_hub_task_id: "df2fcdb1-657f-49d1-b340-79d4f55a9088" ``` 1. Scaffold `FederatedPolicyOverlaysController`: - `index`: table — title, status badge, scope (all hubs / N hubs), enforced from - `show`: full policy text, `applies_to_hubs` list resolved to hub names, linked decisions count - `new` / `create` - `edit` / `update`: draft only; activated overlays are read-only - `ActivateFederatedPolicyAction` — sets `status=active`, records `enforced_from=now()`; once active, no further edits - `RetireFederatedPolicyAction` — sets `status=retired` 2. Add `PolicyComplianceDashboardAction` (global): - For each active overlay: hub scope (all vs specific), count of `DecisionRecord`s in-scope hubs that reference a `PolicyReference` - "Coverage %" = decisions with at least one policy reference / total decisions in scope 3. On `DecisionRecord` show page: list any active `FederatedPolicyOverlay` records whose `applies_to_hubs` includes this decision's hub (or is `[]`) 4. Link "Policies" from global nav **Exit criteria:** Overlay activates; activated overlay cannot be edited; policy compliance dashboard shows coverage metrics; decision show page lists applicable overlays. --- ### T05 — Stewardship Roles: named governance roles per hub ```task id: IHUB-WP-0008-T05 status: done priority: medium state_hub_task_id: "490f37e1-44b2-4667-8213-4498121aaa55" ``` 1. Scaffold `StewardshipRolesController`: - `index`: all roles across all hubs, grouped by hub — role name, assigned to, granted at, revoked at (if any) - `show` - `new` / `create` - `RevokeRoleAction { stewardshipRoleId }` — sets `revoked_at = now()` - No delete, no edit (create a new record to replace) 2. Hub show page: "Active Stewards" section listing current active `StewardshipRole` records for this hub (revoked_at IS NULL) 3. `DecisionRecord` show page: list stewardship roles active at `decided_at` for the decision's hub — `granted_at ≤ decided_at AND (revoked_at IS NULL OR revoked_at > decided_at)` 4. Extend `OperationalReviewBoardView` with a Panel 5: hubs with zero active stewardship roles 5. Link "Stewards" from global nav **Exit criteria:** Roles can be granted and revoked; hub show page lists active stewards; decision show page shows contextual stewards; ops board panel renders. --- ### T06 — Archival and Lineage Inspection ```task id: IHUB-WP-0008-T06 status: done priority: medium state_hub_task_id: "4b59d882-b690-4e14-8460-614bd114ce7a" ``` 1. Scaffold `ArchiveRecordsController`: - `index`: all archive records — subject type, subject ID (linked if applicable), archived at, reason, archived by - `show`: full detail including `lineage_ref` 2. Add `ArchiveWidgetAction { widgetId }`: sets `widgets.is_archived = true`, creates `ArchiveRecord`. Redirect to widget show. Archived widgets show a "Archived" banner; excluded from hub widget counts and triage queries via `filterWhere (#isArchived, False)`. 3. Add `LineageInspectorAction { widgetId }` (widget-scoped for Phase 8): - Fetches the full traceability chain in order: `Widget → InteractionEvents → Annotations → RequirementCandidates → Requirements → DecisionRecords → DeploymentRecords → OutcomeSignals` - Also includes any `ArchiveRecord` for the widget - Read-only timeline view; each step shows count and link to list 4. Link "Lineage" from widget show page 5. Link "Archive" from global nav (`ArchiveRecordsAction`) **Exit criteria:** Widget can be archived; archive record created; `is_archived` flag filters it from active queries; lineage inspector renders the full chain. --- ### T07 — Federated Governance Dashboard: org-wide governance health ```task id: IHUB-WP-0008-T07 status: done priority: medium state_hub_task_id: "0c2f6b98-41a5-4876-8bcc-07af08acaf77" ``` 1. Add `FederatedGovernanceDashboardAction` (global, `autoRefresh`): - **Panel 1 — Ownership coverage**: total widgets / widgets with ownership record / breakdown by type (local / delegated / global); percentage bar - **Panel 2 — Routing activity**: active routing rules count; candidates routed cross-hub in last 30 days; top 5 source → target hub pairs - **Panel 3 — Policy compliance**: active overlay count; hubs in scope; decisions referencing a federated policy / total decisions in scope (%) - **Panel 4 — Stewardship coverage**: hubs with ≥1 active steward / total hubs; list of hubs with no stewards - **Panel 5 — Archive activity**: artifact counts archived in last 90 days, grouped by `subject_type` 2. Link from global nav as "Federation" 3. Link from Operational Review Board as a "Federated Governance →" shortcut **Exit criteria:** Dashboard renders all five panels; live-updates on DB change; all counts are correct against test fixtures. --- ### T08 — Phase 8 gate: tests, consistency, docs ```task id: IHUB-WP-0008-T08 status: done priority: high state_hub_task_id: "422cae8f-5dc6-4393-b78a-77169b00da8a" ``` 1. **Integration tests** (`Test/`): - `WidgetOwnership` create + type transitions (local → delegated → global) - `HubRoutingRule` create + activate + candidate routing (`applyRoutingRules` sets `routed_to_hub_id` for matching candidate) - `FederatedPolicyOverlay` create + activate (edit blocked after activation) - `StewardshipRole` create + revoke; contextual query at a past timestamp - `ArchiveRecord` create; `is_archived = true` excludes widget from active queries; lineage fetch returns full chain - `FederatedGovernanceDashboard` compiles, fetches hubs, returns correct ownership coverage count 2. **Consistency sync** via State Hub MCP: `check_repo_consistency(repo_slug="inter-hub", fix=True)` 3. **Documentation updates:** - Update `SCOPE.md` current state: Phase 8 complete - Write `docs/phase8-summary.md`: ownership model, routing engine, policy overlay immutability, stewardship audit pattern, archival soft-delete, lineage chain, known limitations 4. **Smoke test checklist:** - Assign `delegated` ownership to a widget; verify badge on show page - Create + activate routing rule; create a candidate; verify `routed_to_hub_id` - Create + activate federated policy overlay; attempt to edit (expect blocked) - Grant and revoke a stewardship role; verify ops board Panel 5 - Archive a widget; verify excluded from hub widget list; view lineage chain - Open Federated Governance Dashboard; confirm all five panels **Exit criteria:** All tests pass; consistency sync reports no errors; smoke test completed; SCOPE.md updated. --- ## Phase 8 Dependencies - Phases 1–7 schema stable - `widget_ownerships` requires widgets and hubs (T01 before T02) - `hub_routing_rules` + `routed_to_hub_id` on candidates requires hubs and requirement_candidates (T01 before T03) - `federated_policy_overlays` is independent but compliance dashboard reads decisions (T04 after T01) - `stewardship_roles` requires hubs (T01 before T05) - `archive_records` + `is_archived` on widgets (T01 before T06) - Federated Governance Dashboard aggregates all Phase 8 data (T02–T06 before T07) - All feature tasks (T01–T07) before gate (T08) ## Notes - **Activated policy overlays are immutable.** Create a new overlay to supersede — old overlays remain readable for audit. Same pattern as Phase 6 contracts. - **Ownership records are never deleted.** `effective_until` signals expiry. The latest active record (effective_from ≤ now, effective_until IS NULL or > now) is the authoritative ownership. - **Routing is additive.** Multiple rules can match; only the highest-priority active rule fires. Re-running `RouteNowAction` re-evaluates and may update `routed_to_hub_id`. - **Archival is soft-delete.** `is_archived = true` on widgets; the widget row is preserved. All related records remain intact. Lineage inspection works on archived widgets. - **Stewardship is a point-in-time record.** Querying stewards "at time T" uses `granted_at ≤ T AND (revoked_at IS NULL OR revoked_at > T)`.