Files
inter-hub/workplans/IHUB-WP-0008-ihf-phase8-federated-hub-maturity.md
Bernd Worsch 9265ca2d9c feat(P8): IHF Phase 8 complete — Federated Hub Maturity
Implements the final phase of the IHF v0.1 specification:

- WidgetOwnership: delegated ownership registry (local/delegated/global),
  append-only audit artefacts, ownership badge on widget show page
- HubRoutingRule + RoutingEngine: priority-ordered inter-hub routing engine;
  null-inclusive category/widget-type matching; RouteNowAction for manual
  re-evaluation; RoutedCandidates view per hub
- FederatedPolicyOverlay: draft → active → retired lifecycle; activated
  overlays are immutable (same pattern as Phase 6 contracts); policy
  compliance dashboard with decision coverage metrics
- StewardshipRole: named governance roles per hub; point-in-time revocation
  pattern; hub and ops-board integration
- ArchiveRecord + is_archived: soft-delete on widgets; lineage inspector
  traces full traceability chain (Widget → Events → Annotations → Candidates
  → Requirements → Decisions → Deployments → Signals + ArchiveRecord)
- FederatedGovernanceDashboard: 5-panel autoRefresh org-wide governance view
  (ownership coverage, routing activity, policy compliance, stewardship
  coverage, archive activity)

Schema: widget_ownerships, hub_routing_rules, federated_policy_overlays,
stewardship_roles, archive_records; ALTER widgets ADD is_archived;
ALTER requirement_candidates ADD routed_to_hub_id

Migration: 1743638400-ihf-phase8-federated-hub-maturity.sql
Tests: Phase 8 integration tests appended to Test/Integration.hs
Docs: docs/phase8-summary.md; SCOPE.md updated to Phase 8 complete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 22:53:01 +00:00

441 lines
17 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.
---
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 17 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 17 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 (T02T06 before T07)
- All feature tasks (T01T07) 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)`.