generated from coulomb/repo-seed
feat(WP-0009): IHF GAAF Compliance Foundation — type registries, extension manifests, architectural contracts
Some checks failed
Test / test (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
Implements IHUB-WP-0009: closes four GAAF-2026 gaps before domain hub work begins. - TypeRegistry helper + controllers/views (hub_kind, hub_capability_manifest) - HubCapabilityManifest entity with validation and registry linkage - ARCHITECTURE-LAYERS.md + CI-enforced boundary contracts - Alembic migration 1743724800, fitness tests (Test/Architecture/) - GAAF spec, Operational Architecture spec, domain hub extension guide - Updates to CLAUDE.md, SCOPE.md, Schema.sql, Routes, FrontController, Types state_hub_sync: pending (tunnel was STALE at completion time; run fix-consistency) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
130
Web/View/HubCapabilityManifests/Edit.hs
Normal file
130
Web/View/HubCapabilityManifests/Edit.hs
Normal file
@@ -0,0 +1,130 @@
|
||||
module Web.View.HubCapabilityManifests.Edit where
|
||||
|
||||
import Web.Types
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ViewPrelude
|
||||
import Data.Aeson (Value(..), encode, decode)
|
||||
import qualified Data.Vector as V
|
||||
import qualified Data.ByteString.Lazy.Char8 as BL
|
||||
|
||||
data EditView = EditView
|
||||
{ manifest :: !HubCapabilityManifest
|
||||
, hub :: !Hub
|
||||
, widgetTypeEntries :: ![WidgetTypeRegistry]
|
||||
, eventTypeEntries :: ![EventTypeRegistry]
|
||||
, categoryEntries :: ![AnnotationCategoryRegistry]
|
||||
, policyScopeEntries :: ![PolicyScopeRegistry]
|
||||
}
|
||||
|
||||
instance View EditView where
|
||||
html EditView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={ShowHubCapabilityManifestAction { hubCapabilityManifestId = manifest.id }}
|
||||
class="text-sm text-gray-500 hover:text-gray-700">
|
||||
← {hub.name} Manifest
|
||||
</a>
|
||||
</div>
|
||||
<h1 class="text-xl font-semibold mb-2">Edit Capability Manifest — {hub.name}</h1>
|
||||
<p class="text-sm text-gray-500 mb-6">
|
||||
Declare the type names this hub owns. After saving, activate the manifest to register them.
|
||||
</p>
|
||||
|
||||
{if manifest.status /= "draft"
|
||||
then [hsx|
|
||||
<div class="mb-6 bg-amber-50 border border-amber-200 rounded p-4 text-sm text-amber-800">
|
||||
This manifest is <strong>{manifest.status}</strong> and is read-only.
|
||||
Retire it first to create a new draft amendment.
|
||||
</div>
|
||||
|]
|
||||
else [hsx||]}
|
||||
|
||||
<form method="POST" action={UpdateHubCapabilityManifestAction { hubCapabilityManifestId = manifest.id }}>
|
||||
<div class="space-y-6 max-w-2xl">
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-5 space-y-4">
|
||||
<h2 class="text-sm font-semibold text-gray-700">Manifest Details</h2>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Capability Description</label>
|
||||
{(textareaField #capabilityDescription) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Contact</label>
|
||||
{(textField #contact) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{typeArraySection "Declared Widget Types" "declaredWidgetTypes" manifest.declaredWidgetTypes widgetTypeEntries}
|
||||
{typeArraySection "Declared Event Types" "declaredEventTypes" manifest.declaredEventTypes eventTypeEntries}
|
||||
{typeArraySection2 "Declared Annotation Categories" "declaredAnnotationCategories" manifest.declaredAnnotationCategories categoryEntries}
|
||||
{typeArraySection3 "Declared Policy Scopes" "declaredPolicyScopes" manifest.declaredPolicyScopes policyScopeEntries}
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button type="submit"
|
||||
class="bg-indigo-600 text-white text-sm px-4 py-2 rounded hover:bg-indigo-700"
|
||||
{if manifest.status /= "draft" then ("disabled" :: Text) else ""}>
|
||||
Save
|
||||
</button>
|
||||
{if manifest.status == "draft" then [hsx|
|
||||
<a href={ActivateManifestAction { hubCapabilityManifestId = manifest.id }}
|
||||
class="text-sm bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700">
|
||||
Save & Activate
|
||||
</a>
|
||||
|] else [hsx||]}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|]
|
||||
|
||||
-- | Render a JSON array text area with available registry options shown below.
|
||||
typeArraySection :: Text -> Text -> Value -> [WidgetTypeRegistry] -> Html
|
||||
typeArraySection title fieldName val entries = [hsx|
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-5">
|
||||
<h2 class="text-sm font-semibold text-gray-700 mb-1">{title}</h2>
|
||||
<p class="text-xs text-gray-500 mb-2">
|
||||
JSON array of type names to declare ownership of.
|
||||
Names that don't yet exist in the registry will be created on activation.
|
||||
</p>
|
||||
<textarea name={fieldName}
|
||||
class="w-full border border-gray-300 rounded px-3 py-2 text-sm font-mono"
|
||||
rows="3">{valueText val}</textarea>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
Registered: {intercalate ", " (map (.name) entries)}
|
||||
</p>
|
||||
</div>
|
||||
|]
|
||||
|
||||
typeArraySection2 :: Text -> Text -> Value -> [AnnotationCategoryRegistry] -> Html
|
||||
typeArraySection2 title fieldName val entries = [hsx|
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-5">
|
||||
<h2 class="text-sm font-semibold text-gray-700 mb-1">{title}</h2>
|
||||
<p class="text-xs text-gray-500 mb-2">JSON array of annotation category names.</p>
|
||||
<textarea name={fieldName}
|
||||
class="w-full border border-gray-300 rounded px-3 py-2 text-sm font-mono"
|
||||
rows="3">{valueText val}</textarea>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
Registered: {intercalate ", " (map (.name) entries)}
|
||||
</p>
|
||||
</div>
|
||||
|]
|
||||
|
||||
typeArraySection3 :: Text -> Text -> Value -> [PolicyScopeRegistry] -> Html
|
||||
typeArraySection3 title fieldName val entries = [hsx|
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-5">
|
||||
<h2 class="text-sm font-semibold text-gray-700 mb-1">{title}</h2>
|
||||
<p class="text-xs text-gray-500 mb-2">JSON array of policy scope names.</p>
|
||||
<textarea name={fieldName}
|
||||
class="w-full border border-gray-300 rounded px-3 py-2 text-sm font-mono"
|
||||
rows="3">{valueText val}</textarea>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
Registered: {intercalate ", " (map (.name) entries)}
|
||||
</p>
|
||||
</div>
|
||||
|]
|
||||
|
||||
valueText :: Value -> Text
|
||||
valueText v = cs (BL.unpack (encode v))
|
||||
|
||||
intercalate :: Text -> [Text] -> Text
|
||||
intercalate _ [] = ""
|
||||
intercalate _ [x] = x
|
||||
intercalate sep (x:xs) = x <> sep <> intercalate sep xs
|
||||
77
Web/View/HubCapabilityManifests/Index.hs
Normal file
77
Web/View/HubCapabilityManifests/Index.hs
Normal file
@@ -0,0 +1,77 @@
|
||||
module Web.View.HubCapabilityManifests.Index where
|
||||
|
||||
import Web.Types
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ViewPrelude
|
||||
import Data.Aeson (Value(..))
|
||||
import qualified Data.Vector as V
|
||||
|
||||
data IndexView = IndexView
|
||||
{ manifests :: ![HubCapabilityManifest]
|
||||
, hubs :: ![Hub]
|
||||
}
|
||||
|
||||
instance View IndexView where
|
||||
html IndexView { .. } = [hsx|
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">Hub Capability Manifests</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">Extension registrations for domain and shared hubs</p>
|
||||
</div>
|
||||
<a href={NewHubCapabilityManifestAction}
|
||||
class="bg-indigo-600 text-white text-sm font-medium px-4 py-2 rounded hover:bg-indigo-700">
|
||||
New Manifest
|
||||
</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Hub</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Status</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Widget Types</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Event Types</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Categories</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Scopes</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Activated</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{forEach manifests (renderRow hubs)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|]
|
||||
|
||||
renderRow :: [Hub] -> HubCapabilityManifest -> Html
|
||||
renderRow hubs m = [hsx|
|
||||
<tr class="border-b border-gray-100 hover:bg-gray-50">
|
||||
<td class="px-4 py-3 font-medium">{hubName hubs m.hubId}</td>
|
||||
<td class="px-4 py-3">{statusBadge m.status}</td>
|
||||
<td class="px-4 py-3 text-gray-500 text-xs">{jsonCount m.declaredWidgetTypes}</td>
|
||||
<td class="px-4 py-3 text-gray-500 text-xs">{jsonCount m.declaredEventTypes}</td>
|
||||
<td class="px-4 py-3 text-gray-500 text-xs">{jsonCount m.declaredAnnotationCategories}</td>
|
||||
<td class="px-4 py-3 text-gray-500 text-xs">{jsonCount m.declaredPolicyScopes}</td>
|
||||
<td class="px-4 py-3 text-gray-400 text-xs">{maybe "—" show m.activatedAt}</td>
|
||||
<td class="px-4 py-3 text-right text-xs">
|
||||
<a href={ShowHubCapabilityManifestAction { hubCapabilityManifestId = m.id }}
|
||||
class="text-indigo-600 hover:text-indigo-800">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
|]
|
||||
|
||||
hubName :: [Hub] -> Id Hub -> Text
|
||||
hubName hubs i = maybe "Unknown" (.name) (find (\h -> h.id == i) hubs)
|
||||
|
||||
statusBadge :: Text -> Html
|
||||
statusBadge "active" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800">active</span>|]
|
||||
statusBadge "draft" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800">draft</span>|]
|
||||
statusBadge "retired" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-500">retired</span>|]
|
||||
statusBadge s = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600">{s}</span>|]
|
||||
|
||||
jsonCount :: Value -> Text
|
||||
jsonCount (Array v) | V.null v = "0"
|
||||
| otherwise = tshow (V.length v)
|
||||
jsonCount _ = "0"
|
||||
55
Web/View/HubCapabilityManifests/New.hs
Normal file
55
Web/View/HubCapabilityManifests/New.hs
Normal file
@@ -0,0 +1,55 @@
|
||||
module Web.View.HubCapabilityManifests.New where
|
||||
|
||||
import Web.Types
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ViewPrelude
|
||||
|
||||
data NewView = NewView
|
||||
{ manifest :: !HubCapabilityManifest
|
||||
, hubs :: ![Hub]
|
||||
}
|
||||
|
||||
instance View NewView where
|
||||
html NewView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={HubCapabilityManifestsAction} class="text-sm text-gray-500 hover:text-gray-700">
|
||||
← Capability Manifests
|
||||
</a>
|
||||
</div>
|
||||
<h1 class="text-xl font-semibold mb-6">New Capability Manifest</h1>
|
||||
<div class="bg-amber-50 border border-amber-200 rounded p-4 mb-6 text-sm text-amber-800">
|
||||
A capability manifest lets a domain or shared hub declare the widget types, event types,
|
||||
annotation categories, and policy scopes it owns. Create a draft, declare your types,
|
||||
then activate to register them with the framework.
|
||||
</div>
|
||||
<form method="POST" action={CreateHubCapabilityManifestAction}>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Hub</label>
|
||||
{selectField #hubId (hubOptions hubs)}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Capability Description <span class="text-gray-400 text-xs">(optional)</span>
|
||||
</label>
|
||||
{(textareaField #capabilityDescription) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Contact <span class="text-gray-400 text-xs">(team or person)</span>
|
||||
</label>
|
||||
{(textField #contact) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
<div class="pt-2">
|
||||
<button type="submit"
|
||||
class="bg-indigo-600 text-white text-sm px-4 py-2 rounded hover:bg-indigo-700">
|
||||
Create Draft
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|]
|
||||
|
||||
hubOptions :: [Hub] -> [(Text, Id Hub)]
|
||||
hubOptions hubs = map (\h -> (h.name <> " (" <> h.hubKind <> ")", h.id)) hubs
|
||||
116
Web/View/HubCapabilityManifests/Show.hs
Normal file
116
Web/View/HubCapabilityManifests/Show.hs
Normal file
@@ -0,0 +1,116 @@
|
||||
module Web.View.HubCapabilityManifests.Show where
|
||||
|
||||
import Web.Types
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ViewPrelude
|
||||
import Data.Aeson (Value(..), encode)
|
||||
import qualified Data.Vector as V
|
||||
import qualified Data.ByteString.Lazy.Char8 as BL
|
||||
|
||||
data ShowView = ShowView
|
||||
{ manifest :: !HubCapabilityManifest
|
||||
, hub :: !Hub
|
||||
}
|
||||
|
||||
instance View ShowView where
|
||||
html ShowView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={HubCapabilityManifestsAction} class="text-sm text-gray-500 hover:text-gray-700">
|
||||
← Capability Manifests
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<h1 class="text-2xl font-semibold">{hub.name} — Capability Manifest</h1>
|
||||
{statusBadge manifest.status}
|
||||
</div>
|
||||
|
||||
{if manifest.status == "draft"
|
||||
then [hsx|
|
||||
<div class="mb-4 flex gap-2">
|
||||
<a href={EditHubCapabilityManifestAction { hubCapabilityManifestId = manifest.id }}
|
||||
class="text-sm border border-indigo-300 text-indigo-700 px-3 py-1.5 rounded hover:bg-indigo-50">
|
||||
Edit Draft
|
||||
</a>
|
||||
<a href={ActivateManifestAction { hubCapabilityManifestId = manifest.id }}
|
||||
class="text-sm bg-green-600 text-white px-3 py-1.5 rounded hover:bg-green-700">
|
||||
Activate
|
||||
</a>
|
||||
</div>
|
||||
|]
|
||||
else if manifest.status == "active"
|
||||
then [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={RetireManifestAction { hubCapabilityManifestId = manifest.id }}
|
||||
data-confirm="Retire this manifest? The hub's types will remain registered."
|
||||
class="text-sm border border-gray-300 text-gray-600 px-3 py-1.5 rounded hover:bg-gray-50">
|
||||
Retire
|
||||
</a>
|
||||
</div>
|
||||
|]
|
||||
else [hsx||]}
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wide">Manifest Version</p>
|
||||
<p class="font-mono font-medium mt-1">{manifest.manifestVersion}</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wide">Activated</p>
|
||||
<p class="font-medium mt-1">{maybe "—" show manifest.activatedAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{forEach (maybeText manifest.capabilityDescription) (\d -> [hsx|
|
||||
<p class="text-sm text-gray-600 mb-4">{d}</p>
|
||||
|])}
|
||||
{forEach (maybeText manifest.contact) (\c -> [hsx|
|
||||
<p class="text-xs text-gray-400 mb-6">Contact: {c}</p>
|
||||
|])}
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{jsonArraySection "Declared Widget Types" manifest.declaredWidgetTypes}
|
||||
{jsonArraySection "Declared Event Types" manifest.declaredEventTypes}
|
||||
{jsonArraySection "Declared Annotation Categories" manifest.declaredAnnotationCategories}
|
||||
{jsonArraySection "Declared Policy Scopes" manifest.declaredPolicyScopes}
|
||||
</div>
|
||||
|]
|
||||
|
||||
jsonArraySection :: Text -> Value -> Html
|
||||
jsonArraySection title val = [hsx|
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">{title}
|
||||
<span class="text-gray-400 font-normal ml-1">({arrayLen val})</span>
|
||||
</h3>
|
||||
{renderArrayItems val}
|
||||
</div>
|
||||
|]
|
||||
|
||||
renderArrayItems :: Value -> Html
|
||||
renderArrayItems (Array v) | V.null v =
|
||||
[hsx|<p class="text-xs text-gray-400">None declared</p>|]
|
||||
renderArrayItems (Array v) = [hsx|
|
||||
<ul class="space-y-1">
|
||||
{forEach (V.toList v) renderItem}
|
||||
</ul>
|
||||
|]
|
||||
renderArrayItems _ = [hsx|<p class="text-xs text-gray-400">—</p>|]
|
||||
|
||||
renderItem :: Value -> Html
|
||||
renderItem (String t) = [hsx|<li class="font-mono text-xs text-gray-700">{t}</li>|]
|
||||
renderItem v = [hsx|<li class="font-mono text-xs text-gray-500">{cs (BL.unpack (encode v)) :: Text}</li>|]
|
||||
|
||||
arrayLen :: Value -> Text
|
||||
arrayLen (Array v) = tshow (V.length v)
|
||||
arrayLen _ = "0"
|
||||
|
||||
statusBadge :: Text -> Html
|
||||
statusBadge "active" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800">active</span>|]
|
||||
statusBadge "draft" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800">draft</span>|]
|
||||
statusBadge "retired" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-500">retired</span>|]
|
||||
statusBadge s = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600">{s}</span>|]
|
||||
|
||||
maybeText :: Maybe Text -> [Text]
|
||||
maybeText Nothing = []
|
||||
maybeText (Just t) = [t]
|
||||
Reference in New Issue
Block a user