generated from coulomb/repo-seed
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:
@@ -178,3 +178,58 @@ instance Controller RequirementCandidatesController where
|
||||
, widgets
|
||||
, mStatusFilter = Just "my_queue"
|
||||
}
|
||||
|
||||
action PromoteToRequirementAction { requirementCandidateId } = do
|
||||
candidate <- fetch requirementCandidateId
|
||||
-- Guard: only accepted candidates may be promoted
|
||||
when (candidate.status /= "accepted") do
|
||||
setErrorMessage "Only accepted candidates can be promoted to a requirement"
|
||||
respondWith 422 do
|
||||
redirectTo ShowRequirementCandidateAction { requirementCandidateId }
|
||||
-- Idempotent: if already promoted, redirect to existing requirement
|
||||
case candidate.requirementId of
|
||||
Just rid -> redirectTo ShowRequirementAction { requirementId = rid }
|
||||
Nothing -> do
|
||||
mUser <- currentUserOrNothing
|
||||
let createdBy = fmap (.id) mUser
|
||||
req <- newRecord @Requirement
|
||||
|> set #title candidate.title
|
||||
|> set #description candidate.description
|
||||
|> set #sourceCandidateId requirementCandidateId
|
||||
|> set #status "active"
|
||||
|> set #createdBy (fmap (Id . unId) createdBy)
|
||||
|> createRecord
|
||||
candidate
|
||||
|> set #requirementId (Just req.id)
|
||||
|> updateRecord
|
||||
setSuccessMessage "Promoted to requirement"
|
||||
redirectTo ShowRequirementAction { requirementId = req.id }
|
||||
|
||||
action LinkToDecisionAction { requirementCandidateId } = do
|
||||
candidate <- fetch requirementCandidateId
|
||||
-- Guard: only accepted candidates
|
||||
when (candidate.status /= "accepted") do
|
||||
setErrorMessage "Only accepted candidates can be linked to a decision"
|
||||
respondWith 422 do
|
||||
redirectTo ShowRequirementCandidateAction { requirementCandidateId }
|
||||
-- Idempotent: check if a decision already links to this candidate
|
||||
existing <- query @DecisionRecord
|
||||
|> filterWhere (#candidateId, Just requirementCandidateId)
|
||||
|> fetchOneOrNothing
|
||||
case existing of
|
||||
Just dr -> redirectTo ShowDecisionRecordAction { decisionRecordId = dr.id }
|
||||
Nothing -> do
|
||||
mUser <- currentUserOrNothing
|
||||
let decidedBy = fmap (.id) mUser
|
||||
-- Use promoted requirement id if available
|
||||
let mReqId = candidate.requirementId
|
||||
dr <- newRecord @DecisionRecord
|
||||
|> set #title candidate.title
|
||||
|> set #rationale candidate.description
|
||||
|> set #outcome "accepted"
|
||||
|> set #candidateId (Just requirementCandidateId)
|
||||
|> set #requirementId mReqId
|
||||
|> set #decidedBy (fmap (Id . unId) decidedBy)
|
||||
|> createRecord
|
||||
setSuccessMessage "Decision record created"
|
||||
redirectTo ShowDecisionRecordAction { decisionRecordId = dr.id }
|
||||
|
||||
Reference in New Issue
Block a user