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:
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.
|
||||
Reference in New Issue
Block a user