generated from coulomb/repo-seed
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>
441 lines
17 KiB
Markdown
441 lines
17 KiB
Markdown
---
|
||
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)`.
|