From e9b24b69e81d4b1d76cf7163bc61c560c035ba2c Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 31 Jul 2026 01:41:13 -0700 Subject: [PATCH] fix(dashboard): the duplicate banner judged the canonical by legacy lane ids (#3032) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found by applying the check I proposed on #3028: **re-test each `ALLOWED_OMISSIONS` entry — does the omission still fail once the excuse is removed?** It found a stale excuse on its first run. ## The entry's blocker was never tested ``` "…TaskDetailModal.tsx::isNearDuplicateCanonicalInactive" reason: "…Correct supply needs a fetch — a data change. See the note at the site." ``` The reasoning gets the hard part right: passing `detailColumnFlags` would answer about the **modal's** task, not the canonical, and would type-check while reading as a conversion. Rejecting that is correct. Then it concludes the seam needs a fetch — without checking what is in scope. - `columnFlagsByTaskId` is **already a prop of this component** (declared `:367`, destructured `:727`, used for the fan-out map at `:3718`), keyed by task id. - The canonical is `tasks.find((c) => c.id === nearDuplicateOf)` — drawn from the same loaded set the map covers. If the banner can render at all, the canonical is in `tasks`. So `columnFlagsByTaskId?.get(canonical.id)` is the canonical's own flags, no fetch. **`Column.tsx:307` already does exactly this**, with a comment making the same point about not reusing the row's flags — a sibling call site of the same function, solved. ## What was broken The banner's "this duplicates X" warning stayed up when the canonical had landed in a **renamed** complete lane, because `isNearDuplicateCanonicalInactive` fell back to the legacy ids and never saw it as finished. Same user-visible symptom #2997 fixed for the card chip; this is the modal. ## Verification | state | result | |---|---| | clean | seams gate exit 0; 123/123 across `TaskDetailModal.rendering` + `Column.neardup-flags-arrival` | | revert the supply | **gate exit 1** — `isNearDuplicateCanonicalInactive() — supplied by 10/11 call sites; omitted at TaskDetailModal.tsx:1 (of 2)` | That mutation is the point: with the allow-list entry present, this exact omission passed silently. It is now defended by the gate rather than excused by it. `tsc -p tsconfig.app.json` 0 errors in the file, lint clean, FNXC gate exit 0. ## The general point This is the second allow-list entry in two PRs whose stated blocker was wrong — #3028 removed the other one (*"needs a published-API change"*; the SDK is `private: true` and every consumer was in-repo). An `ALLOWED_OMISSIONS` entry is a deferral **carrying a gate's authority**. It reads as settled, it lives inside the checker, and it turns "nobody tested this" into "someone tested it and concluded no". A stale baseline *number* invites a recount; a stale *paragraph* invites agreement. Both entries this gate carried were stale, and the note at this call site had even been revised once — the revision corrected which flags were wrong to pass, and left the untested "needs a fetch" conclusion standing. Worth a periodic sweep of the remaining entries as they accumulate; with these two gone the list is empty, so the cheapest time to institutionalise it is now. --------- Co-authored-by: Claude Opus 5 (1M context) --- .changeset/detail-modal-canonical-flags.md | 7 ++++++ .../app/components/TaskDetailModal.tsx | 24 +++++++++++++++---- scripts/check-inert-flag-seams.mjs | 6 ----- scripts/lib/lane-wiring-baseline.json | 1 - 4 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 .changeset/detail-modal-canonical-flags.md diff --git a/.changeset/detail-modal-canonical-flags.md b/.changeset/detail-modal-canonical-flags.md new file mode 100644 index 0000000000..6d904ff253 --- /dev/null +++ b/.changeset/detail-modal-canonical-flags.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: The duplicate-warning banner in Task Detail now judges the canonical by its own lane, not the legacy ids. +category: fix +dev: `isNearDuplicateCanonicalInactive` in TaskDetailModal now receives `columnFlagsByTaskId.get(canonical.id)`; the allow-list entry claiming this needed a fetch is removed. diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index bb466151c3..d953c94c4c 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -944,15 +944,31 @@ export function TaskDetailContent({ vs union confusion that `column-role-degraded-flags.test.ts` exists to catch, and it would type-check and read as a conversion. - Supplying it correctly needs the canonical's own resolved flags, which means fetching them: a data - change, out of scope here. So the omission is deliberate and is exempted BY CALL SITE rather than by - function name, so core's sibling site stays guarded. + FNXC:WorkflowResolvedColumns 2026-07-31-03:10 (the "needs a fetch" blocker was never tested): + The paragraph above rejected passing `detailColumnFlags` — correctly, that would answer about the + wrong task — and then concluded the seam needs a data change. It does not. `columnFlagsByTaskId` is + already a prop of this component (declared :367, destructured :727, used for the fan-out map), and + it is keyed by task id. The canonical is `tasks.find(c => c.id === nearDuplicateOf)`, so it is + drawn from the same loaded set the map covers — a `.get(canonical.id)` is the canonical's OWN + flags, with no fetch. + + `Column.tsx:307` already does exactly this, with a comment making the same point about not reusing + the row's flags. The blocker was asserted from the shape of the problem (two different tasks) rather + than tested against what was in scope. + + A canonical the map does not cover yields `undefined`, which is the documented legacy fallback — + strictly better than always-legacy, never a fabricated answer. */ const showNearDuplicateWarning = Boolean(nearDuplicateOf) && workingTask.sourceMetadata?.nearDuplicateDismissed !== true && task.column !== "archived" && task.column !== "done" - && !isNearDuplicateCanonicalInactive(nearDuplicateCanonical); + && !isNearDuplicateCanonicalInactive( + nearDuplicateCanonical, + /* The CANONICAL's own flags, keyed by its id — never `detailColumnFlags`, which describes this + modal's task. Same shape as Column.tsx:307, the sibling site that already does this. */ + nearDuplicateCanonical ? columnFlagsByTaskId?.get(nearDuplicateCanonical.id) : undefined, + ); const [sourceAgent, setSourceAgent] = useState(null); const [selectedSourceAgentId, setSelectedSourceAgentId] = useState(null); const provenanceDisplay = getProvenanceLabel(workingTask, { diff --git a/scripts/check-inert-flag-seams.mjs b/scripts/check-inert-flag-seams.mjs index bbd458d1bd..67a68e007c 100644 --- a/scripts/check-inert-flag-seams.mjs +++ b/scripts/check-inert-flag-seams.mjs @@ -150,12 +150,6 @@ const ALLOWED_OMISSIONS = new Map([ + "change, not local wiring, which is the same 'needs a data change' category as the entry below. " + "Filed for the plugin-API owner rather than bodged here." }, ], - [ - "packages/dashboard/app/components/TaskDetailModal.tsx::isNearDuplicateCanonicalInactive", - { count: 1, reason: "The flags in scope describe the MODAL'S task; the canonical is a different task on a column this " - + "component never resolves. Passing them would type-check, read as a conversion, and answer " - + "about the wrong task. Correct supply needs a fetch — a data change. See the note at the site." }, - ], ]); /* diff --git a/scripts/lib/lane-wiring-baseline.json b/scripts/lib/lane-wiring-baseline.json index 16187a1ec9..1e631f0633 100644 --- a/scripts/lib/lane-wiring-baseline.json +++ b/scripts/lib/lane-wiring-baseline.json @@ -10,7 +10,6 @@ "packages/dashboard/src/routes/register-task-workflow-routes.ts": 1, "packages/dashboard/app/components/Lane.tsx": 1, "packages/dashboard/app/components/ListView.tsx": 1, - "packages/dashboard/app/components/TaskDetailModal.tsx": 1, "packages/dashboard/app/hooks/useBlockerFanout.ts": 1, "packages/cli/src/commands/dashboard-tui/app.tsx": 1, "packages/cli/src/commands/dashboard-tui/bucket-mapping.ts": 1,