diff --git a/workplans/IHUB-WP-0002-ihf-phase2-structured-feedback-and-triage.md b/workplans/IHUB-WP-0002-ihf-phase2-structured-feedback-and-triage.md new file mode 100644 index 0000000..c294913 --- /dev/null +++ b/workplans/IHUB-WP-0002-ihf-phase2-structured-feedback-and-triage.md @@ -0,0 +1,323 @@ +--- +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.