From ae23be79f792c2d2e679469541a0fb137e7f0e0c Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 30 Jul 2026 02:53:18 -0700 Subject: [PATCH] =?UTF-8?q?fleet:=20scheduler.ts=2028=20=E2=86=92=20triage?= =?UTF-8?q?d=20(NOT=20converted)=20+=20repo-wide=20reachability=20measurem?= =?UTF-8?q?ent=20=E2=80=94=20the=20work=20order=20sorts=20on=20a=20number?= =?UTF-8?q?=20that=20doesn't=20predict=20convertibility=20(#2687)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Claim `packages/engine/src/scheduler.ts` — the largest **unclaimed** cluster (28). Triaged, **not converted**, for the reason below. Census unchanged: **722 → 722**. No baseline movement is claimed, because nothing was converted. ## Why not converted Three of us independently hit the same wall on our first file — #2683 and #2684 (`self-healing.ts`), #2685 (helper coverage). This measures the whole backlog **once** so the remaining workers don't each pay that cost. Two constraints gate conversion. Neither is visible in the per-file counts the work order sorts on. ### 1. Location — the helpers aren't importable from 80% of the backlog `isIntakeColumnRole` / `isPreImplementationColumnRole` / `isHoldColumnRole` live in `packages/dashboard/app/utils/columnRoles.ts`, a dashboard-**app** module. | Location | Guards | Share | Importable? | |---|---:|---:|---| | `packages/engine/**` | 316 | 43% | no | | `packages/dashboard/app/**` | 150 | 20% | **yes** | | `packages/core/**` | 148 | 20% | no | | `packages/dashboard/src/**` | 78 | 10% | no | | `packages/cli/**` | 24 | 3% | no | | plugins | 6 | 1% | no | **150 of 722 (20%)** can call them at all. Widening the helper *set* (#2685, correctly) does not move this number — it's the module's location, not its coverage. Core already exports `resolveColumnFlags`, so a core-side predicate module would be *the same* abstraction made reachable, not a new one. It is a prerequisite for the other 80% and **not sufficient** — see below. ### 2. Flag scope — the binding constraint A role predicate needs resolved trait flags. Most guards run in functions handed a bare task row with no IR to resolve from. Threading one in changes a signature and its call graph: a **behavior change, out of scope**. File-level proxy over the 572 non-dashboard guards: **339** in files that reference an IR/flags resolver, **233** in files with none. **That proxy overstates convertibility, and the overstatement is the finding.** Reachability varies *within* one file, so a file-level verdict is unusable. In my claimed cluster: | Site | Context | Convertible? | |---|---|---| | `scheduler.ts:1690` | `resolveWorkflowIrById(...)` + `resolveColumnFlags(c)` in the same block | **yes** | | `scheduler.ts:231` | `isLegacyDependencySatisfied(dep: Task \| undefined)` | no — task only | | `scheduler.ts:341` | `shouldHoldActiveFileScopeLease(...)` | no — task only | So "convert the file" is not a unit of work that exists in this backlog, and the rule *"the baseline must shrink by exactly your converted count"* cannot be satisfied per-file until the count is per-site. ## A guard that must be skipped, not guessed `packages/core/src/task-merge.ts:254`: ```ts if (!options.skipColumnIdentityCheck && task.column !== "in-review") { ``` The parameter is `Pick` — no IR, deliberately. The in-source FNXC comment records that callers who *have* resolved the `merge-blocker` trait pass `skipColumnIdentityCheck` rather than spoofing `{ ...task, column: "in-review" }`. The trait-aware path already exists *beside* this literal. Converting it wouldn't remove a legacy id — it would delete the fallback the option was introduced to make explicit. **Flagged and skipped.** ## Suggested census upgrade (not done here) Emit per-site whether trait flags are resolvable in the enclosing scope. That turns the work order from "largest file" into "largest **convertible** cluster" and makes baseline shrinkage predictable. I did not touch `scripts/lifecycle-column-census.mjs` — it is the shared instrument and changing it unannounced would invalidate everyone's in-flight before/after numbers. ## Method correction worth propagating to every fleet worker Claim-collision scans must compare a branch to its **merge base**, not to `origin/main`. `git diff origin/main origin/ -- ` reports a difference when the branch is merely *stale* (the file didn't exist at its base) — it flagged dashboard and CLI PRs as touching engine E2E files. I nearly skipped a free cluster on that false signal. `feature/code-organization-wave17` is excluded from collision checks: **1556 files, 150 commits behind main, already `DIRTY`**. It must rebase wholesale regardless, and counting it as a claim marks *every* cluster in the backlog as taken. Docs-only — no source, no test, no census change. --- .../fleet-conversion-reachability.md | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 docs/plans/workflow-owned-merge-stack/fleet-conversion-reachability.md diff --git a/docs/plans/workflow-owned-merge-stack/fleet-conversion-reachability.md b/docs/plans/workflow-owned-merge-stack/fleet-conversion-reachability.md new file mode 100644 index 0000000000..589a6f5f27 --- /dev/null +++ b/docs/plans/workflow-owned-merge-stack/fleet-conversion-reachability.md @@ -0,0 +1,113 @@ +# Fleet conversion reachability — measured, repo-wide + + + +## Summary + +The backlog is **722 COLUMN guards**. Two independent constraints gate conversion, and neither is +visible from the census's per-file counts — which is what the work order sorts on. + +| Constraint | Guards affected | Discovered by | +|---|---:|---| +| No role helper for the role | 680 of 722 (94%) | #2685 | +| Helpers not importable from the call site's package | 572 of 722 (79%) | this note | +| Column flags not in scope at the guard | see below | #2683/#2684 (one file), this note (repo-wide) | + +## 1. Where the guards actually live + +The role helpers (`isIntakeColumnRole`, `isPreImplementationColumnRole`, `isHoldColumnRole`) live in +`packages/dashboard/app/utils/columnRoles.ts` — a **dashboard-app** module. Engine, core, CLI, and +the dashboard *server* cannot import it. + +| Location | Guards | Share | Helpers importable? | +|---|---:|---:|---| +| `packages/engine/**` | 316 | 43% | no | +| `packages/dashboard/app/**` | 150 | 20% | **yes** | +| `packages/core/**` | 148 | 20% | no | +| `packages/dashboard/src/**` (server) | 78 | 10% | no | +| `packages/cli/**` | 24 | 3% | no | +| plugins | 6 | 1% | no | + +**150 of 722 (20%)** sit where the existing helpers can be called at all. Widening the helper *set* +(#2685) does not change this number — it is about the module's location, not its coverage. + +Core already exports `resolveColumnFlags`, so a core-side predicate module would be the same +abstraction made reachable rather than a new one. That is a prerequisite for the other 80%, and it +is **not** sufficient — see the next section. + +## 2. The binding constraint: flags are not in scope at most guards + +A role predicate needs the column's resolved trait flags. Many guards run in functions that receive +a bare task row and have no workflow IR to resolve flags from. Threading one in changes a function +signature and its call graph — a **behavior change, out of scope** for fleet conversion. + +File-level proxy over the 572 non-dashboard guards (does the *file* reference +`resolveColumnFlags` / `findColumn` / `WorkflowIr` / `columnFlags` at all?): + +- **339** in files that do reference one — *possibly* convertible +- **233** in files with no IR/flags reference anywhere — not convertible without threading + +**That proxy overstates convertibility, and the overstatement is the point.** Reachability varies +*within* a single file, so a file-level verdict is not usable. Measured in `scheduler.ts`: + +| Site | Context | Convertible? | +|---|---|---| +| `:1690` | `resolveWorkflowIrById(...)` + `resolveColumnFlags(c)` resolved in the same block | **yes** | +| `:231` | `isLegacyDependencySatisfied(dep: Task \| undefined)` — task only | no, without a signature change | +| `:341` | `shouldHoldActiveFileScopeLease(...)` — task only | no, without a signature change | + +`scheduler.ts` is the largest *unclaimed* cluster (28) and is already mixed. So per-site triage is +required for every cluster; "convert the file" is not a unit of work that exists here. + +### Worked example of a guard that must be skipped, not converted + +`packages/core/src/task-merge.ts:254` — `getTaskMergeBlocker`: + +```ts +if (!options.skipColumnIdentityCheck && task.column !== "in-review") { +``` + +The parameter is `Pick` +— no IR, by design. The in-source FNXC comment records that callers who *have* resolved the +`merge-blocker` trait pass `skipColumnIdentityCheck` rather than spoofing +`{ ...task, column: "in-review" }`. The trait-aware path already exists **beside** this literal; +the literal is the fallback for callers that have not proven lane identity. + +Converting it would not remove a legacy id — it would delete the fallback that the option was +introduced to make explicit. **Flag and skip.** + +## 3. What this implies for the work order + +- Sorting by per-file guard count sorts by a number that does not predict convertibility. +- A cluster's real unit of work is *(site, is-flags-in-scope)*, which the census does not emit. +- The two prerequisites are ordered: role-helper coverage (#2685) → a core-reachable module → then + conversion. Claiming clusters before the first two land produces per-file rediscovery, which is + what #2683, #2684, #2685, and this note each are. + +**Useful census upgrade** (not done here — it changes the census, which is the shared instrument and +not mine to change unannounced): emit per-site whether trait flags are resolvable in the enclosing +scope. That turns the work order from "largest file" into "largest *convertible* cluster" and makes +the baseline shrink predictable, which the fleet rules require ("the baseline must shrink by exactly +your converted count"). + +## Method + +- Counts from `node scripts/lifecycle-column-census.mjs --json` (`byFile`), the authoritative + instrument — never grep. +- Package attribution by path prefix on `byFile`. +- Flag-reachability proxy by `grep` for the four resolver identifiers, **file-level**, stated as a + proxy because it is one. +- Per-site verdicts in §2 read from source at the cited lines. +- Claim collisions checked against each open PR's **merge base**, not against `origin/main`: diffing + a branch against main reports a difference when the branch is merely stale (the file did not exist + at its base), which falsely flags unrelated PRs as touching a file. + `feature/code-organization-wave17` is excluded from collision checks — 1556 files, 150 commits + behind main, already `DIRTY`; it must rebase wholesale regardless, and treating it as a claim + would mark every cluster in the backlog as taken.