diff --git a/site/pipeline.html b/site/pipeline.html index cbff7be..445587c 100644 --- a/site/pipeline.html +++ b/site/pipeline.html @@ -162,7 +162,10 @@ .fsm-state.rising .fsm-spark .sd, .fsm-state.lead .fsm-spark .sd { fill: var(--crit); } .fsm-rise { color: var(--crit); font-size: 12px; font-weight: 700; line-height: 1; } .fsm-state .sc { font-size: 23px; font-weight: 650; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; } - .fsm-state .sk { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--ink-2); margin-top: 1px; } + /* A box's label comes off the snapshot too (a descriptor's `label`/`key`), so it wraps + inside its box instead of overflowing the grid. */ + .fsm-state .sk { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--ink-2); margin-top: 1px; + overflow-wrap: anywhere; word-break: break-word; } .fsm-state .sa { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-3); margin-top: 3px; } .fsm-list { margin: 18px 0 4px; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; display: none; } .fsm-list.open { display: block; } @@ -179,6 +182,17 @@ --crit (defined in both themes) plus wording that says which — the colour is never the only carrier. Two classes deep so it outweighs `.fsm-list .empty`'s own colour. */ .fsm-list .empty.miswired { color: var(--crit); } + /* Conservation defect (rain-org-health#164): occupancy the snapshot carries that its own + descriptor list does not claim — inventory the machine renders NOWHERE, the exact shape + of the blocked-on drift that hid 17 PRs. Semantic --crit (defined in every theme scope) + plus wording that names what is unclaimed: the colour is never the only carrier. */ + .fsm-defect { border: 1.5px solid var(--crit); border-radius: 11px; padding: 12px 14px; + margin: 12px 0 4px; color: var(--crit); font-size: 13px; line-height: 1.5; } + .fsm-defect .fd-h { font-weight: 650; } + /* The band quotes UNTRUSTED snapshot strings (state keys, lane names, counts keys), so + one long unbroken token must wrap rather than push the panel sideways. */ + .fsm-defect .fd-li { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; margin-top: 4px; + overflow-wrap: anywhere; word-break: break-word; } .fsm-note { color: var(--ink-3); font-size: 12px; margin: 8px 2px 0; line-height: 1.5; } .fsm-note a, .fsm-note code { color: var(--ink-3); } .fsm-lane { margin: 14px 0 2px; } @@ -397,9 +411,14 @@

Pipeline state machine

// record of a machine that no longer exists. // // A fold maps a retired key onto the state that ABSORBED its inventory — so a retired key - // with no absorbing successor has NO entry. `blockedInfra`'s exit was the run ending - // (issue-pr-cron#108): no surviving inbox measures what it measured, summing it into any - // live series would misstate that series' past, and its samples are drawn nowhere. + // with no absorbing successor has NO entry. `blockedDeploy` (issue-pr-cron#225) and + // `blockedInfra` (issue-pr-cron#108) have none — see RETIRED_NO_SUCCESSOR below: no + // surviving inbox measures what either measured, summing them into any live series would + // misstate that series' past, and their samples are drawn nowhere. + // + // FALLBACK-ONLY: a snapshot carrying `stateDescriptors` names each state's folds itself + // (`histFold`, rain-org-health#164) and the descriptor path passes that list to + // `seriesFor`, so this Map applies only when the frozen STATES table below renders. const HIST_FOLD = new Map([ ["unvetted", ["awaitingReVet"]], ["reject", ["humanReject", "relink"]], @@ -407,10 +426,13 @@

Pipeline state machine

]); // A state's inventory series from the rollup: one point per refresh that CARRIED this // state's counts key, or any key folded into it (a snapshot carrying NONE of them ⇒ no - // point, not a zero). - const seriesFor = (histKey) => { + // point, not a zero). `fold` — when the caller passes one — is the descriptor's own + // `histFold` list and REPLACES the Map lookup outright: an empty list folds nothing, + // never falls back to the hand-maintained entries. Callers rendering the frozen + // fallback pass no fold and get the Map. + const seriesFor = (histKey, fold) => { if (!histKey) return []; - const keys = [histKey, ...(HIST_FOLD.get(histKey) || [])]; + const keys = [histKey, ...(fold || HIST_FOLD.get(histKey) || [])]; const out = []; for (const p of hist) { const counts = p && p.counts; @@ -568,14 +590,21 @@

Pipeline state machine

// // The tool's `human-queue --json` `lanes` object is still keyed by LIFECYCLE phase // (vet-lifecycle / vetter-verdicts / producer-blocked / human-decisions) and is SPARSE - // (only non-empty states emitted). So each state descriptor carries `lane` = where its + // (only non-empty states emitted). So each state entry carries `lane` = where its // { count, prs:[{repo,number,url,title}] } is READ from, independent of `owner` = how - // it is GROUPED. STATES is the canonical shape — a state absent from the JSON renders - // zero (dimmed), keeping the machine's whole shape visible. laneItems maps a state key - // -> its PR list for the click-to-expand detail. (Ownership lives in this map until the - // tool emits an `owner` per state as the single source of truth — an issue-pr-cron - // follow-up, which would also split the two dual-owner states called out below.) + // it is GROUPED. laneItems maps a state key -> its item list for the click-to-expand + // detail. + // + // WHERE THE SHAPE COMES FROM (issue-pr-cron#130 / rain-org-health#164): a snapshot + // carrying `stateDescriptors` IS the machine — the tool emits each state's owner, + // label, act, kind, occupancy source and history folds, and the panel renders exactly + // that, in that order, so adding/renaming/re-laning/retiring a state propagates on the + // next refresh with no edit here. The hand-written STATES table below survives ONLY as + // the FROZEN fallback for descriptor-less snapshots (the same posture as the legacy + // no-`lanes` branch): on such a snapshot a state absent from the JSON renders zero + // (dimmed), keeping the machine's whole shape visible. const hasLanes = hq.lanes && typeof hq.lanes === "object"; + const descriptors = Array.isArray(hq.stateDescriptors) ? hq.stateDescriptors : null; // EVERY rendered state registers its item list here, whichever branch built it — a lane // cell's `prs` for a lane state, the named top-level array for a `fromCounts` state. The // click-to-expand reads ONLY this map, so registration is the single wiring point instead of @@ -594,7 +623,12 @@

Pipeline state machine

issue: !!issue, count: Number(count) || 0, }); - const laneState = (lane, state) => (hq.lanes[lane] && hq.lanes[lane][state]) || null; + // Total over ANY snapshot: the descriptor path reads lane occupancy even when `lanes` + // is absent or junk (sparse-absent ⇒ null ⇒ the box renders 0). + const laneState = (lane, state) => { + const l = hasLanes ? hq.lanes[lane] : null; + return (l && typeof l === "object" && l[state]) || null; + }; const stateBox = (key, label, count, act, kind, series) => { const b = el("button", "fsm-state " + kind + (count ? "" : " zero")); @@ -742,12 +776,15 @@

Pipeline state machine

lead.b.setAttribute("aria-label", lead.label + ": largest queue for this actor (start here)"); }; + // FROZEN (rain-org-health#164): this table renders ONLY for a snapshot with no + // `stateDescriptors` — the same posture as the legacy no-`lanes` branch below. The live + // vocabulary is the tool's own descriptor list; a new, renamed, re-laned or retired + // state reaches the page as data, never as an edit here. + // // Every modeled state → the actor whose inbox it sits in (`owner`), the human-queue // `lane` its items live under, the next `act`, and a display `kind` (flow = moving, // rule = needs a human ruling, blk = a stuck/rework pile). The dual-owner - // resolutions (each defensible against the README FSM's own exit edges): - // ai:blocked-deploy → HUMAN — FSM exit is "human resolves deploy → re-work"; the - // producer only DIAGNOSES the failed deploy, the human funds/dispatches it. + // resolution (defensible against the README FSM's own exit edges): // ai:blocked-on → VETTER — issue-pr-cron#161: deps are typed `--blocked-by` refs // and the vetter's state-load clears the flag the run after every dep merges or // closes, so the vetter's cron run is the mover and no human action sits on the @@ -804,7 +841,7 @@

Pipeline state machine

// only picks the path tried first and github's own /issues/↔/pull/ redirect resolves the // rest — see `urlOf`. { owner: "vetter", state: "closeCandidateUnvetted", fromCounts: "closeCandidateUnvetted", items: "closeCandidateUnvetted", issue: true, hist: "closeCandidateUnvetted", act: "vet the flag", kind: "flow", label: "ai:close-candidate (unvetted)" }, - // Vetter-owned per the dual-owner resolutions above (issue-pr-cron#161): the vetter's + // Vetter-owned per the dual-owner resolution above (issue-pr-cron#161): the vetter's // state-load clears the flag once every typed dep merges/closes, so the act is that // clearance, not a human merge. The tool keys this cell under `vet-lifecycle`; // `staleLane` keeps a snapshot from before that re-key readable — see the lane read @@ -825,7 +862,6 @@

Pipeline state machine

{ owner: "human", state: "leak", fromCounts: "leaks", items: "leaks", issue: false, hist: "leaks", act: "model it", kind: "blk", label: "not in any modeled state" }, { owner: "human", state: "ai:ready", lane: "vetter-verdicts", hist: "ready", act: "merge", kind: "flow" }, { owner: "human", state: "ai:design", lane: "vetter-verdicts", hist: "design", act: "rule on design Q", kind: "rule" }, - { owner: "human", state: "ai:blocked-deploy", lane: "producer-blocked", hist: "blockedDeploy", act: "resolve deploy", kind: "blk" }, { owner: "human", state: "human:design", lane: "human-decisions", hist: "humanDesign", act: "decide", kind: "rule" }, // Close-candidate flags the vetter UPHELD (issue-pr-cron#73/#212): the flag survived // judgement, so a human disposes of the subject. ONE inbox for issues and PRs alike @@ -844,10 +880,15 @@

Pipeline state machine

{ owner: "human", state: "closeCandidateUpheld", fromCounts: "closeCandidateUpheld", items: "closeCandidateUpheld", issue: true, hist: "closeCandidateUpheld", act: "close", kind: "rule", label: "ai:close-candidate (upheld)" }, ]; - // Retired states whose FSM exit was "the run ends" — NOT a transition into a surviving - // state. `ai:blocked-infra` (issue-pr-cron#108): infrastructure being down is a property - // of the moment, not of a PR, so the producer records `infra-down` and ends the run. - // That exit shape is why these are not handled like the ABSORBED retirees + // Retired states NO surviving state absorbed — retiring each of these moved its inventory + // into no successor inbox: + // ai:blocked-deploy (issue-pr-cron#225) — not in the machine at all: no label, no + // classifier arm, no counts emission, no parking transition. `ai:reject` is not its + // successor: a reject is a verdict on the PR's code, not a fact about a deploy, so + // no surviving series measures what this one measured. + // ai:blocked-infra (issue-pr-cron#108) — infrastructure being down is a property of the + // moment, not of a PR, so the producer records `infra-down` and ends the run. + // That absence of a successor is why these are not handled like the ABSORBED retirees // (awaiting-re-vet, ai:relink, human:reject), whose stale cells are ignored outright: an // absorbed retiree's tokens reappear in the surviving state's own cell (a PR that was // awaiting-re-vet IS un-vetted), so its cell is a double-count — but a no-successor @@ -862,7 +903,12 @@

Pipeline state machine

// `prs`) on a `lanes` snapshot, the flat counts key on a pre-`lanes` one; a count that // arrives with no list surfaces through detailBody's missing-list defect, never as an // empty queue. + // + // FALLBACK-ONLY, like STATES: on a snapshot carrying `stateDescriptors` a stale cell + // for a retired state is occupancy no descriptor claims, and it surfaces through the + // conservation defect band instead of through this table. const RETIRED_NO_SUCCESSOR = [ + { owner: "human", state: "ai:blocked-deploy", lane: "producer-blocked", legacy: "blockedDeploy", act: "strip the label", kind: "blk" }, { owner: "human", state: "ai:blocked-infra", lane: "producer-blocked", legacy: "blockedInfra", act: "strip the label", kind: "blk" }, ]; @@ -880,7 +926,305 @@

Pipeline state machine

// the open-issue count nor any age — then the page is exactly what it was, and the legend // below describes no mark that is not on screen. const band = openBand(); - if (hasLanes) { + if (descriptors) { + // ---- Descriptor-driven machine (issue-pr-cron#130 / rain-org-health#164): the tool + // emitted the machine's shape as data, so the panel renders exactly that — descriptor + // order is render order within each owner group. Every field is UNTRUSTED (the + // snapshot is another repo's artifact), so each descriptor is screened on the way in. + // A malformed one renders NO state and is REPORTED in the defect band below — never + // silently dropped, because a dropped descriptor is a vanished state, the drift this + // path exists to kill (any occupancy it meant to claim goes unclaimed, which the + // conservation sweep also reports). `kind` is clamped to the three display kinds so + // snapshot content can never smuggle a meaningful class (`zero`, `rising`, `sel`) + // onto a box; an unknown kind renders with the neutral default border. + const KINDS = new Set(["flow", "rule", "blk"]); + const OWNER_KEYS = new Set(OWNERS.map((o) => o.key)); + const readDescriptor = (d) => { + if (!d || typeof d !== "object" || typeof d.key !== "string" || !d.key) return null; + if (!OWNER_KEYS.has(d.owner)) return null; + // `occupancy` is exactly one of {lane} (count + items from that lane's cell, keyed + // by the state key — sparse-absent ⇒ 0) or {counts, items, itemsAreIssues} (count + // from `counts.`, click-through list from the named top-level array, + // `itemsAreIssues` picking which URL path `urlOf` tries first). + const occ = d.occupancy; + let source = null; + if (occ && typeof occ === "object") { + if (typeof occ.lane === "string" && occ.lane) { + source = { lane: occ.lane }; + } else if ( + typeof occ.counts === "string" && occ.counts && + typeof occ.items === "string" && occ.items + ) { + source = { counts: occ.counts, items: occ.items, issue: !!occ.itemsAreIssues }; + } + } + if (!source) return null; + return { + key: d.key, + owner: d.owner, + label: typeof d.label === "string" && d.label ? d.label : d.key, + act: typeof d.act === "string" ? d.act : "", + kind: KINDS.has(d.kind) ? d.kind : "", + hist: typeof d.hist === "string" ? d.hist : "", + histFold: Array.isArray(d.histFold) ? d.histFold.filter((k) => typeof k === "string") : [], + source, + }; + }; + const defects = []; + const parsed = []; + // Keys already taken. A state key is the panel's IDENTITY for a state: the box's + // `data-t`, and the `register`/`itemsFor` map key the click-to-expand reads. So two + // descriptors sharing one key do not merely duplicate a box — the second's + // registration OVERWRITES the first's, and the first box then opens onto the + // second's list. That is #141's exact failure (a count that opens onto something + // that is not its own list) and neither claim check would catch it: with different + // occupancy sources the two claims are different ids, so both read as singly + // claimed. So a repeat renders NO state and is REPORTED, exactly like a malformed + // entry — and, like a malformed entry, its claim is never registered, so the sweep + // additionally names the cell or counts key it meant to claim. FIRST occurrence + // wins: array order is render order, so the first is the one the tool put first. + const seenKeys = new Set(); + descriptors.forEach((d, i) => { + const p = readDescriptor(d); + if (!p) { + defects.push( + "stateDescriptors[" + i + "] is malformed — it renders no state, and occupancy it meant to claim reports as unclaimed", + ); + return; + } + if (seenKeys.has(p.key)) { + defects.push( + 'stateDescriptors[' + i + '] repeats the key "' + p.key + + '" — it renders no state, and occupancy it meant to claim reports as unclaimed', + ); + return; + } + seenKeys.add(p.key); + parsed.push(p); + }); + // CONSERVATION (the invariant that would have made the blocked-on drift scream + // instead of hiding 17 PRs), per the ratified contract's scoping + // (issue-pr-cron#130): every `lanes` cell must be claimed by exactly ONE descriptor + // — zero exceptions (the tool stopped emitting its one redundant cell in the same + // change that added descriptors) — and every `counts` key must be claimed by exactly + // one descriptor through the keys it READS (its occupancy `counts` source, or the + // `hist` mirror a lane state's series draws from), outside the named frozen-legacy + // set below. A counts-source descriptor's declared `counts`/`items` keys must also + // RESOLVE in the snapshot. Unclaimed occupancy is inventory the machine renders + // NOWHERE; a double claim renders the same inventory twice; an unresolved source is a + // count with no list (or no count at all). All are the tool's defects, and all + // surface in the defect band. + const claims = new Map(); + const itemClaims = new Map(); + const claim = (id) => claims.set(id, (claims.get(id) || 0) + 1); + // The occupancy a lane-sourced descriptor RENDERS, resolved once so the sweep's + // cross-check below and the render loop can never read it two different ways. + const laneCount = (p) => { + const cell = laneState(p.source.lane, p.key); + if (!cell) return 0; + // A cell carries its own two numbers: the `count` it STATES and the `prs` it + // actually holds. `Number(cell.count) || 0` cannot tell a missing or non-numeric + // count from a real zero, so a broken count over a populated list would render 0 + // while the click expanded onto PRs. Render the LARGER: a count that arrives + // broken must never hide inventory the list proves is there, and the box must + // agree with what it opens onto. The disagreement itself is reported below. + const stated = Number(cell.count) || 0; + const listed = Array.isArray(cell.prs) ? cell.prs.length : 0; + return Math.max(stated, listed); + }; + // Claim ids are JSON-encoded so no lane/state/counts spelling can collide or forge + // another key's id. + const laneId = (lane, state) => "lane " + JSON.stringify([lane, state]); + const countsId = (k) => "counts " + JSON.stringify(k); + for (const p of parsed) { + if (p.source.lane) claim(laneId(p.source.lane, p.key)); + else { + if (!Object.prototype.hasOwnProperty.call(c, p.source.counts)) { + defects.push( + 'descriptor "' + p.key + '" names counts source "' + p.source.counts + '" which does not resolve in the snapshot', + ); + } + if (!Array.isArray(hq[p.source.items])) { + defects.push( + 'descriptor "' + p.key + '" names items source "' + p.source.items + '" which does not resolve in the snapshot', + ); + } + } + // Every counts key this descriptor READS is claimed — deduped, because a + // counts-source descriptor's `hist` usually IS its counts key. + // + // `histFold` counts too: those keys are where this state's series draws the + // ABSORBED retirees' samples from, so their inventory does reach the screen — as + // part of this state's line. Leaving them out flags a nonzero absorbed-retiree + // count as unclaimed on a perfectly healthy snapshot: the successor draws the past + // (`ai:reject` folds `humanReject`/`relink`) while the residue rows carry no `hist` + // of their own, precisely so the same history is not rendered twice. Their lane + // cells are claimed by those residue descriptors; their counts keys are claimed + // here. The invariant stays honest in BOTH directions — exactly one descriptor + // claims a fold key, because a fold key must not also be a live row's `hist` (the + // producer side is held to that), so a key claimed as both still reads as the + // double claim it is. + const read = new Set(); + if (p.source.counts) read.add(p.source.counts); + if (p.hist) read.add(p.hist); + for (const k of p.histFold) read.add(k); + for (const k of read) claim(countsId(k)); + // The top-level array a counts-source descriptor lists from is claimed too. It is + // not swept for absence (an array with no `counts` entry is not occupancy this page + // knows how to count), but two descriptors naming the SAME array is a defect the + // other checks cannot see: their keys differ, their counts keys may differ, and yet + // both boxes open onto one list — a count that opens onto something that is not its + // own list, which is #141 again. + if (p.source.items) itemClaims.set(p.source.items, (itemClaims.get(p.source.items) || 0) + 1); + // ---- The page holds TWO numbers for a lane state and is the only place they meet. + // The lane cell is the OCCUPANCY it renders; `hist` names the `counts` key its + // SERIES is drawn from — and the tool computes those two ways (issue-pr-cron#228: + // `counts.ready` off the label bucket, the lane cell off `classify_lane`). When + // they disagree, the box draws one number under a sparkline sitting at the other, + // and every claim check passes because each key is claimed exactly once. Only a + // comparison catches it, so the page makes the disagreement VISIBLE — it does not + // guess which number is right, and it does not drop the claim (every lane state's + // `hist` mirrors a counts key, so dropping it would paint false defects across a + // healthy snapshot). States that agree stay silent. + // The SAME class one level down: a cell's stated `count` against the `prs` it + // carries. The sweep cannot catch this — a claimed cell leaves `misclaimed` at + // n === 1 — so the box would read one number while its click-through showed + // another, silently. Only checked when the list is non-empty: a count over an + // EMPTY list is the missing-list case, which detailBody already reports at the + // click ("the list is missing, not empty") rather than as a snapshot defect. + if (p.source.lane) { + const cell = laneState(p.source.lane, p.key); + const stated = cell ? Number(cell.count) || 0 : 0; + const listed = cell && Array.isArray(cell.prs) ? cell.prs.length : 0; + if (listed > 0 && stated !== listed) { + defects.push( + 'lanes["' + p.source.lane + '"]["' + p.key + '"] states count ' + stated + + " but carries " + listed + " PRs — the box renders " + Math.max(stated, listed) + + ", the larger, so a broken count cannot hide inventory the list proves is there", + ); + } + } + if (p.source.lane && p.hist && Object.prototype.hasOwnProperty.call(c, p.hist)) { + const shown = laneCount(p); + const mirrored = Number(c[p.hist]) || 0; + if (shown !== mirrored) { + defects.push( + 'state "' + p.key + '" is counted two ways and the snapshot disagrees with itself: ' + + 'the rendered occupancy lanes["' + p.source.lane + '"]["' + p.key + '"] holds ' + shown + + ', while counts["' + p.hist + '"] — the key this box\'s series is drawn from — says ' + mirrored + + '. The box renders ' + shown + '; fix the tool\'s emit (issue-pr-cron#228)', + ); + } + } + } + + OWNERS.forEach((O) => { + const own = parsed.filter((p) => p.owner === O.key); + if (!own.length) return; // an actor the tool files no state under draws no empty heading + const g = el("div", "fsm-grid"); + let ownerTotal = 0; + const boxes = []; + own.forEach((p) => { + let count; + if (p.source.lane) { + const cell = laneState(p.source.lane, p.key); + count = laneCount(p); + register(p.key, cell && cell.prs, false, count); + } else { + // Registration is UNCONDITIONAL on both branches (#141): a count whose list + // never arrived reads as detailBody's missing-list defect, never as an empty + // queue. + count = Number(c[p.source.counts]) || 0; + register(p.key, hq[p.source.items], p.source.issue, count); + } + ownerTotal += count; + // The descriptor names its own series key AND its own retired-key folds — + // HIST_FOLD serves only the frozen fallback. An empty histFold folds nothing. + const b = stateBox(p.key, p.label, count, p.act, p.kind, seriesFor(p.hist, p.histFold)); + boxes.push({ b, count, label: p.label }); + g.append(b); + }); + markLead(boxes); + const lane = el("div", "fsm-lane"); + const h = el("div", "fsm-lane-h"); + h.append(el("span", null, O.title), el("span", "lsub", O.sub), el("span", "lct", String(ownerTotal))); + lane.append(h, g); + grid.append(lane); + }); + + // The conservation sweep, over the snapshot's own occupancy keys. `legacy` marks a + // key the ratified contract lets go UNCLAIMED — it never licenses a double claim. + const misclaimed = (id, name, held, legacy) => { + const n = claims.get(id) || 0; + if (n === 1) return; + // Zero inventory renders nowhere VACUOUSLY — nothing is hidden. The live tool + // still emits drained retired counts keys at 0 (kept-while-nonzero applies to the + // descriptor list, not to the keys), so an unclaimed key holding nothing must not + // paint a permanent false defect on a healthy snapshot. A DOUBLE claim is + // STRUCTURAL — the same inventory wired twice — and stays a defect at any count, + // frozen-legacy keys included: two descriptors both drawing `openIssues` would + // render the open-issue population as two state series, which no exception covers. + if (n === 0 && (!held || legacy)) return; + defects.push( + name + " (" + held + " held) " + + (n === 0 + ? "is claimed by no descriptor — its inventory renders nowhere" + : "is claimed by " + n + " descriptors — its inventory renders " + n + " times"), + ); + }; + if (hasLanes) { + for (const lane of Object.keys(hq.lanes)) { + const cells = hq.lanes[lane]; + if (!cells || typeof cells !== "object") continue; + for (const state of Object.keys(cells)) { + const cell = cells[state]; + // What the cell HOLDS is the larger of its stated count and the list it + // actually carries: a cell whose `count` was dropped or arrived non-numeric + // still holds every PR in `prs`, and scoring that 0 would let the zero-held + // carve-out swallow real stranded inventory. This is the one place the sweep + // has the list in hand, so it uses it. + const held = cell && typeof cell === "object" + ? Math.max(Number(cell.count) || 0, Array.isArray(cell.prs) ? cell.prs.length : 0) + : 0; + misclaimed(laneId(lane, state), 'lanes["' + lane + '"]["' + state + '"]', held); + } + } + } + // The FROZEN-LEGACY exception the ratified contract names (issue-pr-cron#130, + // 2026-08-06 clarification) — the ONLY counts keys allowed to go unclaimed: + // - non-state metrics: population/rollup numbers, not inboxes anyone acts on + // (`totalProducerPrs`, `openIssues` — the band above draws it — and + // `archivedRepoPrs`); + // - `closeCandidateIssues`: the legacy duplicate of inventory the two + // close-candidate descriptors already claim. + // Any OTHER unclaimed counts key is state-shaped inventory rendering nowhere. + const LEGACY_UNCLAIMED = new Set([ + "totalProducerPrs", + "openIssues", + "archivedRepoPrs", + "closeCandidateIssues", + ]); + for (const k of Object.keys(c)) { + misclaimed(countsId(k), 'counts["' + k + '"]', Number(c[k]) || 0, LEGACY_UNCLAIMED.has(k)); + } + for (const [k, n] of itemClaims) { + if (n < 2) continue; + defects.push( + 'the top-level array "' + k + '" is named by ' + n + + " descriptors — their boxes open onto the same list", + ); + } + if (defects.length) { + const dbox = el("div", "fsm-defect"); + dbox.append(el( + "div", + "fd-h", + "Snapshot defect — the tool's stateDescriptors do not cover its own occupancy. Fix the tool's emit; nothing here is ignored:", + )); + for (const t of defects) dbox.append(el("div", "fd-li", t)); + grid.append(dbox); + } + } else if (hasLanes) { OWNERS.forEach((O) => { const g = el("div", "fsm-grid"); let ownerTotal = 0; @@ -930,7 +1274,7 @@

Pipeline state machine

grid.append(lane); }); } else { - // Backward-compat: an older human-queue.json with no `lanes` — render the 6 states + // Backward-compat: an older human-queue.json with no `lanes` — render the 5 states // the legacy `counts` keys carry (all human-owned under the actor mapping) from // `counts`, so the panel never blanks. // `items` names a top-level array as in STATES; an entry with none lists from the flat @@ -940,7 +1284,6 @@

Pipeline state machine

{ key: "ai:ready", label: "ai:ready", n: c.ready, hist: "ready", act: "merge", kind: "flow" }, { key: "ai:design", label: "ai:design", n: c.design, hist: "design", act: "rule on design Q", kind: "rule" }, { key: "closeCandidateIssues", label: "ai:close-candidate (issues)", n: c.closeCandidateIssues, items: "closeCandidateIssues", issue: true, hist: "closeCandidateIssues", act: "close issue", kind: "rule" }, - { key: "ai:blocked-deploy", label: "ai:blocked-deploy", n: c.blockedDeploy, hist: "blockedDeploy", act: "resolve deploy", kind: "blk" }, { key: "ai:blocked-on", label: "ai:blocked-on", n: c.blockedOn, hist: "blockedOn", act: "merge dependency", kind: "blk" }, ]; const g = el("div", "fsm-grid"); @@ -1030,6 +1373,13 @@

Pipeline state machine

); if (hq.sample) note.append(" ", el("b", null, "(sample data — live once issue-pr-cron#41 lands)")); note.append("."); + if (descriptors) { + note.append( + " The machine's shape above is the tool's own ", + el("code", null, "stateDescriptors"), + " — states appear, move between actors, and retire on the tool's say-so, with no edit to this page.", + ); + } if (hist.length) { note.append( " Each state carries an inventory sparkline (trailing 4 weeks); a ", diff --git a/test/dashboard.test.js b/test/dashboard.test.js index ecf983d..afc3b83 100644 --- a/test/dashboard.test.js +++ b/test/dashboard.test.js @@ -1387,8 +1387,10 @@ Deno.test("pipeline FSM: states group under the three actor headings, no fourth "ai:design": { count: 1, prs: [] }, "ai:close-candidate": { count: 1, prs: [] }, }, + // Drained residue of a state the machine no longer has (issue-pr-cron#225): at zero + // it draws no box at all. Nonzero it would draw a straggler — its own tests below. "producer-blocked": { - "ai:blocked-deploy": { count: 1, prs: [] }, + "ai:blocked-deploy": { count: 0, prs: [] }, }, "human-decisions": { "human:reject": { count: 1, prs: [] }, @@ -1459,12 +1461,18 @@ Deno.test("pipeline FSM: states group under the three actor headings, no fourth !human.states.includes("ai:blocked-on"), `ai:blocked-on is not in the human's inbox: ${JSON.stringify(human.states)}`, ); + // ai:blocked-deploy is no longer a modeled state (issue-pr-cron#225), so the drained cell + // in the fixture files under NO actor — it is not a human inbox with nothing in it. + assert( + groups.every((g) => !g.states.includes("ai:blocked-deploy")), + `the drained ai:blocked-deploy residue renders nowhere: ${JSON.stringify(groups.map((g) => g.states))}`, + ); // Every state box sits under exactly one heading (no leaks into a fourth group). - // 11 = every STATES entry filed once: producer 2 (ai:reject, the untouched backlog), - // vetter 3 (un-vetted, the unvetted flags, ai:blocked-on), human 6 (leak, ai:ready, - // ai:design, ai:blocked-deploy, human:design, the upheld flags). + // 10 = every STATES entry filed once: producer 2 (ai:reject, the untouched backlog), + // vetter 3 (un-vetted, the unvetted flags, ai:blocked-on), human 5 (leak, ai:ready, + // ai:design, human:design, the upheld flags). const total = groups.reduce((n, g) => n + g.states.length, 0); - assert(total === 11, `all 11 states filed once: ${total}`); + assert(total === 10, `all 10 states filed once: ${total}`); // Both directions, same as awaiting-re-vet: a reintroduction fails here. Each retired // lane key in the fixture above is a DECOY — a stale snapshot still carrying it must // render it in NO group. @@ -1728,13 +1736,12 @@ Deno.test("fsm history: a junk count is skipped like an absent one and cannot po assert(ys[0] === ys[1], `both samples read 6, so the line is flat: ${JSON.stringify(ys)}`); }); -// #69: the historically dual-owner states each resolve to ONE actor. +// #69: a historically dual-owner state resolves to ONE actor. Deno.test("pipeline FSM: ambiguous states resolve to a single owner", () => { const box = fsmBox({ counts: { leaks: 0, ready: 0, closeCandidateIssues: 0 }, lanes: { "producer-blocked": { - "ai:blocked-deploy": { count: 1, prs: [] }, "ai:blocked-on": { count: 1, prs: [] }, }, "human-decisions": { "human:reject": { count: 1, prs: [] } }, @@ -1752,11 +1759,6 @@ Deno.test("pipeline FSM: ambiguous states resolve to a single owner", () => { owner("human:reject") === null, `retired human:reject renders in no group: ${owner("human:reject")}`, ); - // ai:blocked-deploy → human (the actor that actually unblocks it). - assert( - (owner("ai:blocked-deploy") || "").includes("Human action"), - `ai:blocked-deploy is human-owned: ${owner("ai:blocked-deploy")}`, - ); // ai:blocked-on → vetter (issue-pr-cron#161): the vetter's state-load clears the flag // the run after every typed dep merges/closes. This fixture keys the cell under the // pre-re-key `producer-blocked` lane, so the box surfacing at all is the stale-lane @@ -1863,186 +1865,267 @@ Deno.test("pipeline FSM: a stale producer-blocked blocked-on cell still surfaces ); }); -// issue-pr-cron#108: `ai:blocked-infra` is retired with NO successor state — infra being -// down is a property of the moment, not of a PR, so the producer records `infra-down` and -// ends the run. Unlike the absorbed retirees (awaiting-re-vet, ai:relink, human:reject), -// whose tokens reappear in a surviving state's own cell, a PR still carrying this label -// reappears NOWHERE — so a snapshot that still counts the state (an old snapshot, or the -// label reapplied by hand) surfaces as a straggler box in the human group instead of -// vanishing from the board. -Deno.test("pipeline FSM: a nonzero ai:blocked-infra count surfaces as a human-owned straggler box", () => { +// The retired states NO surviving state absorbed. `ai:blocked-deploy` +// (issue-pr-cron#225) is DELETED from the machine — no label, no classifier arm, no counts +// emission, no parking transition — and `ai:reject` is not its successor (a reject is a +// verdict on the PR's code, not a fact about a deploy). `ai:blocked-infra` +// (issue-pr-cron#108) exits by the run ending: infra being down is a property of the +// moment, not of a PR, so the producer records `infra-down` and stops. Unlike the ABSORBED +// retirees (awaiting-re-vet, ai:relink, human:reject), whose tokens reappear in a surviving +// state's own cell, a PR still carrying either label reappears NOWHERE — so a snapshot that +// still counts one (an old snapshot, or the label reapplied by hand) surfaces as a +// straggler box in the human group instead of vanishing from the board. Both are held to +// the same behaviour here, because it is one mechanism: the entries in +// `RETIRED_NO_SUCCESSOR`, not per-state special-casing. +const NO_SUCCESSOR = [ + { state: "ai:blocked-deploy", legacy: "blockedDeploy" }, + { state: "ai:blocked-infra", legacy: "blockedInfra" }, +]; + +Deno.test("pipeline FSM: a nonzero no-successor retiree count surfaces as a human-owned straggler box", () => { const now = Date.parse("2026-08-06T00:00:00Z"); - // History still carrying blockedInfra samples draws NOTHING for it: the series has no - // surviving state to fold into and the straggler box carries no chart. - const history = [ - { t: now - 2 * DAY, counts: { ready: 1, blockedInfra: 5 } }, - { t: now - DAY, counts: { ready: 1, blockedInfra: 4 } }, - { t: now, counts: { ready: 1, blockedInfra: 2 } }, - ]; + for (const { state, legacy } of NO_SUCCESSOR) { + // History still carrying the retired key draws NOTHING for it: the series has no + // surviving state to fold into and the straggler box carries no chart. + const history = [ + { t: now - 2 * DAY, counts: { ready: 1, [legacy]: 5 } }, + { t: now - DAY, counts: { ready: 1, [legacy]: 4 } }, + { t: now, counts: { ready: 1, [legacy]: 2 } }, + ]; + const box = fsmBox({ + counts: { leaks: 0, ready: 1, [legacy]: 2 }, + lanes: { + "vetter-verdicts": { "ai:ready": { count: 1, prs: [] } }, + "producer-blocked": { + [state]: { + count: 2, + prs: [ + { repo: "rainlanguage/rain.flow", number: 12, url: "https://github.com/rainlanguage/rain.flow/pull/12", title: `stale ${state} straggler` }, + { repo: "rainlanguage/rain.dia", number: 34, url: "https://github.com/rainlanguage/rain.dia/pull/34", title: "hand relabelled" }, + ], + }, + }, + }, + }, history); + const b = collect(box, "fsm-state").find((x) => x.dataset.t === state); + assert(b, `${state}: the straggler box renders while the count is nonzero`); + assert( + collect(box, "fsm-state").filter((x) => x.dataset.t === state).length === 1, + `${state}: exactly one straggler box on the whole board`, + ); + assert( + collect(b, "sc")[0].textContent === "2", + `${state}: it carries the snapshot's count: ${collect(b, "sc")[0].textContent}`, + ); + assert( + collect(b, "sa")[0].textContent === "strip the label", + `${state}: the act clears the residue: ${collect(b, "sa")[0].textContent}`, + ); + assert(b.className.includes("blk"), `${state}: a stuck pile, kind blk: ${b.className}`); + assert( + collect(b, "fsm-spark").length === 0, + `${state}: no sparkline — the retired series is drawn nowhere`, + ); + // Filed under the human — the actor who strips the label — and counted in that total + // (1 ready + 2 stragglers). + const human = ownerGroups(box).find((g) => g.title.includes("Human action")); + assert( + human.states.includes(state), + `${state}: the straggler files under the human: ${JSON.stringify(human.states)}`, + ); + assert(human.title.endsWith("3"), `${state}: the human total counts it: ${human.title}`); + // Click-through lists the stragglers like any state box. + b.click(); + const text = textOf(box); + assert(text.includes(`stale ${state} straggler`), `${state}: first straggler PR listed`); + assert(text.includes("rain.dia#34"), `${state}: second straggler PR listed by repo#number`); + } +}); + +// Both retirees at once, on ONE snapshot: two distinct boxes, each opening onto its OWN +// list. They share a lane (`producer-blocked`) and an act, so a mechanism that keyed the +// residue by lane, or registered one list for the pile, would render one box or cross the +// click-throughs — and the total would silently lose a straggler either way. +Deno.test("pipeline FSM: two no-successor retirees on one snapshot render as two independent stragglers", () => { const box = fsmBox({ - counts: { leaks: 0, ready: 1, blockedInfra: 2 }, + counts: { leaks: 0, ready: 1, blockedDeploy: 1, blockedInfra: 2 }, lanes: { "vetter-verdicts": { "ai:ready": { count: 1, prs: [] } }, "producer-blocked": { + "ai:blocked-deploy": { + count: 1, + prs: [{ repo: "rainlanguage/rain.flow", number: 12, url: "https://github.com/rainlanguage/rain.flow/pull/12", title: "deploy residue" }], + }, "ai:blocked-infra": { count: 2, prs: [ - { repo: "rainlanguage/rain.flow", number: 12, url: "https://github.com/rainlanguage/rain.flow/pull/12", title: "stale infra straggler" }, - { repo: "rainlanguage/rain.dia", number: 34, url: "https://github.com/rainlanguage/rain.dia/pull/34", title: "hand relabelled" }, + { repo: "rainlanguage/rain.dia", number: 34, url: "https://github.com/rainlanguage/rain.dia/pull/34", title: "infra residue A" }, + { repo: "rainlanguage/rain.flare", number: 56, url: "https://github.com/rainlanguage/rain.flare/pull/56", title: "infra residue B" }, ], }, }, }, - }, history); - const b = collect(box, "fsm-state").find((x) => x.dataset.t === "ai:blocked-infra"); - assert(b, "the straggler box renders while the count is nonzero"); - assert( - collect(box, "fsm-state").filter((x) => x.dataset.t === "ai:blocked-infra").length === 1, - "exactly one straggler box on the whole board", - ); - assert( - collect(b, "sc")[0].textContent === "2", - `it carries the snapshot's count: ${collect(b, "sc")[0].textContent}`, - ); + }); + const at = (k) => collect(box, "fsm-state").filter((x) => x.dataset.t === k); + assert(at("ai:blocked-deploy").length === 1, "one deploy straggler"); + assert(at("ai:blocked-infra").length === 1, "one infra straggler"); assert( - collect(b, "sa")[0].textContent === "strip the label", - `the act clears the residue: ${collect(b, "sa")[0].textContent}`, + collect(at("ai:blocked-deploy")[0], "sc")[0].textContent === "1" && + collect(at("ai:blocked-infra")[0], "sc")[0].textContent === "2", + "each carries its own count, never the lane's sum", ); - assert(b.className.includes("blk"), `a stuck pile, kind blk: ${b.className}`); + // 1 ready + 1 deploy + 2 infra: both stragglers land in the human total. + const human = ownerGroups(box).find((g) => g.title.includes("Human action")); + assert(human.title.endsWith("4"), `both stragglers count toward the human total: ${human.title}`); + // Each click-through opens onto its own rows, never the other's. + at("ai:blocked-deploy")[0].click(); + const deployText = textOf(box); assert( - collect(b, "fsm-spark").length === 0, - "no sparkline: the retired series is drawn nowhere", + deployText.includes("deploy residue") && !deployText.includes("infra residue"), + `the deploy box lists only its own PRs: ${deployText}`, ); - // Filed under the human — the actor who strips the label — and counted in that total - // (1 ready + 2 stragglers). - const human = ownerGroups(box).find((g) => g.title.includes("Human action")); + at("ai:blocked-deploy")[0].click(); + at("ai:blocked-infra")[0].click(); + const infraText = textOf(box); assert( - human.states.includes("ai:blocked-infra"), - `the straggler files under the human: ${JSON.stringify(human.states)}`, + infraText.includes("infra residue A") && infraText.includes("infra residue B") && + !infraText.includes("deploy residue"), + `the infra box lists only its own PRs: ${infraText}`, ); - assert(human.title.endsWith("3"), `the human total counts it: ${human.title}`); - // Click-through lists the stragglers like any state box. - b.click(); - const text = textOf(box); - assert(text.includes("stale infra straggler"), "first straggler PR listed"); - assert(text.includes("rain.dia#34"), "second straggler PR listed by repo#number"); }); // The straggler renders ONLY while a snapshot still counts the state. Absent — the live // shape, the tool no longer emits it — draws nothing, and so does an emitter that still // writes the cell at zero: a retired state earns no permanent dimmed box describing a // machine that no longer exists (rain-org-health#145). -Deno.test("pipeline FSM: a zero or absent ai:blocked-infra count draws no straggler box", () => { - const none = (box, what) => { +Deno.test("pipeline FSM: a zero or absent no-successor retiree count draws no straggler box", () => { + const none = (box, state, what) => { assert( - !collect(box, "fsm-state").some((x) => x.dataset.t === "ai:blocked-infra"), - `no box is keyed to the retired state (${what})`, + !collect(box, "fsm-state").some((x) => x.dataset.t === state), + `no box is keyed to ${state} (${what})`, ); assert( - !collect(box, "sk").some((s) => s.textContent === "ai:blocked-infra"), - `no box is labelled with the retired state (${what})`, + !collect(box, "sk").some((s) => s.textContent === state), + `no box is labelled ${state} (${what})`, ); }; - none( - fsmBox({ - counts: { leaks: 0, ready: 1 }, - lanes: { "vetter-verdicts": { "ai:ready": { count: 1, prs: [] } } }, - }), - "absent", - ); - none( - fsmBox({ - counts: { leaks: 0, ready: 0, blockedInfra: 0 }, - lanes: { "producer-blocked": { "ai:blocked-infra": { count: 0, prs: [] } } }, - }), - "zero", - ); + // The live shape: neither key emitted at all. + const absent = fsmBox({ + counts: { leaks: 0, ready: 1 }, + lanes: { "vetter-verdicts": { "ai:ready": { count: 1, prs: [] } } }, + }); + for (const { state } of NO_SUCCESSOR) none(absent, state, "absent"); + // An emitter that still writes the cell and the counts key, drained to zero. + for (const { state, legacy } of NO_SUCCESSOR) { + none( + fsmBox({ + counts: { leaks: 0, ready: 0, [legacy]: 0 }, + lanes: { "producer-blocked": { [state]: { count: 0, prs: [] } } }, + }), + state, + "zero", + ); + } }); -// A `lanes` snapshot can carry the COUNT without the cell (`counts.blockedInfra` set, no -// `lanes` cell — the lanes object is sparse). The count still surfaces, and the click -// reads as detailBody's missing-list defect rather than an empty queue: a count with no -// list is #141's exact signature. -Deno.test("pipeline FSM: a counts-only ai:blocked-infra straggler surfaces and reports its missing list", () => { - const box = fsmBox({ - counts: { leaks: 0, ready: 0, blockedInfra: 4 }, - lanes: { "vetter-verdicts": { "ai:ready": { count: 0, prs: [] } } }, - }); - const b = collect(box, "fsm-state").find((x) => x.dataset.t === "ai:blocked-infra"); - assert(b, "the counts-only straggler still renders"); - assert( - collect(b, "sc")[0].textContent === "4", - `it carries the counts key: ${collect(b, "sc")[0].textContent}`, - ); - b.click(); - const text = textOf(box); - assert( - text.includes("the list is missing, not empty"), - `a count with no list reads as the missing-list defect: ${text}`, - ); +// A `lanes` snapshot can carry the COUNT without the cell (the counts key set, no `lanes` +// cell — the lanes object is sparse). The count still surfaces, and the click reads as +// detailBody's missing-list defect rather than an empty queue: a count with no list is +// #141's exact signature. +Deno.test("pipeline FSM: a counts-only no-successor straggler surfaces and reports its missing list", () => { + for (const { state, legacy } of NO_SUCCESSOR) { + const box = fsmBox({ + counts: { leaks: 0, ready: 0, [legacy]: 4 }, + lanes: { "vetter-verdicts": { "ai:ready": { count: 0, prs: [] } } }, + }); + const b = collect(box, "fsm-state").find((x) => x.dataset.t === state); + assert(b, `${state}: the counts-only straggler still renders`); + assert( + collect(b, "sc")[0].textContent === "4", + `${state}: it carries the counts key: ${collect(b, "sc")[0].textContent}`, + ); + b.click(); + const text = textOf(box); + assert( + text.includes("the list is missing, not empty"), + `${state}: a count with no list reads as the missing-list defect: ${text}`, + ); + } }); // A pre-`lanes` snapshot carries the count under the flat legacy key and its items under // the flat `states` map — the straggler surfaces from those, through the same legacy // branch that draws the surviving states. -Deno.test("pipeline FSM: a pre-lanes snapshot with a nonzero blockedInfra count surfaces the straggler", () => { +Deno.test("pipeline FSM: a pre-lanes snapshot with a nonzero no-successor count surfaces the straggler", () => { const now = Date.parse("2026-08-06T00:00:00Z"); - const history = [ - { t: now - DAY, counts: { ready: 1, blockedInfra: 6 } }, - { t: now, counts: { ready: 1, blockedInfra: 3 } }, - ]; - const box = fsmBox({ - counts: { leaks: 0, ready: 1, blockedInfra: 3 }, - states: { - "ai:blocked-infra": [ - { repo: "rainlanguage/rain.flare", number: 5, url: "https://github.com/rainlanguage/rain.flare/pull/5", title: "legacy straggler" }, - ], - }, - }, history); - const b = collect(box, "fsm-state").find((x) => x.dataset.t === "ai:blocked-infra"); - assert(b, "the straggler renders from the legacy counts key"); - assert( - collect(b, "sc")[0].textContent === "3", - `it carries the legacy count: ${collect(b, "sc")[0].textContent}`, - ); - assert( - collect(b, "fsm-spark").length === 0, - "no sparkline on the legacy straggler either", - ); - b.click(); - const text = textOf(box); - assert(text.includes("rain.flare#5"), "click lists the legacy stragglers"); - // A legacy snapshot without the key draws nothing. + for (const { state, legacy } of NO_SUCCESSOR) { + const history = [ + { t: now - DAY, counts: { ready: 1, [legacy]: 6 } }, + { t: now, counts: { ready: 1, [legacy]: 3 } }, + ]; + const box = fsmBox({ + counts: { leaks: 0, ready: 1, [legacy]: 3 }, + states: { + [state]: [ + { repo: "rainlanguage/rain.flare", number: 5, url: "https://github.com/rainlanguage/rain.flare/pull/5", title: "legacy straggler" }, + ], + }, + }, history); + const b = collect(box, "fsm-state").find((x) => x.dataset.t === state); + assert(b, `${state}: the straggler renders from the legacy counts key`); + assert( + collect(b, "sc")[0].textContent === "3", + `${state}: it carries the legacy count: ${collect(b, "sc")[0].textContent}`, + ); + assert( + collect(b, "fsm-spark").length === 0, + `${state}: no sparkline on the legacy straggler either`, + ); + b.click(); + assert(textOf(box).includes("rain.flare#5"), `${state}: click lists the legacy stragglers`); + } + // A legacy snapshot without either key draws nothing. const clean = fsmBox({ counts: { leaks: 0, ready: 1 } }); - assert( - !collect(clean, "fsm-state").some((x) => x.dataset.t === "ai:blocked-infra"), - "an absent legacy key draws no straggler", - ); + for (const { state } of NO_SUCCESSOR) { + assert( + !collect(clean, "fsm-state").some((x) => x.dataset.t === state), + `an absent legacy key draws no ${state} straggler`, + ); + } }); -// `blockedInfra` has NO HIST_FOLD entry: no surviving state absorbed its inventory (the -// exit was the run ending), so a surviving series must read its OWN samples only — folding -// the retired key in would fabricate a step in a series that never held that work. -Deno.test("fsm history: retired blockedInfra samples fold into NO surviving series", () => { +// Neither `blockedDeploy` nor `blockedInfra` has a HIST_FOLD entry: no surviving state +// absorbed either inventory, so a surviving series must read its OWN samples only — +// folding a retired key in would fabricate a step in a series that never held that work. +// `ai:reject` is the discriminating target: it is where the PRs that carried +// `ai:blocked-deploy` now sit, which is exactly the resemblance that tempts a fold, and a +// relabel is not an absorption — a reject measures a verdict on the PR's code, never a +// deploy. +Deno.test("fsm history: retired no-successor samples fold into NO surviving series", () => { const now = Date.parse("2026-08-06T00:00:00Z"); const at = (d) => now - d * DAY; - // blocked-deploy holds flat at 2 while blockedInfra swings 9 → 0. Unfolded, deploy's - // line is FLAT; any fold would step it 11 → 2. + // reject holds flat at 2 while both retired keys swing to 0. Unfolded, reject's line is + // FLAT; folding blockedDeploy would step it 11 → 2, blockedInfra 9 → 2, both 18 → 2. const history = [ - { t: at(1), counts: { blockedDeploy: 2, blockedInfra: 9 } }, - { t: at(0), counts: { blockedDeploy: 2, blockedInfra: 0 } }, + { t: at(1), counts: { reject: 2, ready: 2, blockedDeploy: 9, blockedInfra: 7 } }, + { t: at(0), counts: { reject: 2, ready: 2, blockedDeploy: 0, blockedInfra: 0 } }, ]; const box = fsmBox({ - counts: { leaks: 0, ready: 0 }, - lanes: { "producer-blocked": { "ai:blocked-deploy": { count: 2, prs: [] } } }, + counts: { leaks: 0, ready: 2 }, + lanes: { + "vetter-verdicts": { "ai:reject": { count: 2, prs: [] }, "ai:ready": { count: 2, prs: [] } }, + }, }, history); - const deploy = collect(box, "fsm-state").find((x) => x.dataset.t === "ai:blocked-deploy"); - const points = tags(deploy, "polyline")[0].getAttribute("points"); - const ys = points.split(" ").map((p) => Number(p.split(",")[1])); - assert( - ys[0] === ys[1], - `deploy reads its own flat 2s, untouched by the retired key: ${points}`, - ); + // Every surviving series that draws a line reads flat — no state anywhere absorbs them. + for (const key of ["ai:reject", "ai:ready"]) { + const b = collect(box, "fsm-state").find((x) => x.dataset.t === key); + const points = tags(b, "polyline")[0].getAttribute("points"); + const ys = points.split(" ").map((p) => Number(p.split(",")[1])); + assert( + ys[0] === ys[1], + `${key} reads its own flat 2s, untouched by the retired keys: ${points}`, + ); + } }); // ---- pipeline.html: Theory-of-Constraints flow layer (#32) ---- @@ -3001,8 +3084,11 @@ Deno.test("pipeline FSM: every box's count equals the number of rows it expands // ai:design deliberately absent: a sparse lane must render a zero box that // expands to zero rows, which is the same invariant at n = 0. }, + // A no-successor retiree still counted by an old snapshot: it draws a STRAGGLER box, + // which is held to this invariant like any other — a residue count that opened onto + // someone else's rows would be #141 all over again, in the one place added last. "producer-blocked": { "ai:blocked-deploy": laneCell("deploy", 3) }, - // Retired state in the data (old snapshot): must render NO box and NO rows, so it + // Absorbed retiree in the data (old snapshot): must render NO box and NO rows, so it // cannot create a count/rows mismatch (issue-pr-cron#133/#138). "human-decisions": { "human:reject": laneCell("hreject", 4) }, }, @@ -3023,7 +3109,12 @@ Deno.test("pipeline FSM: every box's count equals the number of rows it expands if (rows !== n) mismatches.push(`${b.dataset.t}: box ${n}, panel ${rows}`); b.click(); } + // 11 = the 10 STATES entries + the ai:blocked-deploy straggler the stale cell surfaces. assert(checked === 11, `the whole machine was walked, got ${checked} boxes`); + assert( + box.querySelectorAll("[data-t]").some((b) => b.dataset.t === "ai:blocked-deploy"), + "the straggler is one of the boxes walked, not skipped by the sweep", + ); // Guard the guard: a fixture that zeroed everything would satisfy the invariant vacuously. assert(nonZero >= 8, `the fixture must exercise non-zero states, got ${nonZero}`); assert( @@ -3113,6 +3204,1050 @@ Deno.test("pipeline FSM: a malformed item renders as a malformed row, taking no ); }); +// ---- rain-org-health#164: the machine rendered from emitted stateDescriptors ---------- +// +// The consumer half of issue-pr-cron#130. A snapshot carrying `stateDescriptors` IS the +// machine: owner grouping, box (key/label/act/kind), occupancy from the descriptor's +// declared source, history folds from `histFold`, descriptor order as render order. The +// hand-written STATES table is the FROZEN fallback for descriptor-less snapshots only. +// Fixtures carry DECOY values on the source a state must NOT read (a counts mirror beside +// a lane cell), so a wrong-source read changes a rendered number instead of passing. + +// A canonical descriptor snapshot: both occupancy kinds, a re-laned state (ai:blocked-on +// under the VETTER reading `vet-lifecycle` — the exact drift the frozen table could not +// follow), and a sparse-absent lane state. +function descHq() { + return { + counts: { + // The lane states' `hist` mirrors, AGREEING with their cells — a self-consistent + // snapshot. A mirror that contradicts its cell is itself a reported defect now + // (the cross-check), so the wrong-source read is discriminated there rather than + // by a decoy that would make every fixture using descHq render a defect band. + ready: 5, + blockedOn: 3, + uncoveredIssues: 4, + leaks: 2, + }, + lanes: { + "vetter-verdicts": { "ai:ready": { count: 5, prs: fcItems("ready", 5) } }, + "vet-lifecycle": { "ai:blocked-on": { count: 3, prs: fcItems("blocked", 3) } }, + }, + uncoveredIssues: fcItems("uncovered", 4), + leaks: fcItems("leak", 2), + stateDescriptors: [ + { + key: "uncoveredIssues", + owner: "producer", + act: "open a PR", + kind: "flow", + hist: "uncoveredIssues", + histFold: [], + occupancy: { counts: "uncoveredIssues", items: "uncoveredIssues", itemsAreIssues: true }, + label: "untouched (no PR)", + }, + { + key: "ai:blocked-on", + owner: "vetter", + act: "clears when every typed dep merges/closes", + kind: "blk", + hist: "blockedOn", + histFold: [], + occupancy: { lane: "vet-lifecycle" }, + }, + { key: "ai:ready", owner: "human", act: "merge", kind: "flow", hist: "ready", histFold: [], occupancy: { lane: "vetter-verdicts" } }, + // Sparse-absent lane cell (renders 0, dimmed) deliberately BETWEEN two non-zero + // states: descriptor order is NOT count order, so an ordering mutant cannot pass by + // sorting. + { key: "ai:design", owner: "human", act: "rule on design Q", kind: "rule", hist: "design", histFold: [], occupancy: { lane: "vetter-verdicts" } }, + { key: "leak", owner: "human", act: "model it", kind: "blk", hist: "leaks", histFold: [], occupancy: { counts: "leaks", items: "leaks" } }, + ], + }; +} + +Deno.test("pipeline FSM: stateDescriptors drive the machine — owner grouping, act, count, and descriptor order", () => { + const box = fsmBox(descHq()); + // Exactly the descriptor states render, in owner-group order then descriptor order — + // and NONE of the frozen table's other boxes (un-vetted, closeCandidateUpheld, …). + const keys = box.querySelectorAll("[data-t]").map((b) => b.dataset.t); + assert( + JSON.stringify(keys) === + JSON.stringify(["uncoveredIssues", "ai:blocked-on", "ai:ready", "ai:design", "leak"]), + `descriptor order is render order, and only descriptor states render: ${JSON.stringify(keys)}`, + ); + const groups = ownerGroups(box); + assert(groups.length === 3, `three actor groups: ${groups.length}`); + const [producer, vetter, human] = groups; + assert( + JSON.stringify(producer.states) === JSON.stringify(["untouched (no PR)"]), + `producer files the descriptor's own states: ${JSON.stringify(producer.states)}`, + ); + // The re-laned state files under the VETTER — the descriptor's owner, not the frozen + // table's human resolution. + assert( + JSON.stringify(vetter.states) === JSON.stringify(["ai:blocked-on"]), + `blocked-on is vetter-owned by descriptor: ${JSON.stringify(vetter.states)}`, + ); + assert( + JSON.stringify(human.states) === JSON.stringify(["ai:ready", "ai:design", "leak"]), + `human states in descriptor order: ${JSON.stringify(human.states)}`, + ); + // Counts come from each descriptor's DECLARED source: the vet-lifecycle cell (3), never + // the counts mirror (99) and never the frozen table's producer-blocked lane (absent). + const byT = (k) => box.querySelectorAll("[data-t]").find((b) => b.dataset.t === k); + assert( + collect(byT("ai:blocked-on"), "sc")[0].textContent === "3", + `blocked-on reads its declared lane cell: ${collect(byT("ai:blocked-on"), "sc")[0].textContent}`, + ); + assert(vetter.title.endsWith("3"), `the vetter total is the cell's count: ${vetter.title}`); + assert( + collect(byT("ai:ready"), "sc")[0].textContent === "5", + `ready reads its lane cell: ${collect(byT("ai:ready"), "sc")[0].textContent}`, + ); + assert( + collect(byT("uncoveredIssues"), "sc")[0].textContent === "4", + `a counts-source state reads counts.: ${collect(byT("uncoveredIssues"), "sc")[0].textContent}`, + ); + // act + kind ride the descriptor. + assert( + collect(byT("ai:blocked-on"), "sa")[0].textContent === "clears when every typed dep merges/closes", + `the act is the descriptor's: ${collect(byT("ai:blocked-on"), "sa")[0].textContent}`, + ); + assert( + byT("ai:blocked-on").className.split(" ").includes("blk"), + `the kind is the descriptor's: ${byT("ai:blocked-on").className}`, + ); + // Sparse-absent lane cell ⇒ 0, dimmed — the existing empty-state language. + assert(collect(byT("ai:design"), "sc")[0].textContent === "0", "sparse-absent reads 0"); + assert( + byT("ai:design").className.split(" ").includes("zero"), + `a zero descriptor state dims like any other: ${byT("ai:design").className}`, + ); + // Everything claimed ⇒ no conservation band. + assert(collect(box, "fsm-defect").length === 0, "a conserved snapshot draws no defect band"); +}); + +Deno.test("pipeline FSM: descriptor occupancy reads its declared source — lane prs and counts+items alike, itemsAreIssues picking the path", () => { + const box = fsmBox(descHq()); + const detail = box.querySelectorAll("#fsmdetail")[0]; + const byT = (k) => box.querySelectorAll("[data-t]").find((b) => b.dataset.t === k); + + // Lane source: the cell's own prs, linked as PRs. + byT("ai:blocked-on").click(); + assert( + collect(detail, "li").length === 3, + `the lane cell's 3 prs list: ${collect(detail, "li").length}`, + ); + assert(textOf(detail).includes("blocked 1"), `the cell's OWN items: ${textOf(detail)}`); + assert( + collect(detail, "li")[0].href === "https://github.com/o/blocked/pull/1", + `lane items link as PRs: ${collect(detail, "li")[0].href}`, + ); + byT("ai:blocked-on").click(); + + // Counts source, itemsAreIssues: true — the /issues/ path is tried first. + byT("uncoveredIssues").click(); + assert( + collect(detail, "li").length === 4, + `the named top-level array lists: ${collect(detail, "li").length}`, + ); + assert( + collect(detail, "li")[0].href === "https://github.com/o/uncovered/issues/1", + `itemsAreIssues links as issues: ${collect(detail, "li")[0].href}`, + ); + byT("uncoveredIssues").click(); + + // Counts source, itemsAreIssues absent — items link as PRs. + byT("leak").click(); + assert( + collect(detail, "li")[0].href === "https://github.com/o/leak/pull/1", + `absent itemsAreIssues links as PRs: ${collect(detail, "li")[0].href}`, + ); + byT("leak").click(); + + // A counts-source descriptor whose named array never arrived: the count still renders + // and the click reads as detailBody's missing-list defect — never as an empty queue. + const noList = descHq(); + delete noList.leaks; + const box2 = fsmBox(noList); + const detail2 = box2.querySelectorAll("#fsmdetail")[0]; + box2.querySelectorAll("[data-t]").find((b) => b.dataset.t === "leak").click(); + assert( + textOf(detail2).includes("missing, not empty"), + `a count with no list reads as the missing-list defect: ${textOf(detail2)}`, + ); +}); + +// The part a naive consumer gets wrong (the schema contract calls it out): retired history +// keys fold into the descriptor's series per ITS histFold — generic keys with no HIST_FOLD +// entry prove the fold is the descriptor's, not the hand Map's. +Deno.test("pipeline FSM: a descriptor's histFold folds retired-key samples into its series", () => { + const now = Date.parse("2026-08-06T00:00:00Z"); + const at = (d) => now - d * DAY; + // Real inventory flat at 10 across a collapse: split 1 + 9 before, one key after. + // Folded ⇒ flat line; unfolded ⇒ a 1 → 10 cliff and a false bottleneck. + const history = [ + { t: at(4), counts: { current: 1, retiredKey: 9 } }, + { t: at(3), counts: { current: 1, retiredKey: 9 } }, + { t: at(2), counts: { current: 1, retiredKey: 9 } }, + { t: at(1), counts: { current: 10 } }, + { t: at(0), counts: { current: 10 } }, + ]; + const box = fsmBox({ + counts: {}, + lanes: { "vet-lifecycle": { st: { count: 10, prs: [] } } }, + stateDescriptors: [ + { key: "st", owner: "vetter", act: "vet", kind: "flow", hist: "current", histFold: ["retiredKey"], occupancy: { lane: "vet-lifecycle" } }, + ], + }, history); + const b = collect(box, "fsm-state").find((x) => x.dataset.t === "st"); + assert(b, "the descriptor box renders"); + const line = tags(b, "polyline")[0]; + assert(line, "the folded series draws a line"); + const ys = line.getAttribute("points").split(" ").map((p) => Number(p.split(",")[1])); + assert(ys.length === 5, `all five samples plot: ${ys.length}`); + assert( + ys.every((y) => y === ys[0]), + `the folded line is continuous, no cliff at the collapse: ${JSON.stringify(ys)}`, + ); + assert(!b.classList.contains("rising"), "a folded flat inventory raises no bottleneck flag"); +}); + +Deno.test("pipeline FSM: a descriptor's empty histFold folds nothing — the hand-maintained fallback Map never applies", () => { + const now = Date.parse("2026-08-06T00:00:00Z"); + const at = (d) => now - d * DAY; + // `reject` HAS a HIST_FOLD entry (humanReject folds in on the frozen fallback). A + // descriptor naming hist "reject" with histFold [] must read ONLY reject: flat 28. + // Falling back to the Map would draw 70, 70, 28 — a cliff the tool never declared. + const history = [ + { t: at(2), counts: { reject: 28, humanReject: 42 } }, + { t: at(1), counts: { reject: 28, humanReject: 42 } }, + { t: at(0), counts: { reject: 28 } }, + ]; + const box = fsmBox({ + counts: {}, + lanes: { "vetter-verdicts": { "ai:reject": { count: 28, prs: [] } } }, + stateDescriptors: [ + { key: "ai:reject", owner: "producer", act: "rework per note", kind: "blk", hist: "reject", histFold: [], occupancy: { lane: "vetter-verdicts" } }, + ], + }, history); + const b = collect(box, "fsm-state").find((x) => x.dataset.t === "ai:reject"); + const line = tags(b, "polyline")[0]; + assert(line, "the series draws"); + const ys = line.getAttribute("points").split(" ").map((p) => Number(p.split(",")[1])); + assert( + ys.length === 3 && ys.every((y) => y === ys[0]), + `an empty histFold reads the bare key — flat, no Map fold: ${JSON.stringify(ys)}`, + ); +}); + +// CONSERVATION, the invariant that would have made the blocked-on drift scream instead of +// hiding 17 PRs: every lanes cell and every counted top-level array is claimed by exactly +// one descriptor, or the panel says so loudly. +Deno.test("pipeline FSM: a lane cell no descriptor claims surfaces as a loud defect, never silence", () => { + const hq = descHq(); + // The tool re-laned a state but its descriptor list missed the old cell: 17 real PRs + // sit in occupancy nothing renders. + hq.lanes["producer-blocked"] = { "ai:blocked-on": { count: 17, prs: fcItems("stranded", 17) } }; + const box = fsmBox(hq); + const bands = collect(box, "fsm-defect"); + assert(bands.length === 1, `one defect band renders: ${bands.length}`); + const text = textOf(bands[0]); + assert( + text.includes('lanes["producer-blocked"]["ai:blocked-on"]'), + `it names the unclaimed cell: ${text}`, + ); + assert(text.includes("17"), `it names the held inventory: ${text}`); + assert(text.includes("renders nowhere"), `it says the failure in words: ${text}`); + assert( + collect(bands[0], "fd-li").length === 1, + `exactly the one unclaimed key is reported: ${collect(bands[0], "fd-li").length}`, + ); + // The band ADDS to the machine — every claimed state still renders beside it. + assert( + collect(box, "fsm-state").length === 5, + `the claimed machine still renders: ${collect(box, "fsm-state").length}`, + ); +}); + +Deno.test("pipeline FSM: a counts key no descriptor claims is a defect; a bare top-level array is not occupancy", () => { + const hq = descHq(); + // Drop the leak descriptor: counts.leaks (and with it the leaks array) goes unclaimed. + hq.stateDescriptors = hq.stateDescriptors.filter((d) => d.key !== "leak"); + // A top-level array with no counts entry is not occupancy the page knows how to count + // — it must not be reported. Neither may the descriptor array itself ever be. + hq.sideChannel = [{ repo: "o/x", number: 1 }]; + const box = fsmBox(hq); + const band = collect(box, "fsm-defect")[0]; + assert(band, "the defect band renders"); + const rows = collect(band, "fd-li").map((r) => r.textContent); + assert(rows.length === 1, `exactly the leaks row is reported: ${JSON.stringify(rows)}`); + assert( + rows[0].includes("leaks") && rows[0].includes("2 held") && rows[0].includes("renders nowhere"), + `it names the array and its held inventory: ${rows[0]}`, + ); +}); + +// Two DISTINCT descriptors reading one counts key: the same inventory wired into two +// series, so it renders twice. Distinct keys are what makes this the double-claim defect +// rather than the duplicate-key one — a lane claim id embeds the claiming state's own +// key, so two descriptors can only collide on a lane id by REPEATING a key, which the +// duplicate guard catches first and more specifically. The counts side has no such +// structural protection: the id is the counts key alone. +Deno.test("pipeline FSM: occupancy claimed by TWO descriptors is a defect too — exactly one claims each key", () => { + const hq = descHq(); + hq.stateDescriptors.push({ + key: "ai:ready-mirror", + owner: "producer", + act: "merge again", + kind: "flow", + // The SAME series `ai:ready` already draws. + hist: "ready", + histFold: [], + occupancy: { lane: "vetter-verdicts" }, + }); + const box = fsmBox(hq); + const band = collect(box, "fsm-defect")[0]; + assert(band, "a double claim surfaces"); + const text = textOf(band); + assert( + text.includes('counts["ready"]') && text.includes("2 descriptors") && text.includes("renders 2 times"), + `it names the key and says the inventory double-renders: ${text}`, + ); + assert( + !text.includes("repeats the key"), + `distinct keys read as a double CLAIM, not a duplicate key: ${text}`, + ); +}); + +Deno.test("pipeline FSM: a malformed descriptor renders no state but is reported — and takes no good descriptor with it", () => { + const box = fsmBox({ + counts: { uncoveredIssues: 4 }, + lanes: {}, + uncoveredIssues: fcItems("uncovered", 4), + stateDescriptors: [ + "junk", + { key: "no-occupancy", owner: "human", act: "x", kind: "blk", occupancy: {} }, + { key: "bad-owner", owner: "misc", act: "x", kind: "blk", occupancy: { lane: "vet-lifecycle" } }, + { + key: "uncoveredIssues", + owner: "producer", + act: "open a PR", + kind: "flow", + hist: "uncoveredIssues", + histFold: [], + occupancy: { counts: "uncoveredIssues", items: "uncoveredIssues", itemsAreIssues: true }, + label: "untouched (no PR)", + }, + ], + }); + const keys = box.querySelectorAll("[data-t]").map((b) => b.dataset.t); + assert( + JSON.stringify(keys) === JSON.stringify(["uncoveredIssues"]), + `only the well-formed descriptor renders a state: ${JSON.stringify(keys)}`, + ); + assert( + collect(box, "sc")[0].textContent === "4", + `and it renders its real count: ${collect(box, "sc")[0].textContent}`, + ); + const band = collect(box, "fsm-defect")[0]; + assert(band, "malformed descriptors surface in the defect band"); + const text = textOf(band); + for (const i of [0, 1, 2]) { + assert( + text.includes("stateDescriptors[" + i + "]"), + `descriptor ${i} is reported by index: ${text}`, + ); + } +}); + +Deno.test("pipeline FSM: a descriptor's kind is clamped to the display kinds — snapshot content cannot smuggle a class onto a box", () => { + const box = fsmBox({ + counts: { uncoveredIssues: 4 }, + lanes: {}, + uncoveredIssues: fcItems("uncovered", 4), + stateDescriptors: [ + { + key: "uncoveredIssues", + owner: "producer", + act: "open a PR", + // Not a display kind: must not land in className, where it would dim a box + // holding 4 real items (or fake a bottleneck / a selection). + kind: "zero", + hist: "", + histFold: [], + occupancy: { counts: "uncoveredIssues", items: "uncoveredIssues", itemsAreIssues: true }, + }, + ], + }); + const b = collect(box, "fsm-state")[0]; + assert(b, "the box renders, with the neutral default border"); + const classes = b.className.split(" "); + for (const cls of ["zero", "rising", "sel", "lead"]) { + assert(!classes.includes(cls), `unknown kind never lands as .${cls}: ${b.className}`); + } +}); + +// The snapshot is untrusted field by field: a lane cell that EXISTS but carries a junk or +// missing `count` renders 0 (dimmed), never NaN — the same coercion the sparse-absent cell +// gets, asserted separately because Number(null) is 0 while Number(undefined) is NaN, so +// only this shape catches a dropped `|| 0`. +Deno.test("pipeline FSM: a descriptor lane cell with a junk count renders 0, never NaN", () => { + const box = fsmBox({ + counts: {}, + lanes: { "vetter-verdicts": { "ai:ready": { count: "junk", prs: [] } } }, + stateDescriptors: [ + { key: "ai:ready", owner: "human", act: "merge", kind: "flow", hist: "", histFold: [], occupancy: { lane: "vetter-verdicts" } }, + ], + }); + const b = collect(box, "fsm-state")[0]; + assert(b, "the box renders"); + assert( + collect(b, "sc")[0].textContent === "0", + `a junk count coerces to 0: ${collect(b, "sc")[0].textContent}`, + ); + assert( + b.className.split(" ").includes("zero"), + `and the box dims like any zero state: ${b.className}`, + ); +}); + +// The occupancy pair exists because the two names CAN disagree (the leak state's key and +// array already do): a descriptor claims both its counts key and its items array, so a +// conserved snapshot with a differently-named items array raises no spurious defect. +Deno.test("pipeline FSM: a descriptor claims BOTH its counts key and a differently-named items array", () => { + const box = fsmBox({ + counts: { leakCount: 2 }, + lanes: {}, + leakItems: fcItems("leak", 2), + stateDescriptors: [ + { key: "leak", owner: "human", act: "model it", kind: "blk", hist: "", histFold: [], occupancy: { counts: "leakCount", items: "leakItems" } }, + ], + }); + assert( + collect(box, "fsm-defect").length === 0, + "the items array is claimed too — no spurious defect", + ); + assert( + collect(box, "sc")[0].textContent === "2", + "the count reads from counts.", + ); + const detail = box.querySelectorAll("#fsmdetail")[0]; + box.querySelectorAll("[data-t]")[0].click(); + assert( + textOf(detail).includes("leak 1"), + "the click lists from the named items array", + ); +}); + +// The retired-residue contract (issue-pr-cron#130, 2026-08-06 clarification): a retired +// state's descriptor is EMITTED ONLY WHILE its occupancy is nonzero, so the consumer +// renders every emitted descriptor and carries NO retired-detection of its own — the +// residue is a state like any other while it lasts, and it leaves the shape when the +// tool stops emitting it (together with its cell and counts key). +Deno.test("pipeline FSM: a residue descriptor renders while emitted; drained, its absence renders nothing and nothing screams", () => { + const withResidue = { + counts: { blockedInfra: 2, ready: 1 }, + lanes: { + "producer-blocked": { "ai:blocked-infra": { count: 2, prs: fcItems("residue", 2) } }, + "vetter-verdicts": { "ai:ready": { count: 1, prs: [] } }, + }, + stateDescriptors: [ + { key: "ai:blocked-infra", owner: "producer", act: "strip the label", kind: "blk", hist: "blockedInfra", histFold: [], occupancy: { lane: "producer-blocked" }, label: "ai:blocked-infra (retired #108)" }, + { key: "ai:ready", owner: "human", act: "merge", kind: "flow", hist: "ready", histFold: [], occupancy: { lane: "vetter-verdicts" } }, + ], + }; + const box = fsmBox(withResidue); + const b = collect(box, "fsm-state").find((x) => x.dataset.t === "ai:blocked-infra"); + assert(b, "the residue renders while the tool emits its descriptor"); + assert( + collect(b, "sc")[0].textContent === "2", + `it carries its cell's count: ${collect(b, "sc")[0].textContent}`, + ); + assert( + collect(b, "sk")[0].textContent === "ai:blocked-infra (retired #108)", + `the retired suffix rides the emitted label: ${collect(b, "sk")[0].textContent}`, + ); + assert(collect(box, "fsm-defect").length === 0, "a claimed residue is conserved"); + + // Drained: the tool drops the descriptor, the cell and the counts key TOGETHER. + const drained = { + counts: { ready: 1 }, + lanes: { "vetter-verdicts": { "ai:ready": { count: 1, prs: [] } } }, + stateDescriptors: [ + { key: "ai:ready", owner: "human", act: "merge", kind: "flow", hist: "ready", histFold: [], occupancy: { lane: "vetter-verdicts" } }, + ], + }; + const box2 = fsmBox(drained); + assert( + !collect(box2, "fsm-state").some((x) => x.dataset.t === "ai:blocked-infra"), + "no box for the drained retired state — no permanent dimmed residue", + ); + assert( + collect(box2, "fsm-state").some((x) => x.dataset.t === "ai:ready"), + "the live machine still renders", + ); + assert(collect(box2, "fsm-defect").length === 0, "and nothing screams: nothing unclaimed remains"); +}); + +// The counts-side scoping the ratified contract froze: exactly four named keys may go +// unclaimed — the non-state metrics (totalProducerPrs, openIssues, archivedRepoPrs) and +// the legacy duplicate of claimed inventory (closeCandidateIssues). Anything else is +// state-shaped occupancy rendering nowhere. +Deno.test("pipeline FSM: the frozen-legacy counts keys never defect; any other unclaimed counts key does", () => { + const conserved = () => ({ + counts: { ready: 2, totalProducerPrs: 145, openIssues: 767, archivedRepoPrs: 4, closeCandidateIssues: 0 }, + lanes: { "vetter-verdicts": { "ai:ready": { count: 2, prs: [] } } }, + closeCandidateIssues: [], + archivedRepoPrs: fcItems("archived", 4), + stateDescriptors: [ + { key: "ai:ready", owner: "human", act: "merge", kind: "flow", hist: "ready", histFold: [], occupancy: { lane: "vetter-verdicts" } }, + ], + }); + assert( + collect(fsmBox(conserved()), "fsm-defect").length === 0, + "the four frozen-legacy keys are the whole exception — no defect band", + ); + const drifted = conserved(); + drifted.counts.blockedOn = 17; + const band = collect(fsmBox(drifted), "fsm-defect")[0]; + assert(band, "an unclaimed state-shaped counts key surfaces"); + const text = textOf(band); + assert( + text.includes("blockedOn") && text.includes("17 held") && text.includes("renders nowhere"), + `it names the key and its held inventory: ${text}`, + ); +}); + +// The same class one level down: a lane cell carries its own two numbers — the `count` it +// STATES and the `prs` it HOLDS. A claimed cell leaves the sweep early (n === 1), so a +// broken or absent count over a populated list would render 0 while the click expanded +// onto real PRs, in silence. +Deno.test("pipeline FSM: a lane cell whose count disagrees with its prs is reported, and the box renders the larger", () => { + for (const [what, count] of [["junk count", "many"], ["absent count", undefined]]) { + const cell = { prs: fcItems("stranded", 17) }; + if (count !== undefined) cell.count = count; + const box = fsmBox({ + counts: {}, + lanes: { "vet-lifecycle": { "ai:blocked-on": cell } }, + stateDescriptors: [ + { key: "ai:blocked-on", owner: "vetter", act: "clears when deps merge/close", kind: "blk", hist: "", histFold: [], occupancy: { lane: "vet-lifecycle" } }, + ], + }); + const b = collect(box, "fsm-state").find((x) => x.dataset.t === "ai:blocked-on"); + assert(b, `the box renders (${what})`); + // The box shows the larger, so the list cannot hide inventory behind a broken count. + assert( + collect(b, "sc")[0].textContent === "17", + `the box renders the PRs it holds, not 0 (${what}): ${collect(b, "sc")[0].textContent}`, + ); + // …and the count-vs-rows invariant holds: the number on the box is the number of rows. + const detail = box.querySelectorAll("#fsmdetail")[0]; + b.click(); + assert( + collect(detail, "li").length === 17, + `the box's number equals the rows it expands to (${what}): ${collect(detail, "li").length}`, + ); + const band = collect(box, "fsm-defect")[0]; + assert(band, `the disagreement is reported (${what})`); + const text = textOf(band); + assert( + text.includes("states count 0") && text.includes("carries 17 PRs") && text.includes("renders 17"), + `it names both numbers and which was rendered (${what}): ${text}`, + ); + } +}); + +Deno.test("pipeline FSM: a lane cell whose count agrees with its prs reports nothing", () => { + const box = fsmBox({ + counts: {}, + lanes: { "vet-lifecycle": { "ai:blocked-on": { count: 3, prs: fcItems("blocked", 3) } } }, + stateDescriptors: [ + { key: "ai:blocked-on", owner: "vetter", act: "clears when deps merge/close", kind: "blk", hist: "", histFold: [], occupancy: { lane: "vet-lifecycle" } }, + ], + }); + assert(collect(box, "fsm-defect").length === 0, "an agreeing cell draws no band"); + const b = collect(box, "fsm-state")[0]; + assert(collect(b, "sc")[0].textContent === "3", "and renders its stated count"); +}); + +// The page holds TWO numbers for a lane state — the lane cell it RENDERS and the `counts` +// key its SERIES is drawn from — and the tool computes them two ways +// (issue-pr-cron#228). Every claim check passes when they disagree (each key is claimed +// exactly once), so only a comparison catches it: the box would draw one number under a +// sparkline sitting at the other, silently. The live witness the moment the producer side +// lands: `counts.ready` is 23 off the label bucket while the `vetter-verdicts` cell is +// ABSENT (all 23 are un-vetted at head), so the box renders 0, dimmed, under a line at 23. +Deno.test("pipeline FSM: a lane state whose counts mirror contradicts its rendered cell is a loud defect naming both numbers", () => { + const box = fsmBox({ + // The label bucket says 23; classify_lane routed all of them to un-vetted, so the + // vetter-verdicts cell for ai:ready is absent entirely. + counts: { ready: 23, unvetted: 24 }, + lanes: { "vet-lifecycle": { "un-vetted": { count: 24, prs: [] } } }, + stateDescriptors: [ + { key: "un-vetted", owner: "vetter", act: "vet at current head", kind: "flow", hist: "unvetted", histFold: [], occupancy: { lane: "vet-lifecycle" } }, + { key: "ai:ready", owner: "human", act: "merge", kind: "flow", hist: "ready", histFold: [], occupancy: { lane: "vetter-verdicts" } }, + ], + }); + // The box renders its DECLARED occupancy — the cell, not the mirror. + const ready = box.querySelectorAll("[data-t]").find((b) => b.dataset.t === "ai:ready"); + assert( + collect(ready, "sc")[0].textContent === "0", + `the box renders the lane cell, absent ⇒ 0: ${collect(ready, "sc")[0].textContent}`, + ); + const band = collect(box, "fsm-defect")[0]; + assert(band, "the disagreement surfaces"); + const text = textOf(band); + assert(text.includes('counts["ready"]') && text.includes("23"), `it names the mirror and its number: ${text}`); + assert( + text.includes('lanes["vetter-verdicts"]["ai:ready"]') && text.includes("holds 0"), + `it names the rendered occupancy and its number: ${text}`, + ); + assert(text.includes("renders 0"), `it says which number is on screen: ${text}`); + assert( + text.includes("disagrees with itself"), + `it reports a snapshot contradiction, not a guess at the right number: ${text}`, + ); + // The agreeing state says nothing. + assert( + !text.includes("un-vetted"), + `a state whose two numbers agree stays silent: ${text}`, + ); + // And the claim is KEPT: no "unclaimed counts key" defect rides along. + assert(!text.includes("renders nowhere"), `the mirror is still claimed: ${text}`); +}); + +Deno.test("pipeline FSM: lane states whose counts mirror agrees draw no divergence defect", () => { + // descHq's mirrors agree with their cells (ready 5, blockedOn 3), and ai:design has a + // sparse-absent cell with no mirror at all — neither shape may report. + assert( + collect(fsmBox(descHq()), "fsm-defect").length === 0, + "a self-consistent snapshot stays silent", + ); +}); + +// The frozen-legacy exception licenses a key going UNCLAIMED; it never licenses the same +// inventory being wired twice. Two descriptors both drawing `openIssues` would render the +// open-issue population as two state series. +Deno.test("pipeline FSM: a frozen-legacy counts key is exempt from unclaimed, never from a DOUBLE claim", () => { + const box = fsmBox({ + counts: { openIssues: 767, ready: 0 }, + lanes: {}, + stateDescriptors: [ + { key: "a", owner: "producer", act: "x", kind: "flow", hist: "openIssues", histFold: [], occupancy: { lane: "vetter-verdicts" } }, + { key: "b", owner: "vetter", act: "y", kind: "flow", hist: "openIssues", histFold: [], occupancy: { lane: "vetter-verdicts" } }, + ], + }); + const band = collect(box, "fsm-defect")[0]; + assert(band, "the double claim on a frozen-legacy key surfaces"); + const text = textOf(band); + assert( + text.includes('counts["openIssues"]') && text.includes("2 descriptors"), + `it names the key and the double claim: ${text}`, + ); +}); + +// Two descriptors naming the same top-level array: distinct keys, possibly distinct +// counts keys, and yet both boxes open onto ONE list — #141 by another route, invisible +// to the lane and counts checks. +Deno.test("pipeline FSM: two descriptors naming the same items array is a defect", () => { + const box = fsmBox({ + counts: { leaks: 2, alsoLeaks: 2 }, + lanes: {}, + leaks: fcItems("leak", 2), + stateDescriptors: [ + { key: "leak", owner: "human", act: "model it", kind: "blk", hist: "leaks", histFold: [], occupancy: { counts: "leaks", items: "leaks" } }, + { key: "leak-copy", owner: "producer", act: "model it", kind: "blk", hist: "alsoLeaks", histFold: [], occupancy: { counts: "alsoLeaks", items: "leaks" } }, + ], + }); + const band = collect(box, "fsm-defect")[0]; + assert(band, "the shared list surfaces"); + const text = textOf(band); + assert( + text.includes('"leaks" is named by 2 descriptors') && text.includes("open onto the same list"), + `it names the array and the collision: ${text}`, + ); + // One array named once is not a defect. + assert( + collect(fsmBox(descHq()), "fsm-defect").length === 0, + "distinct arrays stay silent", + ); +}); + +// The zero-held carve-out must not swallow a cell that HOLDS PRs behind a dropped or +// non-numeric count — the one place the sweep has the list in hand to check. +Deno.test("pipeline FSM: an unclaimed cell holding PRs with no usable count still screams", () => { + for (const [what, cell] of [ + ["absent count", { prs: fcItems("stranded", 17) }], + ["junk count", { count: "many", prs: fcItems("stranded", 17) }], + ]) { + const box = fsmBox({ + counts: { ready: 1 }, + lanes: { + "vetter-verdicts": { "ai:ready": { count: 1, prs: [] } }, + "producer-blocked": { "ai:blocked-on": cell }, + }, + stateDescriptors: [ + { key: "ai:ready", owner: "human", act: "merge", kind: "flow", hist: "ready", histFold: [], occupancy: { lane: "vetter-verdicts" } }, + ], + }); + const band = collect(box, "fsm-defect")[0]; + assert(band, `the stranded cell surfaces (${what})`); + assert( + textOf(band).includes("17 held"), + `the list it carries is what it holds (${what}): ${textOf(band)}`, + ); + } +}); + +// Prototype-shaped strings off the untrusted snapshot must be ordinary data everywhere +// they are used as a lookup or a set member. The code is written to be immune (Map/Set, +// hasOwnProperty.call, Array.isArray); this pins it so a refactor to plain objects — +// where `__proto__` and `constructor` are not ordinary keys — fails loudly instead of +// silently mis-claiming, mis-rendering or throwing. +Deno.test("pipeline FSM: prototype-shaped keys, counts, items and lane names are ordinary data", () => { + // Built through JSON.parse — the only way to get a genuine OWN `__proto__` key (an + // object literal's `__proto__:` sets the prototype instead), and exactly how the page + // receives the snapshot: `fetch(...).then((r) => r.json())`. + const box = fsmBox(JSON.parse(JSON.stringify({ + counts: { PROTO: 3, constructor: 2, toString: 0 }, + lanes: { constructor: { PROTO: { count: 4, prs: fcItems("proto", 4) } } }, + PROTO: fcItems("protoitems", 3), + stateDescriptors: [ + // A lane-sourced state whose key AND lane are prototype-shaped. + { key: "PROTO", owner: "vetter", act: "vet", kind: "flow", hist: "", histFold: [], occupancy: { lane: "constructor" } }, + // A counts-sourced state whose counts key and items array are prototype-shaped. + { key: "protoCounts", owner: "producer", act: "open a PR", kind: "flow", hist: "constructor", histFold: [], occupancy: { counts: "constructor", items: "PROTO", itemsAreIssues: true } }, + ], + }).replace(/PROTO/g, "__proto__"))); + const keys = box.querySelectorAll("[data-t]").map((b) => b.dataset.t); + assert( + JSON.stringify(keys) === JSON.stringify(["protoCounts", "__proto__"]), + `both states render, keyed by their own strings: ${JSON.stringify(keys)}`, + ); + const byT = (k) => box.querySelectorAll("[data-t]").find((b) => b.dataset.t === k); + assert( + collect(byT("__proto__"), "sc")[0].textContent === "4", + `a prototype-shaped lane+key resolves its own cell: ${collect(byT("__proto__"), "sc")[0].textContent}`, + ); + assert( + collect(byT("protoCounts"), "sc")[0].textContent === "2", + `a prototype-shaped counts key resolves its own number: ${collect(byT("protoCounts"), "sc")[0].textContent}`, + ); + // Each opens onto its OWN list — registration is keyed by these strings too. + const detail = box.querySelectorAll("#fsmdetail")[0]; + byT("__proto__").click(); + assert(textOf(detail).includes("proto 1"), `the lane cell's own list: ${textOf(detail)}`); + byT("__proto__").click(); + byT("protoCounts").click(); + assert(textOf(detail).includes("protoitems 1"), `the named array's own list: ${textOf(detail)}`); + // The sweep treats them as data: `counts.__proto__` (3 held) is claimed by nobody and + // reports; the claimed `constructor` does not; `toString` holds 0 and is skipped. + const band = collect(box, "fsm-defect")[0]; + assert(band, "the unclaimed prototype-shaped counts key reports"); + const rows = collect(band, "fd-li").map((r) => r.textContent); + assert( + rows.some((r) => r.includes('counts["__proto__"]') && r.includes("3 held")), + `it names the unclaimed prototype-shaped key: ${JSON.stringify(rows)}`, + ); + assert( + !rows.some((r) => r.includes('counts["constructor"]')), + `a claimed prototype-shaped key is not reported: ${JSON.stringify(rows)}`, + ); + assert( + !rows.some((r) => r.includes('counts["toString"]')), + `a zero-held prototype-shaped key is skipped like any other: ${JSON.stringify(rows)}`, + ); +}); + +// A state key is the panel's IDENTITY for a state — the box's `data-t` and the +// register/itemsFor map key the click-to-expand reads — so two descriptors sharing one +// key let the second's registration OVERWRITE the first's, and the first box opens onto +// the second's list. #141's exact failure, and neither claim check sees it: with +// different occupancy sources the two claims are different ids, so both read as singly +// claimed. The repeat therefore renders no state and is reported, first occurrence wins. +Deno.test("pipeline FSM: a repeated descriptor key renders one box and is reported, with the loser's occupancy named unclaimed", () => { + const box = fsmBox({ + counts: { ready: 5, readyDupe: 9 }, + lanes: { "vetter-verdicts": { "ai:ready": { count: 5, prs: fcItems("first", 5) } } }, + readyDupe: fcItems("second", 9), + stateDescriptors: [ + // FIRST wins: array order is render order. + { key: "ai:ready", owner: "human", act: "merge", kind: "flow", hist: "ready", histFold: [], occupancy: { lane: "vetter-verdicts" } }, + // Same key, a DIFFERENT occupancy source — so the two claims are different ids and + // the sweep alone would call both singly claimed. + { key: "ai:ready", owner: "producer", act: "open a PR", kind: "flow", hist: "readyDupe", histFold: [], occupancy: { counts: "readyDupe", items: "readyDupe", itemsAreIssues: true } }, + ], + }); + const boxes = box.querySelectorAll("[data-t]").filter((b) => b.dataset.t === "ai:ready"); + assert(boxes.length === 1, `exactly one box carries the key: ${boxes.length}`); + assert( + collect(boxes[0], "sc")[0].textContent === "5", + `the FIRST descriptor's occupancy renders: ${collect(boxes[0], "sc")[0].textContent}`, + ); + // Its click-through is its OWN list, not the repeat's — the overwrite this guard exists + // to prevent. + const detail = box.querySelectorAll("#fsmdetail")[0]; + boxes[0].click(); + assert(collect(detail, "li").length === 5, `it opens onto its own 5 rows: ${collect(detail, "li").length}`); + assert(textOf(detail).includes("first 1"), `and onto its own items: ${textOf(detail)}`); + assert(!textOf(detail).includes("second 1"), "never the repeat's list"); + + const band = collect(box, "fsm-defect")[0]; + assert(band, "the repeat is reported"); + const rows = collect(band, "fd-li").map((r) => r.textContent); + assert( + rows.some((r) => r.includes("stateDescriptors[1]") && r.includes('repeats the key "ai:ready"')), + `named by index and key: ${JSON.stringify(rows)}`, + ); + // Its claim was never registered, so the occupancy it meant to claim reads unclaimed. + assert( + rows.some((r) => r.includes("readyDupe") && r.includes("9 held") && r.includes("renders nowhere")), + `the loser's occupancy is named unclaimed: ${JSON.stringify(rows)}`, + ); +}); + +Deno.test("pipeline FSM: all-distinct descriptor keys draw no duplicate-key defect", () => { + assert( + collect(fsmBox(descHq()), "fsm-defect").length === 0, + "a conserved, distinctly-keyed snapshot still renders no band", + ); +}); + +// A descriptor also READS its `histFold` keys — that is where its series draws the +// absorbed retirees' samples from — so those counts keys are claimed by it. The live +// shape that makes this load-bearing: `ai:reject` folds `humanReject`/`relink` while the +// residue rows carry NO `hist` of their own (the successor draws the past, so the same +// history is not rendered twice). Without the fold claim, a nonzero absorbed-retiree +// count paints a FALSE defect on a healthy snapshot. +Deno.test("pipeline FSM: a folded retired counts key is claimed by the folding descriptor, nonzero and all", () => { + const hq = { + counts: { reject: 66, humanReject: 4, relink: 0 }, + lanes: { + "vetter-verdicts": { "ai:reject": { count: 66, prs: [] } }, + "human-decisions": { "human:reject": { count: 4, prs: fcItems("hreject", 4) } }, + }, + stateDescriptors: [ + // The live row folds both retired keys and owns the continuous series. + { key: "ai:reject", owner: "producer", act: "rework per note", kind: "blk", hist: "reject", histFold: ["humanReject", "relink"], occupancy: { lane: "vetter-verdicts" } }, + // The residue row: its LANE cell is its occupancy; it carries no hist, because the + // successor above already draws its past. + { key: "human:reject", owner: "human", act: "strip the label", kind: "blk", hist: "", histFold: [], occupancy: { lane: "human-decisions" }, label: "human:reject (retired #133)" }, + ], + }; + const box = fsmBox(hq); + assert( + collect(box, "fsm-defect").length === 0, + `a folded nonzero retired key is claimed, not a defect: ${textOf(collect(box, "fsm-defect")[0] || makeEl("div"))}`, + ); + // Both rows render, each off its own lane cell. + const byT = (k) => box.querySelectorAll("[data-t]").find((b) => b.dataset.t === k); + assert(collect(byT("ai:reject"), "sc")[0].textContent === "66", "the live row reads its cell"); + assert(collect(byT("human:reject"), "sc")[0].textContent === "4", "the residue row reads its cell"); + + // And the fold is not a blanket amnesty: a retired counts key NO descriptor folds or + // reads still screams when it holds inventory. + const stranded = JSON.parse(JSON.stringify(hq)); + stranded.counts.awaitingReVet = 7; + const band = collect(fsmBox(stranded), "fsm-defect")[0]; + assert(band, "an unfolded, unclaimed retired key still surfaces"); + assert( + textOf(band).includes("awaitingReVet") && textOf(band).includes("7 held"), + `it names the stranded key: ${textOf(band)}`, + ); +}); + +// The fold claim must not weaken the double-claim direction: a key claimed BOTH as one +// descriptor's fold and as another's live hist is the same inventory wired twice — the +// producer side is held to fold keys being retired (never a live row's hist), so this +// reads as the structural defect it is. +Deno.test("pipeline FSM: a counts key claimed as both a fold and another descriptor's hist is still a double claim", () => { + const box = fsmBox({ + counts: { reject: 66, humanReject: 4 }, + lanes: { + "vetter-verdicts": { "ai:reject": { count: 66, prs: [] } }, + "human-decisions": { "human:reject": { count: 4, prs: [] } }, + }, + stateDescriptors: [ + { key: "ai:reject", owner: "producer", act: "rework per note", kind: "blk", hist: "reject", histFold: ["humanReject"], occupancy: { lane: "vetter-verdicts" } }, + // Illegal per the contract: the residue row draws the SAME history the fold above + // already absorbed, so humanReject's inventory would render twice. + { key: "human:reject", owner: "human", act: "strip the label", kind: "blk", hist: "humanReject", histFold: [], occupancy: { lane: "human-decisions" } }, + ], + }); + const band = collect(box, "fsm-defect")[0]; + assert(band, "the double claim surfaces"); + const text = textOf(band); + assert( + text.includes("humanReject") && text.includes("2 descriptors") && text.includes("renders 2 times"), + `it names the key and says it renders twice: ${text}`, + ); +}); + +// Zero inventory renders nowhere VACUOUSLY: the live tool still emits drained retired +// counts keys at 0 (kept-while-nonzero governs the descriptor list, not the keys), so an +// unclaimed key holding nothing is not hidden inventory and must not paint a permanent +// false defect on a healthy snapshot. The same keys NONZERO are the real hazard and +// scream; a DOUBLE claim is structural — the same inventory wired twice — and defects at +// any count, zero included. +Deno.test("pipeline FSM: zero-held unclaimed occupancy is vacuously conserved; nonzero screams; a zero-held double claim still defects", () => { + const base = () => ({ + counts: { ready: 2, blockedInfra: 0 }, + lanes: { + "vetter-verdicts": { "ai:ready": { count: 2, prs: [] } }, + "producer-blocked": { "ai:blocked-deploy": { count: 0, prs: [] } }, + }, + stateDescriptors: [ + { key: "ai:ready", owner: "human", act: "merge", kind: "flow", hist: "ready", histFold: [], occupancy: { lane: "vetter-verdicts" } }, + ], + }); + assert( + collect(fsmBox(base()), "fsm-defect").length === 0, + "a drained retired counts key and an empty unclaimed cell hide nothing — no band", + ); + + const held = base(); + held.counts.blockedInfra = 2; + held.lanes["producer-blocked"]["ai:blocked-deploy"].count = 3; + const band = collect(fsmBox(held), "fsm-defect")[0]; + assert(band, "the same keys nonzero surface"); + const text = textOf(band); + assert( + text.includes("blockedInfra") && text.includes("2 held"), + `the nonzero drained key screams: ${text}`, + ); + assert( + text.includes("ai:blocked-deploy") && text.includes("3 held"), + `the nonzero unclaimed cell screams: ${text}`, + ); + + // Two DISTINCT descriptors reading the SAME zero-held counts key — a double claim, not + // a duplicate key (which is its own, earlier defect). + const doubled = base(); + doubled.stateDescriptors.push( + { key: "ai:blocked-infra", owner: "human", act: "strip the label", kind: "blk", hist: "blockedInfra", histFold: [], occupancy: { lane: "producer-blocked" } }, + { key: "ai:blocked-infra-mirror", owner: "producer", act: "strip the label", kind: "blk", hist: "blockedInfra", histFold: [], occupancy: { lane: "producer-blocked" } }, + ); + const band2 = collect(fsmBox(doubled), "fsm-defect")[0]; + assert(band2, "a double claim on a zero-held cell still surfaces"); + assert( + textOf(band2).includes("2 descriptors"), + `it reads as the structural defect it is: ${textOf(band2)}`, + ); +}); + +// A counts-source descriptor's declared keys must RESOLVE in the snapshot: a tool that +// emits a descriptor pointing at nothing has broken its own contract, and the count/list +// it strands must not degrade into a quiet zero. +Deno.test("pipeline FSM: a descriptor whose counts or items source does not resolve is a loud defect", () => { + const box = fsmBox({ + counts: {}, + lanes: {}, + stateDescriptors: [ + { key: "uncoveredIssues", owner: "producer", act: "open a PR", kind: "flow", hist: "", histFold: [], occupancy: { counts: "uncoveredIssues", items: "uncoveredIssues", itemsAreIssues: true } }, + ], + }); + const band = collect(box, "fsm-defect")[0]; + assert(band, "unresolved sources surface"); + const rows = collect(band, "fd-li").map((r) => r.textContent); + assert(rows.length === 2, `both unresolved sources report: ${JSON.stringify(rows)}`); + assert( + rows.some((r) => r.includes('counts source "uncoveredIssues"') && r.includes("does not resolve")), + `the counts source is named: ${JSON.stringify(rows)}`, + ); + assert( + rows.some((r) => r.includes('items source "uncoveredIssues"') && r.includes("does not resolve")), + `the items source is named: ${JSON.stringify(rows)}`, + ); +}); + +// The frozen fallback: a descriptor-less snapshot renders the hand table exactly as +// before this change — same boxes, same order — with no conservation band. Junk (a +// non-array) is not a descriptor list and falls back the same way. +Deno.test("pipeline FSM: a descriptor-less snapshot renders the frozen hand table, defect-free — junk stateDescriptors falls back too", () => { + const lanesHq = () => ({ + counts: { leaks: 1, ready: 2, uncoveredIssues: 3, closeCandidateUnvetted: 0, closeCandidateUpheld: 0 }, + lanes: { + "vetter-verdicts": { + "ai:ready": { count: 2, prs: [] }, + "ai:reject": { count: 1, prs: [] }, + }, + }, + leaks: fcItems("leak", 1), + uncoveredIssues: fcItems("uncovered", 3), + }); + const frozen = (box, what) => { + const keys = box.querySelectorAll("[data-t]").map((b) => b.dataset.t); + assert( + JSON.stringify(keys) === JSON.stringify([ + "ai:reject", + "uncoveredIssues", + "un-vetted", + "closeCandidateUnvetted", + "ai:blocked-on", + "leak", + "ai:ready", + "ai:design", + "human:design", + "closeCandidateUpheld", + ]), + `the frozen table renders whole and in order (${what}): ${JSON.stringify(keys)}`, + ); + assert( + collect(box, "fsm-defect").length === 0, + `no conservation band on the fallback (${what})`, + ); + }; + frozen(fsmBox(lanesHq()), "absent"); + frozen(fsmBox({ ...lanesHq(), stateDescriptors: "junk" }), "junk descriptors"); +}); + +// An EMPTY stateDescriptors array is a present — broken — declaration: the machine claims +// zero states while the snapshot holds occupancy. The hand table must not resurrect +// (vocabulary lives in the tool), and the conservation band must scream instead. +Deno.test("pipeline FSM: an empty stateDescriptors array screams via conservation rather than resurrecting the hand table", () => { + const box = fsmBox({ + counts: { ready: 2 }, + lanes: { "vetter-verdicts": { "ai:ready": { count: 2, prs: [] } } }, + stateDescriptors: [], + }); + assert( + collect(box, "fsm-state").length === 0, + `no hand-table box resurrects: ${collect(box, "fsm-state").length}`, + ); + const band = collect(box, "fsm-defect")[0]; + assert(band, "the unclaimed occupancy surfaces"); + assert( + textOf(band).includes("ai:ready"), + `it names the stranded cell: ${textOf(band)}`, + ); +}); + +// The panel's existing behaviors survive descriptor mode: sparklines draw from the +// descriptor's hist key, and the per-actor lead fallback marks the largest flat queue. +Deno.test("pipeline FSM: descriptor boxes keep sparklines and the lead fallback", () => { + const now = Date.parse("2026-08-06T00:00:00Z"); + const history = [ + { t: now - 2 * DAY, counts: { ready: 5 } }, + { t: now, counts: { ready: 5 } }, + ]; + const box = fsmBox(descHq(), history); + const byT = (k) => box.querySelectorAll("[data-t]").find((b) => b.dataset.t === k); + const ready = byT("ai:ready"); + assert( + collect(ready, "fsm-spark").length === 1, + "the descriptor's hist key draws a sparkline", + ); + // Human group: ready 5, design 0, leak 2 — nothing rising, so the largest queue leads. + assert( + ready.classList.contains("lead"), + "the lead fallback marks the actor's largest queue", + ); + assert( + !byT("leak").classList.contains("lead"), + "and only that one", + ); +}); + // ---- deployments.html: known owners ---- // renderDeployments takes (document, $, data) as its own params, so bind with no