fleet: scheduler.ts 28 → triaged (NOT converted) + repo-wide reachability measurement — the work order sorts on a number that doesn't predict convertibility (#2687)

## 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<Task, "column" | "paused" | ...>` — 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/<branch> -- <file>`
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.
This commit is contained in:
gsxdsm
2026-07-30 02:53:18 -07:00
committed by GitHub
parent da77e61118
commit ae23be79f7

View File

@@ -0,0 +1,113 @@
# Fleet conversion reachability — measured, repo-wide
<!--
FNXC:WorkflowResolvedColumns 2026-07-30-14:10:
Why this document exists: the fleet work order is "claim the largest unclaimed file cluster and
convert it". Two workers independently discovered that the largest cluster cannot be batch-converted
(#2683, #2684 on self-healing.ts) and a third discovered the helper set covered only 6% of the
backlog (#2685). This measures the whole backlog ONCE so the remaining workers do not each pay the
same discovery cost on their first file.
-->
## 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<Task, "column" | "paused" | "status" | "error" | "steps" | "workflowStepResults">`
— 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.