Implements the full governance layer: - Schema: requirements, decision_records, policy_references, implementation_change_references; requirement_candidates gets requirement_id back-reference - RequirementsController (index/show; promotion-only create) - DecisionRecordsController (CRUD + policy/impl ref management) - GovernanceDashboardAction on HubsController (AutoRefresh) - PromoteToRequirementAction + LinkToDecisionAction on candidates - Outcome immutability enforced at controller level (fill excludes outcome) - Full six-outcome vocabulary with Tailwind color roles - Integration tests for all Phase 3 paths - FrontController: registers Phase 2 missing controllers + all Phase 3 - SCOPE.md + docs/phase3-summary.md updated Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
14 KiB
id, type, title, domain, repo, status, owner, topic_slug, created, updated, state_hub_workstream_id
| id | type | title | domain | repo | status | owner | topic_slug | created | updated | state_hub_workstream_id |
|---|---|---|---|---|---|---|---|---|---|---|
| IHUB-WP-0003 | workplan | IHF Phase 3 — Governance and Decision Linkage | inter_hub | inter-hub | done | custodian | inter_hub | 2026-03-28 | 2026-03-28 | 5f201ee3-5922-4bdc-981d-e51db0a24f5e |
IHF Phase 3 — Governance and Decision Linkage
Goal
Make the framework governance-capable rather than feedback-capable only. Phase 2 established structured, triageable feedback and requirement candidates. Phase 3 promotes accepted candidates into formal Requirements, records the decisions that act on them, links decisions to policy constraints and implementation work items, and surfaces the resulting governance audit trail per hub.
Background
Phase 1 (IHUB-WP-0001) delivered the Minimal Interaction Core. Phase 2 (IHUB-WP-0002) delivered Structured Feedback and Triage — annotation severity, annotation threads, requirement candidates, triage lifecycle, reviewer assignment, and triage dashboard. All Phase 2 exit criteria are met.
Phase 3 is the third of eight phases in the IHF specification
(specs/InteractionHubFrameworkSpecification_v0.1.md, §14 Phase 3). It closes
the central traceability chain:
Widget → InteractionEvent / Annotation
→ RequirementCandidate (Phase 2)
→ [accepted] → Requirement
→ DecisionRecord ← PolicyReference
→ ImplementationChangeReference
→ DeploymentRecord → OutcomeSignal (Phase 4+)
Technology stack: IHP v1.5 (Haskell, Nix), PostgreSQL, AutoRefresh (governance dashboard), IHP forms (CRUD). Outcome immutability enforced at the controller level (no update after creation).
Reference: docs/ihp-overview.md, docs/ihp-data-and-queries.md,
docs/ihp-controllers-views-forms.md, docs/ihp-realtime.md.
Phase 3 Exit Criteria (from IHF spec §14 Phase 3)
- The system can explain why a requirement was or was not acted upon
- Governance records are linked to observed interaction issues (full traceability)
- Decision history is inspectable per hub
Data Artifacts Introduced (Phase 3)
Requirement, DecisionRecord, PolicyReference, ImplementationChangeReference
Also extends: RequirementCandidate (adds requirement_id back-reference)
Tasks
T01 — Schema: DecisionRecord, PolicyReference, Requirement, ImplementationChangeReference
id: IHUB-WP-0003-T01
status: done
priority: high
state_hub_task_id: "829b1121-bde6-4d8e-8c82-2a2e2064f520"
Add Phase 3 tables to Application/Schema.sql and write migration:
CREATE TABLE requirements (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
title TEXT NOT NULL,
description TEXT NOT NULL,
source_candidate_id UUID NOT NULL REFERENCES requirement_candidates(id) ON DELETE RESTRICT,
status TEXT NOT NULL DEFAULT 'active',
created_by UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX requirements_source_candidate_id_idx ON requirements (source_candidate_id);
CREATE TABLE decision_records (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
title TEXT NOT NULL,
rationale TEXT NOT NULL,
outcome TEXT NOT NULL,
requirement_id UUID REFERENCES requirements(id) ON DELETE SET NULL,
candidate_id UUID REFERENCES requirement_candidates(id) ON DELETE SET NULL,
decided_by UUID REFERENCES users(id),
decided_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,
notes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX decision_records_outcome_idx ON decision_records (outcome);
CREATE INDEX decision_records_requirement_id_idx ON decision_records (requirement_id);
CREATE TABLE policy_references (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
decision_id UUID NOT NULL REFERENCES decision_records(id) ON DELETE CASCADE,
policy_scope TEXT NOT NULL,
constraint_note TEXT,
created_by UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX policy_references_decision_id_idx ON policy_references (decision_id);
CREATE TABLE implementation_change_references (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
decision_id UUID NOT NULL REFERENCES decision_records(id) ON DELETE CASCADE,
work_item_ref TEXT NOT NULL,
system TEXT NOT NULL DEFAULT 'github',
linked_by UUID REFERENCES users(id),
linked_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX impl_change_refs_decision_id_idx ON implementation_change_references (decision_id);
-- Back-reference: track which candidate was promoted to a requirement
ALTER TABLE requirement_candidates ADD COLUMN requirement_id UUID REFERENCES requirements(id) ON DELETE SET NULL;
- Valid
decision_records.outcomevalues:accepted,rejected,deferred,split,merged,reframed - Valid
policy_references.policy_scopevalues:internal,external,regulatory,contractual,architectural - Valid
requirements.statusvalues:active,superseded,withdrawn - Verify Haskell types are generated correctly
Exit criteria: migrate runs cleanly; all Phase 3 types available in GHCi.
T02 — Requirement promotion: RequirementCandidate → Requirement
id: IHUB-WP-0003-T02
status: done
priority: high
state_hub_task_id: "9d1edd55-628c-4354-82c3-2bf273f1b827"
- Add
PromoteToRequirementAction { candidateId }(POST from candidate show page) - Validate: candidate must have
status = 'accepted'; return 422 with message otherwise - Idempotent: if
candidate.requirement_idalready set, redirect to existing requirement - On promotion: create
Requirementrecord, setcandidate.requirement_id - Scaffold
RequirementsController: index, show (no new/create — requirements come from promotion only) - Show page: title, description, source candidate link, linked decision (if any), status badge
- Index: table with status, source candidate, linked decision, created_at
Exit criteria: Accepted candidates can be promoted once; second promotion redirects; requirement visible in index and show.
T03 — DecisionRecord controller and views
id: IHUB-WP-0003-T03
status: done
priority: high
state_hub_task_id: "171b38ab-c6e7-4b0e-94c0-ebc35f07488a"
- Scaffold
DecisionRecordsController - Actions: index, show, new, create, edit, update (no delete)
- Fields:
title,rationale(textarea),outcome(select),decidedBy(user select),notes(optional textarea) - Index view: table with outcome badge, linked requirement title, decided_by name, decided_at; filterable by outcome
- Show view: full detail + linked requirement + policy references section + implementation refs section + actor attribution
Exit criteria: Decision records can be created manually, listed, filtered, and viewed with full context.
T04 — Candidate → Decision linkage action
id: IHUB-WP-0003-T04
status: done
priority: high
state_hub_task_id: "eb45a76b-fd75-4a6c-bec6-e47095d5fa36"
- Add "Create Decision" button on
RequirementCandidateshow page (requiresstatus = 'accepted') LinkToDecisionAction { candidateId }(POST): creates aDecisionRecordpre-populated from candidatetitle= candidate titlerationaleseeded from candidate descriptioncandidateIdset on the decision record- If a promoted
Requirementexists, setrequirementIdon the decision too
- Idempotent: if decision already linked to this candidate, redirect to existing decision
- Show "Linked Decision →" on candidate show page after linkage
Exit criteria: Single-click decision creation from an accepted candidate; idempotent; link visible on candidate show page.
T05 — PolicyReference: link decisions to policy scope
id: IHUB-WP-0003-T05
status: done
priority: medium
state_hub_task_id: "4ef86992-d35e-4f62-a601-bd19e3ef63d3"
AddPolicyReferenceAction { decisionId }(POST from decision show page)- Fields:
policyScope(select: internal/external/regulatory/contractual/architectural),constraintNote(optional) - Multiple policy refs per decision allowed
- List policy refs on decision show page: scope badge + constraint note + created_at
- Delete:
DeletePolicyReferenceAction— policy refs may be removed (they are editorial, not audit-critical)
Exit criteria: Policy references can be added and removed from decisions; multiple refs per decision supported.
T06 — ImplementationChangeReference: link decisions to work items
id: IHUB-WP-0003-T06
status: done
priority: medium
state_hub_task_id: "eac1baf2-9df7-48fd-880e-68d07e22a337"
AddImplementationRefAction { decisionId }(POST from decision show page)- Fields:
workItemRef(free text — e.g.#1234,PROJ-456),system(select: github/linear/jira/other) - List refs on decision show page: system badge + ref text + linked_at
- No external API calls — refs are manual pointers only
- Delete:
DeleteImplementationRefAction— refs are editorial, not audit-critical
Exit criteria: Implementation refs can be added and removed; multiple refs per decision; no external API integration required.
T07 — Decision outcomes: full outcome vocabulary
id: IHUB-WP-0003-T07
status: done
priority: high
state_hub_task_id: "eaa425b3-42a7-4498-8aa6-1610959ce16b"
- Validate outcome on create against allowed set:
accepted,rejected,deferred,split,merged,reframed - Outcome is immutable after creation —
UpdateDecisionRecordActionmay not changeoutcome - Color roles per
specs/TailwindForInteractionHubs_v0.2.md:accepted→ greenrejected→ reddeferred→ graysplit→ purplemerged→ indigoreframed→ orange/amber
- For
split/mergedoutcomes:notesfield should capture related candidate IDs or context - Display outcome badge consistently across index, show, and governance dashboard views
Exit criteria: All six outcomes render with correct color; outcome immutable after create; split/merged notes convention documented inline.
T08 — Hub governance audit trail dashboard
id: IHUB-WP-0003-T08
status: done
priority: high
state_hub_task_id: "6bd3f8f2-13c1-4f95-a1cf-53a210b8e366"
- Add
GovernanceDashboardAction { hubId }toHubsControllerwrapped withautoRefresh do - Dashboard panels:
- KPI row: decision counts by outcome (accepted / rejected / deferred / split / merged / reframed)
- Recent decisions (last 20): title, outcome badge, widget origin (via requirement → candidate → widget), decided_at
- Traceability coverage: per widget — ✓/✗ for has annotation, has candidate, has decision
- Open requirements awaiting decision: requirements with no linked
decision_id
- Link from hub Show page alongside "Triage Dashboard"
Exit criteria: Dashboard live-updates on decision/requirement changes. Traceability coverage gives a quick health signal per widget.
T09 — Phase 3 gate: tests, consistency, docs
id: IHUB-WP-0003-T09
status: done
priority: high
state_hub_task_id: "6f1a08f1-c114-4a19-bf71-cbb2421171e1"
- Integration tests (
Test/):- Requirement promotion: accepted candidate → requirement; unaccepted candidate → 422; duplicate → idempotent
- Decision create + link to candidate; link to requirement if promoted
- PolicyReference add + delete
- ImplementationChangeReference add + delete
- Outcome immutability: update attempt on outcome field rejected
- Governance dashboard: data fetch compiles and returns correct counts
- Consistency sync:
Or via State Hub MCP:
cd ~/the-custodian && make fix-consistency REPO=inter-hubcheck_repo_consistency(repo_slug="inter-hub", fix=True) - Documentation updates:
- Update
SCOPE.mdcurrent state section: Phase 3 complete - Write
docs/phase3-summary.md: what was built, known limitations, Phase 4 readiness
- Update
- Smoke test checklist:
devenv up→ clean start- Accept a requirement candidate via triage
- Promote to requirement
- Create decision linked to candidate
- Add policy reference (regulatory)
- Add implementation ref (github,
#42) - Confirm governance dashboard shows decision and traceability coverage
- Confirm outcome cannot be changed after creation
Exit criteria: All tests pass; consistency sync reports no errors; smoke test completed; SCOPE.md updated.
Phase 3 Dependencies
- Phase 2 schema stable (T01 depends on
requirement_candidates,usersfrom Phase 2) requirementsbeforedecision_recordsFK reference (T01 ordering)- Schema (T01) before all controller work (T02–T08)
Requirement(T02) beforeDecisionRecordlinkage (T04)DecisionRecord(T03) beforePolicyReference(T05),ImplementationChangeReference(T06), outcome vocabulary (T07)- All feature tasks (T01–T08) before gate (T09)
Notes
- Outcome is immutable. Unlike
TriageState(which appends rows),DecisionRecord.outcomeis set at creation and never changed. A wrong decision should be superseded by creating a new decision record with a note referencing the original, not by editing the existing one. - No delete on DecisionRecord or Requirement. These are audit artifacts. Use
status = 'withdrawn'on Requirement oroutcome = 'rejected'on DecisionRecord to express nullification. - PolicyReference and ImplementationChangeReference are editorial — they may be added and deleted freely. They do not constitute audit trail themselves; the DecisionRecord is the audit artifact.
- Traceability coverage (T08) is a spot-check UI, not an enforced constraint. Phase 4+ will introduce automated gap detection via outcome signals.
- No state-hub integration in Phase 3. The
the-custodianstate-hub is a separate system; cross-linking IHF decisions to state-hub decision records is Phase 5+ scope.