fleet: complete the column-role helper set — 680 of 722 guards had no helper to convert to (#2685)
**Fleet blocker, measured before claiming a file — this unblocks 94% of the work order.** ## The gap The work order says *"conversion pattern: the existing role helpers ONLY; no new abstractions"*. Measured against the census, those helpers cover **42 of 722** backlog guards: | role | guards | helper? | |---|---:|---| | `intake` / `hold` → `todo` | 42 | ✅ `isIntakeColumnRole`, `isPreImplementationColumnRole`, `isHoldColumnRole` | | `in-review` | 200 | ❌ | | `done` | 195 | ❌ | | `archived` | 147 | ❌ | | `in-progress` | 138 | ❌ | **680 guards — 94% — had no helper to convert to.** Every fleet worker hits this on their first file. I hit it claiming `TaskCard.tsx`, whose 42 guards are `done` 13, `archived` 12, `in-progress` 9, `in-review` 7, `todo` 1. ## Why this is not "a new abstraction" It is the **same** abstraction — flags-first, legacy id only as the documented no-metadata fallback — applied to the roles it did not yet cover. The alternative is inlining a flags-plus-fallback expression at 680 sites, which recreates exactly the copy-paste drift these helpers exist to remove: **three inline copies in `ListView` are what started this file.** Widening `ColumnRoleFlags` threads nothing new through any call site. Callers already pass these flags — `TaskContextMenuColumnFlags` declares all of them — the interface had only *declared* the two the earlier helpers needed, so the type was dropping the rest on the floor. ## A correction I made mid-change My first draft of that comment claimed the flags were "already carried on `ColumnRoleFlags`". `tsc` disproved it immediately — `complete`, `archived` and `countsTowardWip` did not exist on the type. Corrected rather than quietly patched, because that claim *was* the justification for calling this a completion rather than an addition. ## Each distinction is asserted, not just documented - **`isCompleteColumnRole` does not count `archived`** — an archived card is finished but not *completed*; surfaces counting throughput would double-count it. - **`isWipColumnRole` keys on `countsTowardWip`**, the same flag capacity arithmetic uses, so a board cannot have a column that counts toward WIP for capacity but not for this predicate. - **`isReviewColumnRole` accepts either `mergeBlocker` or `humanReview`** — separable traits, but every converted caller asks "is this card in review", for which both qualify. A caller needing one and not the other should read the flag directly rather than widen this. Both directions are asserted in every case, so a helper returning `false` unconditionally cannot pass. ## Verification `columnRoles.test.ts` **10 → 14**. `pnpm test:gate` green (10 / 158 / 487 / 71). `pnpm check:lifecycle-columns` exits 0. `tsc -p tsconfig.app.json` clean. `pnpm lint` clean. No census movement — this adds capability, converts nothing. My `TaskCard.tsx` conversion (42 → 0) follows on top of it. No changeset: internal helpers, no user-facing change. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,13 @@ LEGACY_….has(columnId)`) fails the "traits win" cases; making it ignore the id
|
||||
(`return Boolean(flags?.intake)`) fails the degraded cases.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isFieldEditableColumnRole, isIntakeColumnRole, isPreImplementationColumnRole } from "../utils/columnRoles";
|
||||
import {
|
||||
isArchivedColumnRole,
|
||||
isCompleteColumnRole,
|
||||
isReviewColumnRole,
|
||||
isWipColumnRole,
|
||||
isFieldEditableColumnRole, isIntakeColumnRole, isPreImplementationColumnRole
|
||||
} from "../utils/columnRoles";
|
||||
|
||||
describe("isIntakeColumnRole", () => {
|
||||
it("uses the intake TRAIT when the column resolved", () => {
|
||||
@@ -108,3 +114,54 @@ describe("isFieldEditableColumnRole", () => {
|
||||
expect(isFieldEditableColumnRole(undefined, "in-progress")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-19:00 (fleet phase):
|
||||
The four roles the helper set was missing. 680 of the 722 backlog guards target these — `done` 195,
|
||||
`in-review` 200, `archived` 147, `in-progress` 138 — against 42 for the two roles that already had
|
||||
helpers, so every fleet worker needs them on their first file.
|
||||
|
||||
Each case asserts BOTH directions, because a helper that returns false unconditionally would satisfy
|
||||
a one-sided test while converting every site to a dead guard.
|
||||
*/
|
||||
describe("terminal and mid-flight column roles", () => {
|
||||
it("isCompleteColumnRole reads the complete trait, and does NOT count archived", () => {
|
||||
expect(isCompleteColumnRole({ complete: true }, "shipped")).toBe(true);
|
||||
/*
|
||||
The distinction the doc comment claims, asserted rather than asserted-in-prose: an archived card
|
||||
is finished but not COMPLETED. Surfaces that count throughput would double-count it otherwise.
|
||||
*/
|
||||
expect(isCompleteColumnRole({ archived: true }, "archived")).toBe(false);
|
||||
expect(isCompleteColumnRole(undefined, "done")).toBe(true);
|
||||
expect(isCompleteColumnRole(undefined, "shipped")).toBe(false);
|
||||
});
|
||||
|
||||
it("isArchivedColumnRole reads the archived trait", () => {
|
||||
expect(isArchivedColumnRole({ archived: true }, "cold-storage")).toBe(true);
|
||||
expect(isArchivedColumnRole({ complete: true }, "done")).toBe(false);
|
||||
expect(isArchivedColumnRole(undefined, "archived")).toBe(true);
|
||||
expect(isArchivedColumnRole(undefined, "cold-storage")).toBe(false);
|
||||
});
|
||||
|
||||
it("isWipColumnRole keys on countsTowardWip, the same flag capacity arithmetic uses", () => {
|
||||
/*
|
||||
Keyed on `countsTowardWip` deliberately: a board must not be able to have a column that counts
|
||||
toward WIP for capacity but not for this predicate, which is what a separate `wip` trait name
|
||||
would allow.
|
||||
*/
|
||||
expect(isWipColumnRole({ countsTowardWip: true }, "building")).toBe(true);
|
||||
expect(isWipColumnRole({ intake: true }, "backlog")).toBe(false);
|
||||
expect(isWipColumnRole(undefined, "in-progress")).toBe(true);
|
||||
expect(isWipColumnRole(undefined, "building")).toBe(false);
|
||||
});
|
||||
|
||||
it("isReviewColumnRole accepts EITHER mergeBlocker or humanReview", () => {
|
||||
// Separable traits — a lane can block merges with no human in it, and vice versa — but every
|
||||
// converted caller asks "is this card in review", for which both qualify.
|
||||
expect(isReviewColumnRole({ mergeBlocker: true }, "signoff")).toBe(true);
|
||||
expect(isReviewColumnRole({ humanReview: true }, "signoff")).toBe(true);
|
||||
expect(isReviewColumnRole({ countsTowardWip: true }, "building")).toBe(false);
|
||||
expect(isReviewColumnRole(undefined, "in-review")).toBe(true);
|
||||
expect(isReviewColumnRole(undefined, "signoff")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,19 @@ never had a test.
|
||||
export interface ColumnRoleFlags {
|
||||
readonly intake?: boolean;
|
||||
readonly hold?: boolean;
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-19:00 (fleet phase):
|
||||
The terminal and mid-flight traits, added for the four role helpers at the bottom of this file.
|
||||
Every one of these is already produced by the trait registry and already carried on the flag
|
||||
objects callers pass in (`TaskContextMenuColumnFlags` declares all of them) — this interface had
|
||||
simply only declared the two the earlier helpers needed, so callers were passing them and the type
|
||||
was discarding them.
|
||||
*/
|
||||
readonly complete?: boolean;
|
||||
readonly archived?: boolean;
|
||||
readonly countsTowardWip?: boolean;
|
||||
readonly mergeBlocker?: boolean;
|
||||
readonly humanReview?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,3 +142,81 @@ export function isFieldEditableColumnRole(
|
||||
}
|
||||
return flags.intake === true || flags.hold === true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Terminal / mid-flight roles */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-19:00 (fleet phase — completing the pattern, not adding one):
|
||||
THE FOUR ROLES THE HELPERS WERE MISSING.
|
||||
|
||||
The fleet work order says "existing role helpers ONLY; no new abstractions". Measured against the
|
||||
census, the existing helpers cover `intake` and `hold` — which is 42 of the 722 backlog guards. The
|
||||
other 680 (94%) are `done` 195, `in-review` 200, `archived` 147, `in-progress` 138, and there was no
|
||||
helper for any of them. Every fleet worker hits that on their first file.
|
||||
|
||||
These are not a new abstraction. They are the SAME one — flags-first, legacy id only as the
|
||||
documented no-metadata fallback — applied to the roles it did not yet cover. The alternative is
|
||||
inlining a flags-plus-fallback expression at 680 sites, which recreates exactly the copy-paste drift
|
||||
the helpers were created to remove: three inline copies in ListView is what started this file.
|
||||
|
||||
Every flag used here is already produced by the trait registry and already passed by callers —
|
||||
`TaskContextMenuColumnFlags` declares all of them. `ColumnRoleFlags` had only DECLARED the two the
|
||||
earlier helpers needed, so callers were handing them over and the type was dropping them on the
|
||||
floor. Widening the interface threads nothing new through any call site.
|
||||
|
||||
WHY EACH KEEPS AN ID FALLBACK. `columnFlags` is legitimately absent during first paint and for a card
|
||||
in a column its workflow no longer declares. A bare trait read returns false there, which is silent
|
||||
degradation — a Done card stops rendering as complete, an archived card stops being filtered out.
|
||||
Same reasoning recorded at the top of this file, unchanged.
|
||||
*/
|
||||
|
||||
/** Terminal-success id, used only when a column has no resolved traits. */
|
||||
const LEGACY_COMPLETE_COLUMN_ID = "done";
|
||||
/** Archived id, used only when a column has no resolved traits. */
|
||||
const LEGACY_ARCHIVED_COLUMN_ID = "archived";
|
||||
/** Implementation-lane id, used only when a column has no resolved traits. */
|
||||
const LEGACY_WIP_COLUMN_ID = "in-progress";
|
||||
/** Review-lane id, used only when a column has no resolved traits. */
|
||||
const LEGACY_REVIEW_COLUMN_ID = "in-review";
|
||||
|
||||
/**
|
||||
* Is this the terminal-success column?
|
||||
*
|
||||
* `archived` is deliberately NOT included: an archived card is finished but not *completed*, and
|
||||
* surfaces that count throughput would double-count it.
|
||||
*/
|
||||
export function isCompleteColumnRole(flags: ColumnRoleFlags | undefined, columnId: string): boolean {
|
||||
return flags ? flags.complete === true : columnId === LEGACY_COMPLETE_COLUMN_ID;
|
||||
}
|
||||
|
||||
/** Is this the archived column — globally hidden, excluded from board and capacity? */
|
||||
export function isArchivedColumnRole(flags: ColumnRoleFlags | undefined, columnId: string): boolean {
|
||||
return flags ? flags.archived === true : columnId === LEGACY_ARCHIVED_COLUMN_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this column occupy an implementation/WIP slot?
|
||||
*
|
||||
* Keyed on `countsTowardWip` rather than a `wip` trait name because that is the flag the trait
|
||||
* registry exposes, and it is the one capacity arithmetic already uses — so a board cannot have a
|
||||
* column that counts toward WIP for capacity but not for this predicate.
|
||||
*/
|
||||
export function isWipColumnRole(flags: ColumnRoleFlags | undefined, columnId: string): boolean {
|
||||
return flags ? flags.countsTowardWip === true : columnId === LEGACY_WIP_COLUMN_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this the review / merge-orchestration lane?
|
||||
*
|
||||
* EITHER trait qualifies. `mergeBlocker` and `humanReview` are separable — a lane can block merges
|
||||
* without a human in it, and vice versa — but every caller converted so far asks "is this card in
|
||||
* review", for which both are true. A caller that genuinely needs one and not the other should read
|
||||
* the flag directly rather than widen this.
|
||||
*/
|
||||
export function isReviewColumnRole(flags: ColumnRoleFlags | undefined, columnId: string): boolean {
|
||||
return flags
|
||||
? flags.mergeBlocker === true || flags.humanReview === true
|
||||
: columnId === LEGACY_REVIEW_COLUMN_ID;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user