generated from coulomb/repo-seed
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>
208 lines
8.9 KiB
Markdown
208 lines
8.9 KiB
Markdown
# 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.
|