generated from coulomb/repo-seed
feat(P5): IHF Phase 5 complete — agent-assisted distillation
Some checks failed
Test / test (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
Adds bounded AI support to the IHF governance loop. All AI outputs are attributed (model_ref), reviewable (AgentReviewRecord), and reversible. No autonomous decisions; no silent requirement promotion. - T01: Schema — agent_proposals, agent_review_records, confidence_annotations (migration 1743379200) - T02: AgentProposalsController (index/show/accept/reject, idempotent review guard), global nav "Agent" link - T03: SummarizeClusterAction — Claude API cluster summary on widget show - T04: DraftRequirementAction — AI requirement draft; acceptance creates RequirementCandidate (human-gated) - T05: DetectDuplicatesAction — duplicate_flag proposal on candidate show - T06: DetectPolicySensitivityAction — policy_flag with ConfidenceAnnotations per concern scope - T07: ProposeImplementationAction — impl_proposal from decision show - T08: AgentAuditDashboardAction — autoRefresh; KPI row, unreviewed queue, recent proposals, attribution log matrix - T09: integration tests, SCOPE.md updated, phase5-summary.md, flake.nix adds http-conduit/aeson/string-conversions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -942,3 +942,151 @@ main = do
|
||||
let improvedCount = length (filter (\s -> s.signalType == "improved") signals)
|
||||
improvedCount `shouldBe` 1
|
||||
deleteRecord hub
|
||||
|
||||
-- Phase 5: Agent-Assisted Distillation
|
||||
describe "AgentProposal" do
|
||||
|
||||
it "creates and fetches a proposal with all fields" do
|
||||
hub <- newRecord @Hub |> set #name "P5Hub" |> createRecord
|
||||
widget <- newRecord @Widget
|
||||
|> set #name "p5widget" |> set #widgetType "button"
|
||||
|> set #hubId hub.id |> set #status "active"
|
||||
|> createRecord
|
||||
proposal <- newRecord @AgentProposal
|
||||
|> set #proposalType "summary"
|
||||
|> set #sourceWidgetId (Just widget.id)
|
||||
|> set #content "AI summary text"
|
||||
|> set #modelRef "claude-sonnet-4-6"
|
||||
|> set #status "pending"
|
||||
|> createRecord
|
||||
proposal.proposalType `shouldBe` "summary"
|
||||
proposal.modelRef `shouldBe` "claude-sonnet-4-6"
|
||||
proposal.status `shouldBe` "pending"
|
||||
proposal.confidence `shouldBe` Nothing
|
||||
deleteRecord hub
|
||||
|
||||
it "accept changes proposal status to accepted and creates review record" do
|
||||
hub <- newRecord @Hub |> set #name "P5AccHub" |> createRecord
|
||||
proposal <- newRecord @AgentProposal
|
||||
|> set #proposalType "summary"
|
||||
|> set #content "test"
|
||||
|> set #modelRef "claude-sonnet-4-6"
|
||||
|> set #status "pending"
|
||||
|> createRecord
|
||||
proposal' <- proposal |> set #status "accepted" |> updateRecord
|
||||
_review <- newRecord @AgentReviewRecord
|
||||
|> set #proposalId proposal.id
|
||||
|> set #decision "accepted"
|
||||
|> createRecord
|
||||
proposal'.status `shouldBe` "accepted"
|
||||
reviews <- query @AgentReviewRecord
|
||||
|> filterWhere (#proposalId, proposal.id) |> fetch
|
||||
length reviews `shouldBe` 1
|
||||
(head reviews).decision `shouldBe` "accepted"
|
||||
deleteRecord hub
|
||||
|
||||
it "reject changes proposal status to rejected and creates review record" do
|
||||
hub <- newRecord @Hub |> set #name "P5RejHub" |> createRecord
|
||||
proposal <- newRecord @AgentProposal
|
||||
|> set #proposalType "policy_flag"
|
||||
|> set #content "{}"
|
||||
|> set #modelRef "claude-sonnet-4-6"
|
||||
|> set #status "pending"
|
||||
|> createRecord
|
||||
proposal' <- proposal |> set #status "rejected" |> updateRecord
|
||||
_review <- newRecord @AgentReviewRecord
|
||||
|> set #proposalId proposal.id
|
||||
|> set #decision "rejected"
|
||||
|> createRecord
|
||||
proposal'.status `shouldBe` "rejected"
|
||||
reviews <- query @AgentReviewRecord
|
||||
|> filterWhere (#proposalId, proposal.id) |> fetch
|
||||
(head reviews).decision `shouldBe` "rejected"
|
||||
deleteRecord hub
|
||||
|
||||
it "review record is idempotent (UNIQUE constraint on proposal_id)" do
|
||||
hub <- newRecord @Hub |> set #name "P5IdemHub" |> createRecord
|
||||
proposal <- newRecord @AgentProposal
|
||||
|> set #proposalType "summary"
|
||||
|> set #content "c"
|
||||
|> set #modelRef "claude-sonnet-4-6"
|
||||
|> set #status "pending"
|
||||
|> createRecord
|
||||
newRecord @AgentReviewRecord
|
||||
|> set #proposalId proposal.id
|
||||
|> set #decision "accepted"
|
||||
|> createRecord
|
||||
result <- try (
|
||||
newRecord @AgentReviewRecord
|
||||
|> set #proposalId proposal.id
|
||||
|> set #decision "accepted"
|
||||
|> createRecord
|
||||
) :: IO (Either SomeException AgentReviewRecord)
|
||||
isLeft result `shouldBe` True
|
||||
deleteRecord hub
|
||||
|
||||
describe "ConfidenceAnnotation" do
|
||||
it "creates and links to proposal" do
|
||||
hub <- newRecord @Hub |> set #name "P5CaHub" |> createRecord
|
||||
proposal <- newRecord @AgentProposal
|
||||
|> set #proposalType "policy_flag"
|
||||
|> set #content "{}"
|
||||
|> set #modelRef "claude-sonnet-4-6"
|
||||
|> set #status "pending"
|
||||
|> set #confidence (Just 0.9)
|
||||
|> createRecord
|
||||
_ca <- newRecord @ConfidenceAnnotation
|
||||
|> set #proposalId proposal.id
|
||||
|> set #dimension "policy_alignment"
|
||||
|> set #score 0.9
|
||||
|> set #explanation (Just "High regulatory risk")
|
||||
|> createRecord
|
||||
cas <- query @ConfidenceAnnotation
|
||||
|> filterWhere (#proposalId, proposal.id) |> fetch
|
||||
length cas `shouldBe` 1
|
||||
(head cas).dimension `shouldBe` "policy_alignment"
|
||||
deleteRecord hub
|
||||
|
||||
describe "Duplicate detection proposal" do
|
||||
it "creates duplicate_flag proposal and handles empty duplicates array" do
|
||||
hub <- newRecord @Hub |> set #name "P5DupHub" |> createRecord
|
||||
widget <- newRecord @Widget
|
||||
|> set #name "dupwidget" |> set #widgetType "form"
|
||||
|> set #hubId hub.id |> set #status "active"
|
||||
|> createRecord
|
||||
candidate <- newRecord @RequirementCandidate
|
||||
|> set #title "Slow form" |> set #description "Form is slow"
|
||||
|> set #sourceWidgetId widget.id |> set #category "friction"
|
||||
|> set #status "open" |> createRecord
|
||||
proposal <- newRecord @AgentProposal
|
||||
|> set #proposalType "duplicate_flag"
|
||||
|> set #sourceCandidateId (Just candidate.id)
|
||||
|> set #content "{\"duplicates\": []}"
|
||||
|> set #modelRef "claude-sonnet-4-6"
|
||||
|> set #status "pending"
|
||||
|> createRecord
|
||||
proposal.proposalType `shouldBe` "duplicate_flag"
|
||||
proposal.content `shouldBe` "{\"duplicates\": []}"
|
||||
deleteRecord hub
|
||||
|
||||
describe "Agent audit dashboard data" do
|
||||
it "fetches proposal counts correctly" do
|
||||
hub <- newRecord @Hub |> set #name "P5AuditHub" |> createRecord
|
||||
p1 <- newRecord @AgentProposal
|
||||
|> set #proposalType "summary" |> set #content "s"
|
||||
|> set #modelRef "claude-sonnet-4-6" |> set #status "pending"
|
||||
|> createRecord
|
||||
p2 <- newRecord @AgentProposal
|
||||
|> set #proposalType "policy_flag" |> set #content "p"
|
||||
|> set #modelRef "claude-sonnet-4-6" |> set #status "accepted"
|
||||
|> createRecord
|
||||
_r <- newRecord @AgentReviewRecord
|
||||
|> set #proposalId p2.id |> set #decision "accepted"
|
||||
|> createRecord
|
||||
allProposals <- query @AgentProposal |> fetch
|
||||
allReviews <- query @AgentReviewRecord |> fetch
|
||||
let pending = filter (\p -> p.status == "pending") allProposals
|
||||
accepted = filter (\r -> r.decision == "accepted") allReviews
|
||||
length pending `shouldBe` 1
|
||||
length accepted `shouldBe` 1
|
||||
deleteRecord hub
|
||||
|
||||
Reference in New Issue
Block a user