U11: rank assigned work by lifecycle role (1 -> 0), plus two documented non-conversions (#2563)

Based on `main`. Continuing with unassigned work in my area
(scheduling/ranking core).

## Measured (drift-review tracking)

| file | comparisons before | after |
|---|---:|---:|
| `packages/core/src/assigned-task-ranking.ts` | **1** | **0** |

## What was wrong

`tierForTask` identified the two **actionable** tiers by literal id —
`in-progress` → `in_progress`, `todo` → `ready_todo` /
`partial_blocked`.

The file's own comment already recorded half of this:

> Only treating default `todo`/`in-progress` as titled hid assigned work
as a bare count

But the fix that followed was a **floor, not a fix**: unrecognised
columns fall to `other` so work stays *visible*, while a renamed hold
column loses `ready_todo` and `partial_blocked` entirely. Work that is
genuinely ready to start then ranks **below everything already in
progress**, so an agent reading its Wake Delta sees ready work buried.

Nothing errors and nothing disappears — the ordering is just wrong,
which is how it survived a comment that noticed the adjacent problem.

`partial_blocked` is the sharper loss: it's the **only** tier
distinguishing "ready" from "waiting on a dependency" for hold-column
cards, and it was unreachable for any renamed workflow.

## Two sibling files deliberately NOT converted

Checked before assuming work existed:

**`live-agent-count.ts` — already trait-driven.** Its literals are the
else-branch of `flags ? traits : literals`, and the source says why:
*"The literal fallback is fixture-only; board/store callers always
supply flags/IR."* Converting a fixture-only fallback would be churn.

**`task-priority.ts` → `sortTasksForDisplayColumn` — dead.** No
production caller. The dashboard has its own independent implementation
in `app/components/taskSorting.ts` with a richer signature
(`doneSortMode`, `isArchivedColumn`), and that's the one `Lane.tsx`
imports. Core's copy is reached only by its own tests and the barrel
export.

That's the **third dead export** this unit has found by checking
reachability before converting (after the legacy dispatcher and
`isRunnableQueuedOverlapCandidate`). Deletion is a separate concern from
conversion and is not in this PR.

## Verification

- **Mutation-verified:** not threading `roles` through to `tierForTask`
fails **4 of 6** new tests
- 13 tests green (6 new + the pre-existing ranking suite)
- merge gate green (414 + 10 + 71), tsc clean, lint clean

No changeset: `@fusion/core` is private.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
gsxdsm
2026-07-29 10:26:46 -07:00
committed by GitHub
parent bad35775a1
commit d1cbb8ce90
2 changed files with 132 additions and 4 deletions

View File

@@ -0,0 +1,104 @@
/*
FNXC:WorkflowLifecycleColumns 2026-07-29-13:20 (U11 conversion — assigned-task ranking):
`tierForTask` ranks an agent's assigned work for the Wake Delta inventory, and it
identified the two actionable tiers by literal id: `in-progress` -> `in_progress`,
`todo` -> `ready_todo` / `partial_blocked`.
The file's own comment already documents the consequence for custom workflows —
"Only treating default `todo`/`in-progress` as titled hid assigned work as a bare
count" — and then fixes it only halfway: unrecognised columns fall to the `other`
tier so they stay VISIBLE, but a renamed hold column loses `ready_todo` entirely.
That is the failure this converts: a card that is genuinely ready to start is
ranked at the lowest tier, below every in-progress card, so an agent reading its
Wake Delta sees ready work buried under work already underway. Nothing errors and
nothing disappears — the ordering is just wrong, which is why it survived.
`partial_blocked` matters too: it is the ONLY tier that distinguishes "ready" from
"waiting on a dependency" for hold-column cards, and it is unreachable for a
renamed workflow.
Written against the literal implementation and observed FAILING first.
*/
import { describe, expect, it } from "vitest";
import { rankAssignedTasksForWakeDelta } from "../assigned-task-ranking.js";
type Ranked = ReturnType<typeof rankAssignedTasksForWakeDelta>;
function task(over: Record<string, unknown> = {}) {
return {
id: "FN-1",
title: "t",
column: "drafting",
paused: false,
deletedAt: null,
dependencies: [],
...over,
} as never;
}
const RENAMED = { hold: "drafting", wip: "building" };
function tiersOf(result: Ranked): string[] {
return result.ranked.map((line) => line.tier);
}
describe("assigned-task ranking under a renamed column vocabulary", () => {
it("ranks a card in the RENAMED hold column as ready_todo", () => {
const result = rankAssignedTasksForWakeDelta([task()], { agentId: "a1", roles: RENAMED });
expect(tiersOf(result)).toEqual(["ready_todo"]);
});
it("ranks a dependency-carrying hold card as partial_blocked, not ready", () => {
/* The tier that distinguishes "ready" from "waiting", and the one a renamed
workflow could not reach at all. */
const result = rankAssignedTasksForWakeDelta(
[task({ dependencies: ["FN-DEP"] })],
{ agentId: "a1", roles: RENAMED },
);
expect(tiersOf(result)).toEqual(["partial_blocked"]);
});
it("ranks a card in the RENAMED wip column as in_progress", () => {
const result = rankAssignedTasksForWakeDelta(
[task({ column: "building" })],
{ agentId: "a1", roles: RENAMED },
);
expect(tiersOf(result)).toEqual(["in_progress"]);
});
it("orders ready hold work ABOVE nothing-but-other work", () => {
/*
The observable symptom. Without the conversion the hold card falls to `other`
and sorts below in-progress, so an agent sees ready work buried.
*/
const result = rankAssignedTasksForWakeDelta(
[task({ id: "FN-OTHER", column: "reviewing" }), task({ id: "FN-READY" })],
{ agentId: "a1", roles: RENAMED },
);
const ids = result.ranked.map((l) => l.task.id);
expect(ids.indexOf("FN-READY")).toBeLessThan(ids.indexOf("FN-OTHER"));
});
it("keeps every other tier rule intact under the renamed vocabulary", () => {
/* The conversion must change which id means "hold"/"wip", not which cards are
actionable at all. */
const paused = rankAssignedTasksForWakeDelta([task({ paused: true })], { agentId: "a1", roles: RENAMED });
expect(paused.ranked).toEqual([]);
expect(paused.notActionableCount).toBe(1);
const unknown = rankAssignedTasksForWakeDelta([task({ column: "reviewing" })], { agentId: "a1", roles: RENAMED });
expect(tiersOf(unknown)).toEqual(["other"]);
});
it("defaults to the legacy ids when no roles are supplied", () => {
/* Byte-identical for every caller that cannot resolve a workflow. */
const legacy = rankAssignedTasksForWakeDelta([task({ column: "todo" })], { agentId: "a1" });
expect(tiersOf(legacy)).toEqual(["ready_todo"]);
const renamedWithoutRoles = rankAssignedTasksForWakeDelta([task({ column: "drafting" })], { agentId: "a1" });
expect(tiersOf(renamedWithoutRoles)).toEqual(["other"]);
});
});

View File

@@ -65,10 +65,32 @@ Map known default columns for rank quality; treat all other non-terminal open
columns (including custom workflow columns) as titled `other` so inventory stays
visible. Paused stays count-only to avoid re-chase noise.
*/
function tierForTask(task: AssignedTaskLike): AssignedTaskRankTier | "not_actionable" {
/*
FNXC:WorkflowLifecycleColumns 2026-07-29-13:35 (U11):
The two ACTIONABLE lifecycle roles. `todo`/`in-progress` are only what the builtin
coding workflow calls them. The comment above already records that unrecognised
columns fall to `other` so assigned work stays visible — but that is a floor, not
a fix: a renamed HOLD column loses `ready_todo` and `partial_blocked` entirely, so
work that is genuinely ready to start ranks below everything already in progress.
Nothing errors and nothing disappears; the ordering is just wrong, which is how it
survived.
Defaults to the legacy ids so every unconverted caller is byte-identical.
*/
export interface AssignedTaskRankRoles {
hold: string;
wip: string;
}
const LEGACY_RANK_ROLES: AssignedTaskRankRoles = { hold: "todo", wip: "in-progress" };
function tierForTask(
task: AssignedTaskLike,
roles: AssignedTaskRankRoles = LEGACY_RANK_ROLES,
): AssignedTaskRankTier | "not_actionable" {
if (task.paused) return "not_actionable";
if (task.column === "in-progress") return "in_progress";
if (task.column === "todo") {
if (task.column === roles.wip) return "in_progress";
if (task.column === roles.hold) {
const deps = task.dependencies ?? [];
if (deps.length === 0) return "ready_todo";
// Coarse v1: non-empty deps ⇒ partial_blocked visibility (full dep hydrate deferred).
@@ -96,6 +118,8 @@ export function rankAssignedTasksForWakeDelta(
agentId: string;
boundTaskId?: string | null;
cap?: number;
/** Resolved lifecycle roles; omitted keeps the legacy builtin ids. */
roles?: AssignedTaskRankRoles;
},
): RankAssignedTasksForWakeDeltaResult {
const cap = options.cap ?? WAKE_DELTA_ASSIGNED_TASKS_CAP;
@@ -105,7 +129,7 @@ export function rankAssignedTasksForWakeDelta(
let notActionableCount = 0;
for (const task of open) {
const tierOrNa = tierForTask(task);
const tierOrNa = tierForTask(task, options.roles);
if (tierOrNa === "not_actionable") {
notActionableCount += 1;
continue;