- docs/phase1-summary.md: what was built, known limitations, Phase 2 readiness - SCOPE.md: current state updated to Phase 1 complete - All 12 workplan tasks marked done; workplan status set to done Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
15 KiB
id, type, title, domain, repo, status, owner, topic_slug, created, updated, state_hub_workstream_id
| id | type | title | domain | repo | status | owner | topic_slug | created | updated | state_hub_workstream_id |
|---|---|---|---|---|---|---|---|---|---|---|
| IHUB-WP-0001 | workplan | IHF Phase 1 — Minimal Interaction Core | custodian | inter-hub | done | custodian | custodian | 2026-03-27 | 2026-03-27 | 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
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:
- Install Determinate Nix and
ihp-newif not already present - Run
ihp-new ihfinside/home/worsch/inter-hub/(or initialise in-place) - Verify
devenv upstarts cleanly (app on:8000, IDE on:8001, Postgres managed by Nix) - Commit the baseline scaffold
- 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
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:
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
Hubrecord for local development
Exit criteria: migrate runs cleanly; Hub, Widget, WidgetVersion types available in GHCi.
T03 — Schema design: InteractionEvent and Annotation
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:
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_eventsis append-only: add a PostgreSQL trigger or application-level guard preventing UPDATE/DELETE- Valid
categoryvalues:friction,defect,wish,policy_concern,doc_gap,trust,other - Valid
actor_typevalues:user,agent,automation,anonymous
Exit criteria: Migration runs cleanly; types generated; append-only guard in place.
T04 — Hub controller and views (CRUD)
id: IHUB-WP-0001-T04
status: done
priority: high
state_hub_task_id: "20517418-85c9-4335-a445-dbbf99a81ae5"
Scaffold Hub management:
- Use IHP Code Generator (
localhost:8001/Generators) to scaffoldHubsController - Implement index, show, new, create, edit, update, delete actions
- Index view: list of hubs with slug, domain, widget count
- 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)
id: IHUB-WP-0001-T05
status: done
priority: high
state_hub_task_id: "262bfdb0-896c-4873-981f-36ea865b5dfe"
Scaffold Widget management:
- Scaffold
WidgetsController - Implement index, show, new, create, edit, update actions (no delete — widgets are deprecated, not deleted)
CreateWidgetAction: on create, also insert aWidgetVersionrecord withversion=1and a JSON snapshot of the widgetUpdateWidgetAction: incrementversion, insert newWidgetVersionrecord- Index view: table of widgets with hub, type, status, version, event count
- Show view: widget detail + version history + recent interaction events + annotations
- 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
id: IHUB-WP-0001-T06
status: done
priority: high
state_hub_task_id: "3a48509e-9014-43d1-a244-21d7c322d8cc"
Implement interaction event capture:
POST /widgets/:widgetId/events→CreateInteractionEventAction { widgetId }- Bind:
event_type,actor_id(optional),actor_type,view_context_ref,metadata(JSON) - Validate:
event_typemust 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) - Populate
actor_id/actor_typefromcurrentUserOrNothingwhen the actor is authenticated - Respond with JSON
{ id, widget_id, event_type, occurred_at }for programmatic clients - 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
id: IHUB-WP-0001-T07
status: done
priority: high
state_hub_task_id: "1cc61933-46cd-46d1-b79a-05a8b40cd23b"
Implement annotation CRUD:
- Scaffold
AnnotationsControllerscoped to a widget:/widgets/:widgetId/annotations/ IndexAnnotationsAction { widgetId }— list annotations, threaded byparent_idCreateAnnotationAction { widgetId }— create annotation, auto-setactor_id/actor_typefrom session- Form:
body(textarea),category(select), optionalparentId(for replies),widgetStateRef - Validate:
bodynon-empty;categoryin valid set - List view: threaded annotation tree (root annotations + replies indented)
- 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
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:
- Create
Application/Helper/View.hsfunctionwidgetEnvelope:
widgetEnvelope :: Widget -> Html -> Html
widgetEnvelope widget inner = [hsx|
<div
class="ihf-widget"
data-widget-id={tshow widget.id}
data-widget-type={widget.widgetType}
data-hub-id={tshow widget.hubId}
data-capability-ref={fromMaybe "" widget.capabilityRef}
data-view-context={fromMaybe "" widget.viewContext}
data-policy-scope={widget.policyScope}
data-widget-version={tshow widget.version}
>
{inner}
<div class="ihf-widget-controls">
<a href={pathTo WidgetAnnotationsAction { widgetId = widget.id }}
class="ihf-annotate-btn">Annotate</a>
</div>
</div>
|]
- Document the convention in
docs/widget-envelope-convention.md - 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)
id: IHUB-WP-0001-T09
status: done
priority: high
state_hub_task_id: "b0ca9f93-cd64-421f-a426-999f35db148f"
Implement the live hub operator dashboard:
ShowHubActionwrapped withautoRefresh do- 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)
- Layout must include
{autoRefreshMeta},morphdom.js,ihp-auto-refresh.js - 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
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:
- Add
userstable toSchema.sql:id,email,password_hash,locked_at,failed_login_attempts,name - Configure
initAuthentication @UserinFrontController - Mount
SessionsController - Add
beforeAction = ensureIsUsertoHubsControllerandWidgetsController - Update
CreateInteractionEventActionandCreateAnnotationActionto readcurrentUserOrNothingand setactor_id/actor_typeaccordingly - Seed one admin user for local development (use
hash-passwordCLI)
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
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):
- Widget show page (
ShowWidgetAction) aggregates:- Full annotation thread (threaded, with actor, category, timestamp)
- Interaction event history (paginated, 20 per page)
- Widget version history
- Add a summary KPI row: total events, total annotations, annotation breakdown by category
- 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
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:
- 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
autoRefreshwrapper is present on dashboard action
- Consistency sync:
cd ~/the-custodian && make fix-consistency REPO=inter-hub - Documentation updates:
- Update
SCOPE.mdcurrent state section: Phase 1 complete - Write brief
docs/phase1-summary.md: what was built, known limitations, Phase 2 readiness
- Update
- 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.hswiring automatically.