--- id: IHUB-WP-0011 type: workplan title: "IHF Phase 10 — Hub Registry and Widget Marketplace" domain: inter_hub repo: inter-hub status: done 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: done 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: done 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: done 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: done 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: done 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: done 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: done 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: done 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: done 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: done 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`.