Files
fusion/packages/dashboard/app/utils/prFeedback.ts
gsxdsm 76d77da4d1 fleet: taskSorting + TaskReviewTab 8 → 0 — and Board was faking a column id to force done-sorting (#2744)
Two app-side clusters, 8 → **0**, plus a caller-side hack retired.

## What was broken

**`TaskReviewTab.tsx`** — three of its four questions were `task.column
=== "in-review"`, driving the **Create-PR button**, the **"frozen on
entry to review"** auto-merge hint, and **PR-feedback addressing**. On a
renamed review lane all three took their non-review branch: the button
was absent, and the hint claimed the effective auto-merge value was
*not* frozen when it was.

**`taskSorting.ts`** — `isReviewColumn` decides whether merging cards
float to the top of a lane. Keyed on the id it silently stopped doing
that on any renamed review lane, so the operator loses the "what is
merging right now" ordering with nothing failing.

Both follow the shape this code already established: **caller supplies
the trait, default to the legacy id**. `columnFlags` on the review tab
is optional and wired from `TaskDetailModal`, which already resolved it
for `canEdit` and the actions menu.

## A synthetic column id, retired

`Board.tsx` forced done-sorting by passing the **literal `"done"`** as
the column argument for any complete-flagged lane:

```ts
grouped[column.id] = isWorkflowDoneLikeColumn
  ? sortTasksForDisplayColumn(grouped[column.id] ?? [], "done", doneSortMode)
  : sortTasksForDisplayColumn(grouped[column.id] ?? [], column.id as ColumnType, ...);
```

A synthetic id standing in for a trait — so a custom complete lane
sorted correctly only because its caller **lied about its name**. Both
call sites now pass the real column id and state the trait. (Board's own
census count stays at 2: those two literals were the synthetic ids and
are gone; the 2 remaining are different sites.)

## Revert proof

| reverted | failure |
|---|---|
| `task.column === "in-review"` on the Create-PR guard | `Unable to find
an element by: [data-testid="task-review-create-pr"]` |
| same, on the auto-merge hint | `expected 'Effective: Auto-merge off'
to contain 'frozen on entry to review'` |

A third case pins that the widened test does not treat *every* column as
review.

**None of the 45 existing `TaskReviewTab` cases could have caught this**
— `columnFlags` is optional and they all omit it, so they assert the
legacy fallback. That is the same blind spot as the reconciler's 33 in
#2737, and it keeps recurring: an optional-flags seam means the existing
suite stays green through the conversion *and* through a broken one.

## A process failure worth recording

**I lost this conversion once and had to redo it.** I overwrote four
files with their `origin/main` versions to check whether a failing test
was pre-existing, then "restored" with `git checkout HEAD -- <dir>`.
HEAD was still `origin/main` because I had not committed, so that
**discarded the work**.

Same class as the shared-stash incident two PRs back: an implicit or
positional restore reference. The fix is ordering, not care — **commit
before any baseline comparison**, so `git checkout HEAD -- <file>`
restores my work rather than main's. This PR's commit was created before
the comparison for exactly that reason, and the note is in the commit
message so the next person hits it there too.

## Verification

`pnpm test:gate` **GREEN** (158 + 10 + 487 + 71) · **232 passed** across
TaskReviewTab / taskSorting / Board suites · dashboard `tsc -p
tsconfig.app.json` clean · `pnpm lint` clean · census `--strict` exits
0.

The 1 `board-mobile` failure is **pre-existing** — verified by swapping
in clean `origin/main` copies of all four files and reproducing it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 08:06:04 -07:00

38 lines
2.0 KiB
TypeScript

import type { PrInfo, Task } from "@fusion/core";
import type { ColumnRoleFlags } from "./columnRoles";
import { isReviewColumnRole, isWipColumnRole } from "./columnRoles";
export function getTaskPrimaryPrInfo(task: Pick<Task, "prInfo" | "prInfos">): PrInfo | undefined {
return task.prInfos?.[0] ?? task.prInfo;
}
/*
FNXC:TaskReview 2026-06-28-00:00:
The Address PR feedback affordance must render identically on the task card and Review tab. Gate it on one shared predicate so a linked primary PR with comments or CHANGES_REQUESTED is actionable, while no-PR and no-feedback states render no empty button shell.
FNXC:TaskReview 2026-06-28-16:39:
The button promises an AI session starts, so it must only render for task states the lifecycle route can actually start or wake. Restrict the launch affordance to in-review and in-progress tasks rather than letting terminal/todo cards add steering comments without active work.
*/
export function hasActionablePrFeedback(task: Pick<Task, "prInfo" | "prInfos">): boolean {
const prInfo = getTaskPrimaryPrInfo(task);
if (!prInfo) return false;
return (prInfo.commentCount ?? 0) > 0 || prInfo.lastReviewDecision === "CHANGES_REQUESTED";
}
/*
FNXC:WorkflowResolvedColumns 2026-07-31-11:10 (#2744 review — greptile P1, a half-conversion I shipped):
The lane pair here is the SAME question TaskReviewTab and TaskCard ask, and converting only the callers
left this rejecting custom column ids. Measured consequence: on a renamed review or WIP lane a task with
actionable PR feedback but no loaded display items had the Address-PR-Feedback action stay hidden, because
the caller's role check passed and this returned false.
Optional flags, legacy ids as the documented fallback — so any caller without resolved flags is unchanged.
*/
export function canStartPrFeedbackAddressing(
task: Pick<Task, "column" | "prInfo" | "prInfos">,
columnFlags?: ColumnRoleFlags,
): boolean {
return (isReviewColumnRole(columnFlags, task.column) || isWipColumnRole(columnFlags, task.column))
&& hasActionablePrFeedback(task);
}