feat(T02-T11): IHF Phase 1 schema, controllers, views, and helpers
- Schema: hubs, widgets, widget_versions, interaction_events (append-only
trigger), annotations, users — single migration file
- Web layer: Types, Routes, FrontController with auth + AutoRefresh layout
- Controllers: Hubs (CRUD), Widgets (CRUD + versioning), InteractionEvents
(JSON capture, canonical event_type validation), Annotations (threaded,
append-only)
- Sessions controller for IHP auth
- Views: Hubs (index/show/new/edit), Widgets (index/show/new/edit),
Annotations (index/new), Sessions (login)
- widgetEnvelope helper with full data-* governance attributes
- Integration tests: Hub CRUD, Widget versioning, event capture, append-only
guard, annotation threading, validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 01:42:43 +00:00
module Web.Controller.Widgets where
import Web.Types
import Web.View.Widgets.Index
import Web.View.Widgets.Show
import Web.View.Widgets.New
import Web.View.Widgets.Edit
import Generated.Types
import IHP.Prelude
import IHP.ControllerPrelude
import Data.Aeson ( toJSON , object , ( .= ) )
feat(P5): IHF Phase 5 complete — agent-assisted distillation
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>
2026-03-29 15:54:33 +00:00
import Application.Helper.Controller ( isInRegression , widgetCycleCounts , callClaudeApi )
import Data.List ( intercalate )
feat(T02-T11): IHF Phase 1 schema, controllers, views, and helpers
- Schema: hubs, widgets, widget_versions, interaction_events (append-only
trigger), annotations, users — single migration file
- Web layer: Types, Routes, FrontController with auth + AutoRefresh layout
- Controllers: Hubs (CRUD), Widgets (CRUD + versioning), InteractionEvents
(JSON capture, canonical event_type validation), Annotations (threaded,
append-only)
- Sessions controller for IHP auth
- Views: Hubs (index/show/new/edit), Widgets (index/show/new/edit),
Annotations (index/new), Sessions (login)
- widgetEnvelope helper with full data-* governance attributes
- Integration tests: Hub CRUD, Widget versioning, event capture, append-only
guard, annotation threading, validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 01:42:43 +00:00
instance Controller WidgetsController where
beforeAction = ensureIsUser
action WidgetsAction = do
widgets <- query @ Widget |> orderByAsc # name |> fetch
hubs <- query @ Hub |> fetch
render IndexView { widgets , hubs }
action NewWidgetAction = do
let widget = newRecord @ Widget
2026-03-29 21:14:57 +00:00
hubs <- query @ Hub |> fetch
adapterSpecs <- query @ WidgetAdapterSpec
|> filterWhere ( # status , " active " )
|> orderByAsc # name
|> fetch
render NewView { widget , hubs , adapterSpecs }
feat(T02-T11): IHF Phase 1 schema, controllers, views, and helpers
- Schema: hubs, widgets, widget_versions, interaction_events (append-only
trigger), annotations, users — single migration file
- Web layer: Types, Routes, FrontController with auth + AutoRefresh layout
- Controllers: Hubs (CRUD), Widgets (CRUD + versioning), InteractionEvents
(JSON capture, canonical event_type validation), Annotations (threaded,
append-only)
- Sessions controller for IHP auth
- Views: Hubs (index/show/new/edit), Widgets (index/show/new/edit),
Annotations (index/new), Sessions (login)
- widgetEnvelope helper with full data-* governance attributes
- Integration tests: Hub CRUD, Widget versioning, event capture, append-only
guard, annotation threading, validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 01:42:43 +00:00
action ShowWidgetAction { widgetId } = do
widget <- fetch widgetId
hub <- fetch widget . hubId
versions <- query @ WidgetVersion
|> filterWhere ( # widgetId , widgetId )
|> orderByDesc # version
|> fetch
events <- query @ InteractionEvent
|> filterWhere ( # widgetId , widgetId )
|> orderByDesc # occurredAt
|> limit 20
|> fetch
annotations <- query @ Annotation
|> filterWhere ( # widgetId , widgetId )
|> orderByAsc # createdAt
|> fetch
2026-03-29 12:27:30 +00:00
recentSignals <- query @ OutcomeSignal
|> filterWhere ( # widgetId , widgetId )
|> orderByDesc # observedAt
|> limit 10
|> fetch
allSignals <- query @ OutcomeSignal
|> filterWhere ( # widgetId , widgetId )
|> fetch
let isRegressed = isInRegression allSignals annotations widgetId
-- Recurrence cycle count for this widget
allCandidates <- query @ RequirementCandidate |> filterWhere ( # sourceWidgetId , widgetId ) |> fetch
allRequirements <- query @ Requirement |> fetch
allDecisions <- query @ DecisionRecord |> fetch
allDeployments <- query @ DeploymentRecord |> fetch
let cycleCounts = widgetCycleCounts allCandidates allRequirements allDecisions allDeployments
cycleCount = fromMaybe 0 ( lookup widgetId cycleCounts )
2026-03-29 21:14:57 +00:00
mAdapterSpec <- case widget . adapterSpecId of
Nothing -> pure Nothing
Just sid -> fetchOneOrNothing sid
render ShowView { widget , hub , versions , events , annotations , recentSignals , isRegressed , cycleCount , mAdapterSpec }
feat(T02-T11): IHF Phase 1 schema, controllers, views, and helpers
- Schema: hubs, widgets, widget_versions, interaction_events (append-only
trigger), annotations, users — single migration file
- Web layer: Types, Routes, FrontController with auth + AutoRefresh layout
- Controllers: Hubs (CRUD), Widgets (CRUD + versioning), InteractionEvents
(JSON capture, canonical event_type validation), Annotations (threaded,
append-only)
- Sessions controller for IHP auth
- Views: Hubs (index/show/new/edit), Widgets (index/show/new/edit),
Annotations (index/new), Sessions (login)
- widgetEnvelope helper with full data-* governance attributes
- Integration tests: Hub CRUD, Widget versioning, event capture, append-only
guard, annotation threading, validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 01:42:43 +00:00
action CreateWidgetAction = do
let widget = newRecord @ Widget
2026-03-29 21:14:57 +00:00
hubs <- query @ Hub |> fetch
adapterSpecs <- query @ WidgetAdapterSpec |> filterWhere ( # status , " active " ) |> orderByAsc # name |> fetch
feat(T02-T11): IHF Phase 1 schema, controllers, views, and helpers
- Schema: hubs, widgets, widget_versions, interaction_events (append-only
trigger), annotations, users — single migration file
- Web layer: Types, Routes, FrontController with auth + AutoRefresh layout
- Controllers: Hubs (CRUD), Widgets (CRUD + versioning), InteractionEvents
(JSON capture, canonical event_type validation), Annotations (threaded,
append-only)
- Sessions controller for IHP auth
- Views: Hubs (index/show/new/edit), Widgets (index/show/new/edit),
Annotations (index/new), Sessions (login)
- widgetEnvelope helper with full data-* governance attributes
- Integration tests: Hub CRUD, Widget versioning, event capture, append-only
guard, annotation threading, validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 01:42:43 +00:00
widget
2026-03-29 21:14:57 +00:00
|> fill @ '["name", "widgetType", "hubId", "capabilityRef", "viewContext", "policyScope", "status", "adapterSpecId"]
feat(T02-T11): IHF Phase 1 schema, controllers, views, and helpers
- Schema: hubs, widgets, widget_versions, interaction_events (append-only
trigger), annotations, users — single migration file
- Web layer: Types, Routes, FrontController with auth + AutoRefresh layout
- Controllers: Hubs (CRUD), Widgets (CRUD + versioning), InteractionEvents
(JSON capture, canonical event_type validation), Annotations (threaded,
append-only)
- Sessions controller for IHP auth
- Views: Hubs (index/show/new/edit), Widgets (index/show/new/edit),
Annotations (index/new), Sessions (login)
- widgetEnvelope helper with full data-* governance attributes
- Integration tests: Hub CRUD, Widget versioning, event capture, append-only
guard, annotation threading, validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 01:42:43 +00:00
|> validateField # name nonEmpty
|> validateField # widgetType nonEmpty
|> ifValid \ case
2026-03-29 21:14:57 +00:00
Left widget -> render NewView { widget , hubs , adapterSpecs }
feat(T02-T11): IHF Phase 1 schema, controllers, views, and helpers
- Schema: hubs, widgets, widget_versions, interaction_events (append-only
trigger), annotations, users — single migration file
- Web layer: Types, Routes, FrontController with auth + AutoRefresh layout
- Controllers: Hubs (CRUD), Widgets (CRUD + versioning), InteractionEvents
(JSON capture, canonical event_type validation), Annotations (threaded,
append-only)
- Sessions controller for IHP auth
- Views: Hubs (index/show/new/edit), Widgets (index/show/new/edit),
Annotations (index/new), Sessions (login)
- widgetEnvelope helper with full data-* governance attributes
- Integration tests: Hub CRUD, Widget versioning, event capture, append-only
guard, annotation threading, validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 01:42:43 +00:00
Right widget -> do
widget <- createRecord widget
let snapshot = object
[ " name " .= widget . name
, " widget_type " .= widget . widgetType
, " hub_id " .= widget . hubId
, " capability_ref " .= widget . capabilityRef
, " view_context " .= widget . viewContext
, " policy_scope " .= widget . policyScope
, " status " .= widget . status
, " version " .= widget . version
]
newRecord @ WidgetVersion
|> set # widgetId widget . id
|> set # version 1
|> set # schemaSnapshot snapshot
|> createRecord
setSuccessMessage " Widget registered "
redirectTo ShowWidgetAction { widgetId = widget . id }
action EditWidgetAction { widgetId } = do
2026-03-29 21:14:57 +00:00
widget <- fetch widgetId
hubs <- query @ Hub |> fetch
adapterSpecs <- query @ WidgetAdapterSpec |> filterWhere ( # status , " active " ) |> orderByAsc # name |> fetch
render EditView { widget , hubs , adapterSpecs }
feat(T02-T11): IHF Phase 1 schema, controllers, views, and helpers
- Schema: hubs, widgets, widget_versions, interaction_events (append-only
trigger), annotations, users — single migration file
- Web layer: Types, Routes, FrontController with auth + AutoRefresh layout
- Controllers: Hubs (CRUD), Widgets (CRUD + versioning), InteractionEvents
(JSON capture, canonical event_type validation), Annotations (threaded,
append-only)
- Sessions controller for IHP auth
- Views: Hubs (index/show/new/edit), Widgets (index/show/new/edit),
Annotations (index/new), Sessions (login)
- widgetEnvelope helper with full data-* governance attributes
- Integration tests: Hub CRUD, Widget versioning, event capture, append-only
guard, annotation threading, validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 01:42:43 +00:00
action UpdateWidgetAction { widgetId } = do
2026-03-29 21:14:57 +00:00
widget <- fetch widgetId
hubs <- query @ Hub |> fetch
adapterSpecs <- query @ WidgetAdapterSpec |> filterWhere ( # status , " active " ) |> orderByAsc # name |> fetch
feat(T02-T11): IHF Phase 1 schema, controllers, views, and helpers
- Schema: hubs, widgets, widget_versions, interaction_events (append-only
trigger), annotations, users — single migration file
- Web layer: Types, Routes, FrontController with auth + AutoRefresh layout
- Controllers: Hubs (CRUD), Widgets (CRUD + versioning), InteractionEvents
(JSON capture, canonical event_type validation), Annotations (threaded,
append-only)
- Sessions controller for IHP auth
- Views: Hubs (index/show/new/edit), Widgets (index/show/new/edit),
Annotations (index/new), Sessions (login)
- widgetEnvelope helper with full data-* governance attributes
- Integration tests: Hub CRUD, Widget versioning, event capture, append-only
guard, annotation threading, validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 01:42:43 +00:00
widget
2026-03-29 21:14:57 +00:00
|> fill @ '["name", "widgetType", "hubId", "capabilityRef", "viewContext", "policyScope", "status", "adapterSpecId"]
feat(T02-T11): IHF Phase 1 schema, controllers, views, and helpers
- Schema: hubs, widgets, widget_versions, interaction_events (append-only
trigger), annotations, users — single migration file
- Web layer: Types, Routes, FrontController with auth + AutoRefresh layout
- Controllers: Hubs (CRUD), Widgets (CRUD + versioning), InteractionEvents
(JSON capture, canonical event_type validation), Annotations (threaded,
append-only)
- Sessions controller for IHP auth
- Views: Hubs (index/show/new/edit), Widgets (index/show/new/edit),
Annotations (index/new), Sessions (login)
- widgetEnvelope helper with full data-* governance attributes
- Integration tests: Hub CRUD, Widget versioning, event capture, append-only
guard, annotation threading, validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 01:42:43 +00:00
|> validateField # name nonEmpty
|> validateField # widgetType nonEmpty
|> ifValid \ case
2026-03-29 21:14:57 +00:00
Left widget -> render EditView { widget , hubs , adapterSpecs }
feat(T02-T11): IHF Phase 1 schema, controllers, views, and helpers
- Schema: hubs, widgets, widget_versions, interaction_events (append-only
trigger), annotations, users — single migration file
- Web layer: Types, Routes, FrontController with auth + AutoRefresh layout
- Controllers: Hubs (CRUD), Widgets (CRUD + versioning), InteractionEvents
(JSON capture, canonical event_type validation), Annotations (threaded,
append-only)
- Sessions controller for IHP auth
- Views: Hubs (index/show/new/edit), Widgets (index/show/new/edit),
Annotations (index/new), Sessions (login)
- widgetEnvelope helper with full data-* governance attributes
- Integration tests: Hub CRUD, Widget versioning, event capture, append-only
guard, annotation threading, validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 01:42:43 +00:00
Right widget -> do
let newVersion = widget . version + 1
widget <- widget |> set # version newVersion |> updateRecord
let snapshot = object
[ " name " .= widget . name
, " widget_type " .= widget . widgetType
, " hub_id " .= widget . hubId
, " capability_ref " .= widget . capabilityRef
, " view_context " .= widget . viewContext
, " policy_scope " .= widget . policyScope
, " status " .= widget . status
, " version " .= newVersion
]
newRecord @ WidgetVersion
|> set # widgetId widget . id
|> set # version newVersion
|> set # schemaSnapshot snapshot
|> createRecord
setSuccessMessage " Widget updated "
redirectTo ShowWidgetAction { widgetId = widget . id }
feat(P5): IHF Phase 5 complete — agent-assisted distillation
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>
2026-03-29 15:54:33 +00:00
-- T03: Summarize feedback cluster via Claude API
action SummarizeClusterAction { widgetId } = do
annotations <- query @ Annotation
|> filterWhere ( # widgetId , widgetId )
|> orderByDesc # createdAt
|> limit 20
|> fetch
threads <- query @ AnnotationThread
|> filterWhere ( # widgetId , widgetId )
|> orderByDesc # createdAt
|> limit 20
|> fetch
let annLines = map ( \ a -> " [ " <> a . category <> " / " <> a . severity <> " ] " <> a . body ) annotations
threadLines = map ( \ t -> " [thread] " <> t . title <> " : " <> fromMaybe " " t . description ) threads
userMsg = intercalate " \ n " ( annLines <> threadLines )
result <- liftIO $ callClaudeApi
" You are a distillation assistant for a governed interaction hub. Summarize the following user feedback cluster into a concise, actionable summary (2 \ x20134 sentences). Be factual and neutral. "
userMsg
300
case result of
Left err -> do
setErrorMessage ( " AI summarization failed: " <> err )
redirectTo ShowWidgetAction { widgetId }
Right content -> do
newRecord @ AgentProposal
|> set # proposalType " summary "
|> set # sourceWidgetId ( Just widgetId )
|> set # content content
|> set # modelRef " claude-sonnet-4-6 "
|> set # status " pending "
|> createRecord
setSuccessMessage " Summary proposal created "
redirectTo ShowWidgetAction { widgetId }
-- T04: Draft a requirement candidate via Claude API
action DraftRequirementAction { widgetId } = do
annotations <- query @ Annotation
|> filterWhere ( # widgetId , widgetId )
|> orderByDesc # createdAt
|> limit 20
|> fetch
let annLines = map ( \ a -> " [ " <> a . category <> " / " <> a . severity <> " ] " <> a . body ) annotations
userMsg = intercalate " \ n " annLines
result <- liftIO $ callClaudeApi
" You are a requirements analyst. Given these friction annotations, draft a single structured requirement candidate. Respond with JSON: { \ " title \ " : \ " ... \ " , \ " description \ " : \ " ... \ " }. "
userMsg
400
case result of
Left err -> do
setErrorMessage ( " AI draft failed: " <> err )
redirectTo ShowWidgetAction { widgetId }
Right content -> do
newRecord @ AgentProposal
|> set # proposalType " requirement_draft "
|> set # sourceWidgetId ( Just widgetId )
|> set # content content
|> set # modelRef " claude-sonnet-4-6 "
|> set # status " pending "
|> createRecord
setSuccessMessage " Requirement draft proposal created "
redirectTo ShowWidgetAction { widgetId }