--- id: IHUB-WP-0002 type: workplan title: "IHF Phase 2 — Structured Feedback and Triage" domain: inter_hub repo: inter-hub status: active owner: custodian topic_slug: inter_hub created: "2026-03-28" updated: "2026-03-28" state_hub_workstream_id: "25d4c92c-b213-4c33-9404-822192899de7" --- # IHF Phase 2 — Structured Feedback and Triage ## Goal Transform raw annotations into structured, operable feedback. Phase 1 established the capture layer (widgets, events, annotations). Phase 2 makes that feedback governable: grouping related observations, escalating them into Requirement Candidates, tracking triage status and reviewer ownership, and surfacing the triage queue to operators in a live dashboard. ## Background Phase 1 (IHUB-WP-0001) delivered the Minimal Interaction Core — widget registry, interaction event capture, annotation system, and hub operator dashboard. All Phase 1 exit criteria are met. Phase 2 is the second of eight phases in the IHF specification (`specs/InteractionHubFrameworkSpecification_v0.1.md`). It extends the Phase 1 annotation model with severity markers, threading/grouping, escalation to Requirement Candidates, a triage lifecycle, and reviewer assignment. **Technology stack:** IHP v1.5 (Haskell, Nix), PostgreSQL, AutoRefresh (triage dashboard), HTMX (escalation and triage actions), IHP forms (CRUD). Reference: `docs/ihp-overview.md`, `docs/ihp-data-and-queries.md`, `docs/ihp-controllers-views-forms.md`, `docs/ihp-realtime.md`. ## Phase 2 Exit Criteria (from IHF spec §14 Phase 2) - Feedback volume can be triaged rather than merely stored - Multiple related comments can converge into a structured candidate - Reviewers can track status and ownership ## Data Artifacts Introduced (Phase 2) `AnnotationThread`, `RequirementCandidate`, `TriageState`, `ReviewerAssignment` Also extends: `Annotation` (adds `severity`, `thread_id`) --- ## Tasks ### T01 — Schema: AnnotationThread, RequirementCandidate, TriageState, ReviewerAssignment ```task id: IHUB-WP-0002-T01 status: todo priority: high state_hub_task_id: "eb267a9e-7e80-4913-b7a3-7f5adb04a0f2" ``` Add Phase 2 tables to `Application/Schema.sql` and write migration: ```sql -- Extend annotations with severity and thread grouping ALTER TABLE annotations ADD COLUMN severity TEXT NOT NULL DEFAULT 'medium'; ALTER TABLE annotations ADD COLUMN thread_id UUID REFERENCES annotation_threads(id) ON DELETE SET NULL; CREATE TABLE annotation_threads ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, widget_id UUID NOT NULL REFERENCES widgets(id) ON DELETE CASCADE, title TEXT NOT NULL, description TEXT, created_by UUID REFERENCES users(id), created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE TABLE requirement_candidates ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, title TEXT NOT NULL, description TEXT NOT NULL, source_widget_id UUID NOT NULL REFERENCES widgets(id) ON DELETE RESTRICT, source_thread_id UUID REFERENCES annotation_threads(id) ON DELETE SET NULL, source_annotation_id UUID REFERENCES annotations(id) ON DELETE SET NULL, category TEXT NOT NULL DEFAULT 'friction', status TEXT NOT NULL DEFAULT 'open', created_by UUID REFERENCES users(id), created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE INDEX requirement_candidates_widget_id_idx ON requirement_candidates (source_widget_id); CREATE INDEX requirement_candidates_status_idx ON requirement_candidates (status); CREATE TABLE triage_states ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, candidate_id UUID NOT NULL REFERENCES requirement_candidates(id) ON DELETE CASCADE, status TEXT NOT NULL, notes TEXT, changed_by UUID REFERENCES users(id), changed_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE INDEX triage_states_candidate_id_idx ON triage_states (candidate_id); CREATE TABLE reviewer_assignments ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, candidate_id UUID NOT NULL REFERENCES requirement_candidates(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, assigned_by UUID REFERENCES users(id), assigned_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, UNIQUE (candidate_id) ); ``` - Valid `severity` values: `low`, `medium`, `high`, `critical` - Valid `requirement_candidates.status` values: `open`, `in_review`, `accepted`, `rejected`, `deferred` - Verify Haskell types are generated correctly **Exit criteria:** `migrate` runs cleanly; all Phase 2 types available in GHCi. --- ### T02 — Severity markers on Annotation ```task id: IHUB-WP-0002-T02 status: todo priority: high state_hub_task_id: "fdcbf823-484e-4f0f-a0ca-28f9222520af" ``` 1. Update `CreateAnnotationAction` to bind and validate `severity` (low/medium/high/critical, default medium) 2. Update annotation form: add severity select field 3. Update annotation list and show views to display severity with Tailwind color cues: - `low` → muted/secondary - `medium` → info/neutral - `high` → warning - `critical` → error/destructive (per `specs/TailwindForInteractionHubs_v0.2.md` color roles) **Exit criteria:** New annotations carry severity; existing annotations default to `medium`; severity is visible in all annotation views. --- ### T03 — AnnotationThread controller (duplicate grouping) ```task id: IHUB-WP-0002-T03 status: todo priority: high state_hub_task_id: "35b989a0-5e2a-4300-990b-f43d67de0727" ``` 1. Scaffold `AnnotationThreadsController` scoped to a widget: `/widgets/:widgetId/threads/` 2. Actions: `IndexAnnotationThreadsAction`, `ShowAnnotationThreadAction`, `NewAnnotationThreadAction`, `CreateAnnotationThreadAction` 3. `CreateAnnotationThreadAction`: creates thread with `title`, `description`, `widgetId` 4. Add `AssignAnnotationToThreadAction` (HTMX POST): sets `annotation.thread_id` 5. Thread show view: list of member annotations with severity distribution bar and dominant category badge 6. Thread list view: thread title, annotation count, severity breakdown, created_at **Exit criteria:** Operator can create a thread, assign annotations to it, and view the grouped observations. --- ### T04 — RequirementCandidate controller and views ```task id: IHUB-WP-0002-T04 status: todo priority: high state_hub_task_id: "4eb2a51c-1b3f-4b36-b945-6bfb14c2e680" ``` 1. Scaffold `RequirementCandidatesController` 2. Actions: index, show, new, create, edit, update (no delete) 3. Fields: `title`, `description`, `sourceWidgetId` (select), `sourceThreadId` (optional select), `category` (select), initial status `open` 4. Index view: table with status badge, widget, category, reviewer name, created_at; filterable by status 5. Show view: full detail + linked source (annotation or thread) + triage history log + current reviewer **Exit criteria:** Requirement candidates can be created manually, listed, filtered by status, and viewed with full context. --- ### T05 — Escalation action: Annotation(s) → RequirementCandidate ```task id: IHUB-WP-0002-T05 status: todo priority: high state_hub_task_id: "5c3a154b-38e0-4e40-9e97-57aae1dbc95d" ``` 1. Add "Escalate to Candidate" button on annotation show page (HTMX POST to `EscalateAnnotationAction { annotationId }`) 2. Action pre-populates and creates a `RequirementCandidate` with: - `title` derived from annotation body (first 80 chars) - `category` copied from annotation category - `sourceAnnotationId` set - `sourceWidgetId` from annotation's widget 3. Respond with HTMX swap: replace escalate button with "Escalated →" link to the candidate 4. Add visual indicator on escalated annotations in annotation lists **Exit criteria:** Single-click escalation creates a linked candidate; escalated annotations are visually distinct; no duplicate escalation possible (button replaced after action). --- ### T06 — TriageState lifecycle per RequirementCandidate ```task id: IHUB-WP-0002-T06 status: todo priority: high state_hub_task_id: "cd8c3ef1-e0f7-435f-ae20-e0760df5da83" ``` 1. `UpdateTriageStatusAction { candidateId, newStatus, notes }` (HTMX POST) 2. Validate allowed transitions: - `open` → `in_review` - `in_review` → `accepted`, `rejected`, `deferred` - `deferred` → `in_review` - All others rejected with 422 3. On valid transition: insert `TriageState` row, update `requirement_candidates.status` 4. Show page: render full triage history as an audit trail (status, changed_by, changed_at, notes) 5. Status badge in index updates live via AutoRefresh **Exit criteria:** Triage lifecycle enforced; invalid transitions rejected; full audit trail visible on show page. --- ### T07 — ReviewerAssignment: ownership on RequirementCandidate ```task id: IHUB-WP-0002-T07 status: todo priority: medium state_hub_task_id: "3dc9bfdb-06d0-48a5-8973-2e39c6e0f78a" ``` 1. `AssignReviewerAction { candidateId, userId }` (HTMX POST from candidate show page) 2. Upsert `ReviewerAssignment` (one reviewer per candidate; replacing previous assignment) 3. Show current reviewer name on candidate index and show pages 4. Add "My Queue" view: `MyQueueAction` — lists all open/in_review candidates assigned to `currentUser` 5. Index view: filter by "Unassigned" and "Assigned to me" **Exit criteria:** Candidates can be assigned to reviewers; reviewer sees their queue; unassigned candidates are identifiable. --- ### T08 — Operator triage dashboard (AutoRefresh) ```task id: IHUB-WP-0002-T08 status: todo priority: high state_hub_task_id: "82498422-1626-4479-9daa-3d7c7e088d8e" ``` 1. Add `TriageDashboardAction` (or extend `ShowHubAction`) wrapped with `autoRefresh do` 2. Dashboard panels: - KPI row: open / in_review / accepted / rejected / deferred counts - Triage queue: open candidates sorted oldest-first (title, widget, category, age) - Recent escalations: last 20 with widget and source annotation snippet - Category breakdown: annotation count grouped by category across all hub widgets 3. Layout includes `{autoRefreshMeta}`, `morphdom.js`, `ihp-auto-refresh.js` 4. Test: insert a new candidate via the UI → dashboard updates within ~1s without reload **Exit criteria:** Dashboard live-updates on candidate/triage changes. WebSocket frames visible in DevTools. --- ### T09 — Phase 2 gate: tests, consistency, docs ```task id: IHUB-WP-0002-T09 status: todo priority: high state_hub_task_id: "935de4d7-867f-49aa-bddf-6ff9435215de" ``` 1. **Integration tests** (`Test/`): - AnnotationThread create + assign annotation - Escalation: annotation → candidate, duplicate escalation blocked - Triage transitions: valid path (open → in_review → accepted); invalid transition rejected (422) - ReviewerAssignment: assign, reassign, my-queue filter - Triage dashboard: `autoRefresh` wrapper present 2. **Consistency sync:** ```bash cd ~/the-custodian && make fix-consistency REPO=inter-hub ``` 3. **Documentation updates:** - Update `SCOPE.md` current state section: Phase 2 complete - Write `docs/phase2-summary.md`: what was built, known limitations, Phase 3 readiness 4. **Smoke test checklist:** - `devenv up` → clean start - Annotate a widget, set severity high - Create a thread, assign two annotations to it - Escalate an annotation to a candidate - Run triage: open → in_review → accepted - Assign reviewer, check My Queue - Confirm triage dashboard auto-updates **Exit criteria:** All tests pass; consistency sync reports no errors; smoke test completed; SCOPE.md updated. --- ## Phase 2 Dependencies - Phase 1 schema stable (T01 depends on widgets, annotations, users from Phase 1) - `annotation_threads` before `annotations` FK extension (T01 ordering: create thread table before altering annotations) - Schema (T01) before all controller work (T02–T08) - RequirementCandidate (T04) before Escalation (T05), TriageState (T06), ReviewerAssignment (T07) - All feature tasks (T01–T08) before gate (T09) ## Notes - **Severity on Annotation is a Phase 1 schema extension**, not a new table — it's a simple migration with a safe default. - **No automated duplicate detection in Phase 2.** Grouping is operator-driven via `AnnotationThread`. Automated similarity is Phase 3+ scope. - **TriageState is append-only.** Like `InteractionEvent`, never update/delete triage rows — only insert new status transitions. - **HTMX for in-place actions** (escalate, triage status, assign reviewer) keeps the operator flow fluid without full page reloads. - **No `RequirementCandidate` delete.** Candidates can be rejected or archived, never deleted, to preserve audit trail.