From c5130b8dbf0f1d3127053fb403c8fe91098dbafd Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 6 Aug 2026 10:42:06 +0000 Subject: [PATCH 01/11] pipeline dash: render the machine from emitted stateDescriptors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The consumer half of issue-pr-cron#130 (rain-org-health#164): a snapshot carrying stateDescriptors IS the machine — owner grouping, per-state box (key/label/act/kind), occupancy from each descriptor's declared source ({lane} cell with sparse-absent => 0, or {counts, items, itemsAreIssues}), history series from hist with retired-key samples folded per the descriptor's own histFold, descriptor order as render order. The hand-written STATES table and HIST_FOLD survive only as the frozen fallback for descriptor-less snapshots, the same posture as the legacy no-lanes branch. Conservation: every lanes cell and every counted top-level array must be claimed by exactly one descriptor; unclaimed (and double-claimed) occupancy renders as a loud --crit defect band in words — the invariant that would have made the blocked-on re-lane scream instead of hiding 17 PRs. Malformed descriptors are reported the same way, never silently dropped, and a descriptor's kind is clamped so snapshot content cannot smuggle a meaningful class onto a box. The live snapshot carries no stateDescriptors until the producer side of issue-pr-cron#130 lands, and this render is a behavioral no-op on such snapshots, so it can merge first. Co-Authored-By: Claude Fable 5 --- site/pipeline.html | 207 ++++++++++++++++++- test/dashboard.test.js | 457 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 653 insertions(+), 11 deletions(-) diff --git a/site/pipeline.html b/site/pipeline.html index 09d5a0a..51549b3 100644 --- a/site/pipeline.html +++ b/site/pipeline.html @@ -179,6 +179,14 @@ --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; } + .fsm-defect .fd-li { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; margin-top: 4px; } .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; } @@ -395,6 +403,10 @@

Pipeline state machine

// shape is the tool's to declare, and this page does not own that vocabulary // (issue-pr-cron#130); the rollup is different because those samples are the only surviving // record of a machine that no longer exists. + // + // 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"]], @@ -402,10 +414,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; @@ -563,14 +578,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 @@ -589,7 +611,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")); @@ -737,6 +764,11 @@

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 four dual-owner @@ -850,7 +882,153 @@

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 = []; + descriptors.forEach((d, i) => { + const p = readDescriptor(d); + if (p) parsed.push(p); + else { + defects.push( + "stateDescriptors[" + i + "] is malformed — it renders no state, and occupancy it meant to claim reports as unclaimed", + ); + } + }); + // CONSERVATION (the invariant that would have made the blocked-on drift scream + // instead of hiding 17 PRs): every occupancy key in the snapshot — each `lanes` cell + // and each counted top-level array (a top-level array with a same-named `counts` + // entry) — must be claimed by exactly ONE descriptor. Unclaimed occupancy is + // inventory the machine renders NOWHERE; a double claim renders the same inventory + // twice. Both are the tool's defects, and both surface in the defect band. + const claims = new Map(); + const claim = (id) => claims.set(id, (claims.get(id) || 0) + 1); + // Claim ids are JSON-encoded so no lane/state/array spelling can collide or forge + // another key's id. + const laneId = (lane, state) => "lane " + JSON.stringify([lane, state]); + const arrId = (k) => "array " + JSON.stringify(k); + for (const p of parsed) { + if (p.source.lane) claim(laneId(p.source.lane, p.key)); + else { + claim(arrId(p.source.counts)); + if (p.source.items !== p.source.counts) claim(arrId(p.source.items)); + } + } + + 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 = cell ? Number(cell.count) || 0 : 0; + 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. + const misclaimed = (id, name, held) => { + const n = claims.get(id) || 0; + if (n === 1) 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]; + const held = cell && typeof cell === "object" ? Number(cell.count) || 0 : 0; + misclaimed(laneId(lane, state), 'lanes["' + lane + '"]["' + state + '"]', held); + } + } + } + for (const k of Object.keys(hq)) { + if (k === "stateDescriptors" || !Array.isArray(hq[k])) continue; + if (!Object.prototype.hasOwnProperty.call(c, k)) continue; // only COUNTED arrays are occupancy + misclaimed(arrId(k), k, Number(c[k]) || 0); + } + 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; @@ -977,6 +1155,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 1c417af..93c47ac 100644 --- a/test/dashboard.test.js +++ b/test/dashboard.test.js @@ -2820,6 +2820,463 @@ 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: { + // Decoy mirrors for the lane states: the lane CELL is the declared source. + ready: 99, + blockedOn: 99, + 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" } }, + { key: "leak", owner: "human", act: "model it", kind: "blk", hist: "leaks", histFold: [], occupancy: { counts: "leaks", items: "leaks" } }, + // Sparse-absent lane cell: the state renders 0, dimmed — the shape stays visible. + { key: "ai:design", owner: "human", act: "rule on design Q", kind: "rule", hist: "design", histFold: [], occupancy: { lane: "vetter-verdicts" } }, + ], + }; +} + +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", "leak", "ai:design"]), + `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", "leak", "ai:design"]), + `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, not the 99 decoy mirror: ${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: { ready: 0 }, + 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: { ready: 0 }, + 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 counted top-level array no descriptor claims is a defect; uncounted arrays and stateDescriptors itself are not occupancy", () => { + const hq = descHq(); + // Drop the leak descriptor: counts.leaks + the leaks array go unclaimed. + hq.stateDescriptors = hq.stateDescriptors.filter((d) => d.key !== "leak"); + // An UNcounted top-level array (no counts key) is not occupancy the page knows how to + // count — it must not be reported. Neither must the descriptor array itself. + 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]}`, + ); +}); + +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", + owner: "producer", + act: "merge again", + kind: "flow", + hist: "", + 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("2 descriptors") && text.includes("renders 2 times"), + `it says the inventory double-renders: ${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 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", + "leak", + "ai:ready", + "ai:design", + "ai:blocked-deploy", + "ai:blocked-infra", + "ai:blocked-on", + "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, leak 2, design 0 — 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 From ccfb46607072fa84307860145bc115026ad79c26 Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 6 Aug 2026 10:44:21 +0000 Subject: [PATCH 02/11] test: pin descriptor order against count order, and the two-name occupancy claim The descHq human states now carry the zero-count state BETWEEN two non-zero ones, so a mutant that sorts boxes by size cannot pass the order pin by coincidence; and a descriptor whose items array is named differently from its counts key is pinned to claim BOTH, so the conservation sweep raises no spurious defect on a conserved snapshot. Co-Authored-By: Claude Fable 5 --- test/dashboard.test.js | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/test/dashboard.test.js b/test/dashboard.test.js index 6f61dee..34a7586 100644 --- a/test/dashboard.test.js +++ b/test/dashboard.test.js @@ -3048,9 +3048,11 @@ function descHq() { occupancy: { lane: "vet-lifecycle" }, }, { key: "ai:ready", owner: "human", act: "merge", kind: "flow", hist: "ready", histFold: [], occupancy: { lane: "vetter-verdicts" } }, - { key: "leak", owner: "human", act: "model it", kind: "blk", hist: "leaks", histFold: [], occupancy: { counts: "leaks", items: "leaks" } }, - // Sparse-absent lane cell: the state renders 0, dimmed — the shape stays visible. + // 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" } }, ], }; } @@ -3062,7 +3064,7 @@ Deno.test("pipeline FSM: stateDescriptors drive the machine — owner grouping, const keys = box.querySelectorAll("[data-t]").map((b) => b.dataset.t); assert( JSON.stringify(keys) === - JSON.stringify(["uncoveredIssues", "ai:blocked-on", "ai:ready", "leak", "ai:design"]), + 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); @@ -3079,7 +3081,7 @@ Deno.test("pipeline FSM: stateDescriptors drive the machine — owner grouping, `blocked-on is vetter-owned by descriptor: ${JSON.stringify(vetter.states)}`, ); assert( - JSON.stringify(human.states) === JSON.stringify(["ai:ready", "leak", "ai:design"]), + 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 @@ -3367,6 +3369,34 @@ Deno.test("pipeline FSM: a descriptor's kind is clamped to the display kinds — } }); +// 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, leakItems: 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 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. @@ -3445,7 +3475,7 @@ Deno.test("pipeline FSM: descriptor boxes keep sparklines and the lead fallback" collect(ready, "fsm-spark").length === 1, "the descriptor's hist key draws a sparkline", ); - // Human group: ready 5, leak 2, design 0 — nothing rising, so the largest queue leads. + // 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", From 8ac7fccb5b9f86bb00754a57661e81031c9badcb Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 6 Aug 2026 10:48:29 +0000 Subject: [PATCH 03/11] test: pin junk lane-cell counts to 0 in descriptor mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found by the adversarial mutation pass: Number(cell && cell.count) is equivalent to the real coercion for a sparse-ABSENT cell (Number(null) is 0), so only a cell that exists with a junk or missing count discriminates the dropped `|| 0` — and no descriptor-path test carried that shape. Co-Authored-By: Claude Fable 5 --- test/dashboard.test.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/dashboard.test.js b/test/dashboard.test.js index 34a7586..0cb973e 100644 --- a/test/dashboard.test.js +++ b/test/dashboard.test.js @@ -3369,6 +3369,30 @@ Deno.test("pipeline FSM: a descriptor's kind is clamped to the display kinds — } }); +// 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: { ready: 0 }, + 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. From 63bd6aba5c82e2cb8219b64074c0a25764e0ebb4 Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 6 Aug 2026 10:55:12 +0000 Subject: [PATCH 04/11] pipeline dash: conservation per the ratified counts-side scoping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 2026-08-06 clarification on issue-pr-cron#130: lanes cells stay strictly claimed (zero exceptions); on the counts side every counts key must be claimed by exactly one descriptor through the keys it reads (its occupancy counts source or its hist mirror), outside the four named frozen-legacy keys — the non-state metrics totalProducerPrs / openIssues / archivedRepoPrs and the legacy duplicate closeCandidateIssues — and a counts-source descriptor's declared counts/items keys must resolve in the snapshot or the band reports them. Replaces the counted-top-level-array sweep, which the counts-key sweep subsumes. Retired states arrive as residue descriptors emitted only while nonzero, so the consumer keeps zero retired-detection: pinned by the new residue pair test (present renders with its "(retired #n)" label; drained renders nothing and nothing screams). Co-Authored-By: Claude Fable 5 --- site/pipeline.html | 59 +++++++++++++++----- test/dashboard.test.js | 120 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 158 insertions(+), 21 deletions(-) diff --git a/site/pipeline.html b/site/pipeline.html index 80f1fb4..f61ad15 100644 --- a/site/pipeline.html +++ b/site/pipeline.html @@ -965,23 +965,43 @@

Pipeline state machine

} }); // CONSERVATION (the invariant that would have made the blocked-on drift scream - // instead of hiding 17 PRs): every occupancy key in the snapshot — each `lanes` cell - // and each counted top-level array (a top-level array with a same-named `counts` - // entry) — must be claimed by exactly ONE descriptor. Unclaimed occupancy is - // inventory the machine renders NOWHERE; a double claim renders the same inventory - // twice. Both are the tool's defects, and both surface in the defect band. + // 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 claim = (id) => claims.set(id, (claims.get(id) || 0) + 1); - // Claim ids are JSON-encoded so no lane/state/array spelling can collide or forge + // 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 arrId = (k) => "array " + JSON.stringify(k); + const countsId = (k) => "counts " + JSON.stringify(k); for (const p of parsed) { if (p.source.lane) claim(laneId(p.source.lane, p.key)); else { - claim(arrId(p.source.counts)); - if (p.source.items !== p.source.counts) claim(arrId(p.source.items)); + 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. + const read = new Set(); + if (p.source.counts) read.add(p.source.counts); + if (p.hist) read.add(p.hist); + for (const k of read) claim(countsId(k)); } OWNERS.forEach((O) => { @@ -1040,10 +1060,23 @@

Pipeline state machine

} } } - for (const k of Object.keys(hq)) { - if (k === "stateDescriptors" || !Array.isArray(hq[k])) continue; - if (!Object.prototype.hasOwnProperty.call(c, k)) continue; // only COUNTED arrays are occupancy - misclaimed(arrId(k), k, Number(c[k]) || 0); + // 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)) { + if (LEGACY_UNCLAIMED.has(k)) continue; + misclaimed(countsId(k), 'counts["' + k + '"]', Number(c[k]) || 0); } if (defects.length) { const dbox = el("div", "fsm-defect"); diff --git a/test/dashboard.test.js b/test/dashboard.test.js index 0cb973e..bf54592 100644 --- a/test/dashboard.test.js +++ b/test/dashboard.test.js @@ -3186,7 +3186,7 @@ Deno.test("pipeline FSM: a descriptor's histFold folds retired-key samples into { t: at(0), counts: { current: 10 } }, ]; const box = fsmBox({ - counts: { ready: 0 }, + 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" } }, @@ -3217,7 +3217,7 @@ Deno.test("pipeline FSM: a descriptor's empty histFold folds nothing — the han { t: at(0), counts: { reject: 28 } }, ]; const box = fsmBox({ - counts: { ready: 0 }, + 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" } }, @@ -3262,12 +3262,12 @@ Deno.test("pipeline FSM: a lane cell no descriptor claims surfaces as a loud def ); }); -Deno.test("pipeline FSM: a counted top-level array no descriptor claims is a defect; uncounted arrays and stateDescriptors itself are not occupancy", () => { +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 + the leaks array go unclaimed. + // Drop the leak descriptor: counts.leaks (and with it the leaks array) goes unclaimed. hq.stateDescriptors = hq.stateDescriptors.filter((d) => d.key !== "leak"); - // An UNcounted top-level array (no counts key) is not occupancy the page knows how to - // count — it must not be reported. Neither must the descriptor array itself. + // 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]; @@ -3375,7 +3375,7 @@ Deno.test("pipeline FSM: a descriptor's kind is clamped to the display kinds — // 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: { ready: 0 }, + 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" } }, @@ -3398,7 +3398,7 @@ Deno.test("pipeline FSM: a descriptor lane cell with a junk count renders 0, nev // 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, leakItems: 2 }, + counts: { leakCount: 2 }, lanes: {}, leakItems: fcItems("leak", 2), stateDescriptors: [ @@ -3421,6 +3421,110 @@ Deno.test("pipeline FSM: a descriptor claims BOTH its counts key and a different ); }); +// 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}`, + ); +}); + +// 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. From 14a85e8c416de7947774b061ca001781b4d581ea Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 6 Aug 2026 11:04:27 +0000 Subject: [PATCH 05/11] test: frozen-fallback pin follows master's post-#165 hand table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Master gained rain-org-health#165 after this branch cut: the frozen hand table now files ai:blocked-on under the VETTER reading vet-lifecycle (with a staleLane read of the pre-re-key producer-blocked cell — a fallback-branch-only affordance). The frozen-fallback pin freezes whatever master's table is, so its expected key order moves blocked-on into the vetter group. The descriptor path is untouched: it reads only each descriptor's declared lane, and a stale cell on a descriptor snapshot is conservation-defect territory, never a silent fallback. Co-Authored-By: Claude Fable 5 --- test/dashboard.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dashboard.test.js b/test/dashboard.test.js index d06b875..15f9c15 100644 --- a/test/dashboard.test.js +++ b/test/dashboard.test.js @@ -3661,11 +3661,11 @@ Deno.test("pipeline FSM: a descriptor-less snapshot renders the frozen hand tabl "uncoveredIssues", "un-vetted", "closeCandidateUnvetted", + "ai:blocked-on", "leak", "ai:ready", "ai:design", "ai:blocked-deploy", - "ai:blocked-on", "human:design", "closeCandidateUpheld", ]), From 80df73e6b8ebf75b5ba00c99f1b032e1262b109a Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 6 Aug 2026 11:07:50 +0000 Subject: [PATCH 06/11] pipeline dash: zero-held unclaimed occupancy is vacuously conserved MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The live tool still emits drained retired counts keys at 0 (blockedInfra, relink, humanReject) while the kept-while-nonzero contract drops only the DESCRIPTORS, so the sweep as-was would paint a permanent false defect band on the first healthy descriptor snapshot. An unclaimed key holding nothing hides nothing: n === 0 with 0 held is skipped, in both the lanes sweep and the counts sweep. A double claim is structural — the same inventory wired twice — and stays a defect at any count, zero included. Mutation pass re-run on the merged tree: 23/23 killed, including the two new edges (zero-skip removed → false defects; zero-skip overbroad → zero-held double claims silenced). Co-Authored-By: Claude Fable 5 --- site/pipeline.html | 6 +++++ test/dashboard.test.js | 50 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/site/pipeline.html b/site/pipeline.html index 001ef29..d6eb6e7 100644 --- a/site/pipeline.html +++ b/site/pipeline.html @@ -1049,6 +1049,12 @@

Pipeline state machine

const misclaimed = (id, name, held) => { 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. + if (n === 0 && !held) return; defects.push( name + " (" + held + " held) " + (n === 0 diff --git a/test/dashboard.test.js b/test/dashboard.test.js index 15f9c15..182684e 100644 --- a/test/dashboard.test.js +++ b/test/dashboard.test.js @@ -3613,6 +3613,56 @@ Deno.test("pipeline FSM: the frozen-legacy counts keys never defect; any other u ); }); +// 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}`, + ); + + const doubled = base(); + doubled.stateDescriptors.push( + { key: "ai:blocked-deploy", owner: "human", act: "resolve deploy", kind: "blk", hist: "blockedDeploy", histFold: [], occupancy: { lane: "producer-blocked" } }, + { key: "ai:blocked-deploy", owner: "producer", act: "resolve deploy", kind: "blk", hist: "", 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. From 7d6f1bd6e6392e7907f842a23ea1e1115ab815ec Mon Sep 17 00:00:00 2001 From: David Meister Date: Fri, 7 Aug 2026 06:46:01 +0000 Subject: [PATCH 07/11] pipeline dash: a descriptor claims the counts keys it folds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A descriptor READS its histFold keys — that is where its series draws the absorbed retirees' samples from — so those counts keys are its claim too, deduped alongside the occupancy counts source and hist. Without it, the live shape paints a FALSE defect: 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), so a nonzero humanReject would be claimed by nobody even though its lane cell IS claimed and its history IS drawn. Masked today only because all three retired counts sit at 0 and the zero-held skip swallows it; it fires the moment any absorbed-retiree residue reappears. The invariant stays honest in both directions: 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. Suite 229 green; mutation table re-run at 25/25 killed, including the two new edges (fold claim dropped -> false defects; fold claim widened to blanket amnesty -> stranded retired keys hidden). Co-Authored-By: Claude Fable 5 --- site/pipeline.html | 15 ++++++++- test/dashboard.test.js | 70 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/site/pipeline.html b/site/pipeline.html index d6eb6e7..6ff7606 100644 --- a/site/pipeline.html +++ b/site/pipeline.html @@ -1003,11 +1003,24 @@

Pipeline state machine

); } } - // Every counts key this descriptor reads is claimed — deduped, because a + // 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)); } diff --git a/test/dashboard.test.js b/test/dashboard.test.js index 182684e..5bc2751 100644 --- a/test/dashboard.test.js +++ b/test/dashboard.test.js @@ -3613,6 +3613,76 @@ Deno.test("pipeline FSM: the frozen-legacy counts keys never defect; any other u ); }); +// 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 From ae1ea84531fd8471657a1abeb5c40e4c2c7cafde Mon Sep 17 00:00:00 2001 From: David Meister Date: Fri, 7 Aug 2026 06:54:44 +0000 Subject: [PATCH 08/11] pipeline dash: a repeated descriptor key renders no state and is reported MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: two boxes on screen, both opening the SECOND one's list, the first box's inventory unreachable. That is #141's exact failure, and neither claim check saw it: with different occupancy sources the two claims are different ids, so both read as singly claimed. A repeat now renders NO state and is reported by key and index, in the malformed entry's wording, 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. Two pre-existing double-claim tests manufactured their double claim by repeating a key, which is now the more specific defect; both are rebuilt on DISTINCT keys colliding on a counts id — the genuine remaining double-claim path, since a lane claim id embeds the claiming state's own key and so can only collide via a repeat. Suite 231 green; mutation table 27/27 killed, including the two new edges (guard dropped -> silent mis-wire; last-wins -> the repeat overwrites the first's registration). Co-Authored-By: Claude Fable 5 --- site/pipeline.html | 25 +++++++++++-- test/dashboard.test.js | 79 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/site/pipeline.html b/site/pipeline.html index 6ff7606..9522fcf 100644 --- a/site/pipeline.html +++ b/site/pipeline.html @@ -962,14 +962,35 @@

Pipeline state machine

}; 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) parsed.push(p); - else { + 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 diff --git a/test/dashboard.test.js b/test/dashboard.test.js index 5bc2751..f57e260 100644 --- a/test/dashboard.test.js +++ b/test/dashboard.test.js @@ -3393,14 +3393,21 @@ Deno.test("pipeline FSM: a counts key no descriptor claims is a defect; a bare t ); }); +// 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", + key: "ai:ready-mirror", owner: "producer", act: "merge again", kind: "flow", - hist: "", + // The SAME series `ai:ready` already draws. + hist: "ready", histFold: [], occupancy: { lane: "vetter-verdicts" }, }); @@ -3409,8 +3416,12 @@ Deno.test("pipeline FSM: occupancy claimed by TWO descriptors is a defect too assert(band, "a double claim surfaces"); const text = textOf(band); assert( - text.includes("2 descriptors") && text.includes("renders 2 times"), - `it says the inventory double-renders: ${text}`, + 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}`, ); }); @@ -3613,6 +3624,60 @@ Deno.test("pipeline FSM: the frozen-legacy counts keys never defect; any other u ); }); +// 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 @@ -3720,10 +3785,12 @@ Deno.test("pipeline FSM: zero-held unclaimed occupancy is vacuously conserved; n `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-deploy", owner: "human", act: "resolve deploy", kind: "blk", hist: "blockedDeploy", histFold: [], occupancy: { lane: "producer-blocked" } }, - { key: "ai:blocked-deploy", owner: "producer", act: "resolve deploy", kind: "blk", hist: "", histFold: [], occupancy: { lane: "producer-blocked" } }, + { 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"); From 7e19682eb73e30f4ba981a6d30e71a7105b27a89 Mon Sep 17 00:00:00 2001 From: David Meister Date: Fri, 7 Aug 2026 07:22:27 +0000 Subject: [PATCH 09/11] pipeline dash: cross-check a lane state's two numbers, and four sweep fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SHOULD-FIX — the page holds TWO numbers for a lane state and never compared them: the lane cell it RENDERS, and the counts key its SERIES is drawn from. The tool computes those two ways (issue-pr-cron#228), so the live witness the moment the producer lands is ai:ready rendering 0, dimmed, under a sparkline sitting at 23 — with every claim check passing, because each key is claimed exactly once. The page is the only place the two meet, so it now compares them and reports a divergence naming both numbers and which one is on screen. It does not guess which 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. Also from review: - the frozen-legacy exception no longer suppresses DOUBLE claims on those four keys — it licenses a key going unclaimed, never the same inventory wired twice (two descriptors both drawing openIssues would render the population as two state series); - a descriptor's items array is claimed, so two descriptors naming one array is reported rather than rendering two boxes onto one list; - an unclaimed lane cell's held inventory is max(count, prs.length), so a cell carrying 17 PRs behind a dropped or non-numeric count is no longer scored 0 and skipped by the zero-held carve-out; - .fd-li and .sk wrap long unbroken tokens instead of overflowing, since both quote untrusted snapshot strings. descHq's decoy counts mirrors are now consistent with their cells: a mirror contradicting its cell is itself a reported defect, so the wrong-source read is discriminated by the divergence test instead. Prototype-shaped strings (__proto__, constructor) as key, counts, items and lane name are pinned as ordinary data, built through JSON.parse (the only way to get a genuine own __proto__ key, and how the page really receives the snapshot). Suite 237 green; mutation table 32/32 killed, including the five new edges (divergence dropped/inverted, legacy exception suppressing doubles, items double claim dropped, held ignoring prs). Co-Authored-By: Claude Fable 5 --- site/pipeline.html | 78 +++++++++++++--- test/dashboard.test.js | 200 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 264 insertions(+), 14 deletions(-) diff --git a/site/pipeline.html b/site/pipeline.html index 9522fcf..99d261e 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; } @@ -186,7 +189,10 @@ .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; } - .fsm-defect .fd-li { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; margin-top: 4px; } + /* 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; } @@ -1005,7 +1011,14 @@

Pipeline state machine

// 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); + return cell ? Number(cell.count) || 0 : 0; + }; // 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]); @@ -1043,6 +1056,35 @@

Pipeline state machine

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. + 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) => { @@ -1055,7 +1097,7 @@

Pipeline state machine

let count; if (p.source.lane) { const cell = laneState(p.source.lane, p.key); - count = cell ? Number(cell.count) || 0 : 0; + count = laneCount(p); register(p.key, cell && cell.prs, false, count); } else { // Registration is UNCONDITIONAL on both branches (#141): a count whose list @@ -1079,16 +1121,19 @@

Pipeline state machine

grid.append(lane); }); - // The conservation sweep, over the snapshot's own occupancy keys. - const misclaimed = (id, name, held) => { + // 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. - if (n === 0 && !held) return; + // 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 @@ -1102,7 +1147,14 @@

Pipeline state machine

if (!cells || typeof cells !== "object") continue; for (const state of Object.keys(cells)) { const cell = cells[state]; - const held = cell && typeof cell === "object" ? Number(cell.count) || 0 : 0; + // 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); } } @@ -1122,8 +1174,14 @@

Pipeline state machine

"closeCandidateIssues", ]); for (const k of Object.keys(c)) { - if (LEGACY_UNCLAIMED.has(k)) continue; - misclaimed(countsId(k), 'counts["' + k + '"]', Number(c[k]) || 0); + 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"); diff --git a/test/dashboard.test.js b/test/dashboard.test.js index f57e260..bd55a5f 100644 --- a/test/dashboard.test.js +++ b/test/dashboard.test.js @@ -3128,9 +3128,12 @@ Deno.test("pipeline FSM: a malformed item renders as a malformed row, taking no function descHq() { return { counts: { - // Decoy mirrors for the lane states: the lane CELL is the declared source. - ready: 99, - blockedOn: 99, + // 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, }, @@ -3207,7 +3210,7 @@ Deno.test("pipeline FSM: stateDescriptors drive the machine — owner grouping, 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, not the 99 decoy mirror: ${collect(byT("ai:ready"), "sc")[0].textContent}`, + `ready reads its lane cell: ${collect(byT("ai:ready"), "sc")[0].textContent}`, ); assert( collect(byT("uncoveredIssues"), "sc")[0].textContent === "4", @@ -3624,6 +3627,195 @@ Deno.test("pipeline FSM: the frozen-legacy counts keys never defect; any other u ); }); +// 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 From 368898948bff88bd60e9cf8afb23035892812e2e Mon Sep 17 00:00:00 2001 From: David Meister Date: Fri, 7 Aug 2026 09:02:14 +0000 Subject: [PATCH 10/11] pipeline dash: the deleted ai:blocked-deploy state leaves the machine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ai:blocked-deploy` is not a state of the FSM any more (issue-pr-cron#225): no label, no classifier arm, no counts emission, no parking transition. The frozen fallback table still listed it unconditionally, so once the tool stops emitting the lane cell the box renders permanently at zero under Human action, prescribing "resolve deploy" for a machine that no longer exists — the exact thing this file's own comment forbids (rain-org-health#145). Drop it from STATES, from the legacy no-`lanes` branch and from the dual-owner rationale, and add it to RETIRED_NO_SUCCESSOR beside `ai:blocked-infra`: a snapshot still carrying a nonzero count (an old snapshot, or the label reapplied by hand) surfaces as a straggler box whose act is stripping the residue, and zero or absent draws nothing. No HIST_FOLD entry. Nothing absorbed this inventory — `ai:reject` is where the PRs that carried the label now sit, but a reject measures a verdict on the PR's code, not a fact about a deploy, so folding the samples in would fabricate a step in a series that never held that work. The straggler tests are extended over BOTH no-successor retirees rather than duplicated, since it is one mechanism, plus a new pin that two retirees on one snapshot render as two independent boxes with their own lists and counts. Co-Authored-By: Claude Fable 5 --- site/pipeline.html | 31 ++-- test/dashboard.test.js | 386 +++++++++++++++++++++++++---------------- 2 files changed, 255 insertions(+), 162 deletions(-) diff --git a/site/pipeline.html b/site/pipeline.html index 99d261e..cbe5207 100644 --- a/site/pipeline.html +++ b/site/pipeline.html @@ -411,9 +411,10 @@

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 @@ -783,9 +784,7 @@

Pipeline state machine

// 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 @@ -842,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 @@ -863,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 @@ -882,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 @@ -905,6 +908,7 @@

Pipeline state machine

// 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" }, ]; @@ -1243,7 +1247,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 @@ -1253,7 +1257,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"); diff --git a/test/dashboard.test.js b/test/dashboard.test.js index bd55a5f..6e3ff94 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", - ); + }); + 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, "sc")[0].textContent === "2", - `it carries the snapshot's count: ${collect(b, "sc")[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", ); + // 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, "sa")[0].textContent === "strip the label", - `the act clears the residue: ${collect(b, "sa")[0].textContent}`, + deployText.includes("deploy residue") && !deployText.includes("infra residue"), + `the deploy box lists only its own PRs: ${deployText}`, ); - assert(b.className.includes("blk"), `a stuck pile, kind blk: ${b.className}`); + at("ai:blocked-deploy")[0].click(); + at("ai:blocked-infra")[0].click(); + const infraText = textOf(box); assert( - collect(b, "fsm-spark").length === 0, - "no sparkline: the retired series is drawn nowhere", + infraText.includes("infra residue A") && infraText.includes("infra residue B") && + !infraText.includes("deploy residue"), + `the infra box lists only its own PRs: ${infraText}`, ); - // 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("ai:blocked-infra"), - `the straggler files under the human: ${JSON.stringify(human.states)}`, - ); - 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( @@ -4044,7 +4135,6 @@ Deno.test("pipeline FSM: a descriptor-less snapshot renders the frozen hand tabl "leak", "ai:ready", "ai:design", - "ai:blocked-deploy", "human:design", "closeCandidateUpheld", ]), From 534ab065df19565d6afb742c2cca0f0398616335 Mon Sep 17 00:00:00 2001 From: David Meister Date: Fri, 7 Aug 2026 11:41:00 +0000 Subject: [PATCH 11/11] pipeline dash: a lane cell's count and its prs are cross-checked too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Number(cell.count) || 0` cannot tell a missing or non-numeric count from a real zero, so a broken count over a populated `prs` rendered 0 while the click expanded onto real PRs — and the sweep could not see it, because a claimed cell leaves `misclaimed` early at n === 1. Same class as the count-vs-hist divergence, one level down: the page holds two numbers for one cell and is the only place they meet. The box now renders the LARGER, so a broken count cannot hide inventory the list proves is there and the number on the box is the number of rows it opens onto; the disagreement is reported naming both numbers and which was rendered. Only checked while the list is non-empty — a count over an EMPTY list is the missing-list case detailBody already reports at the click, not a snapshot defect. Targeted mutants (3, all killed): the check dropped; the check widened to fire on an empty list; the render reading the stated count instead of the larger. Co-Authored-By: Claude Fable 5 --- site/pipeline.html | 29 ++++++++++++++++++++++- test/dashboard.test.js | 52 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/site/pipeline.html b/site/pipeline.html index 99d261e..eb3a79e 100644 --- a/site/pipeline.html +++ b/site/pipeline.html @@ -1017,7 +1017,16 @@

Pipeline state machine

// 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); - return cell ? Number(cell.count) || 0 : 0; + 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. @@ -1073,6 +1082,24 @@

Pipeline state machine

// 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; diff --git a/test/dashboard.test.js b/test/dashboard.test.js index bd55a5f..14cbd75 100644 --- a/test/dashboard.test.js +++ b/test/dashboard.test.js @@ -3627,6 +3627,58 @@ Deno.test("pipeline FSM: the frozen-legacy counts keys never defect; any other u ); }); +// 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