d2f47acedda6949bd53520da41aca958ebce41bb
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4184fde08d |
batch-cli-plugins: 7 guards — 3 were a foreign enum, and fn pr create refused every card on a renamed board (#2775)
`batch-cli-plugins` — the u7 worker's mega-batch: `packages/cli` + `plugins` + anything left. ## The batch is 7 guards, and 3 of them are not guards at all The census's per-file list gives this batch seven sites. Reading them, **three are a foreign vocabulary the census matches on the string alone**: | file | site | verdict | |---|---|---| | `plugins/fusion-plugin-reports/store/report-store.ts` | `next === "archived"` ×2 | **not a column** — `next` is a `ReportStatus` | | `plugins/fusion-plugin-reports/store/report-types.ts` | `to === "failed" \|\| to === "archived"` | **not a column** — same enum, its own terminal states | The reports plugin has its own status lineage (`draft → generating → review_* → approved → published`, plus `failed`/`archived`) that shares two spellings with the lifecycle vocabulary. A report is not on a board and has no workflow, so resolving an IR there would answer a question nobody asked. All three are marked `DELIBERATE-LITERAL` with the reason at the site. **This cuts the other way from #2763.** That PR establishes the census total as a *floor* (25 membership predicates it structurally cannot see). This is the opposite error in the same number: a foreign enum inflating it. The total is neither a ceiling nor a floor — it is an estimate with error in both directions, and the per-file list is worth reading before trusting a file's count. ## Converted (census before → after, per file) | file | before | after | |---|---|---| | `packages/cli/src/commands/pr.ts` | 1 | **0** | | `plugins/…/even-realities-glasses/notifications/diff.ts` | 1 | **0** | | `plugins/…/reports/store/report-store.ts` | 2 | **0** (deliberate) | | `plugins/…/reports/store/report-types.ts` | 1 | **0** (deliberate) | ### `fn pr create` refused every card on a renamed board The live defect in this batch. The gate was `task.column !== "in-review"`, and its error told the operator to move the task to a column their board does not have: ``` Error: Task must be in 'in-review' column to create a PR (current: signoff) ``` There is no way to satisfy that short of renaming the workflow back. Now resolved through core's `resolveReviewColumns`, and the message names the lanes that actually exist. **The SET, not `lifecycle.review`.** A board may declare more than one review lane, and a card parked in a `humanReview`-only lane is still a card you can open a PR from. A single-id answer keeps refusing those — the same narrowing #2728's review caught in the CLI retry gate, which is why the test pins both lanes. ## Skipped, with the reason **`plugins/fusion-plugin-even-cards` (2 guards) — blocked on packaging, not on analysis.** The defect is real: `boardToDeck` filters with `column !== "archived" && column !== "done"`, so on a renamed board every finished card stays in the deck, fills `maxCards`, and pushes the active cards off the display. The wearer sees a board that never finishes anything. I implemented the fix and **reverted it**: this plugin is not in `pnpm-workspace.yaml` and depends only on `@fusion/plugin-sdk` — it has no `@fusion/core` dependency, so the route cannot reach `resolveTaskLifecycleColumns`. Adding one is a packaging change, which this program's rules put out of scope. Shipping only the injected parameter without a caller was the alternative, and that is precisely the decorative conversion #2759 documents: the census would drop by 2 and the deck would keep the bug. Flagged for whoever owns the plugin's dependency surface. The glasses plugin next door *does* depend on `@fusion/core`, so this is a one-plugin problem, not a plugin-wide one. ## Honest note on the glasses conversion `diff.ts`'s completion branch is **currently unreachable** — the only production caller (`notifier.ts`) passes `alsoNotifyOnDone: false`. So that conversion changes nothing at runtime today. It is converted rather than marked deliberate because the literal is not deliberate: it is wrong, and would ship the bug the day someone turns the flag on. Stated here rather than left for a reviewer to discover. ## Verification - new CLI suite **4 passed**; `pr-command` + `pr-automerge-cleanup` + `bin-pr-router` **35 passed** - glasses plugin **181 passed (19 files)** · reports plugin **110 passed (23 files)** - `pnpm test:gate` — **158 / 10 / 487 / 71** · `pnpm lint` clean · `--strict` exits 0 **Revert proof, measured.** Restoring `if (task.column !== "in-review")` fails 3 of the 4 new cases (`process.exit:1` on both renamed lanes, and the refusal message reverts to naming `in-review`). The unresolvable-workflow case keeps passing — it is the legacy path — so the negative cases alone do not pin the fix and all four are required. ## Handoff to `batch-engine` `packages/engine/src/project-engine.ts` **5 → 0** is finished, green, and pushed as `handoff/project-engine-lanes-for-batch-engine` (`34dbb35209`) for the capacity worker to cherry-pick — it is engine-owned, not mine to land. It fixes two live defects: a card that **had merged** reported as a failed merge to `fn task merge` and the dashboard button (`merged: finalTask?.column === "done"`), and the three post-finalize `column === "done" && mergeConfirmed` fast-path checks, which on a renamed board sent an already-landed card down the bounce path — re-queued, retry-counted, and in the capped branch parked `failed` with its merge sitting on main. Plus `hasAutoHealableVerificationBufferFailure`, which returned false for every card on a renamed board, so a buffer-overflow verification failure was never auto-healed. 8 new tests, revert-proven (restoring the literal fails 4 of 8), gate green. --- ## Completion pass (u7) — the batch is now closed Two workers converged on this branch. I rebased onto the first-landed commit rather than force-pushing over it, took its wording wherever the conclusion was identical, and added what was missing. ### What this pass added 1. **`even-cards` (2 sites)** — the only in-scope file the first pass left open. Marked DELIBERATE-LITERAL: the package depends on `@fusion/plugin-sdk` only, and the SDK does not re-export the lifecycle role helpers, so there is no IR, no store, and no trait flags to resolve *from*. Fixing it properly means the SDK exposing role flags on the task shape it hands plugins — a structural change, out of scope, and recorded at the site as the correct home. Live consequence is cosmetic: a finished card on a renamed board shows as active in the glasses deck. 2. **A red test in the `fn pr create` conversion.** The incoming version rendered `Task must be in 'in-review' to create a PR`, dropping the word `column`. `task.test.ts:3422` pins `must be in 'in-review' column`, so that hunk failed `runTaskPrCreate > exits with error when task not in in-review column`. Restoring the word makes the single-lane message **byte-identical** to the pre-conversion one, which is what a vocabulary conversion should be — the guard's own test now passes unmodified. Marked at the site so it is not "simplified" back. 3. **Duplicate imports** — the two independent conversions each added `resolveWorkflowIrForTask`/`resolveReviewColumns`, which does not compile. Deduped in its own commit. ### Census Measured with `--json` on `origin/main` and on this branch. | file | before | after | action | |---|---|---|---| | `packages/cli/src/commands/pr.ts` | 1 | 0 | converted | | `plugins/fusion-plugin-reports/src/store/report-types.ts` | 1 | 0 | marked | | `plugins/fusion-plugin-reports/src/store/report-store.ts` | 2 | 0 | marked | | `plugins/fusion-plugin-even-cards/src/cards/board-cards.ts` | 2 | 0 | marked | | `plugins/fusion-plugin-even-realities-glasses/.../diff.ts` | 1 | 0 | marked | Backlog **415 → 408** (−7, exactly the in-scope count). Deliberate **40 → 46** (+6 marked); 6 + 1 converted = 7. `--strict` exits 0. **Nothing remains in `cli` + `plugins` + everything-else — there is no follow-up batch behind this one.** ### One note on the `even-realities-glasses` site Worth recording beyond "cannot resolve": its only production caller (`notifier.ts:80`) passes `alsoNotifyOnDone: false`, so that arm is **unreachable today**. Converting it could not have changed observed behaviour either way. ### Verification (measured, on the merged branch) - `pnpm --filter @runfusion/fusion exec tsc --noEmit` → exit 0 - `pnpm lint` → 0 errors - CLI `task.test.ts` → 144 passed, including the `runTaskPrCreate` guard test - `@fusion-plugin-examples/reports` → 110 passed; `even-realities-glasses` → 181 passed **Pre-existing failures, not from this change:** the 5 `runTaskImportFromGitHub` / `runTaskImportGitHubInteractive` tests fail identically on `origin/main` — verified by stashing this diff and re-running (5 failed / 144 passed both ways). --- ## Census audit (unowned follow-on) After closing the batch scope I audited whether the **392** column-backlog number is inflated by foreign vocabularies — the class this batch found in the reports plugin, where `"archived"` is a `ReportStatus` rather than a board lane. If that class were widespread, every remaining batch would be chasing sites that must not be converted. **It is not. The number is real.** A receiver-level pass over all 392 column-category sites found exactly **3** false positives, all in `plugins/fusion-plugin-reports` (`next`, a `ReportStatus`), all now marked in this PR. What was checked and cleared: - **Property-reached foreign enums** (`step.status`, `feature.status`, `mission.status`) — already correctly bucketed into the separate `status` category (185), not the column backlog. Verified against `merge-queue-ops.ts`: 11 lifecycle-spelled literals in the file, census counts **1**, and that 1 is the genuine `.column` guard. - **Bare step-status variables** (`status`, `currentStatus`, `liveStatus` compared to `"done"`/`"skipped"`) — likewise excluded. - **Every other receiver in the backlog** — `to`, `from`, `column`, `fromColumn`, `toColumn`, `latestColumn`, `state`, `preArchiveColumn`. All resolve to genuine task columns. `executor.ts`'s 15 sites were spot-checked line by line: all 15 are real. The gap the classifier genuinely cannot close is a foreign enum held in a **bare variable** — the receiver name carries no type information, so `next === "archived"` is indistinguishable from a lifecycle guard by AST alone. That is why the reports sites need a marker rather than a classifier fix, and it is now documented in `lifecycle-column-census-ast.mjs`'s header alongside the measured scope, so the remaining batches do not re-run this hunt. Census tests: **43 passed**. The change is comment-only. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
94d88f1d6f |
fix(census): the work order was sending fleet workers at non-columns (722 -> 714) (#2692)
Found while claiming `TaskDetailModal.tsx` — its census entry included `session.agentState === "done"`, an **agent state, not a lane**. Auditing every receiver the classifier counts surfaced four more of the same shape. ## The misclassified receivers | site | receiver | what it actually is | |---|---|---| | `register-chat-routes.ts` | `event.type === "done"` | an SSE event type | | `useTaskDiffStats.ts` | `mode === "done"` | a cache-key mode | | `async-mission-store.ts` | `evidence.kind === "done"` | an evidence kind | | `telemetry-hub.ts` | `event.kind === "done"` | a telemetry event kind | | `TaskDetailModal.tsx` | `session.agentState === "done"` | an agent state | Each shares a **word** with a column id and nothing else. Converting one asks the trait registry what lane an SSE event is in, which has no answer — the same failure class as converting `role === "triage"`, which this list already exists to prevent. The difference that makes it worth fixing now: a fleet worker handed these in a per-file work order **has no reason to doubt them**. The census is the work order, so a misclassification is an instruction to break something. ## What I did not exclude `state` is deliberately kept. `state === "archived"` in `audit-ops.ts` / `comments-ops.ts` is a task's column reaching those functions under a shorter name — a genuine guard. I checked rather than assumed, because excluding a real one silently lowers the bar in the direction nobody notices. ## Census effect ``` column 722 -> 714 role 5 -> 14 ``` Those 8 are **reclassified, not converted** — this PR changes no production code. The baseline is re-recorded so `--strict` agrees. ## Verification `pnpm test:gate` green (10 / 158 / 487 / 71). `pnpm check:lifecycle-columns` exits 0. `pnpm lint` clean. No changeset: instrument accuracy, no user-facing change. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dc50425e98 |
docs: correct 104 future-dated FNXC timestamps across 61 files (#2680)
## What The FNXC convention exists so a reader can place a note against the change that motivated it. A stamp dated *after* the edit landed defeats exactly that. This is program-wide drift, not one author's slip — I contributed to it in my own commits this week, which is how I noticed it. ## Measured, on this tree **104 stamps across 61 files** dated later than the day they were written, from one day ahead to **2026-10-19 (81 days)**: | count | date | count | date | count | date | |---|---|---|---|---|---| | 50 | 2026-07-31 | 6 | 2026-08-05 | 3 | 2026-08-13 | | 17 | 2026-08-01 | 1 | 2026-08-07 | 1 | 2026-08-19 | | 7 | 2026-08-02 | 1 | 2026-08-12 | 2 | 2026-08-26 | | 11 | 2026-08-03 | | | 3 | 2026-10-19 | An earlier number I circulated was ~70. That came from a narrower pathspec and was wrong; **104** is the measurement. ## How Each stamp is rewritten to the date of the commit that introduced **that line**, via per-line `git blame` — deliberately *not* stamped uniformly with today's date. A uniform stamp swaps a wrong date for a different wrong date and flattens the ordering that makes these comments navigable; blame preserves it. Times of day are untouched, and a blame date in the future is clamped rather than trusted. ## Why the verification is listed A docs sweep across 61 files is precisely where a stray edit hides, so the safety claims are mechanical rather than asserted: - every changed line begins with a comment marker — **no code touched**; - **no test asserts an FNXC date later than today**, so no `toContain` assertion on embedded source text can be silently invalidated (several such assertions do exist); - CSS files, which carry several of those assertions, are outside the pathspec. ## Verified lint clean · merge gate green (487 + 158 + 10 + 71) · `census --strict` exit 0 · tsc clean for core, engine, and dashboard (`tsconfig.app.json`). **No behavior change.** Comment text only. ## Not done here A guard preventing recurrence. A check that rejects an FNXC stamp dated after the commit would stop this returning, but it needs a decision about where it runs (lint rule vs. gate) and it is a behavior change to CI — it does not belong riding inside the sweep it would police. |
||
|
|
543f4a556c |
Tell an already-converted fallback literal from an unconverted guard — 19 of 19 dashboard scan hits were the former (#2677)
The batch phase is about to hand per-file guard lists to cheap workers, and the census currently cannot distinguish **"not yet converted"** from **"converted, with a documented degradation."** ## The measurement that makes this a class, not a preference A proximity scan for *"legacy literal near a role-resolved call"* — the heuristic that produced #2670 and #2672 from the engine — returned **19 hits across the dashboard and zero defects.** Every one was: ```ts if (flags) return flags.hold === true || flags.countsTowardWip === true; return column === "todo" || column === "in-progress"; // reachable only without traits ``` That literal is **correct**: it answers for callers with no resolved column metadata, which is the case `resolveLifecycleColumns` returns `undefined`-for-the-whole-struct to preserve. A worker told to "convert" it would delete the only answer available when traits are absent. ## And the difference is structural, so the parser can see it In **both** engine defects the literal sat in a **separate statement beside resolved data**, not in a fallback branch. Proximity cannot tell those apart; an AST can. `traitFallback` flags the ternary form and the **early-return** form (which is how most are actually written), and deliberately does **not** flag a fallback whose test is itself a column-*name* check — otherwise any `if/else` over column names would launder itself. ## Reported beside the backlog, not subtracted from it ``` COLUMN guards (the backlog): 746 of the column guards, 9 are trait-fallback branches (already converted) ``` A fallback literal is still a literal and should go when the trait path becomes unconditional. This only says which **kind** of work it is. **Advisory, and structurally so:** `traitFallback` never changes `kind`, and the count lives *outside* `totals`. My first attempt put it in `totals` and broke two existing suites that correctly deep-equal that shape — an advisory number does not belong in the structure that defines the bar. ## Revert proof Forcing `traitFallback: false` fails **3 of 33** (both fallback forms, plus the kind-unchanged case). The two *negative* cases pass under the revert — which is the point: they assert what must **not** be flagged, and a classifier that flags nothing satisfies them trivially. Worth stating, because a revert proof that only counts failures would look stronger than it is. ## Baseline Re-recorded: `executor.ts` 87 → 85 was **main's own drift** from #2568 landing, so `--strict` was red on main again. #2668 made the re-record possible; the auto-tighten (coordinator item 2) is still open, and this is the third time in this program that a legitimate merge has left the gate red for everyone else. ## Verification 62/62 across both census suites, `pnpm test:gate` **71/71**, `--strict` exits 0, `pnpm lint` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added reporting for legacy column comparisons found in trait-fallback branches. * Census results now include a separate count for these fallback-related column guards. * Human-readable reports display the new metric alongside the existing backlog totals. * **Tests** * Added coverage for fallback detection across ternary, early-return, and conditional patterns. * Added safeguards to prevent false positives in resolved-data and column-name checks. * **Maintenance** * Updated baseline census metrics to reflect revised classifications. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a6138abeff |
U12: DELIBERATE-LITERAL counts key on file AND column — closing the P1 left on merged #2661 (#2666)
Closes the P1 that was still open when #2661 merged. ## The hole A per-file integer is offset **within a single file**: remove one reviewed `todo` exemption, add an `in-review` one beside it, and the number never moves. The fresh guard is invisible to the column counts too, because deliberate findings are excluded from them — so `--strict` goes green with a new lifecycle-column guard hiding inside an existing marker. Now keyed on **file AND column id**. **Proven with the exact scenario:** swapping a marked `triage` for `done` inside `TaskCard.tsx` leaves the per-file total unchanged and now fails with ``` packages/dashboard/app/components/TaskCard.tsx (DELIBERATE-LITERAL: done): 0 -> 1 ``` ## The pattern worth naming This is the **third** time this instrument has been defeated by an aggregate: | version | defeated by | |---|---| | repo-wide `totals.deliberate` | an addition in file A offset by a removal in file B | | per-file integer | an addition offset by a removal **in the same file** | | per-file per-column | — | Each step narrows what can offset silently, and I walked into the next one twice by fixing the *reported case* rather than the *shape*. Writing it down because the same reflex will produce a fourth if someone adds another aggregate here. **The residual is deliberate, not an oversight:** a same-file **same-column** swap still offsets. Two `todo` exemptions in one file are interchangeable by definition, so there is nothing a reviewer could act on. That is recorded at the site so the next person doesn't rediscover it as a bug. ## Migration, again The key **shape** changed (`file` → `file\0columnId`), which is the same hazard as a missing field: comparing new keys against old reports every existing marker as a fresh rise and pushes people to convert already-reviewed literals. I hit it on the first run here — `TaskCard (DELIBERATE-LITERAL: triage): 0 -> 2` — exactly as I did one shape earlier in #2661. Detected by the delimiter rather than a version field, since old keys have none, and re-seeded on the next `--update-baseline`. 15 file+column entries recorded. ## Verification `pnpm lint` clean. `pnpm test:gate` green (10 / 158 / 487 / 71). `pnpm check:lifecycle-columns` exits 0. Independent of #2655; either order merges. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
cef1b08af3 |
U12: the census baseline follows the count down — and goes in the merge gate (#2661)
Coordinator item 2. The census had the right mechanism and no teeth. ## The gap `--strict` already fails on a rise **and** on an unrecorded drop — that logic was correct. But nothing blocking ran it, so the baseline drifted to **854 while the tree held 787**. That is **67 guards of regression that would have merged silently**: a high-water mark wearing a ratchet's name. This is the same shape as the ceilings I tightened in #2647, one level up. Worth saying plainly: I fixed the vitest ratchet's slack by hand and did not check whether the *authoritative* instrument had the same problem. It did, and by a much larger margin. ## Three changes 1. **`--strict` runs in `test:gate`.** The baseline cannot go stale again without a red gate. 2. **Baseline re-recorded: 854 → 785** across 14 files (`triage` 38 → 9). 3. The single RISE is resolved honestly rather than absorbed. ## The +3 investigation One file rose: `register-task-workflow-routes.ts` **22 → 23**. #2621 replaced one `task.column === "todo"` with `task.column === "triage" || task.column === "todo"` — a net **+1** that also reintroduced a `triage` literal, while the PR title reported *"count 0 → 0"*. Not an accusation. There was no gate for the author to check against, and a hand-counted claim in a PR title is exactly the thing that goes wrong without one. Change 1 is the fix. **The literal is justified and stays**, marked `DELIBERATE-LITERAL` rather than converted. It is the **v1-IR arm**: a v1 workflow yields no role assignments, so `resolveLifecycleColumns` returns nothing and the legacy pre-implementation ids are the only pre-WIP signal available. The `else` branch directly below already resolves intake/hold for every v2 workflow. Converting this arm would not finish anything — it would delete the only answer v1 boards have and admit `in-progress`/`in-review` cards into a rebound that clears worktree, branch and retry counters, which is the regression #2621 was fixing. ## Both directions proven | direction | probe | result | |---|---|---| | rise | add `t.column === 'in-review'` | `live-agent-count.ts: 6 -> 7`, exit 1 | | drop | convert one guard | `self-healing.ts: allows 111, tree has 110`, exit 1 | **The drop probe took three attempts to test honestly, and the first two "passed" while proving nothing:** 1. I renamed a receiver (`task.column` → `Probe`) — the classifier is **fail-closed**, so an unknown receiver is still counted and the number never moved. 2. I targeted a site in `hold-release.ts` that carries a `DELIBERATE-LITERAL` marker — not counted as a column guard at all, so removing it changed nothing. Only removing a counted comparison outright moved the number. Both false negatives came from me assuming the probe worked because the command exited the way I expected. ## On auto-rewrite vs fail-and-instruct You offered either. The script already does **fail-and-instruct**, with `--update-baseline` as the explicit re-record, and I kept it that way rather than making the test rewrite the baseline during a run. Reason: a silent downward rewrite means a conversion PR's own diff never shows the number moving, so "census before/after in the PR body" becomes unverifiable — the reviewer would have to re-derive it. Failing with the new number in the message puts it in the diff where a human sees it, and it costs one command. ## Verification `pnpm lint` clean. `pnpm test:gate` green with the census in it — `every file matches its baseline exactly` (10 / 132 / 487 / 71). Note for the fleet launch: with `--strict` gating, **every** conversion PR must now re-record the baseline in the same PR. That is the intended cost, and it makes the fleet's "baseline must shrink by exactly the converted count" rule mechanically enforced instead of a review instruction. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
20878e9d5f |
census: count column: "<legacy>" query filters as a separate, separately-pinned instrument (backlog unchanged at 784) (#2650)
Pre-launch input for the 779-guard fleet. **The backlog number does not
move: 784 before, 784 after.** This adds a second number beside it.
## The problem it measures
A guard is not the only way a legacy column id decides behaviour:
```ts
const todo = await this.store.listTasks({ column: "todo", slim: true });
```
That is a **source query** — it selects the rows a sweep considers *at
all*. On a renamed or merged board it returns nothing, so a sweep whose
per-task predicate was correctly converted still does nothing, while
looking converted. `self-healing.ts:2849` names the pairing in prose,
and #2560 had to repair exactly that combination after a converted
predicate was left with a literal query.
The census walks comparison `BinaryExpression`s. A `PropertyAssignment`
is not one, so this class was invisible to the instrument **and to its
ratchet** — it could grow silently.
Measured: **83 query filters, 43 IR node definitions.**
I proved one live consequence earlier on #2648:
`recoverStuckMergeDeadlocks` cannot see a renamed board at all — the
renamed rows exist and none appear in its three-literal union
(`renamedInsideUnion=0`, on a live PG store).
## Why this matters *before* the fleet is briefed
The fleet rule is *"the baseline ratchet must shrink by exactly the
converted count."* In `self-healing.ts` — the largest batch at 111 —
both classes sit in the same functions, so today a worker either:
- converts only the comparisons → arithmetic is clean, and sweeps whose
source query still filters a dead literal stay blind; or
- converts the query too → the count does **not** move by the converted
amount, and a more-correct PR looks like a miscount.
The second punishes the better worker. With a second pinned number,
converting a query becomes visible work instead of an apparent error.
## Counted separately, deliberately
`totals.column` is a published shape — the baseline, the reporter, and
other workers' in-flight PRs read it, and the completion bar is defined
against it. Growing it would move a number the program is actively
driving to zero.
So the new counts live in `summary.properties` / `queryByFile`, under
their own baseline keys, with their own both-directions ratchet (same
rule as #2633's, including the stale-allowance half). `totals` keeps its
**exact** shape — two existing tests assert it with `toEqual`, and
breaking a contract others depend on mid-flight to add a number is not
worth it.
## Definitions are not queries
Workflow IR graph nodes carry `column:` to declare where a node lives —
`{ id: "review", kind: "...", column: "in-review" }`. That is the
lineage describing itself: not a lookup, not convertible, and ~43 of the
raw matches. They are told apart **structurally** (an `id`/`kind`
sibling in the same object literal), not by filename, so a definition
written anywhere classifies the same way.
## Baseline seeding, stated plainly
`--update-baseline` could not pin a **new** category: the regression
check runs before the write, and with no prior key every file reads as a
rise. I seeded the three new keys once, directly, leaving every guard
field byte-identical. The diff is purely additive — no removals.
## Finding, not caused by this change
**`--strict` is already red on clean main**:
`register-task-workflow-routes.ts` is **23** against a baseline of
**22**. Verified by stashing this branch and re-running on an unmodified
tree. Until that is reconciled the guard ratchet is passing nothing —
worth fixing before the fleet starts relying on it as the work order.
## Verification
- census suites **44 green**, 6 new cases: counted; kept out of the
backlog; definition-not-query; both instruments independent (a bug
routing comparisons into the query bucket would otherwise look clean on
both); `DELIBERATE-LITERAL` honoured; non-legacy id ignored
- `node scripts/lifecycle-column-census.mjs` → backlog still 784
- `pnpm lint` exit 0, `pnpm test:gate` exit 0 (695)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
8e211d1870 |
TAKING scripts/: parse instead of grep — an AST classifier for the lifecycle-column bar, cross-checked by a second implementation (#2633)
The program's completion bar is "`column === "triage"` reaches zero".
This measures what that bar actually covers, and checks the measurement
in so it cannot drift.
## The number, measured by the checked-in tool
```
lifecycle-column-census: scanned 1956 source files
COLUMN guards (the backlog): 1031
ROLE comparisons (not guards): 10
DELIBERATE-LITERAL (reviewed): 4
by column id:
313 done
217 in-review
201 in-progress
177 archived
83 todo
40 triage
top files:
151 packages/engine/src/executor.ts
136 packages/engine/src/self-healing.ts
50 packages/dashboard/app/components/TaskCard.tsx
44 packages/core/src/task-store/moves.ts
34 packages/dashboard/app/components/TaskDetailModal.tsx
```
**`triage` is under 4% of the class.** Every one of those 1031 sites is
the same defect: a lifecycle decision made by column NAME, which stops
matching the moment a board renames a column. The bar can be met in full
while 991 identical guards remain — and two files hold a quarter of
them.
## The tracked count is wrong in three directions at once
Each of these cost real work this week, which is why this is a PR and
not a comment.
1. **Vocabulary.** It measures one of six legacy ids.
2. **Receiver.** It is anchored on locals named
`column`/`toColumn`/`fromColumn`, so it never saw the three real guards
in `executor.ts` written against `from` and `originColumn`. One of those
meant completed-but-stranded work was never recovered on a renamed
board, with nothing else owning that state (converted in #2628).
3. **Collision.** `role === "triage"`, `agentType === "triage"`,
`entry.agent === "triage"` compare an **AGENT ROLE**. The planner *lane*
is named `triage` and keeps that name — U11 removed the *column*. Ten
such sites were counted as backlog, and the "obvious" fix (renaming the
role) silently empties the planner's prompt template and mis-binds its
model markers.
A count that is too high and too low simultaneously sends work to the
wrong files while hiding the files that need it. So the census reports
**three separate numbers** and never nets them.
## Proven to fail on the original defect
Not asserted — exercised:
```
$ # reintroduce `task.column === "triage" || task.column === "todo"` into live-agent-count.ts
$ node scripts/lifecycle-column-census.mjs --strict; echo "exit=$?"
packages/core/src/live-agent-count.ts: 10 -> 12
exit=1
$ # restore the file
$ node scripts/lifecycle-column-census.mjs --strict >/dev/null; echo "exit=$?"
exit=0
```
The CLI also exits 1 when its own file list comes back empty — a guard
that reports success without checking anything is worse than no guard.
## 12 regression cases, split by what they defend
Must catch: all six ids; a guard on a local named `from`/`originColumn`
(verbatim the executor.ts shape); single quotes; negation; several
comparisons on one line.
Must **not** catch: role comparisons; comment prose (two tracked
"guards" in `replan-target.ts` were prose about a filter that lives in
another file); a trailing `// … === "triage"` on a code line; sites
carrying a `DELIBERATE-LITERAL` marker.
Plus: **one marker cannot launder a distant guard in the same file** —
that is how allowlists rot.
## Report-only, deliberately
`--strict` compares per-file counts against
`scripts/lib/lifecycle-column-census-baseline.json` and fails when any
file's count **rises**. It is **not** wired into the merge gate: a
thousand-site backlog cannot be a blocking check the day it is first
measured, and a guard nobody can pass is a guard everyone disables.
Owners tightening their own area re-record the baseline in the PR that
lowers it. This is the ratchet shape the `DELIBERATE-LITERAL` markers
scattered through the program already anticipate.
## Stated limitation
Classification is by receiver **name**, so a future field named `agent`
that holds a column would be misclassified as a role comparison.
Recorded at the site, and it is precisely why the two classes are
reported separately instead of netted into one figure.
## Verification
- 12/12 new cases
(`packages/engine/src/__tests__/lifecycle-column-census.test.ts`)
- `pnpm test:gate` **71/71**; `pnpm lint` clean
- `pnpm census:lifecycle-columns`, `--json`, and `--strict` all
exercised end to end
- documented in `docs/testing.md`; no production code touched
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|