generated from coulomb/repo-seed
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:
134
CLAUDE.md
Normal file
134
CLAUDE.md
Normal 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, T01–T12). 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
128
SCOPE.md
Normal 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).
|
||||
272
docs/ihp-controllers-views-forms.md
Normal file
272
docs/ihp-controllers-views-forms.md
Normal 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`.
|
||||
224
docs/ihp-data-and-queries.md
Normal file
224
docs/ihp-data-and-queries.md
Normal 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
207
docs/ihp-ihf-mapping.md
Normal 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
243
docs/ihp-overview.md
Normal 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 → 500–800 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: 10–15 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
266
docs/ihp-realtime.md
Normal 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 |
|
||||
289
specs/TailwindForInteractionHubs_v0.2.md
Normal file
289
specs/TailwindForInteractionHubs_v0.2.md
Normal 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 IHP’s 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.
|
||||
|
||||
418
workplans/IHUB-WP-0001-ihf-phase1-minimal-interaction-core.md
Normal file
418
workplans/IHUB-WP-0001-ihf-phase1-minimal-interaction-core.md
Normal 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 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: 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 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.
|
||||
Reference in New Issue
Block a user