Files
inter-hub/Web/Controller/HubCapabilityManifests.hs
Bernd Worsch ce42607fca fix(WP-0014/A2): close remaining pure-param and structural compilation errors
Convert all remaining `<- paramOrNothing / param / paramOrDefault /
currentUserOrNothing` monadic binds to `let` — these functions are pure
(ImplicitParams-based) in IHP v1.5, so `<-` is a type error in an IO
do-block.

Controllers fixed:
  AgentDelegations, AiGovernancePolicies, Annotations, ApiConsumers,
  CollectiveProposals, DecisionRecords, DeploymentRecords,
  HubCapabilityManifests, HubRoutingRules, InstitutionalKnowledge,
  OutcomeCorrelations, RequirementCandidates, TypeRegistries,
  WebhookSubscriptions, Widgets,
  Api/V2/{Annotations,InteractionEvents,Token}

WebhookSubscriptions: remove orphaned `Right () ->` case arm that was
left inside a bare `unless` block (structural parse error).

Also carries forward all in-progress fixes from the working tree:
  helpers (AgentBridge, ApiRateLimit, BottleneckDetector,
            CrossHubPropagation, FrictionScore),
  views (CanSelect instances, HSX lambda extraction, formFor wrappers),
  env/build (envrc GHCi perms, flake.nix Tailwind + GHC resource limits,
             static/app.css additional Tailwind output).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 01:14:08 +00:00

170 lines
8.6 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)
import qualified Data.Vector as V
import Data.Maybe (mapMaybe)
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",
"declaredWidgetTypes", "declaredEventTypes",
"declaredAnnotationCategories", "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
("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 =
sqlExec
("INSERT INTO " <> tableName <> " (name, label, owner_hub_id, status) "
<> "VALUES (?, ?, ?, 'active') ON CONFLICT (name) DO NOTHING")
(name, name, hubId)
intercalate :: Text -> [Text] -> Text
intercalate _ [] = ""
intercalate _ [x] = x
intercalate sep (x:xs) = x <> sep <> intercalate sep xs