docs: add specification, reference docs, workplan, and agent guidance

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>
This commit is contained in:
2026-03-27 02:07:13 +01:00
parent 75b88ee760
commit 8b6ce5bbc8
9 changed files with 2181 additions and 0 deletions

134
CLAUDE.md Normal file
View File

@@ -0,0 +1,134 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
**inter-hub** is the reference implementation of the **Interaction Hub Framework (IHF)** — a governed, observable interaction substrate for hub-based AI-enabled software systems. It treats every UI element as a governed artifact, creating a full traceability chain from rendered widget → user interaction → structured feedback → requirement candidate → decision record → implementation change → observed outcome.
**Current state:** Phase 0 (specification) is complete. Phase 1 (Minimal Interaction Core) is the active implementation target. No application code exists yet.
For situational context, read `SCOPE.md`. For architecture depth, read `specs/InteractionHubFrameworkSpecification_v0.1.md`.
## Stack
- **IHP** (Integrated Haskell Platform) v1.5 — full-stack Haskell web framework, server-rendered + optional realtime
- **Haskell** (GHC 9.10) — strongly typed, functional
- **PostgreSQL** — canonical datastore, managed via Nix (no manual DB setup)
- **Nix / devenv** — reproducible environment
- **Tailwind CSS** — see `specs/TailwindForInteractionHubs_v0.2.md` for IHF-specific conventions
## Development Setup
Requires Determinate Nix + direnv:
```bash
# One-time environment setup
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh
nix profile install nixpkgs#ihp-new
nix profile add nixpkgs#direnv
# Bootstrap IHP project (Phase 1, Task T01)
ihp-new inter-hub
cd inter-hub
devenv up
```
After `devenv up`:
- App server: `http://localhost:8000`
- IHP IDE + Schema Designer: `http://localhost:8001`
## Key Commands
```bash
devenv up # Start dev environment (app + postgres + file watchers)
migrate # Run pending migrations
test # Run tests (auto-creates temp Postgres DB)
make static/prod.js static/prod.css # Production asset bundle
deploy-to-nixos production # NixOS deploy
```
Schema editing: use the IHP IDE at `localhost:8001` or edit `Application/Schema.sql` directly. Code generation via `localhost:8001/Generators`.
## Architecture
### Core Domain Model (Phase 1)
| Entity | Role |
|--------|------|
| `Hub` | Bounded domain of responsibility (Dev Hub, Ops Hub, etc.) |
| `Widget` | Smallest semantically governable interaction unit with stable ID |
| `WidgetVersion` | Version history of widget definitions |
| `InteractionEvent` | Recorded user/agent interaction (viewed, clicked, submitted, etc.) — **append-only** (enforced by PostgreSQL trigger) |
| `Annotation` | Structured comment attached to a widget with category |
| `ViewContext` | Logical location in the UI |
| `CapabilityReference` | Link to hub capability |
### Traceability Chain
```
Widget → InteractionEvent / Annotation
→ RequirementCandidate (Phase 2)
→ DecisionRecord (Phase 3)
→ ImplementationChange → DeploymentRecord → OutcomeSignal
```
### IHP Conventions
- Controllers live in `Web/Controller/`, views in `Web/View/`, types in `Web/Types.hs`
- Schema changes go in `Application/Schema.sql`, then generate with IHP IDE
- Use `AutoRefresh` for operator dashboards (server push on DB change) — not DataSync or Server-Side Components in Phase 1
- See `docs/ihp-ihf-mapping.md` for how IHP capabilities map to IHF requirements
### Widget Envelope
Every rendered widget wraps its HSX in a `widgetEnvelope` helper (Task T08) that injects the stable `widget-id` and `view-context` attributes, enabling client-side event capture without coupling to implementation.
## UI Conventions
All hub interfaces follow the Tailwind layer model in `specs/TailwindForInteractionHubs_v0.2.md`:
```
Semantic Role → Visual Primitive → Tailwind Token → Screen Composition
```
Key rules:
- Every interactive element belongs to a named semantic role (`action-primary`, `nav-item`, `data-cell`, etc.)
- Use spacing rhythm from the spec; do not invent ad-hoc spacing
- State cues (hover, active, disabled, error) follow the defined color roles
## Required Environment Variables
| Variable | Purpose |
|----------|---------|
| `IHP_SESSION_SECRET` | Session encryption key |
| `DATABASE_URL` | Postgres connection string |
| `IHP_BASEURL` | External URL (e.g., `https://example.com`) |
## Active Workplan
Phase 1 work is tracked in `workplans/IHUB-WP-0001-ihf-phase1-minimal-interaction-core.md` (12 tasks, T01T12). Use `/ralph-workplan workplans/IHUB-WP-0001-ihf-phase1-minimal-interaction-core.md` to drive implementation loops.
Phase 1 exit criteria:
- Widgets can be addressed and annotated reliably
- Interaction data is captured with actor attribution and view context
- Hub-level inspection of interaction signals is possible via a dashboard
## Key Reference Docs
| File | Purpose |
|------|---------|
| `SCOPE.md` | Situational guide — in/out of scope, terminology, entry points |
| `specs/InteractionHubFrameworkSpecification_v0.1.md` | Full IHF spec (8 phases, risks, design principles) |
| `specs/TailwindForInteractionHubs_v0.2.md` | Agent-optimized Tailwind coding guide |
| `docs/ihp-overview.md` | IHP v1.5 fundamentals and dev workflow |
| `docs/ihp-data-and-queries.md` | Schema design, auto-generated types, query builder, migrations |
| `docs/ihp-controllers-views-forms.md` | Controller patterns, HSX, forms, validation, auth |
| `docs/ihp-realtime.md` | AutoRefresh vs DataSync vs HTMX decision guide |
| `docs/ihp-ihf-mapping.md` | IHP capability → IHF requirement mapping with schema templates |
## Related Repositories
- `hub-core` — planned shared base package for domain/capability registration
- `the-custodian` — State Hub (decision records, workstreams) that IHF governance integrates with
- Downstream consumers: `dev-hub`, `ops-hub`, `fin-hub`

128
SCOPE.md Normal file
View File

@@ -0,0 +1,128 @@
# SCOPE
> This file helps you quickly understand what this repository is about,
> when it is relevant, and when it is not.
> It is intentionally lightweight and may be incomplete.
---
## One-liner
Specification and reference implementation of the Interaction Hub Framework (IHF) — a governed, observable interaction substrate for hub-based AI-enabled software systems.
---
## Core Idea
IHF treats every meaningful UI element as a **governed interaction artifact** rather than mere markup. It connects the full traceability chain from rendered widget → user interaction → structured feedback → requirement candidate → decision record → implementation change → observed outcome. This makes interaction observable, frustration actionable, and UI evolution evidence-based.
---
## In Scope
- Widget identity, lifecycle governance, and semantic addressability
- Interaction event capture and contextual enrichment
- Annotation and structured comment threads attached to widgets
- Requirements distillation from raw feedback clusters
- Governance ledger: decision records linked to requirements and implementations
- Outcome observation and pre/post change comparison
- AI-assisted distillation (bounded, attributable, reviewer-controlled)
- Cross-hub integration of interaction signals
- Platform APIs and conventions for framework-agnostic UI integration
- IHF specification documents and canonical artifact type definitions
---
## Out of Scope
- A complete universal frontend framework or design system
- Pixel-level visual design or CSS conventions
- Full product management methodology
- Replacement for DevOps observability tooling (ops-hub handles that)
- Unrestricted autonomous AI decision-making on requirements
- Mandatory single UI technology for all hub surfaces
---
## Relevant When
- Building or reviewing any hub dashboard or operator surface (dev-hub, ops-hub, fin-hub)
- Capturing and triaging user/operator feedback on existing hub UIs
- Defining governed widget identity for a new capability surface
- Implementing structured requirements distillation from interaction signals
- Designing cross-hub feedback routing or policy-linked decisions
---
## Not Relevant When
- Backend API development with no interaction surface
- Infrastructure provisioning or cluster operations (see railiance-*, ops-bridge)
- Data pipeline or SBOM work (see state-hub / dev-hub)
- One-off scripts or tooling with no end-user UI
---
## Current State
- Status: concept / early spec
- Implementation: Phase 0 complete (specification written); Phase 1 (Minimal Interaction Core) not yet started
- Stability: spec is draft — artifact model and vocabulary are stable enough for repo creation
- Usage: none yet; reference implementation target is IHP + PostgreSQL
---
## How It Fits
- Upstream dependencies: hub-core (for base models, domain/capability registration, MCP tools) — see CUST-WP-0025
- Downstream consumers: dev-hub, ops-hub, fin-hub — any hub with an operator-facing surface
- Often used with: kaizen-agentic (agent-assist module), state-hub (decision records, requirement linkage)
---
## Terminology
- Preferred terms: Widget, Widget Envelope, Interaction Event, Annotation, Requirement Candidate, Decision Record, Outcome Signal
- Also known as: IHF, inter-hub
- Potentially confusing terms: "Hub" here = bounded domain of responsibility (Dev Hub, Ops Hub, etc.) — not the GitHub feature; "Widget" = governed semantic unit, not a visual component library widget
---
## Related / Overlapping Repositories
- `the-custodian` — provides state-hub (decision records, workstreams) that IHF governance ledger will integrate with
- `ops-bridge` — tunnel connectivity for remote hub surfaces
- `kaizen-agentic` — agent personas that map to IHF's Agent Integration Module (§9.8)
- `hub-core` — planned shared base package that IHF will depend on for domain/capability plumbing
---
## Getting Oriented
- Start with: `specs/InteractionHubFrameworkSpecification_v0.1.md` — the full IHF spec (19 sections)
- Key files / directories: `specs/` (specifications), root `SCOPE.md` (this file)
- Entry points: read IHF spec §6 (Key Concepts), §9 (Core Modules), §14 (Phased Implementation Plan)
---
## Provided Capabilities
```capability
type: framework
title: Governed interaction substrate
description: Provides widget registry, interaction event capture, annotation, and requirements distillation for hub-based AI systems.
keywords: [widget, interaction, feedback, annotation, requirements, governance, traceability]
```
```capability
type: specification
title: IHF specification and artifact model
description: Canonical artifact types and traceability chain from Widget → InteractionEvent → RequirementCandidate → DecisionRecord → OutcomeSignal.
keywords: [spec, artifact, traceability, widget, decision, outcome]
```
---
## Notes
Phase 0 is complete (specification foundation). Implementation begins at Phase 1 (widget registry + interaction event capture). The spec is intentionally broader than the first implementation — IHP is the reference technology for Phase 1, but the framework is designed to survive UI technology changes (§12.7, §Phase 6).

View File

@@ -0,0 +1,272 @@
# IHP: Controllers, Views, Forms, and Authentication
---
## Controllers and Actions
Controllers live in `Web/Controller/`. Each action is a constructor in `Web/Types.hs` and a case in the controller's `action` handler.
```haskell
-- Web/Controller/Widgets.hs
instance Controller WidgetsController where
action WidgetsAction = do
widgets <- query @Widget |> fetch
render IndexView { .. }
action ShowWidgetAction { widgetId } = do
widget <- fetch widgetId
events <- query @InteractionEvent
|> filterWhere (#widgetId, widgetId)
|> orderByDesc #occurredAt
|> limit 20
|> fetch
render ShowView { .. }
action CreateWidgetAction = do
let widget = newRecord @Widget
widget
|> fill @'["name", "widgetType", "hubId"]
|> validateField #name nonEmpty
|> ifValid \case
Left widget -> render NewView { widget }
Right widget -> do
widget <- createRecord widget
setSuccessMessage "Widget created"
redirectTo WidgetsAction
```
### Key Patterns
| Pattern | Description |
|---------|-------------|
| `fill @'["fieldA", "fieldB"]` | Type-safe HTTP param binding; compile error if field doesn't exist |
| `validateField #name nonEmpty` | Chains a validator onto the record |
| `ifValid \case Left r -> ... Right r -> ...` | Branches on validation success/failure |
| `createRecord r` | INSERT; returns the persisted record |
| `updateRecord r` | UPDATE |
| `deleteRecord r` | DELETE |
| `redirectTo SomeAction { ... }` | Type-safe redirect |
| `render SomeView { .. }` | Passes in-scope bindings to the view (record wildcard) |
| `respondJson value` | JSON response (no view) |
| `respondHtml someHtml` | Partial HTML response (useful with htmx) |
### Before Filters
```haskell
instance Controller WidgetsController where
beforeAction = do
ensureIsUser -- redirects to login if not authenticated
-- custom per-controller guards here
action ...
```
### ControllerContext
An implicit key-value map threaded through all actions and views. Stores current user, request, flash messages, layout. Access via `fromContext @SomeType` or framework helpers like `currentUser`.
---
## Views and HSX
### The View Typeclass
Views are data types with a `View` instance:
```haskell
data ShowView = ShowView
{ widget :: !Widget
, events :: ![InteractionEvent]
}
instance View ShowView where
html ShowView { .. } = [hsx|
<div class="widget-detail">
<h1>{widget.name}</h1>
<div class="events">
{forEach events renderEvent}
</div>
</div>
|]
```
Views can also implement a `json` method for content-negotiated API responses.
### HSX Syntax
HSX is a quasi-quoter (`[hsx| ... |]`) that compiles to BlazeHtml. Checked at compile time.
```haskell
[hsx|
-- Interpolation (auto-escaped, XSS-safe)
<span>{widget.name}</span>
-- Raw HTML (opt-out of escaping)
<div>{preEscapedToHtml trustedHtml}</div>
-- Conditionals
{if widget.status == "active"
then [hsx|<span class="badge active">Active</span>|]
else [hsx|<span class="badge inactive">Inactive</span>|]}
-- Loops
<ul>{forEach events renderEvent}</ul>
-- Boolean attributes: True → present, False → omitted
<input disabled={isReadOnly} />
-- Maybe attributes: Just → present, Nothing → omitted
<a target={maybeTarget}>Link</a>
-- Multiple root elements are OK (unlike JSX)
<dt>Name</dt>
<dd>{widget.name}</dd>
|]
```
**`<script>` and `<style>` blocks:** curly braces inside are **literal** (not interpolated). Pass values via `data-` attributes instead.
**Tag variants:**
- `hsx` — strict whitelist of known HTML tags/attributes
- `uncheckedHsx` — skips tag/attribute validation (for web components)
- `customHsx` — extends the whitelist
### Layouts
Layouts have type `Layout = Html -> Html`. They wrap the view's `html` output:
```haskell
-- Web/View/Layout.hs
defaultLayout :: Layout
defaultLayout inner = [hsx|
<!DOCTYPE html>
<html>
<head>
<title>IHF</title>
{autoRefreshMeta}
<script src="/vendor/morphdom.js"></script>
<script src="/vendor/ihp-auto-refresh.js"></script>
</head>
<body>
<nav>...</nav>
<main>{inner}</main>
</body>
</html>
|]
```
Set per-controller in `beforeAction` or globally in `FrontController.initContext`:
```haskell
setLayout defaultLayout
```
Data shared between layout and views (e.g., current user for the nav) is passed via `putContext`/`fromFrozenContext`.
---
## Forms and Validation
### Building Forms
`formFor` binds a record to a form, auto-generating method and action:
```haskell
renderForm :: Widget -> Html
renderForm widget = formFor widget [hsx|
{textField #name}
{selectField #widgetType widgetTypeOptions}
{selectField #hubId hubOptions}
{textareaField #description}
{submitButton}
|]
```
IHP detects whether the record is new (POST to `/CreateWidget`) or persisted (POST to `/UpdateWidget`) automatically.
**Available field helpers:** `textField`, `emailField`, `passwordField`, `urlField`, `numberField`, `colorField`, `dateField`, `dateTimeField`, `textareaField`, `selectField`, `radioField`, `checkboxField`, `fileField`, `hiddenField`.
**Field customisation options:** `helpText`, `fieldLabel`, `placeholder`, `required`, `disabled`, `autofocus`, `fieldClass`, `labelClass`, `additionalAttributes`, `disableLabel`, `disableGroup`, `disableValidationResult`.
**Custom forms:** `formForWithOptions` — override `formAction`, `formClass`, form ID, HTTP method, disable AJAX submission.
**AJAX submission:** forms submit via AJAX + TurboLinks by default (no full page reload). Opt out with `disableJavascriptSubmission = True`.
### Validation
```haskell
annotation
|> validateField #body nonEmpty
|> validateField #category (`elem` validCategories)
|> ifValid \case
Left invalid -> render NewView { annotation = invalid }
Right valid -> do
createRecord valid
redirectTo WidgetAnnotationsAction { widgetId = valid.widgetId }
```
**Built-in validators:** `nonEmpty`, `isEmail`, `isPhoneNumber`, `isInRange (min, max)`.
**Custom validators:** return `Success` or `Failure "message"`. For DB-aware validators, use `validateFieldIO`.
**How errors attach:** `validateField` stores failures in `record.meta.annotations` (a `[(fieldName, errorMessage)]` list). When `formFor` renders a field with a failure, it auto-adds `is-invalid` CSS class and renders the error message.
**Manual attachment:** `|> attachFailure #field "Custom message"`.
---
## Authentication
IHP provides built-in session-based authentication using salted password hashing (`pwstore-fast`). Accounts are locked for 1 hour after 10 failed attempts.
### Database Requirements
The `users` table must include: `id`, `email`, `password_hash`, `locked_at`, `failed_login_attempts`.
### Setup
```haskell
-- Web/Types.hs
import IHP.LoginSupport.Types
type CurrentUserRecord = User
instance HasNewSessionUrl User where
newSessionUrl _ = "/NewSession"
-- Web/FrontController.hs
instance InitControllerContext WebApplication where
initContext = do
initAuthentication @User
```
Mount `parseRoute @SessionsController` in routes. The framework handles `NewSessionAction`, `CreateSessionAction`, `DeleteSessionAction`.
### Accessing the Current User
```haskell
currentUser -- User; redirects to login if absent
currentUserOrNothing -- Maybe User
currentUserId -- convenience shortcut
```
### Hooks
`SessionsControllerConfig` instance supports `beforeLogin` callback for login history, account status checks, and external identity validation (relevant for IHF's multi-tenant actor attribution).
### Password Handling
```haskell
-- On registration:
user <- newRecord @User
|> fill @'["email", "passwordHash"]
|> set #passwordHash (hashPassword plaintext)
|> createRecord
-- On update (preserve hash when blank):
updatedUser <- user
|> fill @'["email"]
|> ifPasswordChanged (set #passwordHash . hashPassword)
|> updateRecord
```
### Actor Attribution in IHF
For IHF's interaction capture, the `actor_id` and `actor_type` on `InteractionEvent` and `Annotation` should be populated from `currentUserOrNothing` in controllers. For anonymous/low-trust actors, `actor_type = "anonymous"` with a session token as `actor_id`.

View File

@@ -0,0 +1,224 @@
# IHP: Data Modeling and Queries
> How IHP handles schema definition, type generation, querying, relationships, and migrations.
---
## Schema.sql — Single Source of Truth
All models originate in `Application/Schema.sql`. IHP parses this file and **auto-generates Haskell record types** on every save — no manual codegen step.
```sql
CREATE TABLE widgets (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
widget_type TEXT NOT NULL,
hub_id UUID NOT NULL,
capability_ref TEXT,
policy_scope TEXT,
status TEXT NOT NULL DEFAULT 'active',
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
```
This produces a Haskell type roughly equivalent to:
```haskell
data Widget = Widget
{ id :: !(Id Widget)
, name :: !Text
, widgetType :: !Text
, hubId :: !(Id Hub)
, capabilityRef :: !(Maybe Text)
, policyScope :: !(Maybe Text)
, status :: !Text
, version :: !Int
, createdAt :: !UTCTime
}
```
### Conventions
| SQL | Haskell |
|-----|---------|
| `snake_case` column | `camelCase` field |
| `NOT NULL` | plain type |
| nullable | `Maybe` |
| UUID primary key | `Id Widget` (newtype wrapping UUID) |
| Foreign key `hub_id UUID` | `hubId :: !(Id Hub)` |
The `Id Widget` newtype prevents `Id Hub` from being passed where `Id Widget` is expected — foreign key mixups are compile errors.
### Supported PostgreSQL Types
UUID, Text, Int, Integer, Double, Bool, TIMESTAMP WITH TIME ZONE, DATE, JSONB (`Value` via Aeson), ARRAY types, custom ENUMs, INET, Point, and 30+ others — all auto-marshaled to Haskell equivalents.
---
## Schema Designer
A GUI schema editor at `http://localhost:8001/Tables` during development. All operations modify the AST of `Schema.sql` and write back to the file. Editing `Schema.sql` directly in a code editor works equally well.
---
## Query Builder (v1.5: built on `hasql`)
Type-safe fluent API. All queries require an implicit `?modelContext :: ModelContext` — the compiler tracks which code touches the DB.
```haskell
-- Fetch all
widgets <- query @Widget |> fetch
-- Filter + order + limit
recentEvents <- query @InteractionEvent
|> filterWhere (#widgetId, widgetId)
|> orderByDesc #occurredAt
|> limit 50
|> fetch
-- Fetch single (throws RecordNotFoundException on missing)
widget <- fetch widgetId
-- Fetch maybe (returns Nothing on missing)
mWidget <- fetchMaybe widgetId
-- Count
n <- query @Annotation
|> filterWhere (#widgetId, widgetId)
|> fetchCount
```
### Pipeline Mode (v1.5)
Sends multiple queries in a single network round-trip:
```haskell
(widgets, events) <- fetchPipelined do
widgets <- query @Widget |> fetch
events <- query @InteractionEvent |> fetch
pure (widgets, events)
```
### Typed SQL Quasiquoter (v1.5)
Connects to the dev DB at compile time to verify table names, column names, and types:
```haskell
result <- [typedSql|
SELECT w.id, w.name, COUNT(e.id) as event_count
FROM widgets w
LEFT JOIN interaction_events e ON e.widget_id = w.id
WHERE w.hub_id = ${hubId}
GROUP BY w.id, w.name
|]
```
Type-checks column references, parameter types, and return shape. Compile error on typo or schema mismatch.
---
## Relationships
### Has-Many
```haskell
-- User with their widgets
user <- fetch userId >>= fetchRelated #widgets
-- user.widgets :: [Widget]
```
Uses two queries: one for parent, one `WHERE id IN (...)` for children (no N+1).
**With ordering:**
```haskell
user <- fetch userId
>>= pure . modify #widgets (orderByDesc #createdAt)
>>= fetchRelated #widgets
```
### Belongs-To
```haskell
event <- fetch eventId >>= fetchRelated #widgetId
-- event.widgetId :: Widget (resolved from Id Widget to Widget)
```
### Many-to-Many
Use `innerJoin`, `innerJoinThirdTable`, `labelResults` from the query builder.
### Cascade Deletes
Configure in `Schema.sql`:
```sql
ALTER TABLE interaction_events
ADD CONSTRAINT interaction_events_widget_id_fkey
FOREIGN KEY (widget_id) REFERENCES widgets(id) ON DELETE CASCADE;
```
---
## Migrations
Migration files are plain SQL in `Application/Migration/<timestamp>-description.sql`.
### Generating
```
# Via Code Generator web UI at localhost:8001/Generators
# Or CLI:
new-migration "add widget envelope fields"
```
### Running
```bash
migrate # run all pending migrations
DATABASE_URL=postgres://... migrate # against a specific DB
```
Executed migrations are tracked in `schema_migrations`.
### Development Workflow
1. Use Schema Designer to iterate on `Schema.sql` (visual, live)
2. Once stable, copy the relevant DDL into a new migration file
3. Migration files are what run in production and CI
### Example Migration
```sql
-- Application/Migration/1711500000-add-interaction-events.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 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);
```
---
## IHF Schema Notes
The IHF Phase 1 data artifacts map naturally to `Schema.sql` tables:
| IHF Artifact | Suggested Table | Key Fields |
|-------------|----------------|-----------|
| Widget | `widgets` | `id`, `name`, `widget_type`, `hub_id`, `capability_ref`, `policy_scope`, `status`, `version` |
| WidgetVersion | `widget_versions` | `widget_id`, `version`, `schema_snapshot` (JSONB), `created_at` |
| Hub | `hubs` | `id`, `slug`, `name`, `domain` |
| CapabilityReference | `capability_references` | `id`, `hub_id`, `capability_key`, `description` |
| ViewContext | `view_contexts` | `id`, `widget_id`, `context_path`, `metadata` (JSONB) |
| InteractionEvent | `interaction_events` | `widget_id`, `event_type`, `actor_id`, `actor_type`, `view_context_id`, `occurred_at` |
| Annotation | `annotations` | `widget_id`, `body`, `category`, `actor_id`, `actor_type`, `widget_state_ref`, `created_at` |
All tables should use UUID primary keys (`uuid_generate_v4()`), `NOT NULL` on required fields, and `TIMESTAMP WITH TIME ZONE` for timestamps (never plain `TIMESTAMP`).

207
docs/ihp-ihf-mapping.md Normal file
View File

@@ -0,0 +1,207 @@
# 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 `<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:
```haskell
-- 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:
```nix
{ 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.

243
docs/ihp-overview.md Normal file
View File

@@ -0,0 +1,243 @@
# IHP Framework Overview
> Reference notes for implementing the Interaction Hub Framework (IHF) using IHP.
> Based on IHP v1.5.0 (released March 2026).
---
## What IHP Is
**IHP** (Integrated Haskell Platform) is a batteries-included, full-stack web framework built on Haskell and Nix. Its goal is rapid application development with robust, type-safe code.
- **Language:** Haskell (GHC 9.10 default; GHC 9.12 experimental in v1.5)
- **Paradigm:** Functional, strongly typed, server-rendered with optional realtime
- **Creator:** [digitally induced](https://github.com/digitallyinduced) (Hamburg). Open-sourced 2020, in production since 2017
- **Current version:** v1.5.0 (March 25, 2026) — largest release to date (1,051 commits)
- **License:** MIT
### v1.5 Headline Changes
| Change | Impact |
|--------|--------|
| `postgresql-simple``hasql` driver | Up to 50% lower query latency |
| Dev server RAM: 4 GB → 500800 MB | Practical on smaller machines |
| Session middleware 3×, URL gen 5×, rendering 2× faster | Overall snappier |
| `typedSql` quasiquoter | Compile-time SQL type checking against live dev DB |
| `fetchPipelined` | Multiple queries in one network round-trip |
| Composite primary key support | Needed for join-table models |
| Integration test mode | Temporary Postgres DB per test run |
| 15+ modules on Hackage separately | `ihp-mail`, `ihp-datasync`, etc. |
### Design Philosophy
- Type errors at compile time, not runtime
- Single command (`devenv up`) starts a fully self-contained environment — Postgres included, managed by Nix. No Docker, no Kubernetes required
- Optimized for AI-assisted development — the type system automatically verifies generated code
---
## Core Architecture
### MVC-Influenced Structure
| Layer | IHP Location | Role |
|-------|-------------|------|
| Model | `Application/Schema.sql` + generated types | Schema, query builder, relationships |
| Controller | `Web/Controller/<Name>.hs` | Action handlers, parameter binding, DB calls |
| View | `Web/View/<Name>/<Action>.hs` | HSX templates, `View` typeclass |
| Routing | `Web/Routes.hs` + `Web/FrontController.hs` | URL ↔ action mapping |
| Types | `Web/Types.hs` | All controller action constructors |
| Helpers | `Application/Helper/Controller.hs`, `Application/Helper/View.hs` | Shared logic |
Multi-application support: a single project can contain multiple sub-apps (`Web/`, `Admin/`). `new-application admin` generates an `Admin/` subtree with routes auto-prefixed `/admin/`.
### Type-Safe URL / Action System
The IHP router always maps HTTP requests to **data constructors** defined in `Web/Types.hs`:
```haskell
data WidgetsController
= WidgetsAction
| NewWidgetAction
| ShowWidgetAction { widgetId :: !(Id Widget) }
| CreateWidgetAction
| EditWidgetAction { widgetId :: !(Id Widget) }
| UpdateWidgetAction { widgetId :: !(Id Widget) }
| DeleteWidgetAction { widgetId :: !(Id Widget) }
deriving (Eq, Show, Data)
```
- URLs generated from values, not strings: `pathTo ShowWidgetAction { widgetId = someId }`
- Compile-time guarantee: broken links are type errors, not 404s
- `urlTo` generates absolute URLs (protocol + domain)
### Routing
Defined in `Web/Routes.hs`, registered in `Web/FrontController.hs`.
**AutoRoute** (most common): `instance AutoRoute WidgetsController` — IHP generates RESTful routes from action name prefixes:
| Prefix | HTTP Method |
|--------|------------|
| `Delete` | DELETE |
| `Create`, `Update` | POST/PATCH |
| anything else | GET |
**Custom routes:** implement `CanRoute` (attoparsec parser URL → action) and `HasPath` (reverse). `customRoutes` overrides individual AutoRoute entries. Supports SEO slugs like `/widgets/my-slug`.
HTTP method override for HTML forms: pass `_method=DELETE` (or `PATCH`) as a hidden field.
---
## Development Workflow
### Installation
```bash
# 1. Install Determinate Nix (with Flakes + lazy-trees)
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
# 2. Install ihp-new
nix profile install nixpkgs#ihp-new
# 3. Install direnv and hook into shell
nix profile add nixpkgs#direnv
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc # or zshrc
```
### Creating a Project
```bash
ihp-new my-project
cd my-project
devenv up
```
First startup: 1015 minutes (downloads GHC, Postgres, all Haskell deps via Nix binary cache). Subsequent starts are fast (under 30s).
### Dev Server
`devenv up` starts everything:
- Application server on `http://localhost:8000`
- IHP IDE + Schema Designer on `http://localhost:8001`
- Postgres (managed by Nix; no system Postgres needed)
- Live reloading (typically sub-50ms after save)
### Project File Structure
```
my-project/
├── Application/
│ ├── Schema.sql # Single source of truth for all DB types
│ ├── Migration/ # <timestamp>-description.sql files
│ ├── Helper/
│ │ ├── Controller.hs # Shared controller helpers
│ │ └── View.hs # Shared view helpers
│ └── Script/ # One-off scripts / cron job binaries
├── Web/
│ ├── Types.hs # ALL controller action constructors
│ ├── Routes.hs # AutoRoute instance declarations
│ ├── FrontController.hs # WAI entry; dispatch; auth init; default layout
│ ├── Controller/ # One file per controller
│ ├── View/ # One dir per controller, one file per action
│ │ └── Layout.hs # Default layout (Html -> Html)
│ └── Component/ # Server-Side Components (optional)
├── Config/
│ ├── Config.hs # Env vars, secrets, feature flags
│ └── nix/
│ └── hosts/
│ └── production/ # Declarative NixOS server config
├── Test/ # Integration tests
├── static/ # CSS, JS, images
├── flake.nix # Nix flake — all deps declared here
├── App.cabal # Cabal package definition
├── Main.hs # Entry point
└── Makefile # Build targets
```
### Adding Dependencies
In `flake.nix`:
```nix
ihp = {
enable = true;
projectPath = ./.;
packages = [ pkgs.imagemagick ]; # native deps
haskellPackages = p: [
p.ihp
p.aeson
p.your-library
];
};
```
### Code Generators
Navigate to `http://localhost:8001/Generators` or right-click a table in Schema Designer → "Generate Controller". Scaffolds controllers, views, routes, and type entries to match the table's fields. Also available: Migration, Job, Mail, Script generators.
### Testing
v1.5: integration test mode creates a temporary Postgres DB automatically per test run. Tests live in `Test/`.
---
## Deployment
### Primary: NixOS / `deploy-to-nixos`
The entire server config (nginx, Let's Encrypt, systemd, app config) lives declaratively in `Config/nix/hosts/production/` — version-controlled and reproducible.
```bash
deploy-to-nixos production
```
Runs `nixos-rebuild` remotely over SSH. AWS EC2: NixOS AMI, `t3a.small` min (`t3a.medium` recommended), 60 GiB root disk, ports 22/80/443.
**Required env vars:**
| Variable | Purpose |
|----------|---------|
| `IHP_SESSION_SECRET` | Session encryption key (`new-session-secret` to generate) |
| `DATABASE_URL` | Postgres connection string |
| `IHP_BASEURL` | External URL (e.g., `https://example.com`) |
**Production features built-in:**
- Systemd watchdog (heartbeat 30s; auto-restart after 60s)
- Socket activation (queues requests during restarts — zero-downtime deploys)
- Sentry integration via `ihp-sentry` (IHP Pro)
### Docker (IHP Pro)
```bash
nix build .#unoptimized-docker-image --option sandbox false
cat result | podman load
```
Env vars: same as above + `IHP_ASSET_VERSION` (cache-busting) + `IHP_REQUEST_LOGGER_IP_ADDR_SOURCE=FromHeader` (behind load balancer). Minimal Docker images lack CA certificates — copy `caCertificates` and set `SSL_CERT_FILE`.
### Bare Metal Binary
```bash
nix build .#optimized-prod-server # full optimisation
nix build .#unoptimized-prod-server # faster build, for staging
```
Runtime: `IHP_ENV=Production`, `DATABASE_URL`, `PORT` (default 8000). GHC RTS tunable via `GHCRTS`.
### CSS/JS Bundling
```bash
make static/prod.js static/prod.css
```
Dev uses individual files; production uses bundled/minified versions. Background jobs require deploying the `RunJobs` binary alongside the main app.
---
## Key Links
- [IHP Homepage](https://ihp.digitallyinduced.com/)
- [IHP Guide (full docs)](https://ihp.digitallyinduced.com/Guide/)
- [GitHub: digitallyinduced/ihp](https://github.com/digitallyinduced/ihp)
- [ihp-boilerplate](https://github.com/digitallyinduced/ihp-boilerplate) — template used by `ihp-new`
- [v1.5 release announcement](https://discourse.haskell.org/t/ihp-v1-5-has-been-released/13846)

266
docs/ihp-realtime.md Normal file
View File

@@ -0,0 +1,266 @@
# IHP: Realtime Capabilities
IHP offers four distinct realtime mechanisms. Choosing the right one for a given IHF surface is important — they have very different trade-offs.
---
## Summary Comparison
| Mechanism | Best For | Client Requirement | State Lives |
|-----------|---------|-------------------|-------------|
| AutoRefresh | Read-heavy live dashboards | Vanilla JS (morphdom) | Server / DB |
| DataSync | Reactive JS components embedded in server pages | JS SDK + bundler | DB (via RLS) |
| Server-Side Components | Rich interactive UI with Haskell state machine | Vanilla JS | Server (Haskell) |
| HTMX | Partial page updates triggered by user actions | htmx.js | Server |
---
## 1. AutoRefresh
Enables server-side views to **automatically re-render** when the underlying database changes. No client-side framework or state management required.
### How It Works
1. Wrap an action with `autoRefresh do ...`
2. IHP tracks which DB tables the action's queries touched (via PostgreSQL LISTEN/NOTIFY)
3. A WebSocket connection is established when the page loads
4. On any INSERT/UPDATE/DELETE to tracked tables, the server re-runs the action
5. If the generated HTML differs, the delta is sent over WebSocket; `morphdom` applies it to the DOM with minimal DOM mutations
```haskell
action HubDashboardAction { hubId } = autoRefresh do
hub <- fetch hubId
widgets <- query @Widget |> filterWhere (#hubId, hubId) |> fetch
recentEvents <- query @InteractionEvent
|> filterWhere (#hubId, hubId)
|> orderByDesc #occurredAt
|> limit 20
|> fetch
render HubDashboardView { .. }
```
### Layout Requirements
```haskell
defaultLayout inner = [hsx|
<head>
{autoRefreshMeta}
<script src="/vendor/morphdom.js"></script>
<script src="/vendor/ihp-auto-refresh.js"></script>
</head>
...
|]
```
### Custom SQL Tracking
For `sqlQuery` calls that bypass the query builder, manually register table reads:
```haskell
trackTableRead "interaction_events"
```
### IHF Use Cases
- **Hub operator dashboard** — live widget counts, recent interaction signals, annotation feed
- **Governance ledger view** — live requirement candidate list
- **Triage board** — live annotation queue
AutoRefresh is the right choice for all of these: server-rendered, no JS complexity, updates arrive automatically.
---
## 2. IHP DataSync
A WebSocket-based API that allows **JavaScript to query the database directly**, mirroring the Haskell query builder in JS. For reactive JS components embedded in otherwise server-rendered pages.
### JavaScript API
```javascript
// One-time fetch
const annotations = await query('annotations')
.where('widget_id', widgetId)
.orderBy('created_at', 'DESC')
.fetch();
// Realtime subscription (React hook)
const { records: annotations } = useQuery(
query('annotations')
.where('widget_id', widgetId)
.orderBy('created_at', 'DESC')
);
// Mutations
await createRecord('annotations', { widgetId, body, category });
await updateRecord('annotations', id, { body });
await deleteRecord('annotations', id);
```
When `useQuery()` establishes a subscription, all connected clients automatically re-render when the query result changes.
### Security: Row Level Security
DataSync relies on **PostgreSQL Row Level Security (RLS)**. IHP runs with two DB roles:
- Privileged owner role — used by Haskell server code
- `ihp_authenticated` — used by DataSync (what JS clients see)
Example RLS policy:
```sql
-- Users can only see their own annotations
CREATE POLICY "Users can view own annotations" ON annotations
FOR SELECT
USING (actor_id = ihp_user_id());
-- Anyone can view annotations on public widgets
CREATE POLICY "Public widget annotations are readable" ON annotations
FOR SELECT
USING (EXISTS (
SELECT 1 FROM widgets w
WHERE w.id = annotations.widget_id
AND w.policy_scope = 'public'
));
```
### Setup Requirements
Node.js/npm, a frontend bundler (esbuild recommended), DataSync JS SDK, WebSocket + REST API controllers enabled in `FrontController`.
### IHF Use Cases
- **Annotation composer** — reactive annotation entry form with live thread updates
- **Widget feedback inbox** — live feed of all annotations on a specific widget for the widget owner
- **Cross-tenant widget embeds** — widget UIs embedded in third-party pages; RLS isolates each widget's data
---
## 3. Server-Side Components
React-like components that run entirely on the server. Rich interactive UI with a Haskell state machine — no JS state management needed.
### Structure
A component has:
- **State** — a Haskell data type
- **Actions** — a sum type
- **Render** — HSX function from state to HTML
- **`componentDidMount`** — hook for async data loading on connect
```haskell
-- Web/Component/AnnotationComposer.hs
data AnnotationComposer = AnnotationComposer
{ widgetId :: !(Id Widget)
, body :: !Text
, category :: !Text
, submitted :: !Bool
}
data AnnotationComposerController
= UpdateBodyAction { body :: !Text }
| UpdateCategoryAction { category :: !Text }
| SubmitAnnotationAction
deriving (Eq, Show, Data)
instance Component AnnotationComposer AnnotationComposerController where
initialState = AnnotationComposer
{ widgetId = error "set by mount"
, body = ""
, category = "friction"
, submitted = False
}
action state UpdateBodyAction { body } = pure state { body }
action state UpdateCategoryAction { category } = pure state { category }
action state SubmitAnnotationAction = do
createRecord (newRecord @Annotation
|> set #widgetId state.widgetId
|> set #body state.body
|> set #category state.category)
pure state { submitted = True, body = "" }
render state = [hsx|
<div class="annotation-composer">
<textarea
value={state.body}
onInput={callServerAction' UpdateBodyAction { body = inputValue }}
/>
<select onInput={callServerAction' UpdateCategoryAction { category = selectValue }}>
<option value="friction">Friction</option>
<option value="wish">Wish</option>
<option value="defect">Defect</option>
</select>
<button onclick={callServerAction SubmitAnnotationAction}>Submit</button>
</div>
|]
```
### Lifecycle
1. Client triggers action: `callServerAction('SubmitAnnotationAction')`
2. Server evaluates action handler → produces new state
3. Server re-renders → diffs HTML → sends granular DOM patch over WebSocket
4. `morphdom` applies patch (preserves third-party JS library DOM state)
### Registration
```haskell
-- Web/FrontController.hs
routeComponent @AnnotationComposer
```
```haskell
-- In a view:
{component @AnnotationComposer}
```
### Current Status
Described as "early development stage — expect bugs and API changes." Suitable for experimental use but not for core production paths yet.
### IHF Use Cases
- **Annotation entry UI** with live category validation and preview
- **Widget triage panel** with drag-to-sort and inline editing
- **Governance decision form** with multi-step workflow state
---
## 4. HTMX Integration
IHP pairs cleanly with htmx (both are hypermedia-first). Use `respondHtml` (partial renders without layout) in controller actions:
```haskell
action AppendAnnotationAction { widgetId } = do
let annotation = newRecord @Annotation
annotation
|> fill @'["body", "category"]
|> validateField #body nonEmpty
|> ifValid \case
Left _ -> respondHtml [hsx|<div class="error">Body required</div>|]
Right annotation -> do
annotation <- createRecord annotation
respondHtml (renderAnnotation annotation)
```
Reinitialise htmx after Turbolinks page transitions or AutoRefresh updates:
```javascript
document.addEventListener('turbolinks:load', () => htmx.process(document.body));
```
### IHF Use Cases
- **Governance ledger append** — POST an approval/rejection action → server appends to ledger and returns updated fragment
- **Annotation triage status update** — inline status toggle without page reload
- **Requirement candidate promotion** — single-button action that updates status and returns the updated card
---
## Decision Guide for IHF Phase 1
| Surface | Recommended Mechanism | Rationale |
|---------|----------------------|-----------|
| Hub dashboard (live signal counts, recent events) | AutoRefresh | Server-rendered, zero JS complexity |
| Widget detail page (event history) | AutoRefresh | Same — read-heavy, aggregate view |
| Annotation feed (live thread) | AutoRefresh or DataSync | AutoRefresh if all users see the same view; DataSync if per-user filtered |
| Annotation entry form | Server-Side Components (experimental) or HTMX | SSC for rich state machine; HTMX for simple append |
| Widget registration flow | Standard IHP forms | No realtime needed |
| Triage board (drag, inline edit) | Server-Side Components | Rich interaction state |

View File

@@ -0,0 +1,289 @@
# Tailwind for Interaction Hubs
**Agentic Coding Best-Practice Guide**
*For building governed, observable, antifragile hub interfaces with Tailwind + IHP*
**Version:** 0.2 (Agent-Optimized)
**Status:** Production-Ready Reference
**Audience:** Human developers **and** coding agents (LLMs)
**Framework:** IHP (Integrated Haskell Platform) + Interaction Hub Framework (IHF) layer
**Date:** March 2026
---
## 0. Quick-Start for Coding Agents (READ THIS FIRST)
When you receive a UI task for an Interaction Hub:
1. **Semantic first** — Never start with classes. Ask: “What IHF primitive/widget does this map to?”
2. **Import hierarchy** — Always `import Web.View.IHF.Primitives`, `Web.View.IHF.Widgets`, `Web.View.IHF.Classes`.
3. **Use wrappers** — Render via `widgetFrame`, `signalBadge`, `hubActionButton`, etc.
4. **Token roles only** — Reference `surfaceBase`, `signalSuccess`, `focusRing` (defined in `Theme.hs`).
5. **No raw soup** — If you see >8 utility classes in one element, extract to a helper.
6. **Document for reproducibility** — Every new primitive gets Haddock + example HSX.
This guide is your single source of truth. Follow it → zero drift.
---
## 1. Purpose
This guide makes **Tailwind the shared visual grammar** for all Interaction Hubs built on IHP + IHF, while keeping the **IHF semantic model** (Widget, Panel, SignalBadge, etc.) as the architectural control point.
It guarantees:
- Perfect visual coherence across hubs
- Zero CSS entropy
- Agent-reproducible styling (no folklore)
- Governed evolution via semantic contracts
- Full compatibility with IHPs server-rendered HSX, partial updates, and future islands
**Tailwind is the substrate. IHF widgets are the product.**
---
## 2. Core Principle (Agent Rule #1)
```hs
-- ALWAYS do this
widgetFrame widget content
-- NEVER do this
<div class="rounded-xl border border-slate-700 bg-slate-900/80 px-4 py-3 shadow-sm ...">
```
**Architecture layers (enforced order):**
1. **IHF Semantic Model** (`Widget`, `HubPanel`, `CommentThread`…)
2. **Visual Primitive Contracts** (`Primitives.hs`)
3. **Tailwind Token Application** (centralized in `Classes.hs` + `tailwind.config.js`)
4. **Screen Composition**
---
## 3. IHP + Tailwind Setup (Mandatory Baseline)
Follow the official IHP Tailwind guide, then apply these IHF extensions:
### Required Files (exact locations)
```text
Project Root
├── tailwind/
│ ├── tailwind.config.js ← extend with IHF tokens
│ └── app.css
├── Web/View/IHF/
│ ├── Theme.hs ← semantic roles
│ ├── Tokens.hs ← spacing/radius/shadow maps
│ ├── Classes.hs ← reusable class builders
│ ├── Primitives.hs ← surface, panel, badge, button…
│ ├── Widgets.hs ← widgetFrame, inspectorPanel…
│ ├── Patterns.hs ← triageDashboard, decisionBoard…
│ ├── AnnotationUI.hs
│ └── GovernanceUI.hs
├── Web/View/CustomCSSFramework.hs ← IHP component override (required for JIT)
└── Config/Config.hs ← option customTailwind
```
**Key tailwind.config.js snippet (add to IHF theme):**
```js
theme: {
extend: {
colors: {
surfaceBase: "hsl(var(--surface-base))",
signalSuccess: "hsl(var(--signal-success))",
// … all roles from Theme.hs
},
spacing: { /* baseline rhythm */ },
}
}
```
**CustomCSSFramework.hs** must expose `customTailwind` with Tailwind classes for all IHP form/button/flash helpers.
---
## 4. Recommended Layer Model (Agent Reference)
| Layer | Name | Lives In | Example Functions | Purpose |
|-------|-----------------------------|-------------------|---------------------------------------|---------|
| A | Semantic Interaction | Widgets.hs / Types | `widgetFrame`, `commentRail` | What it *is* |
| B | Visual Primitive | Primitives.hs | `panel`, `signalBadge`, `actionButton` | Reusable building blocks |
| C | Tailwind Utility Application| Classes.hs | `panelClasses`, `badgeVariant` | Actual classes |
| D | Screen Composition | Patterns.hs | `triageDashboard` | Workflow views |
---
## 5. Golden Rules for Agents
- **Rule 1**: Every visual element must have a named Haskell wrapper (no raw `<div class="…">` longer than one line).
- **Rule 2**: All color/spacing decisions go through `Theme.hs` roles.
- **Rule 3**: Variant logic must be exhaustive and named (`primary | ghost | danger`).
- **Rule 4**: Commentability is first-class — every widget primitive exposes `commentable :: Bool`.
- **Rule 5**: Before writing any new class string, check `Classes.hs`. If missing, add it there with documentation.
---
## 6. Visual Language & Tokens (Copy-Paste Ready)
### 6.1 Color Roles (Theme.hs)
```hs
surfaceBase, surfaceRaised, surfaceMuted,
borderSubtle, borderStrong,
textPrimary, textSecondary, textMuted,
signalInfo, signalSuccess, signalWarning, signalDanger,
focusRing, commentAccent, policyAccent, outcomeAccent
```
Map these to CSS variables in `tailwind/app.css` `@layer base`.
### 6.2 Spacing Rhythm (Tokens.hs)
```hs
space1, space2, space3, space4, space6, space8, space12, space16
```
Use only these. Arbitrary values are forbidden.
### 6.3 Radius & Shadow
```hs
roundedPanel = "rounded-xl"
roundedBadge = "rounded-lg"
shadowLight = "shadow-sm"
```
---
## 7. Core Primitives (Primitives.hs — Starter Template)
```hs
-- Example: panel
panel :: [Attribute] -> Html -> Html
panel attrs content = div!
( [ class_ (panelClasses <> " " <> unwords (map renderAttr attrs))
, role_ "region"
] )
content
panelClasses :: Text
panelClasses = "bg-surfaceBase border border-borderSubtle rounded-xl shadow-sm px-4 py-3"
-- Example: signalBadge
signalBadge :: SignalStatus -> Html
signalBadge status = span!
[ class_ (badgeVariant status) ]
[ text (statusLabel status) ]
```
All primitives follow this exact signature pattern.
---
## 8. Widget-Centered Rules (IHF Core)
Every `Widget` **must** render via `widgetFrame`:
```hs
widgetFrame :: Widget -> (Widget -> Html) -> Html
widgetFrame w inner = div!
[ class_ widgetFrameClasses
, dataAttribute "widget-id" (widgetId w)
, dataAttribute "commentable" (show $ isCommentable w)
] $ do
widgetHeader w
inner w
widgetFooter w
```
State cues (inspectable, commentable, in-flight) use **one** consistent visual language (header accent + badge + subtle background shift).
---
## 9. Agent-Friendly Practices
### 9.1 Component Creation Protocol (Mandatory)
When creating a new primitive:
1. Add to `Primitives.hs` with full Haddock:
```hs
{-# LANGUAGE OverloadedStrings #-}
-- | A governed action button. Variants map to IHF policy levels.
actionButton :: ActionVariant -> Text -> Html
```
2. Define variant type in `Types.hs`.
3. Implement class builder in `Classes.hs`.
4. Add example usage in `docs/Examples.hs`.
5. Update `Short Operational Checklist` (section 26).
### 9.2 Prompt Template for Future Agents
> “Generate an IHF widget for X using only primitives from Web.View.IHF. Use semantic roles from Theme.hs. No raw Tailwind soup. Include comment affordance.”
---
## 10. Anti-Patterns (Hard-Coded Agent Rejection Rules)
- Utility soup longer than 6 classes
- Screen-local button/panel/badge reinvention
- Literal color names in component signatures
- Mixed density without system-level justification
- Styling logic inside business controllers
- Any custom CSS outside `tailwind/app.css` `@layer components`
---
## 11. Forms, Tables, Annotations, Governance (IHF-Specific)
- **Forms**: Use `styledFormGroupClass` from `CustomCSSFramework.hs` + `fieldWithPolicyNote`.
- **Tables**: Standardized `sortableTable` in `Patterns.hs` with actor/timestamp/state columns.
- **Annotations**: First-class `annotationThread` + triage variants (`open | reviewed | policySensitive`).
- **Governance**: `decisionBanner`, `policyScopeBadge`, `traceabilityChain` — all with built-in comment hooks.
---
## 12. File & Module Organization (Exact)
See section 3. All IHF UI lives under `Web/View/IHF/`.
Import graph is strict: `Patterns.hs` → `Widgets.hs` → `Primitives.hs` → `Classes.hs` → `Theme.hs`.
---
## 13. Rollout Phases & Minimum Standard
**Phase 1 (MVP)**: Theme + Tokens + Primitives (panel, button, badge, widgetFrame)
**Phase 2**: AnnotationUI + GovernanceUI
**Phase 3**: Hub-specific accents + adaptive patterns
**Every new component** must ship with:
- Purpose
- Type signature
- Allowed variants
- Accessibility notes
- Commentability behavior
- Example HSX
---
## 14. Short Operational Checklist (Agent + Human Merge Gate)
Before any PR/merge:
- [ ] Semantic name used
- [ ] Imported only from IHF modules
- [ ] Classes composed via `Classes.hs`
- [ ] State signaling matches existing system
- [ ] Focus + contrast + color+icon rule satisfied
- [ ] Comment affordance present where applicable
- [ ] No custom CSS added
- [ ] Agent reproduction test passed (copy-paste into new context works)
---
## 15. Motto
**“Use Tailwind for expression. Let IHF widgets define meaning.”**
This guide is the contract between humans and agents. Follow it and Interaction Hubs stay coherent, evolvable, and antifragile forever.

View File

@@ -0,0 +1,418 @@
---
id: IHUB-WP-0001
type: workplan
title: "IHF Phase 1 — Minimal Interaction Core"
domain: custodian
repo: inter-hub
status: active
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: todo
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 1015 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: todo
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: todo
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: todo
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: todo
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: todo
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: todo
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: todo
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|
<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>
|]
```
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: todo
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: todo
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: todo
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: todo
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 T04T07)
- Auth before traceability view (T10 before T11)
- All feature tasks (T01T11) 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 T04T07 scaffolding, then customize. It handles the `Types.hs` / `Routes.hs` / `FrontController.hs` wiring automatically.