fix(core): a hold-only column vanished from the SDLC funnel (+ un-red the census on main) (#2674)
Two things, both small, one urgent. ## 1. Hold-only columns disappeared from the funnel `hold` was absent from `TRAIT_TO_STAGE`, so a column whose only pre-implementation trait is `hold` — a renamed board's wait-for-capacity lane — resolved to `OTHER` and vanished from the SDLC funnel. Measured before the fix: ``` stageForTraits(["hold"]) === "other" ``` The default lineage hid it: its Planning column also carries `intake` and `reset-on-entry`, so it always matched something. Only a board that names its wait lane separately was affected — **exactly the custom shape this trait mapping exists to support**. Revert check: removing the entry gives `expected 'other' to be 'todo'`. ## What I deliberately did NOT fix, and why The merged default Planning column carries `["intake","hold","reset-on-entry"]`, and `stageForTraits` prefers the earliest stage in flow order — so `intake` wins and it still resolves to `triage`. The `todo` stage therefore stays empty on every default board since U11, and the funnel shows a **phantom 100% drop between Triage and Todo**. That is a real defect. It is also not a reversible call: changing which stage Planning reports would retroactively alter how historical analytics read. Flagged on #2669 for a product decision. Adding `hold` does not touch it — `intake` still outranks — and a second test **pins the current behaviour** so the larger question gets answered deliberately rather than drifted into by a future edit to this map. ## 2. `check:lifecycle-columns` is RED on pristine `origin/main` — again ``` census exit on pristine main = 1 packages/engine/src/executor.ts: allows 87, tree has 85 ``` `executor.ts` is a file this PR does not touch, so a merge lowered the count without re-recording and the blocking PR check is failing for **every open PR**. The re-record is mechanical and is included here to unblock it — called out explicitly because it is unrelated to the funnel fix and should not ride along unexplained. This is the second time the baseline has gone stale on main this way. The rule works (`--strict` caught it immediately); what is missing is that it caught it *after* the merge. Worth considering whether the census should run on the merge queue rather than only on PR head — otherwise a PR that is green when opened can still land a stale baseline. ## Verification `pnpm test:gate` green (10 / 158 / 487 / 71). `pnpm check:lifecycle-columns` exits 0 after the re-record. `tsc -p packages/core/tsconfig.json` clean. `pnpm lint` clean. `sdlc-funnel-default-columns.test.ts` 8/8. No changeset: the funnel entry is a correctness fix with no user-facing API change, and the baseline re-record is internal. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved SDLC funnel classification for hold-only columns, placing them in the Todo stage instead of Other. * Preserved correct Planning column behavior when hold-related traits are combined. * **Tests** * Added coverage for hold-related funnel stage mapping and trait ordering. * Updated lifecycle column census baselines to reflect current results. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment node
|
||||
/*
|
||||
FNXC:SdlcFunnelColumns 2026-07-31-09:50 (the default path was the only path, and it used the legacy IR):
|
||||
FNXC:SdlcFunnelColumns 2026-07-30-09:50 (the default path was the only path, and it used the legacy IR):
|
||||
|
||||
THE INVARIANT: the SDLC funnel's built-in column fallback maps the board Fusion actually ships.
|
||||
|
||||
@@ -22,7 +22,7 @@ test stops being evidence and becomes a chore.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildColumnStageMap } from "../activity-analytics.js";
|
||||
import { buildColumnStageMap, stageForTraits } from "../activity-analytics.js";
|
||||
import { resolveDefaultWorkflowIr } from "../builtin-workflows.js";
|
||||
import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js";
|
||||
import type { WorkflowIrColumn } from "../workflow-ir-types.js";
|
||||
@@ -80,7 +80,7 @@ describe("the funnel's built-in column fallback tracks the SHIPPED default board
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:SdlcFunnelColumns 2026-07-31-10:25 (the real fix, and the correction of my own claim):
|
||||
FNXC:SdlcFunnelColumns 2026-07-30-10:25 (the real fix, and the correction of my own claim):
|
||||
|
||||
I first changed only `defaultColumns()` from the legacy IR to `resolveDefaultWorkflowIr()` and described
|
||||
it as fixing renamed boards. IT DOES NOT. Post-U11 the current lineage's column ids are a SUBSET of the
|
||||
@@ -128,3 +128,58 @@ describe("a board whose columns the funnel was not given folds to OTHER", () =>
|
||||
expect(empty.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:SdlcFunnel 2026-07-30-16:00:
|
||||
A HOLD-ONLY column must land in the funnel, not in OTHER.
|
||||
|
||||
`hold` was missing from the trait->stage map, so a renamed board's wait-for-capacity lane resolved to
|
||||
OTHER and disappeared from the SDLC funnel entirely. The default lineage hid this: its Planning column
|
||||
also carries `intake` and `reset-on-entry`, so it always matched something. Only a board that names
|
||||
its wait lane separately — exactly the custom shape this trait mapping exists to support — was
|
||||
affected.
|
||||
|
||||
REVERT CHECK: remove `hold` from TRAIT_TO_STAGE and the first case returns "other".
|
||||
|
||||
NOT covered here, on purpose: the merged default Planning column carries intake AND reset-on-entry, so
|
||||
`stageForTraits` still resolves it to `triage` and the `todo` stage stays empty on default boards.
|
||||
That is a real defect from the U11 merge and a larger, non-reversible call — changing which stage
|
||||
Planning reports would retroactively alter historical analytics. Flagged on PR #2669 for a decision
|
||||
rather than settled silently here. The second case below PINS the current behaviour so that decision
|
||||
is made deliberately and not drifted into.
|
||||
*/
|
||||
describe("SDLC funnel: hold-only columns", () => {
|
||||
it("places a hold-only column in the todo stage rather than OTHER", () => {
|
||||
expect(stageForTraits(["hold"])).toBe("todo");
|
||||
});
|
||||
|
||||
it("maps a hold-only COLUMN through buildColumnStageMap, not just its traits", () => {
|
||||
/*
|
||||
FNXC:SdlcFunnel 2026-07-30-16:00 (PR #2674 review — greptile, and it is the project's own rule):
|
||||
The reported bug was that a hold-only COLUMN disappeared from the funnel. Asserting
|
||||
`stageForTraits` alone tests the helper, not the surface where the defect shows — and the
|
||||
Surface Enumeration rule exists because a fix proven only at the unit it was written in is how
|
||||
the same bug comes back one caller over.
|
||||
|
||||
`buildColumnStageMap` is the seam the funnel actually consumes: it maps column id -> stage for
|
||||
every column on the board. A hold-only lane must appear there with a real stage, not OTHER.
|
||||
*/
|
||||
const stages = buildColumnStageMap([
|
||||
{ id: "waiting", traits: [{ trait: "hold" }] },
|
||||
{ id: "building", traits: [{ trait: "wip" }] },
|
||||
{ id: "shipped", traits: [{ trait: "complete" }] },
|
||||
] as never);
|
||||
|
||||
expect(stages.get("waiting")).toBe("todo");
|
||||
// The neighbours prove the map is populated normally, so the case above cannot pass because the
|
||||
// map is empty or every column collapsed to one stage.
|
||||
expect(stages.get("building")).toBe("in-progress");
|
||||
expect(stages.get("shipped")).toBe("done");
|
||||
});
|
||||
|
||||
it("still resolves the merged Planning column to `triage` — unchanged by this fix", () => {
|
||||
// intake (stage 0) outranks reset-on-entry/hold (stage 1) by flow order. Pinned so the separate
|
||||
// merged-column question cannot be answered accidentally by a future edit to this map.
|
||||
expect(stageForTraits(["intake", "hold", "reset-on-entry"])).toBe("triage");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -607,6 +607,24 @@ const TRAIT_TO_STAGE: Record<string, SdlcStage> = {
|
||||
triage: "triage",
|
||||
// todo
|
||||
"reset-on-entry": "todo",
|
||||
/*
|
||||
FNXC:SdlcFunnel 2026-07-30-16:00:
|
||||
`hold` was absent from this map entirely, so a column whose ONLY pre-implementation trait is
|
||||
`hold` — a renamed board's wait-for-capacity lane — resolved to OTHER and vanished from the
|
||||
funnel. Measured before the fix: `stageForTraits(["hold"]) === "other"`.
|
||||
|
||||
Adding it does NOT change where the merged default Planning column lands. That column carries
|
||||
`["intake","hold","reset-on-entry"]`, and `stageForTraits` prefers the earliest stage in flow
|
||||
order, so `intake` (stage 0) still wins and it resolves to `triage` exactly as before. This is
|
||||
strictly the hold-only case.
|
||||
|
||||
The merged column landing in `triage` while the `todo` stage stays empty is a SEPARATE and larger
|
||||
question — it makes the funnel show a phantom 100% drop between Triage and Todo on every default
|
||||
board since U11 — and it is deliberately not settled here. Changing which stage the Planning column
|
||||
reports would retroactively alter how historical analytics read, which is not a reversible call the
|
||||
way this one is. Flagged for a product decision on PR #2669.
|
||||
*/
|
||||
hold: "todo",
|
||||
// in-progress
|
||||
wip: "in-progress",
|
||||
timing: "in-progress",
|
||||
@@ -714,7 +732,7 @@ interface MoveRow {
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:SdlcFunnelColumns 2026-07-31-09:40:
|
||||
FNXC:SdlcFunnelColumns 2026-07-30-09:40:
|
||||
THE FALLBACK WAS THE LEGACY MONOLITHIC IR, and the only production callers use the fallback.
|
||||
`aggregateActivityAnalytics` (Command Center's `/command-center/activity`, and the OTel exporter) never
|
||||
passes `columns`, so every project's funnel was mapped through `BUILTIN_CODING_WORKFLOW_IR` — the
|
||||
|
||||
Reference in New Issue
Block a user