# IHP ↔ IHF Capability Mapping > How IHP's specific capabilities serve the Interaction Hub Framework's requirements. > Use this as a decision guide when implementing IHF modules. --- ## Core Mapping | IHF Requirement | IHP Capability | Notes | |----------------|---------------|-------| | Widget semantic identity (stable IDs) | `Id Widget` newtype, `Schema.sql` | UUIDs prevent FK mixups at compile time | | Widget registry CRUD | Standard IHP controller + AutoRoute | Code generator scaffolds it in minutes | | Widget envelope metadata | `JSONB` columns + Haskell `Value` | `config`, `metadata`, `context_ref` fields | | Interaction event capture (append-only) | Controller action + `createRecord` | Add DB-level APPEND-only trigger for enforcement | | Annotation threads | Belongs-to relationships + `fetchRelated` | `annotations.parent_id` for threading | | Live dashboard (hub-level signals) | AutoRefresh | Zero client-side framework needed | | Reactive annotation UI | Server-Side Components or HTMX | SSC for rich state; HTMX for simple append | | Multi-tenant widget data isolation | DataSync + PostgreSQL RLS | `ihp_user_id()` in RLS policies | | Governance ledger (decision records) | Append-only table + HTMX | `decisions` table; controllers append, never update | | Actor attribution | `currentUserOrNothing` + `actor_type` field | Supports human/agent/automation attribution | | Traceability chain | FK relationships across tables | Widget → InteractionEvent → Annotation → RequirementCandidate | | Async processing (batch analysis) | IHP Background Jobs | `RunJobs` binary; jobs queued in Postgres | | Reproducible deployment | NixOS + `deploy-to-nixos` | All server config version-controlled | | AI-assisted distillation (Phase 5) | Background Jobs + external API calls | Job fetches annotation cluster, calls AI API, stores `AgentProposal` | --- ## Type Safety as Governance Infrastructure IHP's type system is more than a developer convenience — it is governance infrastructure for IHF: **Widget identity integrity:** `Id Widget` ≠ `Id Annotation` ≠ `Id Hub`. Cross-type ID confusion (a common source of traceability chain breakage) is a compile error, not a runtime bug. **Field existence enforcement:** `fill @'["widgetType", "hubId"]` lists the fields bound from HTTP parameters. Adding or removing a field in `Schema.sql` propagates as a compile error to every controller that uses it — schema drift is caught immediately. **URL correctness:** `redirectTo ShowWidgetAction { widgetId = w.id }` — if `ShowWidgetAction` is renamed or its fields change, every call site fails to compile. Broken governance links are impossible. **View exhaustiveness:** `case` on widget status or annotation category in views will produce a GHC warning if a new constructor is added to the enum — ensuring governance views stay current with the data model. --- ## Schema Design Recommendations for IHF Phase 1 ```sql -- Hubs: bounded domains of responsibility CREATE TABLE hubs ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, slug TEXT NOT NULL UNIQUE, name TEXT NOT NULL, domain TEXT NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); -- Widget registry CREATE TABLE widgets ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, hub_id UUID NOT NULL REFERENCES hubs(id) ON DELETE RESTRICT, name TEXT NOT NULL, widget_type TEXT NOT NULL, -- chart | form | table | action | panel | etc. capability_ref TEXT, -- reference to hub capability view_context TEXT, -- logical location in the UI policy_scope TEXT NOT NULL DEFAULT 'internal', status TEXT NOT NULL DEFAULT 'active', version INTEGER NOT NULL DEFAULT 1, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); -- Widget version history CREATE TABLE widget_versions ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, widget_id UUID NOT NULL REFERENCES widgets(id) ON DELETE CASCADE, version INTEGER NOT NULL, schema_snapshot JSONB NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, UNIQUE (widget_id, version) ); -- Interaction events (append-only; never UPDATE or DELETE) CREATE TABLE interaction_events ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, widget_id UUID NOT NULL REFERENCES widgets(id) ON DELETE CASCADE, event_type TEXT NOT NULL, -- viewed | clicked | submitted | commented | flagged_confusing | etc. actor_id UUID, actor_type TEXT NOT NULL DEFAULT 'user', -- user | agent | automation | anonymous view_context_ref TEXT, metadata JSONB DEFAULT '{}' NOT NULL, occurred_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); -- Annotations (structured comments on widgets) CREATE TABLE annotations ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, widget_id UUID NOT NULL REFERENCES widgets(id) ON DELETE CASCADE, parent_id UUID REFERENCES annotations(id) ON DELETE CASCADE, -- for threads body TEXT NOT NULL, category TEXT NOT NULL DEFAULT 'friction', -- friction | defect | wish | policy_concern | doc_gap | trust | other actor_id UUID, actor_type TEXT NOT NULL DEFAULT 'user', widget_state_ref TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); ``` --- ## AutoRefresh for Hub Dashboards The hub operator dashboard is the primary immediate value of IHF Phase 1. AutoRefresh delivers it with minimal complexity: ```haskell -- Web/Controller/Hubs.hs action ShowHubAction { hubId } = autoRefresh do hub <- fetch hubId widgets <- query @Widget |> filterWhere (#hubId, hubId) |> orderByDesc #createdAt |> fetch recentEvents <- query @InteractionEvent |> filterWhere (#hubId, hubId) |> orderByDesc #occurredAt |> limit 50 |> fetch annotations <- query @Annotation |> filterWhere (#hubId, hubId) |> orderByDesc #createdAt |> limit 20 |> fetch render ShowView { .. } ``` Any insert into `widgets`, `interaction_events`, or `annotations` with this `hub_id` automatically re-renders the dashboard for all connected operators. No WebSocket plumbing needed on the server side beyond the `autoRefresh` wrapper. --- ## HTMX for the Governance Ledger The governance ledger should be append-only. HTMX's hypermedia pattern maps directly: ```haskell -- Append a decision record action CreateDecisionAction { requirementId } = do let decision = newRecord @Decision decision |> fill @'["outcome", "rationale"] |> validateField #outcome nonEmpty |> validateField #rationale nonEmpty |> ifValid \case Left _ -> respondHtml errorFragment Right decision -> do decision <- createRecord decision -- Also update requirement status requirement <- fetch requirementId >>= updateRecord . set #status "decided" respondHtml (renderDecisionCard decision) ``` The client receives a rendered `