--- id: IHUB-WP-0001 type: workplan title: "IHF Phase 1 — Minimal Interaction Core" domain: inter_hub repo: inter-hub status: done owner: custodian topic_slug: custodian created: "2026-03-27" updated: "2026-03-27" state_hub_workstream_id: "4733dbde-bdcf-4e00-b9b8-749f92e50cae" --- # IHF Phase 1 — Minimal Interaction Core ## Goal Implement the minimum viable governed interaction substrate for the Interaction Hub Framework: a working widget registry, interaction event capture, annotation system, and hub-level operator dashboard. This delivers Phase 1 of the IHF specification (`specs/InteractionHubFrameworkSpecification_v0.1.md`). ## Background Phase 0 (specification foundation) is complete. The IHF spec defines 8 phases; Phase 1 establishes the semantic core that all subsequent phases build on. **Technology stack:** IHP v1.5 (Haskell, Nix), PostgreSQL, AutoRefresh (live dashboards), HTMX (governance actions), standard IHP forms (widget/annotation CRUD). Reference: `docs/ihp-overview.md`, `docs/ihp-data-and-queries.md`, `docs/ihp-controllers-views-forms.md`, `docs/ihp-realtime.md`, `docs/ihp-ihf-mapping.md`. ## Phase 1 Exit Criteria (from IHF spec §14 Phase 1) - Widgets can be addressed and commented on reliably - Interaction data is captured with actor attribution and view context - Hub-level inspection of interaction signals is possible ## Data Artifacts Introduced (Phase 1) `Hub`, `Widget`, `WidgetVersion`, `InteractionEvent`, `Annotation`, `CapabilityReference`, `ViewContext` --- ## Tasks ### T01 — IHP project bootstrap ```task id: IHUB-WP-0001-T01 status: done priority: high state_hub_task_id: "e9e83628-d485-4163-9467-0d161f6274f3" ``` Set up the IHP project skeleton for inter-hub: 1. Install Determinate Nix and `ihp-new` if not already present 2. Run `ihp-new ihf` inside `/home/worsch/inter-hub/` (or initialise in-place) 3. Verify `devenv up` starts cleanly (app on `:8000`, IDE on `:8001`, Postgres managed by Nix) 4. Commit the baseline scaffold 5. Note first-startup time (expect 10–15 min for Nix cache population) **Exit criteria:** `devenv up` succeeds; `http://localhost:8000` returns the IHP welcome page. --- ### T02 — Schema design: Hub, Widget, WidgetVersion ```task id: IHUB-WP-0001-T02 status: done priority: high state_hub_task_id: "e7254445-1375-44c3-9c59-111215b70692" ``` Define the widget registry tables in `Application/Schema.sql`: ```sql 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 ); 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, capability_ref TEXT, view_context TEXT, 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 ); 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) ); ``` - Write corresponding migration file in `Application/Migration/` - Verify Haskell types are generated correctly (IHP auto-generates on save) - Seed a dev `Hub` record for local development **Exit criteria:** `migrate` runs cleanly; `Hub`, `Widget`, `WidgetVersion` types available in GHCi. --- ### T03 — Schema design: InteractionEvent and Annotation ```task id: IHUB-WP-0001-T03 status: done priority: high state_hub_task_id: "dac18955-7b2f-464f-97eb-0733c9163088" ``` Define the capture tables in `Application/Schema.sql`: ```sql 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, actor_id UUID, actor_type TEXT NOT NULL DEFAULT 'user', view_context_ref TEXT, metadata JSONB DEFAULT '{}' NOT NULL, occurred_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE INDEX interaction_events_widget_id_idx ON interaction_events (widget_id); CREATE INDEX interaction_events_occurred_at_idx ON interaction_events (occurred_at DESC); 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, body TEXT NOT NULL, category TEXT NOT NULL DEFAULT 'friction', actor_id UUID, actor_type TEXT NOT NULL DEFAULT 'user', widget_state_ref TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE INDEX annotations_widget_id_idx ON annotations (widget_id); ``` - Write migration file - `interaction_events` is append-only: add a PostgreSQL trigger or application-level guard preventing UPDATE/DELETE - Valid `category` values: `friction`, `defect`, `wish`, `policy_concern`, `doc_gap`, `trust`, `other` - Valid `actor_type` values: `user`, `agent`, `automation`, `anonymous` **Exit criteria:** Migration runs cleanly; types generated; append-only guard in place. --- ### T04 — Hub controller and views (CRUD) ```task id: IHUB-WP-0001-T04 status: done priority: high state_hub_task_id: "20517418-85c9-4335-a445-dbbf99a81ae5" ``` Scaffold Hub management: 1. Use IHP Code Generator (`localhost:8001/Generators`) to scaffold `HubsController` 2. Implement index, show, new, create, edit, update, delete actions 3. Index view: list of hubs with slug, domain, widget count 4. Show view: hub details + list of widgets (with event counts) **Exit criteria:** Hubs can be created, listed, viewed, edited, and deleted via the web UI. --- ### T05 — Widget Registry controller and views (CRUD) ```task id: IHUB-WP-0001-T05 status: done priority: high state_hub_task_id: "262bfdb0-896c-4873-981f-36ea865b5dfe" ``` Scaffold Widget management: 1. Scaffold `WidgetsController` 2. Implement index, show, new, create, edit, update actions (no delete — widgets are deprecated, not deleted) 3. `CreateWidgetAction`: on create, also insert a `WidgetVersion` record with `version=1` and a JSON snapshot of the widget 4. `UpdateWidgetAction`: increment `version`, insert new `WidgetVersion` record 5. Index view: table of widgets with hub, type, status, version, event count 6. Show view: widget detail + version history + recent interaction events + annotations 7. Form: `name`, `widget_type` (select), `hubId` (select), `capabilityRef`, `viewContext`, `policyScope` (select: internal/hub/public), `status` **Exit criteria:** Widgets can be registered, listed, and viewed. Version history is tracked on every update. --- ### T06 — Interaction Event capture ```task id: IHUB-WP-0001-T06 status: done priority: high state_hub_task_id: "3a48509e-9014-43d1-a244-21d7c322d8cc" ``` Implement interaction event capture: 1. `POST /widgets/:widgetId/events` → `CreateInteractionEventAction { widgetId }` 2. Bind: `event_type`, `actor_id` (optional), `actor_type`, `view_context_ref`, `metadata` (JSON) 3. Validate: `event_type` must be non-empty and in the canonical list (viewed, clicked, submitted, abandoned, retried, failed, commented, flagged_confusing, flagged_helpful, blocked_by_policy, escalated, accepted_recommendation, rejected_recommendation) 4. Populate `actor_id` / `actor_type` from `currentUserOrNothing` when the actor is authenticated 5. Respond with JSON `{ id, widget_id, event_type, occurred_at }` for programmatic clients 6. No HTML view needed for this action — it's a capture endpoint **Exit criteria:** `POST` to the capture endpoint creates an `InteractionEvent` record with correct actor attribution; unknown `event_type` values are rejected with 422. --- ### T07 — Annotation controller ```task id: IHUB-WP-0001-T07 status: done priority: high state_hub_task_id: "1cc61933-46cd-46d1-b79a-05a8b40cd23b" ``` Implement annotation CRUD: 1. Scaffold `AnnotationsController` scoped to a widget: `/widgets/:widgetId/annotations/` 2. `IndexAnnotationsAction { widgetId }` — list annotations, threaded by `parent_id` 3. `CreateAnnotationAction { widgetId }` — create annotation, auto-set `actor_id`/`actor_type` from session 4. Form: `body` (textarea), `category` (select), optional `parentId` (for replies), `widgetStateRef` 5. Validate: `body` non-empty; `category` in valid set 6. List view: threaded annotation tree (root annotations + replies indented) 7. No edit/delete (append-only); add a "retract" flag if needed (`retracted_at TIMESTAMP`) **Exit criteria:** Annotations can be created and listed per widget with threading. Actor attribution is automatic for logged-in users. --- ### T08 — Widget Envelope convention ```task id: IHUB-WP-0001-T08 status: done priority: medium state_hub_task_id: "d2dfbdf6-fe66-4478-afeb-7ea3f05bea2b" ``` Establish the Widget Envelope as a reusable HSX helper: 1. Create `Application/Helper/View.hs` function `widgetEnvelope`: ```haskell widgetEnvelope :: Widget -> Html -> Html widgetEnvelope widget inner = [hsx|
|] ``` 2. Document the convention in `docs/widget-envelope-convention.md` 3. Demonstrate use in the Hub dashboard view by wrapping at least one widget card **Exit criteria:** `widgetEnvelope` renders the correct `data-*` attributes; the annotate link is functional. --- ### T09 — Hub operator dashboard (AutoRefresh) ```task id: IHUB-WP-0001-T09 status: done priority: high state_hub_task_id: "b0ca9f93-cd64-421f-a426-999f35db148f" ``` Implement the live hub operator dashboard: 1. `ShowHubAction` wrapped with `autoRefresh do` 2. Dashboard shows: - Widget count by type and status - Recent interaction events (last 50, across all hub widgets) - Recent annotations (last 20, across all hub widgets) - Per-widget event count bar (simple table or list) 3. Layout must include `{autoRefreshMeta}`, `morphdom.js`, `ihp-auto-refresh.js` 4. Test: open dashboard in two browser tabs; insert an event via `curl` → both tabs update within ~1s **Exit criteria:** Dashboard auto-updates on new events/annotations without page reload. AutoRefresh diff is confirmed in browser DevTools (WebSocket frames visible). --- ### T10 — Authentication and actor attribution ```task id: IHUB-WP-0001-T10 status: done priority: medium state_hub_task_id: "8ef87232-cb0d-4948-9bca-849048dd82c2" ``` Wire up IHP session auth for the admin/governance users: 1. Add `users` table to `Schema.sql`: `id`, `email`, `password_hash`, `locked_at`, `failed_login_attempts`, `name` 2. Configure `initAuthentication @User` in `FrontController` 3. Mount `SessionsController` 4. Add `beforeAction = ensureIsUser` to `HubsController` and `WidgetsController` 5. Update `CreateInteractionEventAction` and `CreateAnnotationAction` to read `currentUserOrNothing` and set `actor_id`/`actor_type` accordingly 6. Seed one admin user for local development (use `hash-password` CLI) **Exit criteria:** Unauthenticated access to hubs/widgets redirects to login. Annotations and events created by logged-in users carry the correct `actor_id`. --- ### T11 — Manual traceability view: Widget → Annotations ```task id: IHUB-WP-0001-T11 status: done priority: medium state_hub_task_id: "b342d44c-ca41-4373-a55d-c7dcc5121f4a" ``` Implement the traceability entry point (first link in the IHF traceability chain): 1. Widget show page (`ShowWidgetAction`) aggregates: - Full annotation thread (threaded, with actor, category, timestamp) - Interaction event history (paginated, 20 per page) - Widget version history 2. Add a summary KPI row: total events, total annotations, annotation breakdown by category 3. Link to parent hub from widget detail (breadcrumb: Hub > Widget) This is the Phase 1 terminal traceability view: Widget → InteractionEvents + Annotations. **Exit criteria:** The widget show page presents a complete picture of all interaction signals and annotations for a widget, linked back to the hub. --- ### T12 — Phase 1 gate: tests, consistency, and documentation ```task id: IHUB-WP-0001-T12 status: done priority: high state_hub_task_id: "ae5a8713-27ba-445b-a29f-822b5d0acf5a" ``` Gate tasks before Phase 1 is marked complete: 1. **Integration tests** (`Test/`): - Widget CRUD happy path - Event capture with and without authenticated user - Annotation create + list + threading - Validation rejection (empty body, invalid category, invalid event_type) - AutoRefresh: verify `autoRefresh` wrapper is present on dashboard action 2. **Consistency sync:** ```bash cd ~/the-custodian && make fix-consistency REPO=inter-hub ``` 3. **Documentation updates:** - Update `SCOPE.md` current state section: Phase 1 complete - Write brief `docs/phase1-summary.md`: what was built, known limitations, Phase 2 readiness 4. **Smoke test checklist:** - `devenv up` → clean start - Create a hub, create 3 widgets, capture events via API, annotate via UI - Dashboard auto-updates visible - All tests pass **Exit criteria:** All tests pass; consistency sync reports no errors; smoke test completed. --- ## Phase 1 Dependencies - IHP v1.5 installed via Nix (T01) - Schema stabilized before controller scaffolding (T02/T03 before T04–T07) - Auth before traceability view (T10 before T11) - All feature tasks (T01–T11) before gate (T12) ## Notes - **No DataSync in Phase 1.** AutoRefresh is sufficient for the operator dashboard. DataSync (with RLS) is Phase 2 work for widget embeds. - **No requirement candidates or decision records in Phase 1.** Those are Phase 2 (Structured Feedback and Triage) and Phase 3 (Governance and Decision Linkage). - **Append-only events:** the PostgreSQL trigger on `interaction_events` (T03) is critical — enforce it before wiring the capture endpoint. - **IHP Code Generator:** use it aggressively for T04–T07 scaffolding, then customize. It handles the `Types.hs` / `Routes.hs` / `FrontController.hs` wiring automatically.