// stuck.go overlays cad-atlas#11's stuck-state badges onto the pipeline: a // count badge per stage (Node entries + Stage.StuckCount/AwaitingCount), // sourced from mathias/cad#14's `cad stuck` query rather than re-implemented // here. cad's app/domain packages are public Go modules (not internal/) — // this reuses the exact tested classification logic instead of duplicating // the taxonomy, per ADR-0002 ("a small coupling surface, not a monolith"). package atlas import ( "fmt" caddomain "git.d-ma.be/mathias/cad/domain" ) // stageForCategory maps a cad-stuck taxonomy row onto the existing 9-stage // pipeline (atlas.json), following the grouping cad-atlas#11 itself proposes: // Trigger→armed-but-never-fired sits at the human dispatch gate (the label // that authorizes dispatch is on, nothing has claimed it yet); Claim/Execute // collapse into the one Execute stage this pipeline already has (there is no // separate Claim stage); Close maps to the loop-back stage, where a human // closing the issue is the action that actually completes the loop. // // CategoryDoneNotClosed is deliberately absent from the "stuck" class — see // ApplyStuckOverlay, which reads Class rather than Category to decide the // visual treatment. It IS present here for stage placement. var stageForCategory = map[caddomain.StuckCategory]string{ caddomain.CategoryArmedNeverFired: "STAGE 04", caddomain.CategoryClaimedNeverRan: "STAGE 05", caddomain.CategoryCeilingHit: "STAGE 05", caddomain.CategoryReFireLoop: "STAGE 05", caddomain.CategoryNearAttemptCap: "STAGE 05", caddomain.CategoryDoneNotClosed: "STAGE 08", } // stuckNodeCls / awaitingNodeCls are the CSS classes the static shell colours // (cad-atlas.html: .node.stuck, .node.awaiting-human) — the two visual // classes cad-atlas#11 asks for, kept visually distinct so "awaiting human" // (a functioning control) never reads as "stuck" (needs intervention). const ( stuckNodeCls = "stuck" awaitingNodeCls = "awaiting-human" ) // ApplyStuckOverlay assigns each finding to the stage stageForCategory names, // and for every stage in stages marked `"generate":"stuck-badges"`, sets its // StuckCount/AwaitingCount and prepends one Node per finding assigned there // (title, class, repo tag, and a URL to the finding's first locator — this // overlay never adds a control, only a link out to Gitea). Findings whose // category has no stage mapping are silently omitted from the pipeline view // rather than misplaced — cad-atlas#11 is a read of the query, not a second // copy of its taxonomy, so an unmapped category is a gap to notice in // stageForCategory, not something to guess a stage for. // // stages is returned unmodified aside from the stuck-badges stages; this is a // pure function, no I/O, so it is fixture-testable without a live query. func ApplyStuckOverlay(stages []Stage, findings []caddomain.StuckFinding) []Stage { byStage := make(map[string][]caddomain.StuckFinding) for _, f := range findings { stageNo, ok := stageForCategory[f.Category] if !ok { continue } byStage[stageNo] = append(byStage[stageNo], f) } out := make([]Stage, len(stages)) copy(out, stages) for i := range out { if out[i].Generate != "stuck-badges" { continue } assigned := byStage[out[i].No] var stuckN, awaitingN int nodes := make([]Node, 0, len(assigned)) for _, f := range assigned { cls := stuckNodeCls if f.Class == caddomain.ClassAwaitingHuman { cls = awaitingNodeCls awaitingN++ } else { stuckN++ } var url string if len(f.Locators) > 0 { url = f.Locators[0] } nodes = append(nodes, Node{ Title: fmt.Sprintf("#%d %s", f.Issue, f.Category), Desc: f.Detail, Cls: cls, Tags: []string{"live · cad-stuck", f.Repo}, URL: url, }) } out[i].StuckCount = stuckN out[i].AwaitingCount = awaitingN out[i].Nodes = append(nodes, out[i].Nodes...) } return out }