Skip to content

docs(core): resolveReviewColumns is the BROAD set — and one of its consumers must NOT migrate onto it - #2750

Merged
gsxdsm merged 1 commit into
mainfrom
fix/review-columns-arity-doc
Jul 30, 2026
Merged

docs(core): resolveReviewColumns is the BROAD set — and one of its consumers must NOT migrate onto it#2750
gsxdsm merged 1 commit into
mainfrom
fix/review-columns-arity-doc

Conversation

@gsxdsm

@gsxdsm gsxdsm commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

A flaw in the helper I merged in #2730

Found by trying to do the migration I had been advocating for three rounds. One name was answering two questions:

question answer
broad "is this card in a lane where review happens?" every mergeOrchestration lane + every mergeBlocker/humanReview lane — this function
narrow "is this card in the review lane the engine acts on?" resolveLifecycleColumns().review = columnsWithFlag(ir, "mergeOrchestration")[0]one lane

The narrow answer is what the executor, the scheduler and project-engine act on. A caller that admits on the broad set and then MOVES the card moves cards the engine does not consider in review.

The correction I owe

I have been arguing across #2722, #2723 and #2728 that the inline review unions should converge on this helper. For the notifier that is right — over-admission there just means an extra notification.

For register-task-workflow-routes.ts it is wrong. That resolver is deliberately narrower (#2723): its re-engagement moves the card, so admitting a second merge lane is a state change the engine will not agree with. Its local copy is not drift from this helper — it is the other question. Migrating it would reintroduce precisely the over-admission that PR's review round reasoned away.

I was about to make that change. Reading both implementations side by side is the only thing that stopped me, and "consolidate the duplicates" would have looked like an obvious cleanup to the next person too.

What this PR does

Nothing to behaviour. It writes the distinction down at the helper, where a consumer reaching for "the review columns" will see it, and pins the difference with a test.

The test needs a board declaring mergeOrchestration twice — no default lineage does, which is exactly why the two answers look identical everywhere else and why the conflation survived review.

Mutation: narrowing this helper to the first merge lane — the consolidation someone would reasonably attempt — fails the test.

Verification

30 trait tests green · core tsc clean · lint clean (0 errors) · gate green (487 + 158 + 10 + 71). No census movement.

Not done here

Migrating the notifier and the CLI copies onto this helper. Those genuinely should converge, but both live in open PRs (#2722, #2728/#2736) with live review threads; switching them under their authors mid-flight is worse than letting them adopt it once this distinction is documented.

Summary by CodeRabbit

  • Documentation

    • Clarified the distinction between broad review-capable lanes and the workflow’s primary review lane.
    • Documented how review lane selection affects workflow state handling.
  • Tests

    • Added coverage confirming that review detection includes all matching merge lanes.
    • Verified lifecycle review selection continues to use only the primary merge lane.

…he difference

A flaw in the helper I merged in #2730, found by trying to migrate its consumers
onto it. One NAME was answering two questions:

  BROAD   "is this card in a lane where review happens?" — every mergeOrchestration
          lane plus every mergeBlocker/humanReview lane. Safe where over-admission
          is harmless: notifications, badges, read-only surfaces. This function.

  NARROW  "is this card in THE review lane the engine acts on?" —
          `resolveLifecycleColumns().review` is `columnsWithFlag(ir,
          "mergeOrchestration")[0]`, ONE lane, and that is what the executor, the
          scheduler and project-engine act on.

A caller that ADMITS on the broad set and then MOVES the card moves cards the
engine does not consider in review. `register-task-workflow-routes.ts` keeps its
own narrower resolver for exactly that reason (#2723) — its re-engagement moves
the card, so admitting a second merge lane is a state change the engine will not
agree with.

THAT LOCAL COPY IS NOT DRIFT, and this is the correction that matters: I have been
arguing for three rounds that the inline copies should converge on this helper. For
the notifier they should. For those routes they must NOT — migrating them would
reintroduce the over-admission #2723's review round reasoned away. I was about to
do exactly that, and only reading both implementations side by side stopped me.

No behaviour change. What changes is that the distinction is written down at the
helper and pinned by a test, so the next consumer reaching for "the review columns"
on a state-changing path can see which one it wants.

The test needs a board declaring `mergeOrchestration` TWICE, because no default
lineage does and the two answers are identical everywhere else. Mutation: narrowing
this helper to the first merge lane — the "consolidation" someone would reasonably
attempt — fails it.

Verified: 30 trait tests green, core tsc clean, lint clean (0 errors), gate green
(487 + 158 + 10 + 71).
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a61a086-5940-46d0-b789-af133772559d

📥 Commits

Reviewing files that changed from the base of the PR and between 72d4265 and e583eae.

📒 Files selected for processing (2)
  • packages/core/src/__tests__/workflow-lifecycle-traits.test.ts
  • packages/core/src/workflow-lifecycle-traits.ts

📝 Walkthrough

Walkthrough

The PR documents the distinction between broad review-column resolution and the narrow lifecycle review lane, and adds a test covering multiple merge lanes. No runtime logic changes are included.

Changes

Review lane semantics

Layer / File(s) Summary
Resolver contract and coverage
packages/core/src/workflow-lifecycle-traits.ts, packages/core/src/__tests__/workflow-lifecycle-traits.test.ts
Documents that resolveReviewColumns returns all review-capable lanes while resolveLifecycleColumns(...).review selects the first engine review lane, with test coverage for multiple merge lanes.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

  • Runfusion/Fusion#1418: Introduced the workflow-defined column and lifecycle trait behavior covered by this clarification.
  • Runfusion/Fusion#2467: Added the workflow-owned lifecycle column resolution that this PR contrasts with broad review-column resolution.
  • Runfusion/Fusion#2612: Uses lifecycle column resolution for retriage lane detection, directly relating to the documented resolver distinction.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: documenting that resolveReviewColumns is broad and should not replace the narrower consumer.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/review-columns-arity-doc

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Clarifies the broad-versus-narrow workflow review-column distinction.

  • Documents that resolveReviewColumns returns every review-capable lane and is intended for read-only admission.
  • Adds regression coverage proving that this broad set differs from the engine’s single canonical review lane when two merge lanes exist.

Confidence Score: 5/5

The PR appears safe to merge because it changes only documentation and regression coverage while accurately preserving existing behavior.

The new test directly captures the intended difference between the broad review-column helper and the engine’s narrow lifecycle review lane, and no production logic is modified.

Important Files Changed

Filename Overview
packages/core/src/workflow-lifecycle-traits.ts Adds an FNXC architectural comment documenting existing broad review-column semantics without changing runtime behavior.
packages/core/src/tests/workflow-lifecycle-traits.test.ts Adds focused coverage demonstrating that all merge lanes are returned while lifecycle resolution selects only the first.

Reviews (1): Last reviewed commit: "docs(core): resolveReviewColumns is the ..." | Re-trigger Greptile

gsxdsm added a commit that referenced this pull request Jul 30, 2026
…d helper (#2734 review)

My own code. The producer built `lifecycleColumnSets.review` from
`columnsWithFlag(ir, "mergeOrchestration")` alone, so a workflow hosting review on
a `humanReview`- or `mergeBlocker`-only lane produced an EMPTY review set — and
`applyInReviewEnterEffects` never ran for a card plainly in review, leaving the
recovery counters it clears set.

Fixed by calling `resolveReviewColumns` (core, merged in #2730), making this the
first consumer to use the shared answer instead of a fifth inline union. That is
the BROAD set, which is correct here: these hooks ASK the question and move
nothing on the answer. A caller that admits and then MOVES wants the narrow single
lane — #2750 documents the split at the helper.

THE FIRST TEST I WROTE FOR THIS PROVED NOTHING, and it is worth recording because
it is the fourth time today. I asserted the HOOK directly, passing
`lifecycleColumnSets: { review: ["signoff"] }` by hand — which exercises the hook,
already correct, and passes with the producer's bug fully in place. Reverting the
producer left it green.

The producer is the thing that was wrong, so the test now drives a real move
through a real store: a `humanReview`-only lane, seeded `recoveryRetryCount: 3`,
asserted cleared after the move. Mutation: reverting the producer fails it. A
paired negative keeps the widening from making every column a review lane.

Verified: 20 core tests green (2 PG + 18 unit), core tsc clean, lint clean
(0 errors), gate green (487 + 158 + 10 + 71), census --strict exit 0.
@gsxdsm
gsxdsm merged commit b9f3fb0 into main Jul 30, 2026
7 checks passed
@gsxdsm
gsxdsm deleted the fix/review-columns-arity-doc branch July 30, 2026 13:35
gsxdsm added a commit that referenced this pull request Jul 30, 2026
Filed on #2718 but pointing at the guard I converted in #2709, so it is fixed
where it lives.

`.review` is the single `mergeOrchestration` column, so a board hosting review on
a `humanReview`- or `mergeBlocker`-only lane failed this check. `TaskContextMenu`
asks by ROLE, so it OFFERED "Bypass failed review" and the store refused it — the
operator's only escape from a stranded failed pre-merge step returned a conflict.
Both halves were individually reasonable, which is why it took a third party to
see them together.

Now `resolveReviewColumns` (core, #2730). THE BROAD SET IS RIGHT HERE, and that
is a decision rather than a default: this guard REFUSES or PERMITS an operator
action and moves nothing, so admitting every lane where review happens cannot send
a card anywhere the engine disagrees with. #2750 documents the split — a caller
that admits and then MOVES wants the narrow single lane.

The message still names the lanes the check actually used, keeping #2709's fix:
telling an operator to move to a column their board does not have is worse than
refusing.

Test: a `humanReview`-only lane passes the guard. Mutation: reverting to the
singular `.review` fails it while the existing ten cases stay green — they all run
the default board, where the two answers coincide.

Lint caught the now-dead `resolveTaskLifecycleColumns` import; removed.

Verified: 11 bypass tests green, core tsc clean, lint clean (0 errors), gate green
(487 + 158 + 10 + 71), census --strict exit 0.
gsxdsm added a commit that referenced this pull request Jul 30, 2026
…ixed (#2722 review)

I added `resolveReviewColumnsForTask` here to fix a `humanReview`-only lane
resolving to nothing — and built it from `mergeBlocker` + `humanReview`, leaving
`mergeOrchestration` OUT. So a renamed review lane carrying only the merge trait
was excluded, and moving a task there skipped the operator's review notification.
Same silent miss, pointed the other way: no error, no log, the card just arrives
unannounced.

Now `resolveReviewColumns` (core, #2730), which covers all three flags. Both
directions go through one definition, so the pair cannot drift apart again — which
is the whole reason that helper exists.

THE BROAD SET IS RIGHT HERE, and #2750 makes that a decision rather than a
default: this path emits a notification and moves nothing, so over-admission costs
an extra notification while under-admission costs the operator their signal. A
caller that admits and then MOVES wants the narrow single lane.

Mutation: restoring the two-flag union fails the new merge-only case while the
humanReview-only case stays green — the two halves are only distinguishable
together, which is exactly how I shipped one and missed the other.

Verified: 54 notification tests green, engine tsc clean, lint clean (0 errors),
gate green (487 + 158 + 10 + 71), census --strict exit 0.
gsxdsm added a commit that referenced this pull request Jul 30, 2026
Filed on #2718 but pointing at the guard I converted in #2709, so it is fixed
where it lives.

`.review` is the single `mergeOrchestration` column, so a board hosting review on
a `humanReview`- or `mergeBlocker`-only lane failed this check. `TaskContextMenu`
asks by ROLE, so it OFFERED "Bypass failed review" and the store refused it — the
operator's only escape from a stranded failed pre-merge step returned a conflict.
Both halves were individually reasonable, which is why it took a third party to
see them together.

Now `resolveReviewColumns` (core, #2730). THE BROAD SET IS RIGHT HERE, and that
is a decision rather than a default: this guard REFUSES or PERMITS an operator
action and moves nothing, so admitting every lane where review happens cannot send
a card anywhere the engine disagrees with. #2750 documents the split — a caller
that admits and then MOVES wants the narrow single lane.

The message still names the lanes the check actually used, keeping #2709's fix:
telling an operator to move to a column their board does not have is worse than
refusing.

Test: a `humanReview`-only lane passes the guard. Mutation: reverting to the
singular `.review` fails it while the existing ten cases stay green — they all run
the default board, where the two answers coincide.

Lint caught the now-dead `resolveTaskLifecycleColumns` import; removed.

Verified: 11 bypass tests green, core tsc clean, lint clean (0 errors), gate green
(487 + 158 + 10 + 71), census --strict exit 0.
gsxdsm added a commit that referenced this pull request Jul 30, 2026
…ixed (#2722 review)

I added `resolveReviewColumnsForTask` here to fix a `humanReview`-only lane
resolving to nothing — and built it from `mergeBlocker` + `humanReview`, leaving
`mergeOrchestration` OUT. So a renamed review lane carrying only the merge trait
was excluded, and moving a task there skipped the operator's review notification.
Same silent miss, pointed the other way: no error, no log, the card just arrives
unannounced.

Now `resolveReviewColumns` (core, #2730), which covers all three flags. Both
directions go through one definition, so the pair cannot drift apart again — which
is the whole reason that helper exists.

THE BROAD SET IS RIGHT HERE, and #2750 makes that a decision rather than a
default: this path emits a notification and moves nothing, so over-admission costs
an extra notification while under-admission costs the operator their signal. A
caller that admits and then MOVES wants the narrow single lane.

Mutation: restoring the two-flag union fails the new merge-only case while the
humanReview-only case stays green — the two halves are only distinguishable
together, which is exactly how I shipped one and missed the other.

Verified: 54 notification tests green, engine tsc clean, lint clean (0 errors),
gate green (487 + 158 + 10 + 71), census --strict exit 0.
gsxdsm added a commit that referenced this pull request Jul 30, 2026
…parate review findings (#2761)

## The pattern

`workflowMoveMetadata` outlives a task switch, so while the modal is
open its flags describe the **previous** task for a render. This
component gates editability, the execution-mode replan decision, the
intake affordance, the actions menu and the review tab on them.

**The guard already existed** at line 961:

```ts
const detailFlagsAreForThisTask = workflowMoveMetadata?.taskId === task.id;
const detailColumnFlags = detailFlagsAreForThisTask ? workflowMoveMetadata?.currentColumnFlags : undefined;
```

Five call sites read **around** it. Each was found separately — #2744
(review tab), #2696 (`handleDelete` deps), and the four converted here:

| line | consumer |
|---|---|
| 1827 | `canEdit` |
| 2147 | execution-mode replan on save |
| 2285 | execution-mode replan on mode change |
| 3156 | `isIntakeColumn` |
| 3782 | actions-menu model |

**Six review rounds for one root cause.** Converting them together
retires the class instead of paying a round per site — the same
arithmetic as the review-lane family, which took #2730 and #2750 to end
rather than eight per-file patches.

Only the definition line still reads the raw value, which is the point
of it.

## Not covered by a new test — stated rather than papered over

is internal state populated by a fetch, not a prop, so the stale-flags
scenario needs the workflow-metadata request mocked **plus** a task
switch mid-render. That is a real test worth writing. It is not a line I
can add honestly in passing, and I have shipped four tests today that
passed with the bug fully in place — I would rather flag the gap than
repeat that.

This file's suite also carries **6 pre-existing failures**, confirmed
identical on clean by stashing and re-running, which would muddy the
signal from a new case.

## What is verified

- the narrowing is mechanical and **total** — one remaining raw read,
the definition;
- pre-existing failure count **unchanged at 6/45** with and without this
change;
- 106 passed + 5 skipped across both `TaskDetailModal` suites;
- dashboard `tsc` clean (`tsconfig.app.json`), lint clean (0 errors),
gate green (487 + 158 + 10 + 71).

No census movement — this changes which value is read, not whether a
column id is compared.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant