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>
This commit is contained in:
2026-03-29 10:38:50 +00:00
parent 840b0e5c7b
commit 7f9a8dd441
23 changed files with 2039 additions and 19 deletions

View File

@@ -67,6 +67,9 @@ instance View ShowView where
{renderReviewerSection candidate mAssignment users}
</div>
<!-- Phase 3: Promote to Requirement / Link to Decision -->
{renderGovernanceActions candidate}
<!-- Triage history -->
<div class="bg-white rounded-lg border border-gray-200 px-6 py-4">
<h2 class="text-sm font-semibold text-gray-700 mb-3">Triage History</h2>
@@ -167,6 +170,51 @@ renderTriageRow ts = [hsx|
</li>
|]
renderGovernanceActions :: RequirementCandidate -> Html
renderGovernanceActions candidate
| candidate.status == "accepted" = [hsx|
<div class="bg-white rounded-lg border border-indigo-200 px-6 py-4">
<h2 class="text-sm font-semibold text-gray-700 mb-3">Governance</h2>
<div class="flex flex-wrap gap-3">
{renderPromoteButton candidate}
{renderLinkDecisionButton candidate}
</div>
</div>
|]
| otherwise = mempty
renderPromoteButton :: RequirementCandidate -> Html
renderPromoteButton candidate =
case candidate.requirementId of
Just rid -> [hsx|
<a href={ShowRequirementAction { requirementId = rid }}
class="text-sm text-green-700 bg-green-50 border border-green-200 px-3 py-1.5 rounded hover:bg-green-100">
Requirement
</a>
|]
Nothing -> [hsx|
<form method="POST"
action={PromoteToRequirementAction { requirementCandidateId = candidate.id }}>
{hiddenField "authenticity_token"}
<button type="submit"
class="text-sm bg-indigo-600 text-white px-3 py-1.5 rounded hover:bg-indigo-700">
Promote to Requirement
</button>
</form>
|]
renderLinkDecisionButton :: RequirementCandidate -> Html
renderLinkDecisionButton candidate = [hsx|
<form method="POST"
action={LinkToDecisionAction { requirementCandidateId = candidate.id }}>
{hiddenField "authenticity_token"}
<button type="submit"
class="text-sm bg-gray-700 text-white px-3 py-1.5 rounded hover:bg-gray-800">
Create Decision Record
</button>
</form>
|]
statusClass :: Text -> Text
statusClass "open" = "bg-blue-100 text-blue-700"
statusClass "in_review" = "bg-yellow-100 text-yellow-800"