Files
inter-hub/workplans/IHUB-WP-0003-ihf-phase3-governance-and-decision-linkage.md
Bernd Worsch 7f9a8dd441 feat(P3): IHF Phase 3 complete — Governance and Decision Linkage
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>
2026-03-29 10:42:56 +00:00

14 KiB
Raw Blame History

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.outcome values: accepted, rejected, deferred, split, merged, reframed
  • Valid policy_references.policy_scope values: internal, external, regulatory, contractual, architectural
  • Valid requirements.status values: 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"
  1. Add PromoteToRequirementAction { candidateId } (POST from candidate show page)
  2. Validate: candidate must have status = 'accepted'; return 422 with message otherwise
  3. Idempotent: if candidate.requirement_id already set, redirect to existing requirement
  4. On promotion: create Requirement record, set candidate.requirement_id
  5. Scaffold RequirementsController: index, show (no new/create — requirements come from promotion only)
  6. Show page: title, description, source candidate link, linked decision (if any), status badge
  7. 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"
  1. Scaffold DecisionRecordsController
  2. Actions: index, show, new, create, edit, update (no delete)
  3. Fields: title, rationale (textarea), outcome (select), decidedBy (user select), notes (optional textarea)
  4. Index view: table with outcome badge, linked requirement title, decided_by name, decided_at; filterable by outcome
  5. 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"
  1. Add "Create Decision" button on RequirementCandidate show page (requires status = 'accepted')
  2. LinkToDecisionAction { candidateId } (POST): creates a DecisionRecord pre-populated from candidate
    • title = candidate title
    • rationale seeded from candidate description
    • candidateId set on the decision record
    • If a promoted Requirement exists, set requirementId on the decision too
  3. Idempotent: if decision already linked to this candidate, redirect to existing decision
  4. 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.


id: IHUB-WP-0003-T05
status: done
priority: medium
state_hub_task_id: "4ef86992-d35e-4f62-a601-bd19e3ef63d3"
  1. AddPolicyReferenceAction { decisionId } (POST from decision show page)
  2. Fields: policyScope (select: internal/external/regulatory/contractual/architectural), constraintNote (optional)
  3. Multiple policy refs per decision allowed
  4. List policy refs on decision show page: scope badge + constraint note + created_at
  5. 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.


id: IHUB-WP-0003-T06
status: done
priority: medium
state_hub_task_id: "eac1baf2-9df7-48fd-880e-68d07e22a337"
  1. AddImplementationRefAction { decisionId } (POST from decision show page)
  2. Fields: workItemRef (free text — e.g. #1234, PROJ-456), system (select: github/linear/jira/other)
  3. List refs on decision show page: system badge + ref text + linked_at
  4. No external API calls — refs are manual pointers only
  5. 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"
  1. Validate outcome on create against allowed set: accepted, rejected, deferred, split, merged, reframed
  2. Outcome is immutable after creation — UpdateDecisionRecordAction may not change outcome
  3. Color roles per specs/TailwindForInteractionHubs_v0.2.md:
    • accepted → green
    • rejected → red
    • deferred → gray
    • split → purple
    • merged → indigo
    • reframed → orange/amber
  4. For split / merged outcomes: notes field should capture related candidate IDs or context
  5. 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"
  1. Add GovernanceDashboardAction { hubId } to HubsController wrapped with autoRefresh do
  2. 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
  3. 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"
  1. 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
  2. Consistency sync:
    cd ~/the-custodian && make fix-consistency REPO=inter-hub
    
    Or via State Hub MCP: check_repo_consistency(repo_slug="inter-hub", fix=True)
  3. Documentation updates:
    • Update SCOPE.md current state section: Phase 3 complete
    • Write docs/phase3-summary.md: what was built, known limitations, Phase 4 readiness
  4. 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, users from Phase 2)
  • requirements before decision_records FK reference (T01 ordering)
  • Schema (T01) before all controller work (T02T08)
  • Requirement (T02) before DecisionRecord linkage (T04)
  • DecisionRecord (T03) before PolicyReference (T05), ImplementationChangeReference (T06), outcome vocabulary (T07)
  • All feature tasks (T01T08) before gate (T09)

Notes

  • Outcome is immutable. Unlike TriageState (which appends rows), DecisionRecord.outcome is 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 or outcome = '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-custodian state-hub is a separate system; cross-linking IHF decisions to state-hub decision records is Phase 5+ scope.