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>
38 lines
2.0 KiB
TypeScript
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);
|
|
}
|