Files
inter-hub/Web/Controller/HubCapabilityManifests.hs
tegwick 2106000cc7
Some checks failed
Test / test (push) Has been cancelled
fix: resolve all GHC 9.10.3 / IHP 1.5 compile errors (all 616 modules load)
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>
2026-04-29 10:46:50 +02:00

169 lines
8.8 KiB
Haskell

module Web.Controller.HubCapabilityManifests where
import Web.Types
import Web.View.HubCapabilityManifests.Index
import Web.View.HubCapabilityManifests.Show
import Web.View.HubCapabilityManifests.New
import Web.View.HubCapabilityManifests.Edit
import Generated.Types
import IHP.Prelude
import IHP.ControllerPrelude
import Data.Aeson (Value, Array, decode, encode, toJSON)
import qualified Data.Vector as V
import Data.Maybe (mapMaybe)
import Control.Monad (void)
instance Controller HubCapabilityManifestsController where
beforeAction = ensureIsUser
action HubCapabilityManifestsAction = autoRefresh do
manifests <- query @HubCapabilityManifest
|> orderByDesc #createdAt
|> fetch
hubs <- query @Hub |> fetch
render IndexView { manifests, hubs }
action ShowHubCapabilityManifestAction { hubCapabilityManifestId } = do
manifest <- fetch hubCapabilityManifestId
hub <- fetch manifest.hubId
render ShowView { manifest, hub }
action NewHubCapabilityManifestAction = do
let mHubId = paramOrNothing @(Id Hub) "hubId"
hubs <- query @Hub |> orderByAsc #name |> fetch
let manifest = newRecord @HubCapabilityManifest
case mHubId of
Just hubId -> do
-- Check if a manifest already exists for this hub
existing <- query @HubCapabilityManifest
|> filterWhere (#hubId, hubId)
|> fetchOneOrNothing
case existing of
Just m -> redirectTo EditHubCapabilityManifestAction { hubCapabilityManifestId = m.id }
Nothing -> render NewView { manifest = manifest |> set #hubId hubId, hubs }
Nothing -> render NewView { manifest, hubs }
action CreateHubCapabilityManifestAction = do
hubs <- query @Hub |> orderByAsc #name |> fetch
let manifest = newRecord @HubCapabilityManifest
manifest
|> fill @'["hubId", "manifestVersion", "capabilityDescription", "contact"]
|> set #status "draft"
|> validateField #hubId nonEmpty
|> ifValid \case
Left manifest -> render NewView { manifest, hubs }
Right manifest -> do
manifest <- createRecord manifest
setSuccessMessage "Capability manifest created (draft)"
redirectTo EditHubCapabilityManifestAction { hubCapabilityManifestId = manifest.id }
action EditHubCapabilityManifestAction { hubCapabilityManifestId } = do
manifest <- fetch hubCapabilityManifestId
hub <- fetch manifest.hubId
widgetTypeEntries <- sqlQuery "SELECT * FROM widget_type_registry WHERE status = 'active' ORDER BY label ASC" ()
eventTypeEntries <- sqlQuery "SELECT * FROM event_type_registry WHERE status = 'active' ORDER BY label ASC" ()
categoryEntries <- sqlQuery "SELECT * FROM annotation_category_registry WHERE status = 'active' ORDER BY label ASC" ()
policyScopeEntries <- sqlQuery "SELECT * FROM policy_scope_registry WHERE status = 'active' ORDER BY label ASC" ()
render EditView { manifest, hub, widgetTypeEntries, eventTypeEntries, categoryEntries, policyScopeEntries }
action UpdateHubCapabilityManifestAction { hubCapabilityManifestId } = do
manifest <- fetch hubCapabilityManifestId
hub <- fetch manifest.hubId
widgetTypeEntries <- sqlQuery "SELECT * FROM widget_type_registry WHERE status = 'active' ORDER BY label ASC" ()
eventTypeEntries <- sqlQuery "SELECT * FROM event_type_registry WHERE status = 'active' ORDER BY label ASC" ()
categoryEntries <- sqlQuery "SELECT * FROM annotation_category_registry WHERE status = 'active' ORDER BY label ASC" ()
policyScopeEntries <- sqlQuery "SELECT * FROM policy_scope_registry WHERE status = 'active' ORDER BY label ASC" ()
when (manifest.status == "active") do
setErrorMessage "Active manifests are read-only. Retire the current manifest and create a new draft to amend."
redirectTo ShowHubCapabilityManifestAction { hubCapabilityManifestId }
manifest
|> fill @'["manifestVersion", "capabilityDescription", "contact"]
|> set #declaredWidgetTypes (toJSON (paramList @Text "declaredWidgetTypes"))
|> set #declaredEventTypes (toJSON (paramList @Text "declaredEventTypes"))
|> set #declaredAnnotationCategories (toJSON (paramList @Text "declaredAnnotationCategories"))
|> set #declaredPolicyScopes (toJSON (paramList @Text "declaredPolicyScopes"))
|> ifValid \case
Left manifest -> render EditView { manifest, hub, widgetTypeEntries, eventTypeEntries, categoryEntries, policyScopeEntries }
Right manifest -> do
updateRecord manifest
setSuccessMessage "Manifest updated"
redirectTo EditHubCapabilityManifestAction { hubCapabilityManifestId }
action ActivateManifestAction { hubCapabilityManifestId } = do
manifest <- fetch hubCapabilityManifestId
hub <- fetch manifest.hubId
-- Collect declared type names from JSONB arrays
let wTypes = jsonArrayTexts manifest.declaredWidgetTypes
eTypes = jsonArrayTexts manifest.declaredEventTypes
cats = jsonArrayTexts manifest.declaredAnnotationCategories
scopes = jsonArrayTexts manifest.declaredPolicyScopes
-- Conflict detection: check that each declared name is either
-- unregistered or already owned by this hub.
conflicts <- fmap concat $ mapM (checkConflict "widget_type_registry" hub.id) wTypes
eConflicts <- fmap concat $ mapM (checkConflict "event_type_registry" hub.id) eTypes
cConflicts <- fmap concat $ mapM (checkConflict "annotation_category_registry" hub.id) cats
pConflicts <- fmap concat $ mapM (checkConflict "policy_scope_registry" hub.id) scopes
let allConflicts = conflicts <> eConflicts <> cConflicts <> pConflicts
if not (null allConflicts)
then do
setErrorMessage ("Activation blocked — type name conflicts: " <> intercalate ", " allConflicts)
redirectTo ShowHubCapabilityManifestAction { hubCapabilityManifestId }
else do
-- Register declared types (idempotent — skip if already present)
mapM_ (upsertType "widget_type_registry" hub.id) wTypes
mapM_ (upsertType "event_type_registry" hub.id) eTypes
mapM_ (upsertType "annotation_category_registry" hub.id) cats
mapM_ (upsertType "policy_scope_registry" hub.id) scopes
now <- getCurrentTime
manifest |> set #status "active" |> set #activatedAt (Just now) |> updateRecord
setSuccessMessage "Manifest activated — all declared types are now registered"
redirectTo ShowHubCapabilityManifestAction { hubCapabilityManifestId }
action RetireManifestAction { hubCapabilityManifestId } = do
manifest <- fetch hubCapabilityManifestId
manifest |> set #status "retired" |> updateRecord
setSuccessMessage "Manifest retired. Types remain in registry but this manifest is no longer current."
redirectTo HubCapabilityManifestsAction
-- | Extract text values from a JSONB array (e.g. '["foo","bar"]').
jsonArrayTexts :: Value -> [Text]
jsonArrayTexts val = case val of
_ -> case decode (encode val) of
Just (arr :: Array) -> mapMaybe extractText (V.toList arr)
Nothing -> []
where
extractText (String t) = Just t
extractText _ = Nothing
-- | Check if 'name' in 'tableName' is owned by a different hub.
-- Returns [] if no conflict, or [error message] if conflict.
checkConflict ::
(?modelContext :: ModelContext) =>
Text -> Id Hub -> Text -> IO [Text]
checkConflict tableName hubId name = do
rows <- sqlQuery
(fromString $ cs ("SELECT owner_hub_id FROM " <> tableName <> " WHERE name = ?"))
(Only name)
case rows of
[] -> pure []
[Only Nothing] -> pure [] -- framework-level, no owner conflict
[Only (Just ownerId)] ->
if ownerId == hubId
then pure []
else pure ["Type '" <> name <> "' in " <> tableName <> " is already owned by another hub"]
_ -> pure []
-- | Insert a type name into the registry table if it doesn't exist.
upsertType ::
(?modelContext :: ModelContext) =>
Text -> Id Hub -> Text -> IO ()
upsertType tableName hubId name =
void $ sqlExec
(fromString $ cs ("INSERT INTO " <> tableName <> " (name, label, owner_hub_id, status) "
<> "VALUES (?, ?, ?, 'active') ON CONFLICT (name) DO NOTHING"))
(name, name, hubId)