diff --git a/workplans/IHUB-WP-0011-ihf-phase10-hub-registry.md b/workplans/IHUB-WP-0011-ihf-phase10-hub-registry.md new file mode 100644 index 0000000..79d8b41 --- /dev/null +++ b/workplans/IHUB-WP-0011-ihf-phase10-hub-registry.md @@ -0,0 +1,495 @@ +--- +id: IHUB-WP-0011 +type: workplan +title: "IHF Phase 10 — Hub Registry and Widget Marketplace" +domain: inter_hub +repo: inter-hub +status: todo +owner: custodian +topic_slug: inter_hub +created: "2026-04-01" +updated: "2026-04-01" +state_hub_sync: done +state_hub_workstream_id: "bc81e097-e91c-4750-80b6-c809ebcf7ef9" +--- + +# IHF Phase 10 — Hub Registry and Widget Marketplace + +## Goal + +Enable reuse of proven widget patterns, governance templates, and hub +configurations across deployments. Phase 9 made the IHF externally consumable. +Phase 10 makes it composable: hubs and widgets can be discovered, rated, +adopted, and evolved as shared platform assets. + +## Background + +Phases 1–9 and IHUB-WP-0009 (GAAF Compliance Foundation) are complete. All +Phase 10 entry gates are satisfied: + +- `HubCapabilityManifest` table and activation workflow operational ✓ +- Four type registries seeded and validated ✓ +- `/api/v2/` REST API with OpenAPI 3.1 spec live ✓ +- `HubHealthSnapshot` data available ✓ +- ARCHITECTURE-LAYERS.md scorecard at 3.41 (approaching Strong) ✓ +- Architectural fitness functions in CI ✓ + +Phase 10's Hub Registry IS the `HubCapabilityManifest` table with a public-facing +discovery UI. No new hub registry table is required. The data already exists; +Phase 10 adds browsability, pattern publishing, and adoption mechanics. + +Reference: `specs/InteractionHubFrameworkSpecification_v0.2.md` §Phase 10. + +## GAAF Architectural Constraints + +All new code in this workplan must comply with: + +1. **No bare TEXT type discriminators** — any new type discriminator column must + FK to a registry table or carry a CHECK constraint. +2. **WidgetPattern.widget_type must FK to widget_type_registry** — no unregistered + widget types may be referenced by a pattern. +3. **GovernanceTemplate categories must FK to annotation_category_registry** — + template categories are registered vocabulary, not free text. +4. **Core tables are frozen** — `widgets`, `interaction_events`, `annotations`, + `hubs`, and Phase 1–4 dependents must not gain columns without a corresponding + `/contracts/core/` update. +5. **Append-only invariant is permanent** — no migration may add UPDATE or DELETE + capability to `interaction_events` or `outcome_signals`. +6. **Manifest amendment is the extension mechanism** — when a hub adopts a pattern + or governance template that introduces new types, those types must be added to + the hub's `HubCapabilityManifest` via a draft amendment, not inserted directly + into the registry. + +## Data Artifacts Introduced + +`WidgetPattern`, `WidgetPatternVersion`, `PatternAdoption`, `GovernanceTemplate`, +`GovernanceTemplateClone` + +Note: No `HubRegistry` table — the hub registry is a view over existing +`hub_capability_manifests`, `hub_health_snapshots`, and `hubs` tables. + +Schema additions: + +```sql +-- widget_patterns: reusable widget definitions tied to registered types +CREATE TABLE widget_patterns ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + hub_id UUID NOT NULL REFERENCES hubs(id), + name TEXT NOT NULL, + description TEXT, + widget_type TEXT NOT NULL REFERENCES widget_type_registry(name), + is_cross_hub BOOLEAN NOT NULL DEFAULT FALSE, + is_published BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL +); + +-- widget_pattern_versions: explicit version history +CREATE TABLE widget_pattern_versions ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + widget_pattern_id UUID NOT NULL REFERENCES widget_patterns(id) ON DELETE CASCADE, + version_number INTEGER NOT NULL, + definition JSONB NOT NULL, -- widget envelope definition snapshot + changelog TEXT, + published_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + UNIQUE (widget_pattern_id, version_number) +); + +-- pattern_adoptions: which hubs have adopted which patterns +CREATE TABLE pattern_adoptions ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + widget_pattern_id UUID NOT NULL REFERENCES widget_patterns(id), + adopting_hub_id UUID NOT NULL REFERENCES hubs(id), + pinned_version_id UUID REFERENCES widget_pattern_versions(id), + is_version_pinned BOOLEAN NOT NULL DEFAULT FALSE, + is_anonymous BOOLEAN NOT NULL DEFAULT FALSE, -- opt-out of aggregate feedback + adopted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + UNIQUE (widget_pattern_id, adopting_hub_id) +); + +-- governance_templates: requirement distillation and decision templates +CREATE TABLE governance_templates ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + hub_id UUID NOT NULL REFERENCES hubs(id), + name TEXT NOT NULL, + description TEXT, + -- categories: array of annotation_category_registry names this template uses + -- Stored as JSONB array; each element must exist in annotation_category_registry + categories JSONB NOT NULL DEFAULT '[]', + template_body JSONB NOT NULL, -- structured template definition + is_published BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL +); + +-- governance_template_clones: adoption record for governance templates +CREATE TABLE governance_template_clones ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + governance_template_id UUID NOT NULL REFERENCES governance_templates(id), + cloning_hub_id UUID NOT NULL REFERENCES hubs(id), + cloned_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + UNIQUE (governance_template_id, cloning_hub_id) +); +``` + +--- + +## Tasks + +### T01 — Schema: WidgetPattern, WidgetPatternVersion, PatternAdoption, GovernanceTemplate, GovernanceTemplateClone + +```task +id: IHUB-WP-0011-T01 +status: todo +priority: high +state_hub_task_id: "9c26859d-d910-4c5d-a684-3d94ea8019d9" +``` + +Add all Phase 10 tables to `Application/Schema.sql` and write a migration. + +**GAAF constraints:** +- `widget_patterns.widget_type` must FK to `widget_type_registry(name)` — not + TEXT, not CHECK, but a true FK to the registry +- `governance_templates.categories` is JSONB; validate each element against + `annotation_category_registry` in the controller layer (array FK not expressible + in SQL; controller validates at write time) +- No `HubRegistry` table — registry is a query over existing tables + +**Migration file:** `Application/Migration/-ihf-phase10-hub-registry.sql` + +Run `migrate` after writing. + +--- + +### T02 — Hub Registry UI: browsable view over manifests + health snapshots + GAAF compliance indicator + +```task +id: IHUB-WP-0011-T02 +status: todo +priority: high +state_hub_task_id: "718b93a7-4e0a-4f79-af15-53af13ef9a92" +``` + +**Controller:** `Web/Controller/HubRegistry.hs` + +**Query:** +```sql +SELECT h.id, h.name, h.hub_kind, h.domain, + m.id AS manifest_id, m.status AS manifest_status, + m.declared_widget_types, m.declared_event_types, + m.declared_annotation_categories, m.declared_policy_scopes, + s.overall_score, s.snapshot_taken_at +FROM hubs h +LEFT JOIN hub_capability_manifests m + ON m.hub_id = h.id AND m.status = 'active' +LEFT JOIN LATERAL ( + SELECT overall_score, snapshot_taken_at + FROM hub_health_snapshots + WHERE hub_id = h.id + ORDER BY snapshot_taken_at DESC LIMIT 1 +) s ON TRUE +ORDER BY h.name +``` + +**GAAF compliance indicator per hub:** +- Has active manifest: `manifest_status = 'active'` +- Registered type count: length of each `declared_*` JSONB array +- Fitness function status: derive from `m.declared_widget_types` completeness + (flag if hub has widgets not in registry — approximate; full check is in CI) + +**Views:** `Web/View/HubRegistry/Index.hs`, `Web/View/HubRegistry/Show.hs` +(Show renders full manifest vocabulary + health history + adopted patterns) + +Add route and nav link. + +--- + +### T03 — Widget Pattern Library: publish, list, show with version history + +```task +id: IHUB-WP-0011-T03 +status: todo +priority: high +state_hub_task_id: "5d2ce269-25de-4251-afae-0478901f85f6" +``` + +**Controller:** `Web/Controller/WidgetPatterns.hs` + +Actions: +- `WidgetPatternsAction` — list all published patterns with adopter count +- `ShowWidgetPatternAction { widgetPatternId }` — detail with version history +- `NewWidgetPatternAction` — form (hub selector, widget_type from registry) +- `CreateWidgetPatternAction` — validate `widget_type` exists in + `widget_type_registry`; if owned by a different hub, set `is_cross_hub = True` +- `EditWidgetPatternAction { widgetPatternId }` / `UpdateWidgetPatternAction` +- `PublishWidgetPatternAction { widgetPatternId }` — set `is_published = True`, + creates first `WidgetPatternVersion` with `version_number = 1` + +**Cross-hub type check:** +```haskell +typeOwner <- query @WidgetTypeRegistry + |> filterWhere (#name, pattern.widgetType) + |> fetchOne +let isCrossHub = typeOwner.hubId /= Just pattern.hubId +``` + +**Views:** `Web/View/WidgetPatterns/{Index,Show,New,Edit}.hs` + +--- + +### T04 — Pattern versioning: WidgetPatternVersion with pin/follow-latest per adoption + +```task +id: IHUB-WP-0011-T04 +status: todo +priority: medium +state_hub_task_id: "33003835-48fd-45d1-addd-75db85340968" +``` + +**Actions to add to `WidgetPatterns` controller:** + +- `PublishNewVersionAction { widgetPatternId }` — form asks for `definition` + (JSONB) and `changelog`; increments `version_number`; creates + `WidgetPatternVersion` record +- Show page lists all versions in descending order + +**Adoption version choice:** + +In `AdoptPatternAction` (T05), offer two options: +1. Follow latest — `is_version_pinned = False`, `pinned_version_id = Nothing` +2. Pin to current — `is_version_pinned = True`, `pinned_version_id = currentVersionId` + +Show pinned vs follow-latest status in the adopter hub's pattern list. + +--- + +### T05 — Pattern adoption workflow: adopt pattern, trigger manifest amendment draft + +```task +id: IHUB-WP-0011-T05 +status: todo +priority: high +state_hub_task_id: "44b354ac-b94a-4c71-9c43-79f5e67f671f" +``` + +**Action:** `AdoptPatternAction { widgetPatternId }` in `WidgetPatterns` controller. + +```haskell +-- 1. Resolve adopting hub (from session user's hub) +-- 2. Create PatternAdoption record +adoption <- newRecord @PatternAdoption + |> set #widgetPatternId widgetPatternId + |> set #adoptingHubId hubId + |> set #isAnonymous (param "isAnonymous") + |> createRecord + +-- 3. Check if pattern's widget_type is in the hub's manifest +manifest <- query @HubCapabilityManifest + |> filterWhere (#hubId, hubId) + |> filterWhere (#status, "active") + |> fetchOneOrNothing + +let needsAmendment = case manifest of + Nothing -> True + Just m -> not (pattern.widgetType `elem` m.declaredWidgetTypes) + +-- 4. If amendment needed, create draft manifest +when needsAmendment $ do + let newTypes = maybe [pattern.widgetType] + (\m -> m.declaredWidgetTypes ++ [pattern.widgetType]) + manifest + _draft <- newRecord @HubCapabilityManifest + |> set #hubId hubId + |> set #status "draft" + |> set #declaredWidgetTypes newTypes + -- ... carry over other declared types from existing manifest + |> createRecord + setSuccessMessage "Pattern adopted. A manifest amendment draft has been created — please review and activate it." + redirectTo (ShowHubCapabilityManifestAction draftId) +``` + +When no amendment is needed, redirect to hub's pattern list with success message. + +--- + +### T06 — Governance Template Library: CRUD and clone with manifest amendment + +```task +id: IHUB-WP-0011-T06 +status: todo +priority: medium +state_hub_task_id: "f31b86d3-573e-4a87-b179-609872565b0c" +``` + +**Controller:** `Web/Controller/GovernanceTemplates.hs` + +Actions: +- `GovernanceTemplatesAction` — list published templates with clone count +- `ShowGovernanceTemplateAction { governanceTemplateId }` — detail with clones +- `NewGovernanceTemplateAction` / `CreateGovernanceTemplateAction` — validate + each category in the `categories` JSONB array exists in + `annotation_category_registry` +- `CloneGovernanceTemplateAction { governanceTemplateId }` — creates + `GovernanceTemplateClone`; checks each template category against the cloning + hub's `declared_annotation_categories`; if any missing, creates manifest + amendment draft with the new categories added + +**Category validation helper:** +```haskell +validateCategories :: [Text] -> IO (Either [Text] ()) +validateCategories cats = do + registered <- query @AnnotationCategoryRegistry + |> filterWhere (#isActive, True) + |> fetch + let known = map (.name) registered + let unknown = filter (`notElem` known) cats + pure $ if null unknown then Right () else Left unknown +``` + +**Views:** `Web/View/GovernanceTemplates/{Index,Show,New}.hs` + +--- + +### T07 — Adoption tracking and aggregate friction/outcome view per pattern + +```task +id: IHUB-WP-0011-T07 +status: todo +priority: medium +state_hub_task_id: "5642dd12-4255-42d7-9411-63e032cc2b57" +``` + +**On pattern Show page**, add aggregate panel (only for non-anonymous adopters): + +```sql +SELECT + COUNT(pa.id) AS adopter_count, + AVG(fs.score) AS mean_friction_score, + COUNT(os.id) AS outcome_signal_count +FROM pattern_adoptions pa +JOIN widgets w + ON w.hub_id = pa.adopting_hub_id + AND w.widget_type = ? -- pattern's widget_type +LEFT JOIN friction_scores fs + ON fs.widget_id = w.id +LEFT JOIN outcome_signals os + ON os.widget_id = w.id +WHERE pa.widget_pattern_id = ? + AND pa.is_anonymous = FALSE +``` + +Show: +- Adopter count (total, including anonymous) +- Mean friction score across non-anonymous adopter hubs (NULL if none) +- Outcome signal count +- "N hubs opted out of aggregate feedback" note if `is_anonymous` count > 0 + +**Hub's adopted patterns list** (`Web/View/HubRegistry/Show.hs`): list all +`PatternAdoption` records for the hub with pinned/follow-latest status. + +--- + +### T08 — Marketplace dashboard: search and browse patterns and templates + +```task +id: IHUB-WP-0011-T08 +status: todo +priority: medium +state_hub_task_id: "01ea4d7d-cbd3-4149-b772-7e131f4f7e9c" +``` + +**Controller:** `Web/Controller/MarketplaceDashboard.hs` + +Use `autoRefresh`. Two sections: + +**Widget Patterns:** +- Full-text search: `WHERE to_tsvector('english', wp.name || ' ' || COALESCE(wp.description,'')) @@ plainto_tsquery(?)` +- Filter by `widget_type` (dropdown from `widget_type_registry`) +- Sort: Most adopted (default) | Recently published | Alphabetical +- Trending: most `pattern_adoptions` created in last 30 days + +**Governance Templates:** +- Same search pattern over `governance_templates.name + description` +- Filter by category +- Sort: Most cloned | Recent | Alphabetical + +**Layout:** Two-column tab layout (Widget Patterns | Governance Templates). +Hub Registry link at top of page. + +**Views:** `Web/View/MarketplaceDashboard/Show.hs` + +Add route and nav link ("Marketplace"). + +--- + +### T09 — API v2: hub registry and widget pattern endpoints + +```task +id: IHUB-WP-0011-T09 +status: todo +priority: medium +state_hub_task_id: "34d3339a-cf17-4475-b848-eeb077ede8e6" +``` + +Add to `/api/v2/` surface (all behind `requireApiConsumer` + `checkRateLimitAndLog`): + +**Endpoints:** + +| Method | Path | Description | +|---|---|---| +| GET | `/api/v2/hub-registry` | List hubs with active manifest summary + GAAF indicator | +| GET | `/api/v2/hub-registry/:hubId` | Single hub detail (manifest vocabulary, health) | +| GET | `/api/v2/widget-patterns` | List published patterns (paginated) | +| GET | `/api/v2/widget-patterns/:id` | Pattern detail with version history | +| POST | `/api/v2/widget-patterns/:id/adopt` | Create PatternAdoption for authenticated consumer's hub | + +**New controllers:** +- `Web/Controller/Api/V2/HubRegistry.hs` +- `Web/Controller/Api/V2/WidgetPatterns.hs` + +**OpenAPI update:** Add paths to `Web/Controller/Api/V2/OpenApi.hs`. The +`widget_type` enum in widget pattern responses continues to reference the live +registry query (no hardcoding). + +**Routes and Types:** Update `Web/Routes.hs` and `Web/Types.hs` for new actions. + +--- + +### T10 — ARCHITECTURE-LAYERS.md scorecard update + exit criteria validation + +```task +id: IHUB-WP-0011-T10 +status: todo +priority: medium +state_hub_task_id: "9af8cd05-7864-438d-92a2-052d0af3bcbc" +``` + +**Exit criteria checklist (from v0.2 spec §Phase 10):** + +- [ ] Hub registry renders all registered hubs with active manifest vocabulary + and current health score +- [ ] Widget pattern library lists published patterns with version history; + each pattern's widget_type links to its registry entry +- [ ] A pattern can be published from one hub and adopted into another; + adoption triggers manifest amendment draft when new types are introduced +- [ ] Adoption tracking shows which hubs use which patterns +- [ ] Governance template cloning works end-to-end; new categories appear in + the adopting hub's manifest amendment +- [ ] Marketplace dashboard renders search and browse +- [ ] Hub registry GAAF compliance indicator renders correctly for all hubs + +**Scorecard updates:** + +Phase 10's primary improvement target is the **Customization layer** (currently +2.5 → target ≥3.0): the manifest amendment workflow (T05, T06) constitutes a +formal per-hub configuration contract with migration support, which is the +specific criterion for Customization improvement. + +Overall target: ≥3.5 (Strong). + +**CLAUDE.md updates:** +- Move IHUB-WP-0011 to completed list +- Set active workplan to IHUB-WP-0012 (Phase 11 — Advanced AI Federation) +- Update "Current state" description + +**Commit all changes** and mark workplan `status: done`.