fix(dashboard): expose column roles to browser bundle (#3151)

## Summary
- export the browser-safe `@fusion/core/column-roles` subpath
- keep Vite/Vitest aliases ahead of broad `@fusion/core` aliases
- restore production dashboard builds after task undo classification
adopted shared column-role helpers

## Test plan
- `node scripts/check-no-node-only-core-imports-in-dashboard.mjs`
- `FUSION_DASHBOARD_DEEP=1 pnpm --filter @fusion/dashboard exec vitest
run app/utils/__tests__/taskRevert.test.ts --pool=threads
--maxWorkers=1`
- `pnpm --filter @fusion/core typecheck`
- `pnpm --filter @fusion/dashboard typecheck`
- `CI=true pnpm check:changesets`
- `pnpm build`


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
  * Fixed dashboard build compatibility for browser-based environments.
* Improved reliability when importing column role functionality across
supported application components.

* **Refactor**
* Made column role utilities available through a dedicated browser-safe
entry point.

* **Chores**
* Updated development and test configurations to consistently resolve
the new entry point.
* Documented the browser-safe module classification and recorded the
release patch.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Phil Larson
2026-07-31 06:46:19 -07:00
committed by GitHub
parent 110d6fd150
commit 920d68e10f
14 changed files with 31 additions and 6 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Fix the dashboard build after task undo classification began using shared column-role helpers.
category: fix
dev: Export the browser-safe column-roles core leaf and mirror its alias across Vite and Vitest consumers so broad core aliases cannot swallow the subpath.

View File

@@ -29,6 +29,11 @@
"source": "./src/task-delete-attribution.ts", "source": "./src/task-delete-attribution.ts",
"import": "./dist/task-delete-attribution.js" "import": "./dist/task-delete-attribution.js"
}, },
"./column-roles": {
"types": "./src/column-roles.ts",
"source": "./src/column-roles.ts",
"import": "./dist/column-roles.js"
},
"./detect-content-language": { "./detect-content-language": {
"types": "./src/detect-content-language.ts", "types": "./src/detect-content-language.ts",
"source": "./src/detect-content-language.ts", "source": "./src/detect-content-language.ts",

View File

@@ -1,5 +1,5 @@
import type { Task } from "@fusion/core"; import type { Task } from "@fusion/core";
import { isTerminalColumnRole, type ColumnRoleTraitFlags } from "@fusion/core"; import { isTerminalColumnRole, type ColumnRoleTraitFlags } from "@fusion/core/column-roles";
/** /**
* FNXC:TaskRevert 2026-07-04-00:00: * FNXC:TaskRevert 2026-07-04-00:00:

View File

@@ -141,6 +141,7 @@ export default defineConfig({
Ordered before the `@fusion/core` entry because Vite matches aliases in order and the broader key would otherwise swallow this subpath — the exact failure this line was added to fix (`"FUSION_CLIENT_HEADER" is not exported by ../core/src/types.ts`). Ordered before the `@fusion/core` entry because Vite matches aliases in order and the broader key would otherwise swallow this subpath — the exact failure this line was added to fix (`"FUSION_CLIENT_HEADER" is not exported by ../core/src/types.ts`).
*/ */
"@fusion/core/task-delete-attribution": resolve(__dirname, "../core/src/task-delete-attribution.ts"), "@fusion/core/task-delete-attribution": resolve(__dirname, "../core/src/task-delete-attribution.ts"),
"@fusion/core/column-roles": resolve(__dirname, "../core/src/column-roles.ts"),
"@fusion/core/detect-content-language": resolve(__dirname, "../core/src/detect-content-language.ts"), "@fusion/core/detect-content-language": resolve(__dirname, "../core/src/detect-content-language.ts"),
"@fusion/core": resolve(__dirname, "../core/src/types.ts"), "@fusion/core": resolve(__dirname, "../core/src/types.ts"),
"@fusion/dashboard/app/components/TaskCard": resolve(__dirname, "app/components/TaskCard.tsx"), "@fusion/dashboard/app/components/TaskCard": resolve(__dirname, "app/components/TaskCard.tsx"),

View File

@@ -534,6 +534,7 @@ export default defineConfig({
Keep this exact alias before the broader core alias so Vite does not rewrite the subpath. Keep this exact alias before the broader core alias so Vite does not rewrite the subpath.
*/ */
"@fusion/core/task-delete-attribution": resolve(__dirname, "../core/src/task-delete-attribution.ts"), "@fusion/core/task-delete-attribution": resolve(__dirname, "../core/src/task-delete-attribution.ts"),
"@fusion/core/column-roles": resolve(__dirname, "../core/src/column-roles.ts"),
"@fusion/core": resolve(__dirname, "../core/src/index.ts"), "@fusion/core": resolve(__dirname, "../core/src/index.ts"),
"@fusion/engine": resolve(__dirname, "../engine/src/index.ts"), "@fusion/engine": resolve(__dirname, "../engine/src/index.ts"),
"@fusion/plugin-sdk": resolve(__dirname, "../plugin-sdk/src/index.ts"), "@fusion/plugin-sdk": resolve(__dirname, "../plugin-sdk/src/index.ts"),

View File

@@ -11,6 +11,7 @@ const fusionAliases = {
transitively — this project aliases `@fusion/dashboard`, and `app/api/client.ts` imports the transitively — this project aliases `@fusion/dashboard`, and `app/api/client.ts` imports the
browser-safe delete-attribution leaf. browser-safe delete-attribution leaf.
*/ */
"@fusion/core/column-roles": resolve(__dirname, "../core/src/column-roles.ts"),
"@fusion/core/task-delete-attribution": resolve(__dirname, "../core/src/task-delete-attribution.ts"), "@fusion/core/task-delete-attribution": resolve(__dirname, "../core/src/task-delete-attribution.ts"),
"@fusion/core": resolve(__dirname, "../core/src/index.ts"), "@fusion/core": resolve(__dirname, "../core/src/index.ts"),
"@fusion/dashboard": resolve(__dirname, "../dashboard/src/index.ts"), "@fusion/dashboard": resolve(__dirname, "../dashboard/src/index.ts"),

View File

@@ -168,7 +168,7 @@ pgDescribe("scheduler parked-column resolution against a live store", () => {
it("REGRESSION — a dependent in a RENAMED hold column IS unblocked when its blocker is deleted", async () => { it("REGRESSION — a dependent in a RENAMED hold column IS unblocked when its blocker is deleted", async () => {
/* /*
FNXC:WorkflowResolvedColumns 2026-08-01-02:20 (fleet — the flip this test was written to catch): FNXC:WorkflowResolvedColumns 2026-07-31-06:35 (fleet — the flip this test was written to catch):
This was a CHARACTERIZATION of the inert sync read: `resolveTaskParkedColumnsSync` answered This was a CHARACTERIZATION of the inert sync read: `resolveTaskParkedColumnsSync` answered
`{ hold: "todo" }` for a board whose hold column is `backlog`, so the reconciliation queried a `{ hold: "todo" }` for a board whose hold column is `backlog`, so the reconciliation queried a
column that does not exist, found no dependents, and left `blockedBy` pointing at a deleted task. column that does not exist, found no dependents, and left `blockedBy` pointing at a deleted task.

View File

@@ -450,7 +450,7 @@ function mergeParkedColumns(
} }
/* /*
FNXC:WorkflowResolvedColumns 2026-08-01-01:40 (fleet): FNXC:WorkflowResolvedColumns 2026-07-31-06:35 (fleet):
The ASYNC twin. The sync one below cannot answer for a custom workflow in production, so any guard that The ASYNC twin. The sync one below cannot answer for a custom workflow in production, so any guard that
can reach this one must. can reach this one must.
@@ -477,7 +477,7 @@ async function resolveTaskParkedColumns(store: TaskStore, taskId: string): Promi
archived, archived,
terminal: new Set([complete, archived]), terminal: new Set([complete, archived]),
/* /*
FNXC:WorkflowResolvedColumns 2026-08-01-02:05 (fleet): FNXC:WorkflowResolvedColumns 2026-07-31-06:35 (fleet):
The wake set UNIONS the legacy ids rather than replacing them, and that is load-bearing rather The wake set UNIONS the legacy ids rather than replacing them, and that is load-bearing rather
than defensive. Post-U11 the default lineage has no `triage` column, so a RESOLVED answer returns than defensive. Post-U11 the default lineage has no `triage` column, so a RESOLVED answer returns
`intake: "todo"` where the old inert path fell back to `"triage"`. Converting without the union `intake: "todo"` where the old inert path fell back to `"triage"`. Converting without the union
@@ -1268,7 +1268,7 @@ export class Scheduler {
schedulerLog.warn(`Failed to reset dispatch oscillation state for ${task.id} on unpause: ${error instanceof Error ? error.message : String(error)}`); schedulerLog.warn(`Failed to reset dispatch oscillation state for ${task.id} on unpause: ${error instanceof Error ? error.message : String(error)}`);
}); });
} }
/* FNXC:WorkflowResolvedColumns 2026-08-01-01:40 (fleet): the answer only gates `schedule()`, /* FNXC:WorkflowResolvedColumns 2026-07-31-06:35 (fleet): the answer only gates `schedule()`,
which is async and fire-and-forget, so resolving it properly costs nothing observable. */ which is async and fire-and-forget, so resolving it properly costs nothing observable. */
void (async () => { void (async () => {
const unpausedParked = await resolveTaskParkedColumns(this.store, task.id); const unpausedParked = await resolveTaskParkedColumns(this.store, task.id);
@@ -1299,7 +1299,7 @@ export class Scheduler {
this.planningTaskIds.add(task.id); this.planningTaskIds.add(task.id);
} else if (this.planningTaskIds.has(task.id)) { } else if (this.planningTaskIds.has(task.id)) {
this.planningTaskIds.delete(task.id); this.planningTaskIds.delete(task.id);
/* FNXC:WorkflowResolvedColumns 2026-08-01-01:40 (fleet): as with the unpause wake above, the /* FNXC:WorkflowResolvedColumns 2026-07-31-06:35 (fleet): as with the unpause wake above, the
answer only gates `schedule()`. The `planningTaskIds.delete` stays SYNCHRONOUS — it is the answer only gates `schedule()`. The `planningTaskIds.delete` stays SYNCHRONOUS — it is the
edge-trigger bookkeeping, and deferring it would let a second update re-enter this branch. */ edge-trigger bookkeeping, and deferring it would let a second update re-enter this branch. */
void (async () => { void (async () => {

View File

@@ -14,6 +14,7 @@ export default defineConfig({
transitively — this project aliases `@fusion/dashboard`, and `app/api/client.ts` imports the transitively — this project aliases `@fusion/dashboard`, and `app/api/client.ts` imports the
browser-safe delete-attribution leaf. browser-safe delete-attribution leaf.
*/ */
"@fusion/core/column-roles": resolve(__dirname, "../core/src/column-roles.ts"),
"@fusion/core/task-delete-attribution": resolve(__dirname, "../core/src/task-delete-attribution.ts"), "@fusion/core/task-delete-attribution": resolve(__dirname, "../core/src/task-delete-attribution.ts"),
"@fusion/core": resolve(__dirname, "../core/src/index.ts"), "@fusion/core": resolve(__dirname, "../core/src/index.ts"),
"@fusion/test-utils": resolve(__dirname, "../core/src/__test-utils__/workspace.ts"), "@fusion/test-utils": resolve(__dirname, "../core/src/__test-utils__/workspace.ts"),

View File

@@ -32,6 +32,7 @@ export default defineConfig({
transitively — this project aliases `@fusion/dashboard`, and `app/api/client.ts` imports the transitively — this project aliases `@fusion/dashboard`, and `app/api/client.ts` imports the
browser-safe delete-attribution leaf. browser-safe delete-attribution leaf.
*/ */
"@fusion/core/column-roles": fileURLToPath(new URL("../../packages/core/src/column-roles.ts", import.meta.url)),
"@fusion/core/task-delete-attribution": fileURLToPath(new URL("../../packages/core/src/task-delete-attribution.ts", import.meta.url)), "@fusion/core/task-delete-attribution": fileURLToPath(new URL("../../packages/core/src/task-delete-attribution.ts", import.meta.url)),
"@fusion/core": fileURLToPath(new URL("../../packages/core/src/index.ts", import.meta.url)), "@fusion/core": fileURLToPath(new URL("../../packages/core/src/index.ts", import.meta.url)),
"@fusion/plugin-sdk": fileURLToPath(new URL("../../packages/plugin-sdk/src/index.ts", import.meta.url)), "@fusion/plugin-sdk": fileURLToPath(new URL("../../packages/plugin-sdk/src/index.ts", import.meta.url)),

View File

@@ -52,6 +52,7 @@ export default defineConfig({
transitively — this project aliases `@fusion/dashboard`, and `app/api/client.ts` imports the transitively — this project aliases `@fusion/dashboard`, and `app/api/client.ts` imports the
browser-safe delete-attribution leaf. browser-safe delete-attribution leaf.
*/ */
{ find: "@fusion/core/column-roles", replacement: fileURLToPath(new URL("../../packages/core/src/column-roles.ts", import.meta.url)) },
{ find: "@fusion/core/task-delete-attribution", replacement: fileURLToPath(new URL("../../packages/core/src/task-delete-attribution.ts", import.meta.url)) }, { find: "@fusion/core/task-delete-attribution", replacement: fileURLToPath(new URL("../../packages/core/src/task-delete-attribution.ts", import.meta.url)) },
{ find: "@fusion/core", replacement: fileURLToPath(new URL("../../packages/core/src/index.ts", import.meta.url)) }, { find: "@fusion/core", replacement: fileURLToPath(new URL("../../packages/core/src/index.ts", import.meta.url)) },
{ {

View File

@@ -33,6 +33,7 @@ export default defineConfig({
transitively — this project aliases `@fusion/dashboard`, and `app/api/client.ts` imports the transitively — this project aliases `@fusion/dashboard`, and `app/api/client.ts` imports the
browser-safe delete-attribution leaf. browser-safe delete-attribution leaf.
*/ */
{ find: "@fusion/core/column-roles", replacement: fileURLToPath(new URL("../../packages/core/src/column-roles.ts", import.meta.url)) },
{ find: "@fusion/core/task-delete-attribution", replacement: fileURLToPath(new URL("../../packages/core/src/task-delete-attribution.ts", import.meta.url)) }, { find: "@fusion/core/task-delete-attribution", replacement: fileURLToPath(new URL("../../packages/core/src/task-delete-attribution.ts", import.meta.url)) },
{ find: "@fusion/core", replacement: fileURLToPath(new URL("../../packages/core/src/index.ts", import.meta.url)) }, { find: "@fusion/core", replacement: fileURLToPath(new URL("../../packages/core/src/index.ts", import.meta.url)) },
{ {

View File

@@ -32,6 +32,7 @@ export default defineConfig({
transitively — this project aliases `@fusion/dashboard`, and `app/api/client.ts` imports the transitively — this project aliases `@fusion/dashboard`, and `app/api/client.ts` imports the
browser-safe delete-attribution leaf. browser-safe delete-attribution leaf.
*/ */
"@fusion/core/column-roles": fileURLToPath(new URL("../../packages/core/src/column-roles.ts", import.meta.url)),
"@fusion/core/task-delete-attribution": fileURLToPath(new URL("../../packages/core/src/task-delete-attribution.ts", import.meta.url)), "@fusion/core/task-delete-attribution": fileURLToPath(new URL("../../packages/core/src/task-delete-attribution.ts", import.meta.url)),
"@fusion/core": fileURLToPath(new URL("../../packages/core/src/index.ts", import.meta.url)), "@fusion/core": fileURLToPath(new URL("../../packages/core/src/index.ts", import.meta.url)),
// FNXC:Clipboard 2026-07-12-00:00: The reports plugin imports the dashboard clipboard helper through its package subpath export; keep this exact alias ahead of the package root alias so vitest does not collapse the subpath to src/index.ts. // FNXC:Clipboard 2026-07-12-00:00: The reports plugin imports the dashboard clipboard helper through its package subpath export; keep this exact alias ahead of the package root alias so vitest does not collapse the subpath to src/index.ts.

View File

@@ -60,6 +60,11 @@
"reason": "2026-07-26: Delete caller-attribution constants and the pure header resolver are shared by the browser client that stamps `x-fusion-client` and the route that reads it; the module has no imports at all.", "reason": "2026-07-26: Delete caller-attribution constants and the pure header resolver are shared by the browser client that stamps `x-fusion-client` and the route that reads it; the module has no imports at all.",
"verifiedAt": "2026-07-26" "verifiedAt": "2026-07-26"
}, },
{
"module": "column-roles",
"reason": "2026-07-31: Column role predicates depend only on erased trait types and provide browser-safe flags-first lifecycle classification.",
"verifiedAt": "2026-07-31"
},
{ {
"module": "live-agent-count", "module": "live-agent-count",
"reason": "2026-07-21: FN-8453 shares pure workflow-trait-based Running and Waiting predicates with dashboard capacity indicators; its dependency graph has no Node-only modules.", "reason": "2026-07-21: FN-8453 shares pure workflow-trait-based Running and Waiting predicates with dashboard capacity indicators; its dependency graph has no Node-only modules.",