feat(P7): IHF Phase 7 complete — advanced observability and operational integration
Some checks failed
Test / test (push) Has been cancelled

T01 schema: friction_scores, bottleneck_records, hub_health_snapshots,
cross_hub_propagations + migration 1743552000.

T02 Widget Pain Heatmap: computeFrictionScore (formula documented), RecomputeFriction
action, colour-coded grid view (green/yellow/amber/red).

T03 Workflow Bottleneck Analysis: detectBottlenecks across 4 pipeline stages
(candidate 30d, requirement 60d, decision 30d, observation 14d), idempotent,
severity from age ratio, resolve action.

T04 Hub Health Correlation: computeHubHealth (deduction table documented),
append-only HubHealthSnapshot, health history view, badge on hub Show page.

T05 Cross-Hub Propagation: annotation_cluster + widget_type_friction heuristics,
idempotent detection, acknowledge/resolve lifecycle.

T06 Operational Review Board: 4-panel AutoRefresh global dashboard — health matrix,
top-10 friction, bottleneck stage counts, open propagations.

T07 gate: 5 describe blocks in Test/Integration.hs; SCOPE.md updated Phase 7
complete; docs/phase7-summary.md written.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-29 21:49:22 +00:00
parent c0b4b984b0
commit 98fb159582
22 changed files with 1638 additions and 262 deletions

View File

@@ -0,0 +1,37 @@
module Web.Controller.CrossHubPropagations where
import Web.Types
import Web.View.CrossHubPropagations.Index
import Generated.Types
import IHP.Prelude
import IHP.ControllerPrelude
import Application.Helper.CrossHubPropagation (detectPropagations)
instance Controller CrossHubPropagationsController where
beforeAction = ensureIsUser
action CrossHubPropagationsAction = autoRefresh do
propagations <- query @CrossHubPropagation
|> orderByDesc #detectedAt
|> fetch
hubs <- query @Hub |> fetch
render IndexView { propagations, hubs }
action DetectPropagationsAction = do
hubs <- query @Hub |> fetch
widgets <- query @Widget |> fetch
annotations <- query @Annotation |> fetch
frictionScores <- query @FrictionScore |> fetch
_ <- detectPropagations hubs annotations widgets frictionScores
setSuccessMessage "Propagation detection complete"
redirectTo CrossHubPropagationsAction
action AcknowledgePropagationAction { crossHubPropagationId } = do
p <- fetch crossHubPropagationId
p |> set #status "acknowledged" |> updateRecord
redirectTo CrossHubPropagationsAction
action ResolvePropagationAction { crossHubPropagationId } = do
p <- fetch crossHubPropagationId
p |> set #status "resolved" |> updateRecord
redirectTo CrossHubPropagationsAction

View File

@@ -10,10 +10,17 @@ import Web.View.Hubs.GovernanceDashboard
import Web.View.Hubs.AntifragilityDashboard
import Web.View.Hubs.AgentAuditDashboard
import Web.View.Hubs.AdapterCompatibilityDashboard
import Web.View.Hubs.FrictionHeatmap
import Web.View.Hubs.BottleneckDashboard
import Web.View.Hubs.HubHealthHistory
import Web.View.Hubs.OperationalReviewBoard
import Generated.Types
import IHP.Prelude
import IHP.ControllerPrelude
import Application.Helper.Controller (regressedWidgetIds, widgetCycleCounts)
import Application.Helper.FrictionScore (computeFrictionScore)
import Application.Helper.BottleneckDetector (detectBottlenecks)
import Application.Helper.HubHealth (computeHubHealth)
instance Controller HubsController where
beforeAction = ensureIsUser
@@ -237,3 +244,117 @@ instance Controller HubsController where
envelopes <- query @EnvelopeEmissionContract |> filterWhere (#status, "active") |> fetch
reportings <- query @InteractionReportingContract |> filterWhere (#status, "active") |> fetch
render AdapterCompatibilityDashboardView { hub, specs, widgets, envelopes, reportings }
action FrictionHeatmapAction { hubId } = autoRefresh do
hub <- fetch hubId
widgets <- query @Widget |> filterWhere (#hubId, hubId) |> fetch
let widgetIds = map (.id) widgets
frictionScores <- query @FrictionScore
|> filterWhereIn (#widgetId, widgetIds)
|> fetch
render FrictionHeatmapView { hub, widgets, frictionScores }
action RecomputeFrictionAction { hubId } = do
hub <- fetch hubId
widgets <- query @Widget |> filterWhere (#hubId, hubId) |> fetch
let widgetIds = map (.id) widgets
annotations <- query @Annotation |> filterWhereIn (#widgetId, widgetIds) |> fetch
events <- sqlQuery "SELECT * FROM interaction_events WHERE widget_id = ANY(?)"
(Only (PGArray widgetIds))
signals <- query @OutcomeSignal |> filterWhereIn (#widgetId, widgetIds) |> fetch
candidates <- query @RequirementCandidate |> filterWhereIn (#sourceWidgetId, widgetIds) |> fetch
let regressionWids = regressedWidgetIds signals annotations
mapM_ (\w ->
let wAnnotations = filter (\a -> a.widgetId == w.id) annotations
wEvents = filter (\e -> e.widgetId == w.id) events
wCandidates = filter (\c -> c.sourceWidgetId == w.id) candidates
isRegressed = w.id `elem` regressionWids
in computeFrictionScore w.id wAnnotations wEvents isRegressed wCandidates
) widgets
setSuccessMessage "Friction scores recomputed"
redirectTo FrictionHeatmapAction { hubId }
action BottleneckDashboardAction { hubId } = autoRefresh do
hub <- fetch hubId
widgets <- query @Widget |> filterWhere (#hubId, hubId) |> fetch
bottlenecks <- query @BottleneckRecord
|> filterWhere (#hubId, hubId)
|> orderByAsc #stalledSince
|> fetch
render BottleneckDashboardView { hub, widgets, bottlenecks }
action DetectBottlenecksAction { hubId } = do
hub <- fetch hubId
widgets <- query @Widget |> filterWhere (#hubId, hubId) |> fetch
let widgetIds = map (.id) widgets
candidates <- query @RequirementCandidate |> filterWhereIn (#sourceWidgetId, widgetIds) |> fetch
let candidateIds = map (.id) candidates
acceptedIds = map (.id) (filter (\c -> c.status == "accepted") candidates)
requirements <- query @Requirement |> filterWhereIn (#sourceCandidateId, acceptedIds) |> fetch
let reqIds = map (.id) requirements
decisions <- query @DecisionRecord
|> filterWhereIn (#requirementId, map Just reqIds)
|> fetch
let decisionIds = map (.id) decisions
deployments <- query @DeploymentRecord |> filterWhereIn (#decisionId, decisionIds) |> fetch
_ <- detectBottlenecks hubId widgets candidates requirements decisions deployments
setSuccessMessage "Bottleneck detection complete"
redirectTo BottleneckDashboardAction { hubId }
action ResolveBottleneckAction { bottleneckRecordId } = do
bottleneck <- fetch bottleneckRecordId
now <- getCurrentTime
bottleneck |> set #resolvedAt (Just now) |> updateRecord
setSuccessMessage "Bottleneck resolved"
redirectTo BottleneckDashboardAction { hubId = bottleneck.hubId }
action SnapshotHubHealthAction { hubId } = do
hub <- fetch hubId
widgets <- query @Widget |> filterWhere (#hubId, hubId) |> fetch
let widgetIds = map (.id) widgets
candidates <- query @RequirementCandidate |> filterWhereIn (#sourceWidgetId, widgetIds) |> fetch
let acceptedIds = map (.id) (filter (\c -> c.status == "accepted") candidates)
requirements <- query @Requirement |> filterWhereIn (#sourceCandidateId, acceptedIds) |> fetch
let reqIds = map (.id) requirements
decisions <- query @DecisionRecord |> filterWhereIn (#requirementId, map Just reqIds) |> fetch
let decisionIds = map (.id) decisions
deployments <- query @DeploymentRecord |> filterWhereIn (#decisionId, decisionIds) |> fetch
signals <- query @OutcomeSignal |> filterWhereIn (#widgetId, widgetIds) |> fetch
annotations <- query @Annotation |> filterWhereIn (#widgetId, widgetIds) |> fetch
bottlenecks <- query @BottleneckRecord
|> filterWhere (#hubId, hubId)
|> filterWhereSql (#resolvedAt, "IS NULL")
|> fetch
_ <- computeHubHealth hubId widgets candidates decisions deployments signals annotations bottlenecks
setSuccessMessage "Hub health snapshot taken"
redirectTo HubHealthHistoryAction { hubId }
action HubHealthHistoryAction { hubId } = autoRefresh do
hub <- fetch hubId
snapshots <- query @HubHealthSnapshot
|> filterWhere (#hubId, hubId)
|> orderByDesc #computedAt
|> fetch
render HubHealthHistoryView { hub, snapshots }
action OperationalReviewBoardAction = autoRefresh do
hubs <- query @Hub |> orderByAsc #name |> fetch
allSnapshots <- query @HubHealthSnapshot |> orderByDesc #computedAt |> fetch
topFrictionScores <- query @FrictionScore |> orderByDesc #score |> limit 10 |> fetch
topWidgets <- mapM (\fs -> fetch fs.widgetId) topFrictionScores
bottlenecks <- query @BottleneckRecord
|> filterWhereSql (#resolvedAt, "IS NULL")
|> orderByAsc #stage
|> fetch
propagations <- query @CrossHubPropagation
|> orderByDesc #detectedAt
|> fetch
let openPropagations = filter (\p -> p.status `elem` ["open","acknowledged"]) propagations
render OperationalReviewBoardView
{ hubs
, allSnapshots
, topFrictionScores
, topWidgets
, bottlenecks
, openPropagations
}