human-queue --json emits stateDescriptors: the machine's shape as data - #223
Conversation
Closes #130 A consumer hand-copying this pipeline's state vocabulary drifts silently in every direction: a state deleted here renders forever at zero there, an added one never renders, a renamed one flatlines, and a re-laned one hides real inventory (#161's 17 blocked-on PRs rendering nowhere). The fix is the machine's shape as data: `human-queue --json` now carries a top-level `stateDescriptors` array — per state its key, owner, act, kind, history key, retired-history folds, occupancy source, and display label — in render order, per the ratified schema contract on #130. ONE table (amendment 2): the descriptor rows are the same constants `classify_lane` reads — HUMAN_DECISION_LABELS, VETTER_VERDICT_LABELS and the three retired-label constants are now spelled from the rows, as are the classifier's synthesised `un-vetted` and `leak` states — so a state cannot exist for the classifier and be missing from the emitted shape. The `ai:close-candidate` hand-off keeps no row of its own: the flag machinery inventories its subjects into the two close-candidate rows (#211/#212). The four states outside `lanes` cells (amendment 1) — uncoveredIssues, leak, closeCandidateUnvetted, closeCandidateUpheld — declare top-level `{counts, items, itemsAreIssues}` occupancy; `lanes` stays the sparse occupancy it was. History folds carry only keys `counts` no longer emits: `awaitingReVet` into un-vetted (#128) and `closeCandidatePrs` into the upheld inbox (#211/#212); `humanCloseCandidate` folds nowhere by design (#213 made close decide+do with no state between). Conservation is pinned structurally: a sweep built from the classifier's own label constants asserts every reachable state has exactly one descriptor and every lane-occupancy descriptor names a reachable cell; every declared occupancy source and history key must resolve in the document the same binary emits, and every `counts` key must be claimed by a descriptor or be a named non-state rollup; the ratified schema is pinned verbatim, order included. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 8 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
WalkthroughThe PR centralizes metadata for queue states. Classification, lane occupancy, counts, review sections, JSON descriptors, history handling, tests, and documentation now use the shared descriptor model. ChangesState descriptor and queue state flow
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant classify_lane
participant lanes_doc
participant QueueDocument
participant StateDescriptor
classify_lane->>StateDescriptor: resolve descriptor-derived state key
StateDescriptor-->>classify_lane: return state key
lanes_doc->>QueueDocument: provide lane-cell populations and top-level inventories
QueueDocument->>StateDescriptor: serialize occupied descriptors
StateDescriptor-->>QueueDocument: return ordered stateDescriptors
Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pr-review-report-rs/src/main.rs`:
- Around line 47140-47149: Replace the hard-coded "ai:close-candidate" label in
the label setup loop with the existing PENDING_CLOSE_FLAG constant, keeping the
other labels unchanged so the test uses the transition’s shared definition.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 78632f53-59f8-431c-8bfd-b4b38eb7413b
📒 Files selected for processing (1)
pr-review-report-rs/src/main.rs
…ired Two ratified contract clarifications on #130 (#130 (comment)), both surfaced by verifying the emission against the consumer's conservation requirements: A retired state exists only as its residue (clarification 1). Live rows always emit — an empty live state stays visible — while a residue row (ai:relink, ai:blocked-deploy, ai:blocked-infra, human:reject, and human:design pending its #219 deletion) emits only while its `counts.<hist>` is nonzero, so a drained retired state leaves the shape instead of renting a permanent dimmed box (rain-org-health#145's rejected outcome). The `counts` object is assembled first and the emission filters against it; no `retired` field reaches the wire, and the "(retired #n)" suffix stays in `label` while the residue lasts. Every emitted `lanes` cell is claimed (clarification 2). `lanes_doc` no longer emits the redundant `lanes.leak` cell: the leak descriptor claims the top-level `leaks` array — the one carrying each leak's `reason` — and nothing consumed the cell (the review path prints leaks from the array; pipeline.html reads counts.leaks/items leaks). The conservation test now also sweeps the cells `lanes_doc` actually emits and requires each to be claimed by exactly one lane-occupancy descriptor, zero declared exceptions. The schema pin moves to full occupancy (all rows emitted) and a new test pins the gate: the live set at zero occupancy, a residue row returning at its table position with its label intact, and human:design gating identically with no special-casing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eries `human:reject` (#133 — a reject is a reject whoever ruled it) and `ai:relink` (#135 — a linkage error IS a reject) were consolidated INTO `ai:reject` by exactly the rename shape #130's history paragraph is about: "a descriptor needs to be able to say 'my series also draws from these retired keys', or a rename quietly truncates every chart that spans it". `un-vetted` already folds `awaitingReVet` for the same reason. Without the fold, descriptor mode truncated the reject sparkline's past relative to the frozen fallback — inventory that sat under `humanReject`/`relink` vanished from the series. `STATE_REJECT.hist_fold` is now `["humanReject", "relink"]`, and the two absorbed residue rows carry NO series of their own: `hist` is an `Option`, `None` for them, omitted from the wire rather than emitted as null. The successor draws the past, so the same inventory never draws in two boxes — rain-org-health#162's design language for absorbed retirees. They still gate on their kept-while-nonzero counts, so the straggler stays visible as a state: `Emit::WhileOccupied` carries its gate key rather than reading `hist`, which keeps the gate and the series claim independent. The conservation checks stay coherent and get stricter: a fold key must not be any row's claimed `hist` (an ownership transfer, not a share) and must fold into exactly one series; every gate key must be an emitted count; and the counts-claim sweep now accepts a key claimed as a FOLDED past, so `humanReject`/`relink` are claimed rather than unclaimed while they still emit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two defects found by adversarial review, both proven by probe. `human:design` was gated as residue although it is LIVE: `/design` still writes the label and #111 routes each delegated ruling back to the producer, so the population empties and refills in the ordinary course. Gating it made the box vanish the moment the queue drained and reappear on the next ruling — a live state rendering nowhere, the mirror of the drift #130 exists to kill. It is now `Emit::Always`; clarification 1 is explicit that only a state whose existence IS its residue may be gated. (#219 proposes deleting the state, which removes the row wholesale — a different thing from gating a state the machine still writes.) The `ai:blocked-deploy` / `ai:blocked-infra` gates measured a DIFFERENT POPULATION from the occupancy they gate: `counts.blockedDeploy` counts the first `ai:*` label (`ai_state_label`) while the declared occupancy is the `classify_lane` PRECEDENCE cell. Both directions were wrong on two-label PRs, which `classify_lane`'s own comments call normal: `["human:design", "ai:blocked-deploy"]` gave count 1 over an EMPTY cell — a permanent dimmed box holding nothing, exactly what clarification 1 removes — and `["ai:ready", "ai:blocked-deploy"]` gave count 0 over a FULL cell, hiding a real PR from a consumer whose conservation check then fires. Rather than re-point those two keys, the gate now reads the row's DECLARED occupancy (`descriptor_occupancy`, the same place it points consumers), so `Emit` carries no key of its own and gate and occupancy cannot diverge by construction. The conservation sweep now asserts on the WIRE: it builds a document from a fixture that INCLUDES multi-label PRs — every ordered pair of state labels, the only inputs where a label-bucket key and a precedence winner disagree — and checks `stateDescriptors` AS EMITTED, through the `Emit` filter, against the emitted `lanes`. Both probe cases passed the old table-level sweep unchanged. It runs both directions: no emitted cell unclaimed, and no residue row emitted over an empty cell. Also: `counts` is completed with `openIssues` BEFORE the emission reads it, so a future gate on a late-added key cannot read absent-as-zero; and `lanes_doc`'s comment no longer claims it emits a `leak` cell three lines above the code that skips it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`counts.<state>` was computed from the LABEL BUCKET (`ai_state_label`, the first `ai:*` label a PR carries) while `lanes[<lane>][<state>]` was computed from `classify_lane` PRECEDENCE. Both populations are defensible and nothing in the document said they measure different things, so it answered "how many PRs are in this state?" twice with different numbers and nothing compared them. On the 2026-08-06 snapshot that was `counts.ready` 23 over an ABSENT `vetter-verdicts`.`ai:ready` cell — every one of the 23 un-vetted at its current head — which the dashboard drew as a merge inbox of 0 beneath a sparkline plotted at 23. Same failure class as the residue-emission gate this branch already fixed, one layer down, so it is fixed the same way: `StateOccupancy::Lane` now names the `counts` key its cell fills, and `lane_counts_json` writes every such key through `descriptor_occupancy` — the accessor `Emit::WhileOccupied` reads. A key, its gate and its rendered cell are one number by construction. Ten keys derive this way: the five that were the label bucket (`ready`, `design`, `blockedOn`, `blockedInfra`, `blockedDeploy`) and the five that already read a cell through their own hand-written `lane_state_count(…)` call, which are gone — a re-laned state must carry its key rather than need a second edit. The label population needs no key of its own: the top-level `states` object IS that census, click-through, and has never meant anything else. A second `counts.*` for it would restate `states.<label>.length` and land unclaimed under this branch's counts-claim sweep. The daily human-readable review had the same defect: four sections printed the bucket, so the tool disagreed with the `--json` the same run emits. They are now `review_lane_sections(lanes)`, handed the cells and nothing else, so there is no second population in scope to print. Each section names its descriptor row, and a residue section is gated by that row's own `Emit`. `human-queue-history.jsonl` copies `counts` verbatim, so `ready`, `design`, `blockedOn`, `blockedInfra` and `blockedDeploy` change meaning at this commit. The past is NOT rewritten — those lines are true measurements of the machine as it then was. Only `ready` steps visibly (23 → 0 on the 2026-08-06 snapshot); the inventory did not move, the measurement did. Stated in `lane_counts_json`, on `queue_history_line` (the one writer of every line in that file) and in the README, rather than left as a silent step in the chart. Closes #228 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Around line 1356-1359: Update the README description of the listed counts keys
to call them “lane-state counts” rather than the “full-machine view,” since
additional keys such as closeCandidateUnvetted and closeCandidateUpheld are
documented elsewhere. Keep the existing key list unchanged.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: b34e6421-ac11-4302-ba36-ba0cac26357d
📒 Files selected for processing (2)
README.mdpr-review-report-rs/src/main.rs
The list names every key derived from a lane cell, and calling it the full-machine view read as the complete `counts` schema — the close-candidate split and the non-state rollups sit beside it and measure top-level arrays, not lanes. Names what the list is and what it excludes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR 225 deleted the state entirely. This branch carried five resurrection sites, and the trap is that only ONE of them is reachable by the compiler unless `RETIRED_BLOCKED_DEPLOY_LABEL` is DELETED rather than re-literalised — give it a string literal to make the tree compile and `counts.blockedDeploy` comes back green. The constant is deleted. rustc then walked to three hard references: the `STATE_DESCRIPTORS` entry, the `REVIEW_LANE_SECTIONS` entry, and the label still sitting in the test-side `state_label_sets()` list. The rest are string literals no compiler can see, and were removed by hand: the golden entry in `state_descriptors_emit_the_ratified_shape`, and the state-table doc inventory. `counts.blockedDeploy` needed no deletion of its own. Since counts derive from the descriptor table, dropping the row drops the key by construction — there is no `"blockedDeploy": …` line anywhere to survive a stale patch. That is verified, not assumed: main's own `the_deleted_state_leaves_no_blockedDeploy_key` passes unchanged. The residue fixture in `a_residue_row_emits_only_while_occupied` is RE-POINTED at `ai:blocked-infra` rather than deleted: it exists to pin the gate mechanism, not this label, so deleting it would have quietly retired the coverage. Two conflicts resolved against this branch's own design: the counts block stays descriptor-driven (main's side was the pre-#228 inline label-bucket block), and the daily review's state sections stay in `review_lane_sections`. One stale assertion from main — `count("leak", "leak") == 1` — is removed: #130 clarification 2 retired that cell, and the branch already asserts its absence. Comments that narrated the deleted state as a live hazard are rewritten to describe current behavior: the `Emit` gate's rationale no longer cites a `counts.blockedDeploy` divergence that cannot now be constructed, and the history-series note records `blockedDeploy` as a series that ENDS rather than one whose meaning changed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stry
PR 224 deleted `human:design` from both subject types. This branch carried its
descriptor row (`Emit::Always`), its `humanDesign` counts key, its
`REVIEW_LANE_SECTIONS` entry, and `HUMAN_DECISION_LABELS` — a const spelled
from the row. All deleted, none re-literalised, so rustc had to walk to the
consumers rather than being quietly satisfied.
The compiler was NECESSARY BUT NOT SUFFICIENT, again. After the deletions it
reported clean while four data-level sites were still wrong, none of them
reachable by type checking: the golden row in
`state_descriptors_emit_the_ratified_shape`, both emitted-key lists in
`a_residue_row_emits_only_while_occupied`, and a
`descriptor("human:design").emit` assertion that would only have blown up at
runtime. Found by grep after the compiler pass.
So the hand-checks become coverage instead, because hand verification does not
survive the next merge:
- `DELETED_STATE_LABELS` is a registry of `(label, the counts key it used to
emit)`, and `a_deleted_state_leaves_no_descriptor_no_counts_key_and_no_lane_cell`
asserts over it: no descriptor names the label, neither spelling survives in
`counts`, no lane carries a cell, and the classifier treats it as the ordinary
un-vetted fallthrough. Both merged deletions are now rows of data; the next
one costs a row, not a sweep.
The key is DECLARED beside the label, not derived from it. Deriving it was the
first thing tried and it was wrong: `ai:*` states drop the namespace
(`ai:blocked-deploy` → `blockedDeploy`) while `human:*` states keep it
(`human:design` → `humanDesign`), so the inference asserted that
`counts.design` — the LIVE `ai:design` series — must not exist. The test
caught that on its first run.
- `a_lane_whose_only_states_are_residue_disappears_when_it_drains` pins what an
empty lane does rather than leaving it to be eyeballed. #219 left
`human-decisions` holding only the retired `human:reject` residue, so when
that drains the lane is ABSENT from `lanes` and no descriptor is emitted for
it — correct, because `lanes` is sparse occupancy and the only remaining row
is residue-gated. A straggler brings both back.
- The `un-vetted` fixture total is DERIVED from the fixture and the registry
instead of hand-written. A literal there is right on each side of a merge and
wrong once two deletions land, with no conflict marker on the line — which is
exactly how it sat at `3` over a four-member bucket.
Conflicts resolved against this branch's design: the counts block stays
descriptor-driven, the review's sections stay in `review_lane_sections`, and the
`lanes.leak` assertion main reintroduced is dropped again (#130 clarification 2
retired that cell). Conflict 4 was hand-merged rather than taken from a side:
main's `human:design` cell assertion is correct AND this branch's `/leak`
assertion is correct.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
README.md (1)
113-118: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse the full
pr-review-reportinvocation in both command-documentation sites.The README currently presents transition names without the required executable wrapper. This makes copied commands non-executable.
README.md#L113-L118: Prefix every transition in the table withpr-review-report.README.md#L299-L299: Prefixhuman-rule … design --reworkor identify it as an internal subcommand name.As per coding guidelines, command documentation must invoke
pr-review-reportfor all transitions.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@README.md` around lines 113 - 118, Update README.md lines 113-118 so every transition command is shown with the full pr-review-report executable prefix. Also update README.md line 299 to use the full pr-review-report invocation for the human-rule … design --rework command, or explicitly identify it as an internal subcommand; ensure all command documentation is directly executable or clearly marked internal.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Around line 1366-1370: Revise the “One population per state” documentation to
apply the lane-cell inventory and counts-key explanation only to lane state
descriptors. Add the contrasting StateOccupancy::Counts behavior, clarifying
that top-level arrays such as closeCandidateUnvetted and closeCandidateUpheld
determine counts by their length.
---
Outside diff comments:
In `@README.md`:
- Around line 113-118: Update README.md lines 113-118 so every transition
command is shown with the full pr-review-report executable prefix. Also update
README.md line 299 to use the full pr-review-report invocation for the
human-rule … design --rework command, or explicitly identify it as an internal
subcommand; ensure all command documentation is directly executable or clearly
marked internal.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: d9237d79-0a27-4fe7-860c-88db7b0fbfdf
📒 Files selected for processing (2)
README.mdpr-review-report-rs/src/main.rs
💤 Files with no reviewable changes (1)
- pr-review-report-rs/src/main.rs
The section said a state's inventory is its lane cell, which is only true of LANE states. A top-level state (`uncoveredIssues`, `leak`, and the two close-candidate inboxes) declares `StateOccupancy::Counts`: its inventory is a top-level array and its key IS that array's length. Both directions named, so the wording matches the two-variant declaration the code actually has. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Main added two MCP tools and their commands; this branch changes no tool surface, so main's tool table, command lists, plugin manifest and the counts that measure them (22 tools, 7 narrowing) carry over untouched. The one conflict is `lanes_doc_emits_every_state_with_the_right_members`, where both sides replaced the same hand-written totals for the same reason — a literal that is right on each side of a merge and wrong once both land. Main's is the more general mechanism: each fixture row carries its own expected `(lane, state)` and every total is counted off those annotations, so it is kept and used throughout. This branch's two semantic claims are applied on top of it: - the `leak` bucket emits NO lane cell (#130 clarification 2) — its inventory is the top-level `leaks` array, which is what its descriptor declares as its occupancy. Row #14 is therefore annotated `NO_LANE`, the same hand-off #7 already had, and the retirement is asserted both ways: the annotations expect no `leak` cell, and the emitted doc has no `leak` key at all; - the un-vetted total keeps its second, independent reading off the `DELETED_STATE_LABELS` registry beside main's annotation count. The annotations say where each PR lands; the registry says why the bucket is that size, so deleting a state without retiring its label fails here instead of quietly re-annotating itself. `next_leak` is unaffected by the retired lane cell: it builds its rows from `producer_pr_inventory` through `is_leak_candidate` and `classify_lane`, never from `lanes_doc` — which is the property #222's own test pins. The cross-lane total is now counted off the `NO_LANE` annotations rather than written as `prs.len() - 2`, because that subtraction is exactly the shape that survives a merge silently while the fixture hands off a different number. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Reviewed d7fd5c0: approve — read the diff and verified the merge independently. This emits |
Closes #130 — as amended by the ratified schema contract in #130 (comment) and the two ratified clarifications in #130 (comment).
Closes #228 — the same divergence one layer down, folded in here rather than chased as a follow-up because it is the same bug in the same region (see "One population per state (#228)" below).
What this emits
human-queue --jsongains a top-levelstateDescriptorsarray — the machine's SHAPE as data, so a consumer renders what the pipeline says exists instead of hand-copying the vocabulary and drifting silently. Per state:key,owner(producer|vetter|human),act,kind(flow|rule|blk),hist,histFold,occupancy, optionallabel. Array order is the render order.lanesstays the sparse occupancy.occupancyis exactly one of the contract's two forms:{ "lane": "<lane>" }— count+prs from thatlanescell, sparse-absent ⇒ 0;{ "counts": "<key>", "items": "<array>", "itemsAreIssues": <bool> }— amendment 1's four non-lane states (uncoveredIssues,leak,closeCandidateUnvetted,closeCandidateUpheld) declare where their occupancy lives at the top level.leakreads the top-levelleaksarray — the one carrying each leak'sreason.Clarification 1 — a retired state exists only as its residue. Live rows always emit (an empty live state stays visible — the shape is what exists). A residue row —
ai:relink,ai:blocked-deploy,ai:blocked-infra,human:reject— emits only while it is OCCUPIED, so a drained retired state leaves the shape instead of renting a permanent dimmed box (rain-org-health#145's rejected outcome, the box rainlanguage/rain-org-health#162 just removed). Noretiredfield reaches the wire; the "(retired #n)" suffix stays inlabelwhile the residue lasts.human:designWAS live here — the row emittedEmit::Alwayson the reasoning that a delegated ruling refills the population in the ordinary course. #219 has since deleted the state outright (merged as #224), which is the wholesale row drop that reasoning distinguished itself from, so the row is gone rather than gated.human-decisionsnow holds exactly one state, the retiredhuman:rejectresidue.The gate names no key of its own — it reads the row's DECLARED occupancy, the same place it points consumers. A separately-keyed gate can measure a different population than the occupancy it gates, and then both directions are wrong. That was real, not hypothetical:
counts.blockedDeploycounts the firstai:*label (ai_state_label) while the cell isclassify_lanePRECEDENCE, so on two-label PRs — normal perclassify_lane's own comments —["human:design", "ai:blocked-deploy"]gave count 1 over an EMPTY cell (a dimmed box holding nothing) and["ai:ready", "ai:blocked-deploy"]gave count 0 over a FULL cell (a real PR rendering nowhere). Reading declared occupancy makes both unconstructible. (That example is now historical twice over: #228 below made every lane row'scountskey the cell itself, and #221 deletedai:blocked-deployoutright.)Clarification 2 — every emitted
lanescell is claimed.lanes_docno longer emits the redundantlanes.leakcell (verified unconsumed before dropping: the tool's own review path prints leaks from the top-level array, andsite/pipeline.htmlreadsfromCounts: "leaks", items: "leaks"— nolanes.leakread exists). Every emitted lane cell is claimed by exactly one descriptor, with zero declared exceptions on the lanes side.One table (amendment 2)
The descriptors are not a second list inside the tool. There is ONE row table, and
classify_lane's own constants are spelled from it:HUMAN_DECISION_LABELS,VETTER_VERDICT_LABELS,RETIRED_HUMAN_REJECT_LABEL,RETIRED_BLOCKED_DEPLOY_LABEL,RETIRED_STATE_LABEL, and the classifier's synthesisedun-vetted/leakwords are all row references — so a state cannot exist for the classifier and be missing from the emitted vocabulary. Theai:close-candidatehand-off keeps no row of its own: the flag machinery inventories its subjects into the two close-candidate rows (#211 / #212), and a row for it would name occupancy that lives nowhere.That is what the sibling deletion of
human:design(#219) did when it landed: it removed the state from these same shared constants, and the descriptor dropped with it — no special-casing, andHUMAN_DECISION_LABELSwent with it rather than being re-literalised to keep the tree compiling.Owners are the machine's rulings
ai:blocked-on→ vetter (ai:blocked-on sits with the vetter: typed dependency refs, automated clearance on dep merge, human polling removed #161): its state-load clears the flag the run after every typed dep merges/closes. This is the live drift instance the amendment names — the dashboard still files it under Human off aproducer-blockedlane read, rendering 17 blocked PRs nowhere.ai:blocked-infraresidue → producer (Producer should exit and log an error when infra is down — and blocked-infra should not be a state #108): nothing parks on it; the next pass re-enters the ordinary lifecycle.ai:blocked-deployresidue andhuman:rejectresidue → human (eyes-on triage per Retire ai:blocked-deploy: deploys never block merges under the split lifecycle — collapse into typed ai:blocked-on refs to the repo's migration #162;migrate-rejectper ai:reject and human:reject are one state: same owner, same action, split only by who ruled #133).History folds
histFoldnames the keys a series draws BESIDE its own — an absorbed retiree's past, claimed by exactly one successor and by no row as its ownhist, so each sample draws exactly once:un-vettedfoldsawaitingReVet(Vetting is stateless: stamp verdicts with a vet protocol, collapse re-vet into un-vetted #128 — vetting is a pure function of the PR at its head).closeCandidateUpheldfoldscloseCandidatePrs(Close-candidate vetting misses PR-shaped flags: gh search issues scopes to type:issue #211/Upheld close-candidates are one human inbox: merge closeCandidateUpheld and the vetted-PR lane state #212's retired PR-side flag presentation, which measured that inbox).ai:rejectfoldshumanRejectandrelink. Both were consolidated INTO it by exactly that rename shape — ai:reject and human:reject are one state: same owner, same action, split only by who ruled #133 (a reject is a reject whoever ruled it) and ai:relink is a reject with a specific note — and the producer has no tool to execute it #135 (a linkage error IS a reject) — and without the fold, descriptor mode truncated the reject sparkline's past: inventory that sat under those keys vanished from the series.The two absorbed residue rows (
human:reject,ai:relink) therefore carry no series of their own:histis optional and omitted from the wire for them, so the successor draws the past and the same inventory never draws in two boxes (rain-org-health#162's design language for absorbed retirees). They still gate on their kept-while-nonzero counts, so a straggler stays visible as a state —Emit::WhileOccupiedcarries its gate key rather than readinghist, keeping the gate and the series claim independent.humanCloseCandidatefolds nowhere by design: a close ruling ishuman-close's decide+do with no state between (#213), so no live series measures what it measured.One population per state (#228)
The same divergence this branch's
Emitgate stopped having, one layer down.counts.<state>was computed from the label bucket (ai_state_label, the firstai:*label a PR carries) whilelanes[<lane>][<state>]was computed fromclassify_laneprecedence — so the document answered "how many PRs are in this state?" twice, with different numbers, and nothing compared them. Live on 2026-08-06:counts.ready23 over an absentvetter-verdicts.ai:readycell (all 23 stale at head, soun-vettedheld every one), which the dashboard drew as a merge inbox of 0 beneath a sparkline plotted at 23.blockedDeployhad the same shape available to it — this branch's ownEmitdoc named it — and would have fired the moment a PR carried two state labels; #221 has since deleted that state entirely, so it is a hazard that expired rather than one that was survived.Every lane row's
countskey is now its cell, and by the same route the gate takes:StateOccupancy::Lanegained thecountskey its cell fills, so both occupancy variants name one, andlane_counts_jsonwrites each key throughdescriptor_occupancy— the accessorEmit::WhileOccupiedalready reads. A key, its gate and its rendered cell are one number by construction, not by three call sites agreeing. Eight keys are written this way: the four that were the label bucket (ready,design,blockedOn,blockedInfra) and the five that already read a cell through their own hand-writtenlane_state_count(…)call (unvetted,reject,relink,humanReject) — the hand-written calls are gone too, because a re-laned state has to carry its key with it rather than need a second edit someone remembers.The
counts.<counts>field is deliberately not on the wire: the ratified occupancy form for a lane row is{lane}and nothing else. A consumer reads the cell, which is the population; the key is the producer-side mirror, already named ashistby every row that owns a series. The kept-while-nonzero contract for the retired keys becomes structural as a side effect — every lane row emits its key at 0 rather than being dropped, and it leaves only when the row does.No new key for the label bucket. The population is real and has a reader ("which PRs carry this label, whatever their head") but it was already emitted: the top-level
statesobject is that census, click-through like every other array, and it has never meant anything else. A secondcounts.*Labelledkey would restatestates.<label>.lengthand would land as an unclaimedcountskey under this branch's own conservation sweep. The two differ exactly where the classifier routes a PR past its own label — a stale-at-headai:readyPR is instates["ai:ready"]and in theun-vettedcell, because it carries the word and is not in the state.The daily human-readable review had the same defect and is fixed with it. Four of its sections (
ai:ready,ai:design, and the two blocked residues) printed the label bucket, so the tool disagreed with the--jsonthe same run emits — it listed those 23 PRs underMERGE — ai:readywhile its own document put every one inun-vetted. The sections are nowreview_lane_sections(lanes), a pure function handed the cells and nothing else: there is no bucket in scope to print, which makes the regression unconstructible rather than merely corrected. Each section names its descriptor row rather than a (lane, state) pair, so the section, thecountskey and the emitted descriptor read one declaration — and a residue section is gated by that row's ownEmit, so a drained retiree leaves the review exactly as it leaves the emitted shape.One cosmetic consequence, stated because the snapshot is committed: the crate serialises with serde_json's
preserve_order, socountskey ORDER follows insertion — now the lane rows in table order, then the non-state rollups. No key is added or dropped and no value changes because of it, but the nextrefresh-human-queuetick rewriteshuman-queue.jsonwith the keys in the new order, once.The step this puts in
human-queue-history.jsonlqueue-history-linecopiescountsverbatim, so a series means whatever its key meant on the day it was sampled. Four keys change meaning at this commit and the past is not rewritten — those lines are true measurements of the machine as it then was, which is the same rulehistFoldalready applies to a renamed series. Documented inlane_counts_json's own rationale, onqueue_history_line(the one writer of every line in that file), and in the README section the series is described in, so the discontinuity is stated rather than left as a silent step in the chart.Only
readysteps visibly — 23 → 0 on the 2026-08-06 snapshot — becauseai:readyis the only labelclassify_lanesplits on head drift. The inventory did not move (un-vettedalready held those PRs, and still reads 29 against the same snapshot); the measurement did. The other three have no such split, so they can differ only on a PR carrying two state labels, which is off-protocol and empirically absent: on that snapshotdesign24,blockedOn17 andblockedInfra0 are already equal to their cells and do not move at all.blockedDeployis a separate lineage: it does not change meaning, it ends — #221 deleted the state, so the key stops being emitted and its past stands as the record of a state the machine no longer has.Conservation tests (amendment 2, producer side)
every_classifier_state_has_exactly_one_descriptor— a sweep built from the classifier's own label constants (not a hand list) asserts every reachable (lane, state) has exactly one descriptor claiming it, the close-candidate hand-off is inventoried by both cc descriptors, and every lane-occupancy descriptor names a classifier-reachable cell.every_emitted_lane_cell_is_claimed_by_an_emitted_descriptor— conservation on the wire:stateDescriptorsAS EMITTED (through theEmitfilter, not the table behind it) against the emittedlanes, over a fixture that INCLUDES multi-label PRs — every ordered pair of state labels, the only inputs where a label-bucket key and a precedence winner disagree. Both directions: no emitted cell unclaimed, and no residue row emitted over an empty cell.every_descriptor_occupancy_source_resolves_in_the_emitted_document— every declaredhist,counts,itemsand gate key resolves in a document built by the samehuman_queue_docthat ships; a fold key must not be any row's claimedhist(an ownership transfer, not a share) and must fold into exactly one series; everycountskey is claimed by exactly one descriptor — as a live series or as a folded past — or is a named non-state rollup (closeCandidateIssues,totalProducerPrs,archivedRepoPrs,openIssues); and the emission is wired and gated by the document's owncounts.state_descriptors_emit_the_ratified_shape— the contract pinned verbatim at full occupancy (all 14 rows), order included.a_residue_row_emits_only_while_occupied— the live set pinned at zero occupancy (every live row visible while empty); a nonzeroproducer-blocked.ai:blocked-infraCELL returns exactly that row at its table position with its "(retired Producer should exit and log an error when infra is down — and blocked-infra should not be a state #108)" label; and each ABSORBED residue row (human:reject,ai:relink), while occupied, emits with nohistof its own whileai:rejectcarries that key in itshistFold.every_lane_states_counts_key_is_its_lane_cell(human-queue --json: counts.* (label bucket) and lanes cells (classifier) measure the same state differently — counts.ready 23 vs an absent lane cell #228) — the agreement, swept by ITERATING the table rather than naming states, over the same multi-label fixture: for every lane row,counts.<its declared key>equals the cell it declares as its occupancy. Two anti-vacuity guards, because a sweep that covers nothing is green: the visit count must equal the table's own lane-row count, and the fixture must contain at least one state whose label bucket differs from its cell.every_lane_state_has_exactly_one_review_section(human-queue --json: counts.* (label bucket) and lanes cells (classifier) measure the same state differently — counts.ready 23 vs an absent lane cell #228) — both directions between the table and the daily review: a lane row with no section is inventory the human is never shown, and a section over a non-lane row lists PRs from a cell that is never emitted.a_stale_at_head_ready_pr_prints_as_un_vetted_not_as_merge(human-queue --json: counts.* (label bucket) and lanes cells (classifier) measure the same state differently — counts.ready 23 vs an absent lane cell #228) — the live 2026-08-06 shape reduced to one PR: a labelledai:readyPR pushed past its verdict prints under UN-VETTED, and the human's MERGE inbox prints at 0 without listing it.a_retired_states_review_section_appears_only_while_it_has_residue(human-queue --json: counts.* (label bucket) and lanes cells (classifier) measure the same state differently — counts.ready 23 vs an absent lane cell #228) — the review's residue gate, off the row's ownEmit: a drained retiree leaves the review while an empty LIVE state still prints at zero.a_deleted_state_leaves_no_descriptor_no_counts_key_and_no_lane_cell— a DELETED state leaves no trace, as a property over aDELETED_STATE_LABELSregistry of(label, the counts key it used to emit): no descriptor names it, neither spelling survives incounts, no lane carries a cell, and the classifier treats it as the ordinary un-vetted fallthrough. Both merged deletions (RULED: migrate the ai:blocked-deploy residue to ai:reject — rework each PR for the split release lifecycle #221, RULED: an answered design question routes straight to producer rework, same as a rejection #219) are rows of DATA; the next one costs a row rather than a manual sweep. The counts key is DECLARED beside the label, not derived from it — deriving it was tried first and was wrong, because anai:*state drops the namespace (blockedDeploy) while ahuman:*state keeps it (humanDesign), so the inference asserted the LIVEcounts.designmust not exist. The test caught that on its first run.a_lane_whose_only_states_are_residue_disappears_when_it_drains— what an EMPTY lane does, pinned rather than eyeballed. RULED: an answered design question routes straight to producer rework, same as a rejection #219 lefthuman-decisionsholding only the retiredhuman:rejectresidue, so when it drains the lane is ABSENT fromlanesand no descriptor is emitted for it — correct, becauselanesis sparse occupancy and the only remaining row is residue-gated; a lane rendering as an empty box would be the permanent dimmed box clarification 1 removes. A straggler brings both back.Verified at
5abe53e(main merged in twice — #221 and #219): fullcargo testgreen via the flake's rust shell (1021 unit + 78 integration), CI's exactrainix-rs-static(rustfmt + clippy, changed file touched first) exit 0, the rainixpre-commit run --all-filesbundle exit 0,nix build .#pr-review-reportgreen (its in-derivation release-profile suite included).Merge coordination —
ai:blocked-deploy(#225 merged 2026-08-07) — DONEmainis merged in at71ac743(a merge, never a rebase, so review history stands). The plan below is what was executed, and the trap it names is real: the constant was DELETED, never re-literalised. GivingRETIRED_BLOCKED_DEPLOY_LABELa string literal to make the tree compile resurrectscounts.blockedDeployand lands green, because rustc only enumerates the remaining consumers when the const is gone.With it deleted, the compiler surfaced three hard references — the
STATE_DESCRIPTORSentry, theREVIEW_LANE_SECTIONSentry (this branch's own), and the label still sitting in the test-sidestate_label_sets()list. The other two are string literals no compiler can reach and were removed by hand: the golden row instate_descriptors_emit_the_ratified_shape, and theRETIRED_BLOCKED_DEPLOY_LABELmention in the state-table doc inventory.counts.blockedDeployneeded no deletion of its own, and this is the one place the plan changed because of #228: counts now derive from the descriptor table, so dropping the row drops the key by construction — there is no"blockedDeploy": …line anywhere that a stale patch could leave behind. Verified, not assumed: main's ownsubject_ref_tests::the_deleted_blocked_deploy_state_emits_no_count_keypasses unchanged, asserting the key is ABSENT (not zero) whileblockedInfra/blockedOn/ready/designsurvive.The residue fixture in
a_residue_row_emits_only_while_occupiedis re-pointed atai:blocked-infrarather than deleted — it exists to pin the gate mechanism, not this label, so deleting it would have quietly retired that coverage.Two conflicts were resolved against this branch's design rather than main's: the
countsblock stays descriptor-driven (main's side was the pre-#228 inline label-bucket block, which is exactly what #228 removes), and the daily review's state sections stay inreview_lane_sections. One stale assertion from main —count("leak", "leak") == 1— is removed: #130 clarification 2 retired that cell and this branch already asserts its absence, so the two contradicted each other and the suite caught it.Verified after the merge: no
STATE_BLOCKED_DEPLOY, noRETIRED_BLOCKED_DEPLOY_LABEL, and no descriptor naming the label anywhere in the source; every surviving mention of the string is either a comment explaining the absence, theflag-blocked-deployrefusal that still parses on purpose, or a test pinning that the string models nothing.Merge coordination —
human:design(#224 merged 2026-08-07) — DONEMerged in at
cf8822c. Same discipline as #221 and the same lesson, harder: the compiler is necessary but not sufficient.STATE_HUMAN_DESIGN, itshumanDesigncounts key, itsREVIEW_LANE_SECTIONSentry andHUMAN_DECISION_LABELS(a const spelled FROM the row) were all deleted, none re-literalised. After that the compiler reported clean while four data-level sites were still wrong, none reachable by type checking: the golden row instate_descriptors_emit_the_ratified_shape, BOTH emitted-key lists ina_residue_row_emits_only_while_occupied, and adescriptor("human:design").emitassertion that would only have blown up at runtime. Found by grep after the compiler pass.counts.humanDesignneeded no deletion of its own — same asblockedDeploy, it is written from the descriptor row, so dropping the row drops the key. Now asserted rather than assumed, by the registry property above.Conflict shape: four in
main.rsplus one in the README. Three resolved to this branch's design (the descriptor machinery, the descriptor-drivencountsblock, the review's sections). The fourth was hand-merged rather than taken from a side — main'scount("human-decisions","human:design") == 0is correct AND this branch's/leakabsence assertion is correct, while main's re-introducedcount("leak","leak") == 1is not.The consumer half — rendering from these instead of the hand-copied
STATEStable, plus the loud unclaimed-occupancy defect with the two named frozen-legacy count categories — is the rain-org-health follow-up the issue scopes out.QA
state_descriptor_tests::every_classifier_state_has_exactly_one_descriptor,…::every_descriptor_occupancy_source_resolves_in_the_emitted_document,…::state_descriptors_emit_the_ratified_shape,…::a_residue_row_emits_only_while_occupied— each fails on base (base has noSTATE_DESCRIPTORStable and nostateDescriptorskey: the tests do not compile against base, and the base behaviors reconstructed as mutants below — no emission, descriptor absent,lanes.leakemitted — are each killed)state_descriptor_tests::every_lane_states_counts_key_is_its_lane_cell,…::every_lane_state_has_exactly_one_review_section,…::a_stale_at_head_ready_pr_prints_as_un_vetted_not_as_merge,…::a_retired_states_review_section_appears_only_while_it_has_residue— these do not compile against the pre-human-queue --json: counts.* (label bucket) and lanes cells (classifier) measure the same state differently — counts.ready 23 vs an absent lane cell #228 parente8be1b7(it has noStateOccupancy::Lane { counts, .. }field and noREVIEW_LANE_SECTIONS), so the parent's behavior is reconstructed as mutants instead:counts.readyback to the label bucket, and the review'sai:readysection back to a bucket-fed one — both killed, belowcounts.readyreverted tobuckets.get("ai:ready")→ killed byevery_lane_states_counts_key_is_its_lane_cell;lane_counts_jsonstops writing theai:readykey → killed by that test + the counts-claim sweep +adding_url_leaves_every_count_equal_to_its_arrays_length;STATE_READY.occupancy.counts"ready"→"design"→ same three; the agreement sweep broken to visit nothing (.take(0)) → killed by its own visit-count guard, which is what stops it passing vacuously;descriptor_occupancyreporting every lane cell empty → killed by six tests including the agreement sweep, so the shared accessor cannot lie to thecountskey and the gate at once; the review printing a drained retired state → killed bya_retired_states_review_section_appears_only_while_it_has_residue; the review dropping theai:readysection → killed byevery_lane_state_has_exactly_one_review_section+a_stale_at_head_ready_pr_prints_as_un_vetted_not_as_merge. RULED: an answered design question routes straight to producer rework, same as a rejection #219 (the second merge), the three the coverage amendment asks for: a NOT-deleted state (ai:relink, which still has a row, acounts.relinkkey and a cell) added toDELETED_STATE_LABELSwithout being deleted from the code → killed by the registry property AND by the derived total, which is correctly coupled to the registry; a residue-only lane kept emitting when drained (STATE_HUMAN_REJECT.emitWhileOccupied→Always) → killed bya_lane_whose_only_states_are_residue_disappears_when_it_drains; the derivedun-vettedtotal reverted to the literal3→ killed bylanes_doc_emits_every_state_with_the_right_members, which is the 224-merge bug reproduced and caught. RULED: migrate the ai:blocked-deploy residue to ai:reject — rework each PR for the split release lifecycle #221 (the first merge):counts.blockedDeployresurrected as a hand-written key in the rollups block → killed by the counts-claim sweep (no descriptor claims it) and by main's ownthe_deleted_blocked_deploy_state_emits_no_count_key— the resurrection hazard the merge note names is caught, not merely avoided. human-queue --json emits counts but not the state vocabulary, so consumers hand-copy it and drift silently #130, re-verified after the merge: all twelve of the table above still kill (223-6re-pointed atSTATE_BLOCKED_INFRA, since the state it used to name no longer exists) —STATE_DESCRIPTORSdropping&STATE_BLOCKED_ON,STATE_LEAK.occupancy.items"leaks"→"leak",STATE_BLOCKED_ON.occupancyre-laned,STATE_BLOCKED_ON.ownerVetter→Human, the emission unwired toValue::Array(vec![]),STATE_BLOCKED_DEPLOY.emitWhileOccupied→Always,lanes_docre-emitting theleakcell,STATE_REJECT.hist_fold→[],STATE_HUMAN_REJECT.histNone→Some("humanReject"), a second row foldingrelink, the gate readingcounts.<hist>instead of declared occupancy, and a live row re-gated as residue (re-pointed atSTATE_DESIGN, sincehuman:design's row is deleted). One note on that second-to-last one: it is now killed only by the absorbed residue rows (ai:relink/human:rejectcarry nohist, so the mutated gate reads 0 and withholds them while occupied). The other half of its old kill —counts.<hist>disagreeing with the cell — is gone, because after this commit they are the same number by construction. That is the fix landing, not coverage lost.human-queue-history.jsonl's own key lineage for the retired fold keys — all independent of the implementation under test.statesalready IS that census under a key that has only ever meant it, and a secondcounts.*would land unclaimed under this branch's own sweep; structural agreement test iterating the shared table — covered, with two anti-vacuity guards; history-series meaning documented, data not rewritten — covered inlane_counts_json,queue_history_lineand the README. Beyond the issue's letter: the daily human-readable review had the same defect in four sections and is fixed with it, since leaving it would have the tool disagreeing with its own--json.histFold) — covered; (c) shape vs sparse occupancy kept separate — covered; amendment 1 occupancy-source field for the four non-lane states — covered; amendment 2 producer-side conservation derived from the shared constants — covered; clarification 1 residue-gated emission — covered; clarification 2 no unclaimed lane cell — covered; amendment 2 consumer-side conservation defect — Refs only, because the issue scopes the consumer half to the rain-org-health follow-up.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation