Adds all Phase 0 content that was created but never committed: - CLAUDE.md and SCOPE.md — agent and developer orientation - specs/TailwindForInteractionHubs_v0.2.md — IHF Tailwind coding guide - docs/ — five IHP v1.5 reference guides (overview, data, controllers, realtime, ihf-mapping) - workplans/IHUB-WP-0001 — Phase 1 implementation plan (12 tasks, state-hub synced) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.9 KiB
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
-- 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:
-- 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:
-- 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 <div class="decision-card"> fragment and appends it to the ledger list. No full page reload; no separate API contract; the server is the single source of truth.
Background Jobs for Async Distillation (Future — Phase 5)
When IHF Phase 5 (Agent-Assisted Distillation) is implemented, IHP's job system handles the async pipeline:
-- Application/Job/DistillAnnotationsJob.hs
instance Job DistillAnnotationsJob where
perform DistillAnnotationsJob { widgetId } = do
annotations <- query @Annotation
|> filterWhere (#widgetId, widgetId)
|> filterWhere (#requirementId, Nothing) -- unprocessed
|> fetch
when (length annotations >= 3) do
-- Call AI API for clustering + proposal draft
proposal <- callAIDistillation annotations
createRecord proposal
-- Trigger AutoRefresh on governance views
notifyTable "requirement_candidates"
Jobs are queued as Postgres records and processed by the RunJobs binary, which runs alongside the main app in production.
Deployment for IHF
IHF infrastructure can be fully declared in Config/nix/hosts/ and deployed with deploy-to-nixos. A minimal configuration.nix for Phase 1:
{ config, pkgs, ... }: {
services.ihp = {
enable = true;
domain = "ihf.yourdomain.com";
ihpPackage = (import ./ihf.nix).ihf;
dbName = "ihf_production";
sessionSecret = config.age.secrets.ihpSessionSecret.path;
};
services.nginx.enable = true;
security.acme.defaults.email = "admin@yourdomain.com";
security.acme.acceptTerms = true;
}
All secrets (session key, DB password) managed via agenix — encrypted in git, decrypted on the NixOS host at deploy time.