fix: resolve all GHC 9.10.3 / IHP 1.5 compile errors (all 616 modules load)
Some checks failed
Test / test (push) Has been cancelled

Fix 13 modules that blocked compilation on Alpine:

- FrontController: remove annotationLauncherScript helper (IHP Html is a
  constrained type synonym); add (?context, ?request) constraint to
  defaultLayout matching what setLayout expects
- HubCapabilityManifests: switch JSONB fill to paramList+toJSON; fix dynamic
  SQL Text→Query via fromString/cs; void sqlExec; add Control.Monad.void
- Hubs: replace raw Array sqlQuery with filterWhereIn query builder;
  fix isInList validators
- DecisionRecords: remove unregistered DistilDecisionAction; fix hub
  resolution chain via candidateId→sourceWidgetId; BridgeResponse(..)
- RequirementCandidates: BridgeResponse(..); remove @Widget type apps from
  fetchOneOrNothing; void ConfidenceAnnotation createRecord
- AdaptiveThresholds: fix sqlQuery tuple param (Only hubId)
- AgentDelegations, AgentRegistrations, Widgets: BridgeResponse(..)
- Annotations, DeploymentRecords, GovernanceTemplates: minor type fixes
- DecisionRecords/Edit view: extract formAction before HSX block

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-29 10:46:50 +02:00
parent 209c77dd31
commit 2106000cc7
13 changed files with 71 additions and 130 deletions

View File

@@ -27,7 +27,7 @@ instance Controller AdaptiveThresholdsController where
weakCats <- sqlQuery weakCats <- sqlQuery
"SELECT annotation_category FROM outcome_correlations \ "SELECT annotation_category FROM outcome_correlations \
\ WHERE hub_id = ? AND correlation_score < 0.3" \ WHERE hub_id = ? AND correlation_score < 0.3"
[hubId] (Only hubId)
:: IO [Only Text] :: IO [Only Text]
-- Step 2: compute bottleneck threshold override = mean friction score -- Step 2: compute bottleneck threshold override = mean friction score
@@ -46,7 +46,7 @@ instance Controller AdaptiveThresholdsController where
\ WHERE rc.source_widget_id = w.id \ \ WHERE rc.source_widget_id = w.id \
\ AND os.signal_type NOT IN ('success','adoption','satisfaction') \ \ AND os.signal_type NOT IN ('success','adoption','satisfaction') \
\ )" \ )"
[hubId] (Only hubId)
:: IO [Only (Maybe Double)] :: IO [Only (Maybe Double)]
now <- getCurrentTime now <- getCurrentTime

View File

@@ -10,7 +10,7 @@ import qualified Data.Aeson as A
import Application.Helper.AgentBridge import Application.Helper.AgentBridge
( callAgentWithBudget ( callAgentWithBudget
, BridgeError(..) , BridgeError(..)
, BridgeResponse , BridgeResponse(..)
, bridgeErrorMessage , bridgeErrorMessage
) )
@@ -38,7 +38,7 @@ instance Controller AgentDelegationsController where
tokenBudget = paramOrDefault @Int 1000 "tokenBudget" tokenBudget = paramOrDefault @Int 1000 "tokenBudget"
delegatingAgentId <- case (proposal.agentRegistrationId :: Maybe (Id AgentRegistration)) of delegatingAgentId <- case (proposal.agentRegistrationId :: Maybe (Id AgentRegistration)) of
Just aid -> pure aid Just aid -> pure aid
Nothing -> respondAndExit =<< renderNotFound Nothing -> renderNotFound >> error "unreachable"
receivingAgent <- fetch receivingAgentId receivingAgent <- fetch receivingAgentId

View File

@@ -107,6 +107,7 @@ instance Controller AgentRegistrationsController where
\ WHERE ap.agent_registration_id = ? \ \ WHERE ap.agent_registration_id = ? \
\ AND ap.created_at >= NOW() - INTERVAL '30 days'" \ AND ap.created_at >= NOW() - INTERVAL '30 days'"
(Only agentRegistrationId) (Only agentRegistrationId)
:: IO [(Int, Int, Int, Int, Maybe Double)]
case rows of case rows of
[(accepted, rejected, _other, total, mConf)] -> do [(accepted, rejected, _other, total, mConf)] -> do
now <- getCurrentTime now <- getCurrentTime

View File

@@ -56,7 +56,7 @@ instance Controller AnnotationsController where
|> set #actorId (fmap coerce actorId) |> set #actorId (fmap coerce actorId)
|> set #actorType actorType |> set #actorType actorType
|> validateField #body nonEmpty |> validateField #body nonEmpty
|> validateField #severity (`elem` validSeverities) |> validateField #severity (isInList validSeverities)
|> (case categoryResult of |> (case categoryResult of
Left msg -> attachFailure #category msg Left msg -> attachFailure #category msg
Right () -> \x -> x) Right () -> \x -> x)

View File

@@ -8,9 +8,8 @@ import Web.View.DecisionRecords.Edit
import Generated.Types import Generated.Types
import IHP.Prelude import IHP.Prelude
import IHP.ControllerPrelude import IHP.ControllerPrelude
import Application.Helper.AgentBridge (callAgent, checkGovernancePolicy, bridgeErrorMessage, BridgeResponse) import Application.Helper.AgentBridge (callAgent, checkGovernancePolicy, bridgeErrorMessage, BridgeResponse(..))
import Application.Helper.ModelRouter (resolveAgent) import Application.Helper.ModelRouter (resolveAgent)
import Data.List (intercalate)
import IHP.ModelSupport (sqlQuery) import IHP.ModelSupport (sqlQuery)
import qualified Data.Aeson as A import qualified Data.Aeson as A
import Data.Coerce (coerce) import Data.Coerce (coerce)
@@ -95,7 +94,7 @@ instance Controller DecisionRecordsController where
|> set #decidedBy (fmap coerce decidedBy) |> set #decidedBy (fmap coerce decidedBy)
|> validateField #title nonEmpty |> validateField #title nonEmpty
|> validateField #rationale nonEmpty |> validateField #rationale nonEmpty
|> validateField #outcome (`elem` validOutcomes) |> validateField #outcome (isInList validOutcomes)
|> ifValid \case |> ifValid \case
Left record -> render NewView { record, requirements, candidates, users } Left record -> render NewView { record, requirements, candidates, users }
Right record -> do Right record -> do
@@ -188,10 +187,14 @@ instance Controller DecisionRecordsController where
mRequirement <- case record.requirementId of mRequirement <- case record.requirementId of
Nothing -> pure Nothing Nothing -> pure Nothing
Just rid -> fetchOneOrNothing rid Just rid -> fetchOneOrNothing rid
-- Resolve hub from the source widget via requirement candidate -- Resolve hub via the decision's linked candidate → source widget
mHubId <- case mRequirement >>= (.sourceWidgetId) of mHubId <- case record.candidateId of
Nothing -> pure Nothing Nothing -> pure Nothing
Just wid -> fmap (.hubId) <$> fetchOneOrNothing @Widget wid Just cid -> do
mCand <- fetchOneOrNothing cid
case mCand of
Nothing -> pure Nothing
Just cand -> fmap (.hubId) <$> fetchOneOrNothing cand.sourceWidgetId
let implLines = map (\r -> r.system <> ": " <> r.workItemRef) implRefs let implLines = map (\r -> r.system <> ": " <> r.workItemRef) implRefs
reqDesc = maybe "" (.description) mRequirement reqDesc = maybe "" (.description) mRequirement
userMsg = "Decision: " <> record.title userMsg = "Decision: " <> record.title
@@ -243,59 +246,3 @@ instance Controller DecisionRecordsController where
setSuccessMessage "Implementation proposal created" setSuccessMessage "Implementation proposal created"
redirectTo ShowDecisionRecordAction { decisionRecordId } redirectTo ShowDecisionRecordAction { decisionRecordId }
-- T05 / Phase 12: Distil decision into institutional knowledge entry
action DistilDecisionAction { decisionRecordId } = do
record <- fetch decisionRecordId
outcomes <- sqlQuery
"SELECT os.signal_type, os.value FROM outcome_signals os \
\ JOIN deployment_records dep ON dep.id = os.deployment_id \
\ WHERE dep.decision_id = ?"
[decisionRecordId]
:: IO [(Text, Maybe Double)]
let signalText = intercalate ", " $
map (\(st, mv) -> st <> maybe "" (\v -> "=" <> show v) mv) outcomes
prompt = "Distil this decision into a 2-3 sentence institutional knowledge entry. "
<> "Include the outcome data.\n\nDecision: " <> record.title
<> "\nRationale: " <> record.rationale
<> "\nOutcome: " <> record.outcome
<> "\nSignals: " <> signalText
-- Resolve hub from requirement chain
mHubId <- case record.requirementId of
Nothing -> pure Nothing
Just rid -> do
mReq <- fetchOneOrNothing rid
pure $ case mReq >>= (.sourceWidgetId) of
Nothing -> Nothing
Just _ -> Nothing -- hub resolution via widget lookup below
mHubIdResolved <- case record.requirementId of
Nothing -> pure Nothing
Just rid -> do
mReq <- fetchOneOrNothing rid
case mReq >>= (.sourceWidgetId) of
Nothing -> pure Nothing
Just wid -> fmap (.hubId) <$> fetchOneOrNothing @Widget wid
case mHubIdResolved of
Nothing -> do
setErrorMessage "Cannot resolve hub — ensure decision has a linked requirement with a source widget"
redirectTo ShowDecisionRecordAction { decisionRecordId }
Just hubId -> do
mAgent <- resolveAgent hubId "synthesis"
case mAgent of
Nothing -> do
setErrorMessage "No routing policy for 'synthesis' task type"
redirectTo ShowDecisionRecordAction { decisionRecordId }
Just agent -> do
result <- liftIO $ callAgent agent prompt
case result of
Left err -> do
setErrorMessage ("Distillation failed: " <> bridgeErrorMessage err)
redirectTo ShowDecisionRecordAction { decisionRecordId }
Right resp -> do
newRecord @InstitutionalKnowledgeEntry
|> set #hubId hubId
|> set #decisionRecordId (Just decisionRecordId)
|> set #summary resp.content
|> set #tags (A.toJSON ["decision" :: Text])
|> createRecord
setSuccessMessage "Knowledge entry created"
redirectTo ShowDecisionRecordAction { decisionRecordId }

View File

@@ -11,6 +11,7 @@ import Data.Time.Clock (addUTCTime, NominalDiffTime)
import Text.Read (readMaybe) import Text.Read (readMaybe)
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.Coerce (coerce) import Data.Coerce (coerce)
import Data.Scientific (Scientific)
instance Controller DeploymentRecordsController where instance Controller DeploymentRecordsController where
beforeAction = ensureIsUser beforeAction = ensureIsUser
@@ -88,7 +89,7 @@ instance Controller DeploymentRecordsController where
action RecordOutcomeSignalAction { deploymentRecordId } = do action RecordOutcomeSignalAction { deploymentRecordId } = do
let signalType = param @Text "signalType" let signalType = param @Text "signalType"
mValue = paramOrNothing @Double "value" mValue = fmap realToFrac (paramOrNothing @Double "value") :: Maybe Scientific
mUser = currentUserOrNothing mUser = currentUserOrNothing
let validTypes = ["improved", "regressed", "neutral", "inconclusive"] :: [Text] let validTypes = ["improved", "regressed", "neutral", "inconclusive"] :: [Text]
unless (signalType `elem` validTypes) do unless (signalType `elem` validTypes) do

View File

@@ -10,20 +10,20 @@ import IHP.ControllerPrelude
import Data.Aeson (Value(..), decode, encode, toJSON) import Data.Aeson (Value(..), decode, encode, toJSON)
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import Data.Text (intercalate) import Data.Text (intercalate)
import qualified Data.Map.Strict as Map
instance Controller GovernanceTemplatesController where instance Controller GovernanceTemplatesController where
beforeAction = ensureIsUser beforeAction = ensureIsUser
-- List published templates with clone count -- List published templates with clone count
action GovernanceTemplatesAction = autoRefresh do action GovernanceTemplatesAction = autoRefresh do
templates <- sqlQuery rawTemplates <- query @GovernanceTemplate
"SELECT gt.*, COUNT(gtc.id) AS clone_count \ |> filterWhere (#isPublished, True)
\ FROM governance_templates gt \ |> orderByAsc #name
\ LEFT JOIN governance_template_clones gtc ON gtc.governance_template_id = gt.id \ |> fetch
\ WHERE gt.is_published = TRUE \ allClones <- query @GovernanceTemplateClone |> fetch
\ GROUP BY gt.id \ let countMap = foldr (\c m -> Map.insertWith (+) c.governanceTemplateId 1 m) Map.empty allClones
\ ORDER BY clone_count DESC, gt.name ASC" templates = map (\t -> (t, Map.findWithDefault 0 t.id countMap)) rawTemplates
()
render IndexView { templates } render IndexView { templates }
-- Template detail with clone count -- Template detail with clone count

View File

@@ -8,9 +8,10 @@ import Web.View.HubCapabilityManifests.Edit
import Generated.Types import Generated.Types
import IHP.Prelude import IHP.Prelude
import IHP.ControllerPrelude import IHP.ControllerPrelude
import Data.Aeson (Value, Array, decode, encode) import Data.Aeson (Value, Array, decode, encode, toJSON)
import qualified Data.Vector as V import qualified Data.Vector as V
import Data.Maybe (mapMaybe) import Data.Maybe (mapMaybe)
import Control.Monad (void)
instance Controller HubCapabilityManifestsController where instance Controller HubCapabilityManifestsController where
beforeAction = ensureIsUser beforeAction = ensureIsUser
@@ -76,9 +77,11 @@ instance Controller HubCapabilityManifestsController where
setErrorMessage "Active manifests are read-only. Retire the current manifest and create a new draft to amend." setErrorMessage "Active manifests are read-only. Retire the current manifest and create a new draft to amend."
redirectTo ShowHubCapabilityManifestAction { hubCapabilityManifestId } redirectTo ShowHubCapabilityManifestAction { hubCapabilityManifestId }
manifest manifest
|> fill @'["manifestVersion", "capabilityDescription", "contact", |> fill @'["manifestVersion", "capabilityDescription", "contact"]
"declaredWidgetTypes", "declaredEventTypes", |> set #declaredWidgetTypes (toJSON (paramList @Text "declaredWidgetTypes"))
"declaredAnnotationCategories", "declaredPolicyScopes"] |> set #declaredEventTypes (toJSON (paramList @Text "declaredEventTypes"))
|> set #declaredAnnotationCategories (toJSON (paramList @Text "declaredAnnotationCategories"))
|> set #declaredPolicyScopes (toJSON (paramList @Text "declaredPolicyScopes"))
|> ifValid \case |> ifValid \case
Left manifest -> render EditView { manifest, hub, widgetTypeEntries, eventTypeEntries, categoryEntries, policyScopeEntries } Left manifest -> render EditView { manifest, hub, widgetTypeEntries, eventTypeEntries, categoryEntries, policyScopeEntries }
Right manifest -> do Right manifest -> do
@@ -142,7 +145,7 @@ checkConflict ::
Text -> Id Hub -> Text -> IO [Text] Text -> Id Hub -> Text -> IO [Text]
checkConflict tableName hubId name = do checkConflict tableName hubId name = do
rows <- sqlQuery rows <- sqlQuery
("SELECT owner_hub_id FROM " <> tableName <> " WHERE name = ?") (fromString $ cs ("SELECT owner_hub_id FROM " <> tableName <> " WHERE name = ?"))
(Only name) (Only name)
case rows of case rows of
[] -> pure [] [] -> pure []
@@ -158,12 +161,8 @@ upsertType ::
(?modelContext :: ModelContext) => (?modelContext :: ModelContext) =>
Text -> Id Hub -> Text -> IO () Text -> Id Hub -> Text -> IO ()
upsertType tableName hubId name = upsertType tableName hubId name =
sqlExec void $ sqlExec
("INSERT INTO " <> tableName <> " (name, label, owner_hub_id, status) " (fromString $ cs ("INSERT INTO " <> tableName <> " (name, label, owner_hub_id, status) "
<> "VALUES (?, ?, ?, 'active') ON CONFLICT (name) DO NOTHING") <> "VALUES (?, ?, ?, 'active') ON CONFLICT (name) DO NOTHING"))
(name, name, hubId) (name, name, hubId)
intercalate :: Text -> [Text] -> Text
intercalate _ [] = ""
intercalate _ [x] = x
intercalate sep (x:xs) = x <> sep <> intercalate sep xs

View File

@@ -39,13 +39,17 @@ instance Controller HubsController where
|> filterWhere (#hubId, hubId) |> filterWhere (#hubId, hubId)
|> orderByAsc #name |> orderByAsc #name
|> fetch |> fetch
widgetIds <- pure (map (.id) widgets) let widgetIds = map (.id) widgets
recentEvents <- sqlQuery recentEvents <- query @InteractionEvent
"SELECT * FROM interaction_events WHERE widget_id = ANY(?) ORDER BY occurred_at DESC LIMIT 50" |> filterWhereIn (#widgetId, widgetIds)
(Only (PGArray widgetIds)) |> orderByDesc #occurredAt
recentAnnotations <- sqlQuery |> limit 50
"SELECT * FROM annotations WHERE widget_id = ANY(?) ORDER BY created_at DESC LIMIT 20" |> fetch
(Only (PGArray widgetIds)) recentAnnotations <- query @Annotation
|> filterWhereIn (#widgetId, widgetIds)
|> orderByDesc #createdAt
|> limit 20
|> fetch
mManifest <- query @HubCapabilityManifest mManifest <- query @HubCapabilityManifest
|> filterWhere (#hubId, hubId) |> filterWhere (#hubId, hubId)
|> fetchOneOrNothing |> fetchOneOrNothing
@@ -58,7 +62,7 @@ instance Controller HubsController where
|> validateField #slug nonEmpty |> validateField #slug nonEmpty
|> validateField #name nonEmpty |> validateField #name nonEmpty
|> validateField #domain nonEmpty |> validateField #domain nonEmpty
|> validateField #hubKind (`elem` ["domain", "shared"]) |> validateField #hubKind (isInList ["domain", "shared"])
-- 'framework' cannot be set via the UI -- 'framework' cannot be set via the UI
|> ifValid \case |> ifValid \case
Left hub -> render NewView { hub } Left hub -> render NewView { hub }
@@ -78,7 +82,7 @@ instance Controller HubsController where
|> validateField #slug nonEmpty |> validateField #slug nonEmpty
|> validateField #name nonEmpty |> validateField #name nonEmpty
|> validateField #domain nonEmpty |> validateField #domain nonEmpty
|> validateField #hubKind (`elem` ["framework", "domain", "shared"]) |> validateField #hubKind (isInList ["framework", "domain", "shared"])
|> ifValid \case |> ifValid \case
Left hub -> render EditView { hub } Left hub -> render EditView { hub }
Right hub -> do Right hub -> do
@@ -265,8 +269,7 @@ instance Controller HubsController where
widgets <- query @Widget |> filterWhere (#hubId, hubId) |> fetch widgets <- query @Widget |> filterWhere (#hubId, hubId) |> fetch
let widgetIds = map (.id) widgets let widgetIds = map (.id) widgets
annotations <- query @Annotation |> filterWhereIn (#widgetId, widgetIds) |> fetch annotations <- query @Annotation |> filterWhereIn (#widgetId, widgetIds) |> fetch
events <- sqlQuery "SELECT * FROM interaction_events WHERE widget_id = ANY(?)" events <- query @InteractionEvent |> filterWhereIn (#widgetId, widgetIds) |> fetch
(Only (PGArray widgetIds))
signals <- query @OutcomeSignal |> filterWhereIn (#widgetId, widgetIds) |> fetch signals <- query @OutcomeSignal |> filterWhereIn (#widgetId, widgetIds) |> fetch
candidates <- query @RequirementCandidate |> filterWhereIn (#sourceWidgetId, widgetIds) |> fetch candidates <- query @RequirementCandidate |> filterWhereIn (#sourceWidgetId, widgetIds) |> fetch
let regressionWids = regressedWidgetIds signals annotations let regressionWids = regressedWidgetIds signals annotations

View File

@@ -8,7 +8,7 @@ import Web.View.RequirementCandidates.Edit
import Generated.Types import Generated.Types
import IHP.Prelude import IHP.Prelude
import IHP.ControllerPrelude import IHP.ControllerPrelude
import Application.Helper.AgentBridge (callAgent, checkGovernancePolicy, bridgeErrorMessage, BridgeResponse) import Application.Helper.AgentBridge (callAgent, checkGovernancePolicy, bridgeErrorMessage, BridgeResponse(..))
import Application.Helper.ModelRouter (resolveAgent) import Application.Helper.ModelRouter (resolveAgent)
import Data.Aeson (decode, Value(..), Array) import Data.Aeson (decode, Value(..), Array)
import Data.Aeson.Lens (key, _String) import Data.Aeson.Lens (key, _String)
@@ -18,6 +18,7 @@ import Web.Job.WebhookDeliveryJob (dispatchWebhooks)
import Control.Concurrent (forkIO) import Control.Concurrent (forkIO)
import Control.Monad (void) import Control.Monad (void)
import Data.Coerce (coerce) import Data.Coerce (coerce)
import Data.Scientific (Scientific)
import Data.Aeson ((.=), object) import Data.Aeson ((.=), object)
import Data.Text.Encoding (encodeUtf8) import Data.Text.Encoding (encodeUtf8)
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
@@ -95,7 +96,7 @@ instance Controller RequirementCandidatesController where
|> set #createdBy (fmap coerce createdBy) |> set #createdBy (fmap coerce createdBy)
|> validateField #title nonEmpty |> validateField #title nonEmpty
|> validateField #description nonEmpty |> validateField #description nonEmpty
|> validateField #category (`elem` validCategories) |> validateField #category (isInList validCategories)
|> ifValid \case |> ifValid \case
Left candidate -> render NewView { candidate, widgets, threads } Left candidate -> render NewView { candidate, widgets, threads }
Right candidate -> do Right candidate -> do
@@ -127,7 +128,7 @@ instance Controller RequirementCandidatesController where
|> fill @'["title", "description", "sourceWidgetId", "sourceThreadId", "category"] |> fill @'["title", "description", "sourceWidgetId", "sourceThreadId", "category"]
|> validateField #title nonEmpty |> validateField #title nonEmpty
|> validateField #description nonEmpty |> validateField #description nonEmpty
|> validateField #category (`elem` validCategories) |> validateField #category (isInList validCategories)
|> ifValid \case |> ifValid \case
Left candidate -> render EditView { candidate, widgets, threads } Left candidate -> render EditView { candidate, widgets, threads }
Right candidate -> do Right candidate -> do
@@ -260,10 +261,8 @@ instance Controller RequirementCandidatesController where
action DetectDuplicatesAction { requirementCandidateId } = do action DetectDuplicatesAction { requirementCandidateId } = do
target <- fetch requirementCandidateId target <- fetch requirementCandidateId
others <- query @RequirementCandidate |> fetch others <- query @RequirementCandidate |> fetch
-- Resolve hub from the source widget -- Resolve hub from the source widget (sourceWidgetId is non-nullable)
mHubId <- case target.sourceWidgetId of mHubId <- fmap (.hubId) <$> fetchOneOrNothing target.sourceWidgetId
Nothing -> pure Nothing
Just wid -> fmap (.hubId) <$> fetchOneOrNothing @Widget wid
let otherLines = map (\c -> show c.id <> " | " <> c.title <> ": " <> c.description) let otherLines = map (\c -> show c.id <> " | " <> c.title <> ": " <> c.description)
(filter (\c -> c.id /= requirementCandidateId) others) (filter (\c -> c.id /= requirementCandidateId) others)
targetLine = "TARGET: " <> target.title <> ": " <> target.description targetLine = "TARGET: " <> target.title <> ": " <> target.description
@@ -315,13 +314,9 @@ instance Controller RequirementCandidatesController where
-- T06: Detect policy sensitivity via routed agent -- T06: Detect policy sensitivity via routed agent
action DetectPolicySensitivityAction { requirementCandidateId } = do action DetectPolicySensitivityAction { requirementCandidateId } = do
candidate <- fetch requirementCandidateId candidate <- fetch requirementCandidateId
mWidget <- case candidate.sourceWidgetId of -- sourceWidgetId is non-nullable; fetchOneOrNothing handles missing widget
Nothing -> pure Nothing mWidget <- fetchOneOrNothing candidate.sourceWidgetId
Just wid -> fetchOneOrNothing wid mHubId <- fmap (.hubId) <$> fetchOneOrNothing candidate.sourceWidgetId
-- Resolve hub for routing
mHubId <- case candidate.sourceWidgetId of
Nothing -> pure Nothing
Just wid -> fmap (.hubId) <$> fetchOneOrNothing @Widget wid
let policyCtx = maybe "unknown" (.policyScope) mWidget let policyCtx = maybe "unknown" (.policyScope) mWidget
userMsg = "Title: " <> candidate.title userMsg = "Title: " <> candidate.title
<> "\nDescription: " <> candidate.description <> "\nDescription: " <> candidate.description
@@ -358,7 +353,7 @@ instance Controller RequirementCandidatesController where
setErrorMessage ("Policy check failed: " <> bridgeErrorMessage err) setErrorMessage ("Policy check failed: " <> bridgeErrorMessage err)
redirectTo ShowRequirementCandidateAction { requirementCandidateId } redirectTo ShowRequirementCandidateAction { requirementCandidateId }
Right resp -> do Right resp -> do
let confidenceScore = extractSeverityScore resp.content let confidenceScore = realToFrac (extractSeverityScore resp.content) :: Scientific
proposal <- newRecord @AgentProposal proposal <- newRecord @AgentProposal
|> set #proposalType "policy_flag" |> set #proposalType "policy_flag"
|> set #sourceCandidateId (Just requirementCandidateId) |> set #sourceCandidateId (Just requirementCandidateId)
@@ -379,10 +374,10 @@ instance Controller RequirementCandidatesController where
case (concern ^? key "scope" . _String case (concern ^? key "scope" . _String
,concern ^? key "note" . _String) of ,concern ^? key "note" . _String) of
(Just scope, noteM) -> (Just scope, noteM) ->
newRecord @ConfidenceAnnotation void $ newRecord @ConfidenceAnnotation
|> set #proposalId proposal.id |> set #proposalId proposal.id
|> set #dimension scope |> set #dimension scope
|> set #score confidenceScore |> set #score (confidenceScore :: Scientific)
|> set #explanation noteM |> set #explanation noteM
|> createRecord |> createRecord
_ -> pure () _ -> pure ()

View File

@@ -11,7 +11,7 @@ import IHP.ControllerPrelude
import Data.Aeson (toJSON, object, (.=)) import Data.Aeson (toJSON, object, (.=))
import Application.Helper.Controller (isInRegression, widgetCycleCounts) import Application.Helper.Controller (isInRegression, widgetCycleCounts)
import Application.Helper.TypeRegistry (validateWidgetType, validatePolicyScope, activeWidgetTypes, activePolicyScopes) import Application.Helper.TypeRegistry (validateWidgetType, validatePolicyScope, activeWidgetTypes, activePolicyScopes)
import Application.Helper.AgentBridge (callAgent, checkGovernancePolicy, bridgeErrorMessage, BridgeResponse) import Application.Helper.AgentBridge (callAgent, checkGovernancePolicy, bridgeErrorMessage, BridgeResponse(..))
import Application.Helper.ModelRouter (resolveAgent) import Application.Helper.ModelRouter (resolveAgent)
instance Controller WidgetsController where instance Controller WidgetsController where

View File

@@ -2,11 +2,11 @@ module Web.FrontController where
import IHP.RouterPrelude import IHP.RouterPrelude
import IHP.LoginSupport.Middleware import IHP.LoginSupport.Middleware
import IHP.ControllerPrelude (getAppConfig) import IHP.ControllerPrelude
import IHP.ViewPrelude (Html, hsx, Layout, autoRefreshMeta)
import Generated.Types import Generated.Types
import Web.Types import Web.Types
import Web.Routes () import Web.Routes ()
import Config (AnnotationLauncherEnabled (..))
-- Controllers -- Controllers
import Web.Controller.Hubs () import Web.Controller.Hubs ()
@@ -146,14 +146,7 @@ instance InitControllerContext WebApplication where
setLayout defaultLayout setLayout defaultLayout
initAuthentication @User initAuthentication @User
annotationLauncherScript :: (?context :: ControllerContext) => Html defaultLayout :: (?context :: ControllerContext, ?request :: Request) => Layout
annotationLauncherScript =
let AnnotationLauncherEnabled enabled = getAppConfig @AnnotationLauncherEnabled
in if enabled
then [hsx|<script src="/js/ihf-annotation-launcher.js"></script>|]
else mempty
defaultLayout :: Layout
defaultLayout inner = [hsx| defaultLayout inner = [hsx|
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
@@ -165,7 +158,7 @@ defaultLayout inner = [hsx|
<link rel="stylesheet" href="/app.css" /> <link rel="stylesheet" href="/app.css" />
<script src="/vendor/morphdom.js"></script> <script src="/vendor/morphdom.js"></script>
<script src="/vendor/ihp-auto-refresh.js"></script> <script src="/vendor/ihp-auto-refresh.js"></script>
{annotationLauncherScript} <script src="/js/ihf-annotation-launcher.js"></script>
</head> </head>
<body class="bg-gray-50 text-gray-900"> <body class="bg-gray-50 text-gray-900">
<nav class="bg-white border-b border-gray-200 px-6 py-3 flex items-center gap-6"> <nav class="bg-white border-b border-gray-200 px-6 py-3 flex items-center gap-6">

View File

@@ -15,7 +15,9 @@ data EditView = EditView
} }
instance View EditView where instance View EditView where
html EditView { .. } = [hsx| html EditView { .. } =
let formAction = UpdateDecisionRecordAction { decisionRecordId = record.id }
in [hsx|
<div class="mb-6 flex items-center gap-2 text-sm text-gray-500"> <div class="mb-6 flex items-center gap-2 text-sm text-gray-500">
<a href={DecisionRecordsAction} class="hover:text-gray-700">Decisions</a> <a href={DecisionRecordsAction} class="hover:text-gray-700">Decisions</a>
<span>/</span> <span>/</span>
@@ -30,6 +32,6 @@ instance View EditView where
<p class="text-sm text-amber-600 mb-6"> <p class="text-sm text-amber-600 mb-6">
Note: outcome is immutable and cannot be changed here. Note: outcome is immutable and cannot be changed here.
</p> </p>
{renderForm record requirements candidates users (UpdateDecisionRecordAction { decisionRecordId = record.id })} {renderForm record requirements candidates users formAction}
</div> </div>
|] |]