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>
This commit is contained in:
@@ -194,3 +194,135 @@ describe("sibling detection uses the enclosing expression, not a line window", (
|
||||
expect(result.role).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-29-20:10 (query-filter category):
|
||||
|
||||
A guard is not the only way a legacy column id decides behaviour. `listTasks({ column: "todo" })`
|
||||
is a SOURCE QUERY — it selects the rows a sweep considers at all — so on a board that renamed or
|
||||
merged that column it returns nothing, and 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.
|
||||
|
||||
The comparison walk cannot see these: a PropertyAssignment is not a BinaryExpression. So they were
|
||||
invisible to both the census and its ratchet, and the class could grow silently.
|
||||
|
||||
These cases pin the three decisions that make the category meaningful: it is counted, it is counted
|
||||
SEPARATELY from the backlog, and an IR node definition is not mistaken for a query.
|
||||
*/
|
||||
describe("query-filter category", () => {
|
||||
it("counts a legacy column id used as a source-query filter", () => {
|
||||
const result = summarize(census(`await store.listTasks({ column: "todo", slim: true });`));
|
||||
expect(result.properties.query).toBe(1);
|
||||
expect(result.queryByColumnId).toEqual({ todo: 1 });
|
||||
});
|
||||
|
||||
it("keeps queries OUT of the guard backlog, which is the completion bar", () => {
|
||||
/* The load-bearing decision. Folding these into `totals.column` would move a number the
|
||||
program is actively driving to zero, and would make every fleet PR's before/after
|
||||
arithmetic disagree with the bar. */
|
||||
const result = summarize(census(`await store.listTasks({ column: "triage" });`));
|
||||
expect(result.totals.column).toBe(0);
|
||||
expect(result.byColumnId).toEqual({});
|
||||
expect(result.byFile).toEqual([]);
|
||||
expect(result.properties.query).toBe(1);
|
||||
});
|
||||
|
||||
it("does NOT count a workflow IR node definition as a query", () => {
|
||||
/* `column:` on a graph node declares WHERE the node lives — the lineage describing itself,
|
||||
not a lookup, and not convertible. Told apart structurally (an `id`/`kind` sibling) rather
|
||||
than by filename, so a definition written anywhere is classified the same way. */
|
||||
const result = summarize(census(`const ir = { nodes: [{ id: "review", kind: "review", column: "in-review" }] };`));
|
||||
expect(result.properties.query).toBe(0);
|
||||
expect(result.properties.definition).toBe(1);
|
||||
expect(result.totals.column).toBe(0);
|
||||
});
|
||||
|
||||
it("still counts a comparison in the same file, so the two instruments are independent", () => {
|
||||
/* Without this, a bug that routed comparisons into the query bucket would look like a clean
|
||||
pass on both numbers. */
|
||||
const result = summarize(census(`
|
||||
const rows = await store.listTasks({ column: "todo" });
|
||||
if (task.column === "todo") { act(); }
|
||||
`));
|
||||
expect(result.properties.query).toBe(1);
|
||||
expect(result.totals.column).toBe(1);
|
||||
});
|
||||
|
||||
it("honours a DELIBERATE-LITERAL marker on a query filter", () => {
|
||||
const result = summarize(census(`
|
||||
/* ${DELIBERATE_MARKER}: reviewed — this lineage genuinely declares todo. */
|
||||
const rows = await store.listTasks({ column: "todo" });
|
||||
`));
|
||||
expect(result.properties.query).toBe(0);
|
||||
expect(result.totals.deliberate).toBe(1);
|
||||
});
|
||||
|
||||
it("ignores a column property whose value is not a legacy id", () => {
|
||||
const result = summarize(census(`await store.listTasks({ column: "backlog" });`));
|
||||
expect(result.properties.query).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-29-21:05 (the `outcome` receiver):
|
||||
|
||||
`outcome` names a RESULT enum, not a column. The live instance is
|
||||
`deterministicReconcile.outcome === "archived"` — the verdict of a duplicate reconciliation, which
|
||||
merely shares a word with a column id.
|
||||
|
||||
This is pinned because losing it is not hypothetical: the shipped classifier counted these five
|
||||
sites, the baseline recorded by the SAME PR did not, and that gap kept `--strict` RED on main from
|
||||
#2633 until it was restored. A silent one-word regression in a token list took the ratchet offline
|
||||
without failing anything, which is the same class of defect the ratchet exists to catch.
|
||||
*/
|
||||
describe("outcome is a result enum, not a column", () => {
|
||||
it("does not count `outcome === \"<column id>\"` as a guard", () => {
|
||||
const result = summarize(census(`if (reconcile.outcome === "archived") { return; }`));
|
||||
expect(result.totals.column).toBe(0);
|
||||
expect(result.totals.role).toBe(1);
|
||||
});
|
||||
|
||||
it("still counts a real column comparison in the same file", () => {
|
||||
/* Guards the exclusion from being written too broadly — a rule that swallowed the neighbouring
|
||||
column guard would look identical on the count above. */
|
||||
const result = summarize(census(`
|
||||
if (reconcile.outcome === "archived") { return; }
|
||||
if (task.column === "archived") { hide(); }
|
||||
`));
|
||||
expect(result.totals.column).toBe(1);
|
||||
expect(result.totals.role).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-29-21:50 (state/phase/result enums are not columns):
|
||||
|
||||
Four measured sites compared a state, phase, or result enum against a word that happens to be a
|
||||
column id. The fleet would have been sent to convert them and found nothing to convert.
|
||||
|
||||
Classified by the SIBLING vocabulary, never by the receiver's name — and that distinction is load-
|
||||
bearing rather than stylistic. `state` looks like exactly this class and is NOT: in
|
||||
`comments-ops.ts` it holds `await getLiveTaskColumn(...)`, a real column. A name-based rule would
|
||||
have silently deleted a genuine guard from the backlog while appearing to clean it up.
|
||||
*/
|
||||
describe("state, phase and result enums are not column guards", () => {
|
||||
it.each([
|
||||
["step state", `stepState === "done" ? a : stepState === "active" ? b : c`],
|
||||
["agent state", `agentState === "done" || agentState === "busy" || agentState === "ready"`],
|
||||
["tui phase", `phase === "done" || phase === "pushing" || phase === "confirm"`],
|
||||
["result kind", `kind === "done" || kind === "stopped" || kind === "exhausted"`],
|
||||
])("does not count %s as a guard", (_label, source) => {
|
||||
expect(totals(source).column).toBe(0);
|
||||
});
|
||||
|
||||
it("STILL counts a column held in a variable called `state`", () => {
|
||||
/* The case that makes a receiver-name rule wrong. This is the real shape from
|
||||
comments-ops.ts, and it must stay in the backlog. */
|
||||
const result = summarize(census(`
|
||||
const state = await getLiveTaskColumn(db, id, projectId);
|
||||
if (state === "archived") throw new Error("read-only");
|
||||
`));
|
||||
expect(result.totals.column).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,6 +42,19 @@ export const LEGACY_COLUMN_IDS = ["triage", "todo", "in-progress", "in-review",
|
||||
/** Receiver names that denote an agent role / lane rather than a task column. */
|
||||
export const ROLE_RECEIVER_TOKENS = [
|
||||
"role", "agentType", "agent", "lane", "capability", "sessionPurpose", "surface", "purpose", "agentRole",
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-29-20:50 (restores the pinned baseline):
|
||||
`outcome` names a RESULT enum, not a column. The one live instance is
|
||||
`deterministicReconcile.outcome === "archived"` — the verdict of a duplicate reconciliation, which
|
||||
happens to share a word with a column id.
|
||||
|
||||
This is not a preference: the shipped classifier counted it, the pinned baseline did not, and that
|
||||
single site is the entire 22-vs-23 gap that has kept `--strict` RED on main since #2633 merged.
|
||||
So the baseline was recorded by a classifier that excluded it, and the exclusion was lost before
|
||||
the code shipped. Restoring it makes the instrument agree with its own pin rather than raising the
|
||||
pin to match a miscount — which would have quietly conceded a guard that does not exist.
|
||||
*/
|
||||
"outcome",
|
||||
];
|
||||
|
||||
/*
|
||||
@@ -51,7 +64,28 @@ merger` and `StepStatus` is `pending | in-progress | done | skipped`; the member
|
||||
column ids. This is the signal that caught `sessionPurpose` and `surface`, which a name list missed.
|
||||
*/
|
||||
const ROLE_ONLY_VALUES = new Set(["executor", "reviewer", "merger"]);
|
||||
const STATUS_ONLY_VALUES = new Set(["pending", "skipped"]);
|
||||
const STATUS_ONLY_VALUES = new Set([
|
||||
"pending", "skipped",
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-29-21:40 (widen the sibling vocabulary, not the name list):
|
||||
Members of state/phase/result enums that are NEVER column ids. Each earns its place by a measured
|
||||
site whose siblings prove the vocabulary:
|
||||
|
||||
stepState { active, done } DashboardLoader.tsx:123
|
||||
agentState { busy, ready, starting, done } TaskDetailModal.tsx:339
|
||||
phase { confirm, pushing, done } dashboard-tui/app.tsx:3139
|
||||
kind { exhausted, existing, invalid-deleted, missing, async-mission-store.ts:1175
|
||||
nonterminal, stopped, done }
|
||||
|
||||
Deliberately extending the VALUE vocabulary rather than the receiver-name list, because names are
|
||||
unreliable here and provably so: `state` looked like the same class but holds
|
||||
`await getLiveTaskColumn(...)` — a real column, correctly counted. A name rule would have deleted
|
||||
that guard from the backlog. The sibling signal is the mechanism that already caught
|
||||
`sessionPurpose` and `surface`.
|
||||
*/
|
||||
"active", "busy", "ready", "starting", "confirm", "pushing",
|
||||
"exhausted", "existing", "invalid-deleted", "missing", "nonterminal", "stopped",
|
||||
]);
|
||||
|
||||
export const DELIBERATE_MARKER = "DELIBERATE-LITERAL";
|
||||
|
||||
@@ -144,6 +178,52 @@ function hasDeliberateMarker(sourceFile, node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-29-19:20 (query-filter category):
|
||||
|
||||
A guard is not the only way a legacy column id decides behaviour. `listTasks({ column: "todo" })`
|
||||
is a SOURCE QUERY: it selects the rows a sweep will consider, and on a board that renamed or merged
|
||||
that column it returns nothing — so a sweep whose per-task predicate was correctly converted still
|
||||
does nothing, and looks converted while being dead. `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. One measured consequence: `recoverStuckMergeDeadlocks` cannot see a renamed board at
|
||||
all (proven on a live store: the renamed rows exist and none appear in its three-literal union).
|
||||
|
||||
The comparison walk cannot see these — a PropertyAssignment is not a BinaryExpression — so they
|
||||
were invisible to the census and to its ratchet, meaning the class could grow silently.
|
||||
|
||||
COUNTED SEPARATELY, deliberately. `totals.column` and the per-column/per-file backlog are left
|
||||
byte-identical, so the completion bar ("triage guards to 0") keeps its existing meaning and the
|
||||
pinned baseline does not move. This adds a second, independently pinned number.
|
||||
|
||||
DEFINITIONS ARE NOT QUERIES. Workflow IR graph nodes carry `column:` to declare where a node lives
|
||||
(`{ id: "review", kind: "...", column: "in-review" }`), which is the lineage DEFINING itself — the
|
||||
builtin IR files hold ~32 of these. Converting one would be nonsense. They are told apart
|
||||
structurally rather than by filename: a definition's object literal also carries `id:` or `kind:`,
|
||||
a query's does not.
|
||||
*/
|
||||
function classifyColumnProperty(node) {
|
||||
const object = node.parent;
|
||||
if (!object || !ts.isObjectLiteralExpression(object)) return "query";
|
||||
const hasDefinitionSibling = object.properties.some(
|
||||
(property) =>
|
||||
property !== node
|
||||
&& ts.isPropertyAssignment(property)
|
||||
&& ts.isIdentifier(property.name)
|
||||
&& (property.name.text === "id" || property.name.text === "kind"),
|
||||
);
|
||||
return hasDefinitionSibling ? "definition" : "query";
|
||||
}
|
||||
|
||||
/** True for a `column: "<legacy id>"` property assignment. */
|
||||
function columnPropertyLiteral(node) {
|
||||
if (!ts.isPropertyAssignment(node)) return undefined;
|
||||
const name = ts.isIdentifier(node.name) || ts.isStringLiteral(node.name) ? node.name.text : undefined;
|
||||
if (name !== "column") return undefined;
|
||||
if (!ts.isStringLiteral(node.initializer)) return undefined;
|
||||
return LEGACY_COLUMN_IDS.includes(node.initializer.text) ? node.initializer.text : undefined;
|
||||
}
|
||||
|
||||
/** Parse one file and classify every comparison against a legacy column id. */
|
||||
export function findComparisons(filePath, source) {
|
||||
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
||||
@@ -169,6 +249,16 @@ export function findComparisons(filePath, source) {
|
||||
});
|
||||
}
|
||||
}
|
||||
const columnProperty = columnPropertyLiteral(node);
|
||||
if (columnProperty) {
|
||||
findings.push({
|
||||
file: filePath,
|
||||
line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
|
||||
columnId: columnProperty,
|
||||
receiver: "column",
|
||||
kind: hasDeliberateMarker(sourceFile, node) ? "deliberate" : classifyColumnProperty(node),
|
||||
});
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sourceFile);
|
||||
@@ -182,7 +272,25 @@ export function summarize(findings) {
|
||||
const byColumnId = {};
|
||||
const byFile = new Map();
|
||||
|
||||
/*
|
||||
Kept OUT of `totals` on purpose. `totals` is a published shape: the baseline file, the reporter,
|
||||
and other workers' in-flight PRs all read it, and the completion bar is defined against
|
||||
`totals.column`. Growing that object would move a number people are mid-way through driving to
|
||||
zero. The property-assignment counts are a second, independent instrument and live beside it.
|
||||
*/
|
||||
const properties = { query: 0, definition: 0 };
|
||||
const queryByFile = new Map();
|
||||
const queryByColumnId = {};
|
||||
|
||||
for (const finding of findings) {
|
||||
if (finding.kind === "query" || finding.kind === "definition") {
|
||||
properties[finding.kind] += 1;
|
||||
if (finding.kind === "query") {
|
||||
queryByColumnId[finding.columnId] = (queryByColumnId[finding.columnId] ?? 0) + 1;
|
||||
queryByFile.set(finding.file, (queryByFile.get(finding.file) ?? 0) + 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
totals[finding.kind] += 1;
|
||||
if (finding.kind !== "column") continue;
|
||||
byColumnId[finding.columnId] = (byColumnId[finding.columnId] ?? 0) + 1;
|
||||
@@ -193,6 +301,9 @@ export function summarize(findings) {
|
||||
totals,
|
||||
byColumnId,
|
||||
byFile: [...byFile].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])),
|
||||
properties,
|
||||
queryByColumnId,
|
||||
queryByFile: [...queryByFile].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +1,40 @@
|
||||
{
|
||||
"generatedFrom": "node scripts/lifecycle-column-census.mjs --strict --update-baseline",
|
||||
"totals": {
|
||||
"column": 776,
|
||||
"role": 0,
|
||||
"status": 182,
|
||||
"deliberate": 16
|
||||
"column": 769,
|
||||
"role": 5,
|
||||
"status": 187,
|
||||
"deliberate": 12
|
||||
},
|
||||
"byColumnId": {
|
||||
"done": 205,
|
||||
"done": 201,
|
||||
"in-progress": 146,
|
||||
"in-review": 208,
|
||||
"archived": 152,
|
||||
"in-review": 209,
|
||||
"archived": 148,
|
||||
"todo": 60,
|
||||
"triage": 5
|
||||
},
|
||||
"byFile": {
|
||||
"packages/engine/src/self-healing.ts": 111,
|
||||
"packages/engine/src/self-healing.ts": 110,
|
||||
"packages/engine/src/executor.ts": 104,
|
||||
"packages/dashboard/app/components/TaskCard.tsx": 42,
|
||||
"packages/core/src/task-store/moves.ts": 39,
|
||||
"packages/dashboard/app/components/TaskDetailModal.tsx": 31,
|
||||
"packages/dashboard/app/components/TaskDetailModal.tsx": 30,
|
||||
"packages/engine/src/scheduler.ts": 28,
|
||||
"packages/dashboard/src/routes/register-task-workflow-routes.ts": 21,
|
||||
"packages/dashboard/src/routes/register-task-workflow-routes.ts": 20,
|
||||
"packages/core/src/store.ts": 12,
|
||||
"packages/engine/src/project-engine.ts": 12,
|
||||
"packages/dashboard/app/components/TaskContextMenu.tsx": 10,
|
||||
"packages/engine/src/mission-execution-loop.ts": 10,
|
||||
"packages/cli/src/commands/task.ts": 9,
|
||||
"packages/core/src/task-store/async-comments-attachments.ts": 9,
|
||||
"packages/dashboard/src/github-tracking-comments.ts": 9,
|
||||
"packages/dashboard/src/github-tracking-reconciler.ts": 9,
|
||||
"packages/engine/src/notification/notification-service.ts": 9,
|
||||
"packages/cli/src/commands/dashboard.ts": 8,
|
||||
"packages/cli/src/commands/task.ts": 8,
|
||||
"packages/core/src/default-workflow-hooks.ts": 7,
|
||||
"packages/core/src/task-store/update-task-deps.ts": 7,
|
||||
"packages/dashboard/app/components/Column.tsx": 7,
|
||||
"packages/engine/src/agent-tools.ts": 7,
|
||||
"packages/core/src/live-agent-count.ts": 6,
|
||||
"packages/core/src/task-merge.ts": 6,
|
||||
"packages/core/src/task-store/branch-group-ops.ts": 6,
|
||||
@@ -46,6 +45,7 @@
|
||||
"packages/cli/src/extension.ts": 5,
|
||||
"packages/core/src/task-store/merge-queue-ops-2.ts": 5,
|
||||
"packages/dashboard/app/hooks/useTaskDiffStats.ts": 5,
|
||||
"packages/engine/src/agent-tools.ts": 5,
|
||||
"packages/engine/src/merger.ts": 5,
|
||||
"packages/engine/src/restart-recovery-coordinator.ts": 5,
|
||||
"packages/core/src/agent-store.ts": 4,
|
||||
@@ -54,7 +54,6 @@
|
||||
"packages/core/src/task-store/task-store-helpers.ts": 4,
|
||||
"packages/dashboard/app/components/TaskReviewTab.tsx": 4,
|
||||
"packages/dashboard/app/components/taskSorting.ts": 4,
|
||||
"packages/dashboard/app/utils/worktreeGrouping.ts": 4,
|
||||
"packages/dashboard/src/gitlab-tracking-comments.ts": 4,
|
||||
"packages/dashboard/src/routes/register-git-github.ts": 4,
|
||||
"packages/engine/src/agent-heartbeat.ts": 4,
|
||||
@@ -70,9 +69,9 @@
|
||||
"packages/dashboard/app/components/TaskChangesTab.tsx": 3,
|
||||
"packages/dashboard/app/components/TaskChatTab.tsx": 3,
|
||||
"packages/dashboard/app/utils/taskActivity.ts": 3,
|
||||
"packages/dashboard/app/utils/worktreeGrouping.ts": 3,
|
||||
"packages/dashboard/src/chat.ts": 3,
|
||||
"packages/dashboard/src/routes/register-chat-routes.ts": 3,
|
||||
"packages/engine/src/cli-agent/state-machine.ts": 3,
|
||||
"packages/engine/src/gridlock-detector.ts": 3,
|
||||
"packages/engine/src/planner-overseer.ts": 3,
|
||||
"packages/engine/src/worktree-pool.ts": 3,
|
||||
@@ -92,6 +91,7 @@
|
||||
"packages/core/src/team-analytics.ts": 2,
|
||||
"packages/core/src/workflow-analytics.ts": 2,
|
||||
"packages/dashboard/app/components/Board.tsx": 2,
|
||||
"packages/dashboard/app/components/command-center/MissionControlPanel.tsx": 2,
|
||||
"packages/dashboard/app/components/DocumentsView.tsx": 2,
|
||||
"packages/dashboard/app/components/effective-model-resolution.ts": 2,
|
||||
"packages/dashboard/app/components/WorkflowResultsTab.tsx": 2,
|
||||
@@ -105,6 +105,7 @@
|
||||
"packages/dashboard/src/server.ts": 2,
|
||||
"packages/engine/src/agent-reflection.ts": 2,
|
||||
"packages/engine/src/auto-merge-finalization.ts": 2,
|
||||
"packages/engine/src/cli-agent/state-machine.ts": 2,
|
||||
"packages/engine/src/merger-scope-auto-widen.ts": 2,
|
||||
"plugins/fusion-plugin-even-cards/src/cards/board-cards.ts": 2,
|
||||
"plugins/fusion-plugin-reports/src/store/report-store.ts": 2,
|
||||
@@ -121,7 +122,6 @@
|
||||
"packages/core/src/task-timing.ts": 1,
|
||||
"packages/dashboard/app/components/ChangesDiffModal.tsx": 1,
|
||||
"packages/dashboard/app/components/command-center/liveSnapshotMetrics.ts": 1,
|
||||
"packages/dashboard/app/components/DashboardLoader.tsx": 1,
|
||||
"packages/dashboard/app/components/DevServerView.tsx": 1,
|
||||
"packages/dashboard/app/components/MergeDetails.tsx": 1,
|
||||
"packages/dashboard/app/components/PrPanel.tsx": 1,
|
||||
@@ -130,6 +130,7 @@
|
||||
"packages/dashboard/app/components/TaskPlannerChatTab.tsx": 1,
|
||||
"packages/dashboard/app/hooks/useExecutorStats.ts": 1,
|
||||
"packages/dashboard/app/hooks/useTasks.ts": 1,
|
||||
"packages/dashboard/app/utils/columnRoles.ts": 1,
|
||||
"packages/dashboard/app/utils/inReviewStallCopy.ts": 1,
|
||||
"packages/dashboard/app/utils/quickAddStart.ts": 1,
|
||||
"packages/dashboard/app/utils/stalePausedReviewCopy.ts": 1,
|
||||
@@ -147,7 +148,6 @@
|
||||
"packages/engine/src/auto-recovery-handlers/branch-worktree.ts": 1,
|
||||
"packages/engine/src/backlog-pressure-reporter.ts": 1,
|
||||
"packages/engine/src/cli-agent/task-session.ts": 1,
|
||||
"packages/engine/src/cli-agent/telemetry-hub.ts": 1,
|
||||
"packages/engine/src/ephemeral-worker-manager.ts": 1,
|
||||
"packages/engine/src/merger-integration-worktree.ts": 1,
|
||||
"packages/engine/src/merger-orphan-rehome.ts": 1,
|
||||
@@ -155,5 +155,42 @@
|
||||
"packages/engine/src/pr-comment-handler.ts": 1,
|
||||
"plugins/fusion-plugin-even-realities-glasses/src/notifications/diff.ts": 1,
|
||||
"plugins/fusion-plugin-reports/src/store/report-types.ts": 1
|
||||
},
|
||||
"properties": {
|
||||
"query": 83,
|
||||
"definition": 43
|
||||
},
|
||||
"queryByColumnId": {
|
||||
"todo": 10,
|
||||
"archived": 7,
|
||||
"done": 10,
|
||||
"in-progress": 19,
|
||||
"in-review": 35,
|
||||
"triage": 2
|
||||
},
|
||||
"queryByFile": {
|
||||
"packages/engine/src/self-healing.ts": 48,
|
||||
"packages/engine/src/project-engine.ts": 7,
|
||||
"packages/engine/src/backlog-pressure-reporter.ts": 3,
|
||||
"packages/core/src/task-store/async-persistence.ts": 2,
|
||||
"packages/core/src/task-store/merge-queue-ops.ts": 2,
|
||||
"packages/core/src/task-store/moves.ts": 2,
|
||||
"packages/engine/src/executor.ts": 2,
|
||||
"packages/engine/src/stale-task-reporter.ts": 2,
|
||||
"packages/cli/src/extension.ts": 1,
|
||||
"packages/core/src/async-mission-store.ts": 1,
|
||||
"packages/core/src/eval-automation.ts": 1,
|
||||
"packages/core/src/store.ts": 1,
|
||||
"packages/core/src/task-store/archive-lifecycle-2.ts": 1,
|
||||
"packages/core/src/task-store/async-archive-lineage.ts": 1,
|
||||
"packages/core/src/task-store/async-self-healing.ts": 1,
|
||||
"packages/core/src/task-store/task-artifacts-ops.ts": 1,
|
||||
"packages/core/src/task-store/task-store-helpers.ts": 1,
|
||||
"packages/dashboard/src/routes/register-gitlab.ts": 1,
|
||||
"packages/engine/src/agent-tools.ts": 1,
|
||||
"packages/engine/src/auto-merge-finalization.ts": 1,
|
||||
"packages/engine/src/restart-recovery-coordinator.ts": 1,
|
||||
"packages/engine/src/scheduler.ts": 1,
|
||||
"packages/engine/src/workflow-node-handlers.ts": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,17 @@ if (json) {
|
||||
console.log(` COLUMN guards (the backlog): ${summary.totals.column}`);
|
||||
console.log(` ROLE comparisons (not guards): ${summary.totals.role}`);
|
||||
console.log(` STATUS comparisons (not guards): ${summary.totals.status}`);
|
||||
console.log(` DELIBERATE-LITERAL (reviewed): ${summary.totals.deliberate}\n`);
|
||||
console.log(` DELIBERATE-LITERAL (reviewed): ${summary.totals.deliberate}`);
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-29-19:40:
|
||||
Reported BESIDE the backlog, never inside it. A `column: "todo"` source query decides which rows
|
||||
a sweep even considers, so it can kill a sweep whose per-task guard was correctly converted —
|
||||
but it is not a guard, and folding it into `totals.column` would move a number the program is
|
||||
actively driving to zero. Definitions (workflow IR graph nodes declaring where a node lives) are
|
||||
counted apart again: they are the lineage describing itself and are not convertible.
|
||||
*/
|
||||
console.log(` QUERY filters (column: "<legacy>"): ${summary.properties.query}`);
|
||||
console.log(` IR node definitions (not convertible): ${summary.properties.definition}\n`);
|
||||
console.log(" by column id:");
|
||||
for (const [id, count] of Object.entries(summary.byColumnId).sort((a, b) => b[1] - a[1])) {
|
||||
console.log(` ${String(count).padStart(4)} ${id}`);
|
||||
@@ -107,7 +117,7 @@ if (compare) {
|
||||
const regressions = ["column", "role", "status", "deliberate"].filter(
|
||||
(kind) => text.totals[kind] > summary.totals[kind],
|
||||
);
|
||||
if (regressions.length > 0) {
|
||||
if (regressions.length > 0) {
|
||||
console.error(
|
||||
`\nlifecycle-column-census --compare: the regex found MORE than the parser for ${regressions.join(", ")}.\n` +
|
||||
"The parser has a blind spot; its count cannot be the bar until this is closed.",
|
||||
@@ -150,10 +160,43 @@ for (const [file, allowed] of baselineByFile) {
|
||||
if (!currentByFile.has(file) && allowed > 0) stale.push({ file, count: 0, allowed });
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-31-06:10 (PR #2650 review — greptile):
|
||||
MOVED OUT OF THE `--compare` BRANCH, where it could not work in either mode.
|
||||
|
||||
Inside `--compare` it read `baseline`, `regressions` and `stale` — all declared
|
||||
BELOW, in the `--strict` section — so the documented `--compare` command died with
|
||||
`ReferenceError: Cannot access 'baseline' before initialization` before printing
|
||||
anything. And `--strict` on its own never reached the block at all, so the query
|
||||
ratchet it adds was enforcing nothing in the one mode that gates.
|
||||
|
||||
Reproduced both halves before moving it: `--compare` threw, and `--strict` ran to
|
||||
completion without a single query comparison.
|
||||
|
||||
It belongs here, after the strict guards are declared and beside the guard ratchet
|
||||
whose both-directions rule it mirrors.
|
||||
*/
|
||||
/*
|
||||
The query ratchet, same both-directions rule as the guard ratchet above and pinned separately.
|
||||
Kept as its own list so a failure names which instrument moved: a worker converting a sweep will
|
||||
often lower `queryByFile` and `byFile` together, and a mixed message would be unreadable.
|
||||
*/
|
||||
const baselineQueryByFile = new Map(Object.entries(baseline.queryByFile ?? {}));
|
||||
const currentQueryByFile = new Map(summary.queryByFile);
|
||||
for (const [file, count] of currentQueryByFile) {
|
||||
const allowed = baselineQueryByFile.get(file) ?? 0;
|
||||
if (count > allowed) regressions.push({ file, count, allowed, kind: "query" });
|
||||
else if (count < allowed) stale.push({ file, count, allowed, kind: "query" });
|
||||
}
|
||||
for (const [file, allowed] of baselineQueryByFile) {
|
||||
if (!currentQueryByFile.has(file) && allowed > 0) stale.push({ file, count: 0, allowed, kind: "query" });
|
||||
}
|
||||
|
||||
|
||||
if (regressions.length > 0) {
|
||||
console.error("\nlifecycle-column-census --strict: column-guard count ROSE\n");
|
||||
for (const r of regressions) {
|
||||
console.error(` ${r.file}: ${r.allowed} -> ${r.count}`);
|
||||
console.error(` ${r.file}${r.kind === "query" ? " (query filter)" : ""}: ${r.allowed} -> ${r.count}`);
|
||||
}
|
||||
console.error(
|
||||
"\nResolve a lifecycle column from the task's own workflow (resolveLifecycleColumns /\n" +
|
||||
@@ -172,6 +215,9 @@ if (stale.length > 0) {
|
||||
totals: summary.totals,
|
||||
byColumnId: summary.byColumnId,
|
||||
byFile: Object.fromEntries(summary.byFile),
|
||||
properties: summary.properties,
|
||||
queryByColumnId: summary.queryByColumnId,
|
||||
queryByFile: Object.fromEntries(summary.queryByFile),
|
||||
}, null, 2)}\n`,
|
||||
);
|
||||
console.log(`\nlifecycle-column-census --strict: baseline TIGHTENED for ${stale.length} file(s).`);
|
||||
|
||||
Reference in New Issue
Block a user