feat(validation): split topology validation from link repair into publishable nx package#11864
feat(validation): split topology validation from link repair into publishable nx package#11864christian-byrne wants to merge 6 commits intomainfrom
Conversation
…lishable nx package Extract workflow zod schemas, link topology validator, and link repair into a new `@comfyorg/workflow-validation` package under `packages/`. The package has zero litegraph coupling — it operates on plain serialised JSON shapes — so it can be consumed by Node.js CI scripts and a future backend validator. Behaviour changes: - Replace the four `'Error. Expected node to match patched data.'` invariant throws in the link fixer with `LinkRepairAbortedError`, which carries a structured `TopologyError` describing the offending `[linkId, src, srcSlot, tgt, tgtSlot]`. - Add a pure `validateLinkTopology(graph)` that walks the link table and reports out-of-bounds slots, missing nodes, and endpoint mismatches as a structured `TopologyError[]`. This catches the seedance-style breakage (links targeting slots that don't exist on the node) which the schema cannot detect. - Surface topology errors via toast when `Comfy.Validation.Workflows` is enabled, instead of swallowing them with `console.error`. Unrepairable workflows raise an `error` toast with structured details and `useWorkflowValidation` returns `null` so the caller falls back to the original graph. Migration is via tsconfig + vite path aliases mirroring the existing `@/utils/formatUtil` precedent, so the 76 importers of `workflowSchema` and the lone importer of `linkFixer` continue to compile without changes. Adds `release-npm-workflow-validation.yaml`, a `workflow_dispatch` publisher mirroring the existing `release-npm-types` pattern, so a follow-up PR on Comfy-Org/workflow_templates can pin the published package for per-template topology CI.
…ublish Address code review feedback: - Annotate `zGraphDefinitions.subgraphs`'s lazy resolver with the same explicit `z.ZodArray<z.ZodType<SubgraphDefinitionBase<...>>>` shape the recursive `zSubgraphDefinition` already uses. This breaks the TS7056 'inferred type exceeds maximum length' error when emitting declarations for `zComfyWorkflow`. - Drop `rollupTypes` from the package's `vite-plugin-dts` config. Per-file `.d.ts` emit avoids both the api-extractor entry-point failure and the recursive-type rollup blowup; `index.d.ts` re-exports per-file declarations so consumers still see one entry. - Restore the full subpath export surface in the published manifest (`./linkRepair`, `./linkTopology`, `./workflowSchema`, `./serialised`) and resolve catalog versions for runtime deps so the published package matches the workspace contract. - Ignore `*.tsbuildinfo` so package builds don't litter the index.
The workflow file requires `workflows` permission on the GitHub App posting the PR, which isn't granted today. The release workflow can be added in a follow-up commit by a maintainer; the package itself is already buildable via `pnpm --dir packages/workflow-validation run build`.
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR adds a new ChangesWorkflow Validation Package Creation & Integration
sequenceDiagram
participant UI as useWorkflowValidation
participant V as validateLinkTopology
participant R as repairLinks
participant E as LinkRepairAbortedError
UI->>V: validateLinkTopology(graph)
V-->>UI: TopologyError[]
alt Validation errors present
UI->>UI: warn + toast top N errors
end
UI->>R: repairLinks(graph, { fix: true })
alt Repair succeeds
R->>R: patch slots, collect deleted links
R-->>UI: RepairResult { fixed, patched, deleted }
UI->>UI: show success toast with summary
else Unreconcilable divergence
R-->>E: throw LinkRepairAbortedError(topologyError)
E-->>UI: structured topology error
UI->>UI: show abort toast, return { aborted: true }
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 6 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (6 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Review rate limit: 0/5 reviews remaining, refill in 7 minutes and 34 seconds. Comment |
🌐 Website E2ETip All tests passed.
|
🎭 Playwright: ✅ 1465 passed, 0 failed · 1 flaky📊 Browser Reports
|
🎨 Storybook: ✅ Built — View Storybook |
📦 Bundle: 5.26 MB gzip 🔴 +2.03 kBDetailsSummary
Category Glance App Entry Points — 22.6 kB (baseline 22.6 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.24 MB (baseline 1.24 MB) • ⚪ 0 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 81.8 kB (baseline 81.8 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 9 added / 9 removed / 2 unchanged Panels & Settings — 489 kB (baseline 489 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 10 added / 10 removed / 11 unchanged User & Accounts — 17.5 kB (baseline 17.5 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 5 added / 5 removed / 2 unchanged Editors & Dialogs — 112 kB (baseline 112 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 4 added / 4 removed UI Components — 62.9 kB (baseline 62.9 kB) • ⚪ 0 BReusable component library chunks
Status: 5 added / 5 removed / 9 unchanged Data & Services — 3.05 MB (baseline 3.04 MB) • 🔴 +7.05 kBStores, services, APIs, and repositories
Status: 13 added / 13 removed / 4 unchanged Utilities & Hooks — 365 kB (baseline 365 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 16 added / 16 removed / 15 unchanged Vendor & Third-Party — 9.94 MB (baseline 9.94 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 8.84 MB (baseline 8.84 MB) • 🔴 +1.33 kBBundles that do not match a named category
Status: 62 added / 62 removed / 73 unchanged ⚡ Performance Report
No regressions detected. All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-05-03T07:15:40.166Z",
"gitSha": "f371e934e210d91dbccb0fb5ffac6b7c2f171b8c",
"branch": "glary/workflow-validation-package",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2033.693999999997,
"styleRecalcs": 5,
"styleRecalcDurationMs": 4.375999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 382.50700000000006,
"heapDeltaBytes": -2036996,
"heapUsedBytes": 45802792,
"domNodes": -270,
"jsHeapTotalBytes": 14282752,
"scriptDurationMs": 15.474000000000002,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-idle",
"durationMs": 2013.6029999999892,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.044,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 337.199,
"heapDeltaBytes": 22727148,
"heapUsedBytes": 71085264,
"domNodes": 17,
"jsHeapTotalBytes": 15204352,
"scriptDurationMs": 14.925,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2018.6619999999493,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.573999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 326.09900000000005,
"heapDeltaBytes": 23059736,
"heapUsedBytes": 71714268,
"domNodes": 18,
"jsHeapTotalBytes": 14680064,
"scriptDurationMs": 16.039,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1818.3129999999892,
"styleRecalcs": 74,
"styleRecalcDurationMs": 35.752,
"layouts": 12,
"layoutDurationMs": 3.333,
"taskDurationMs": 773.424,
"heapDeltaBytes": 1073556,
"heapUsedBytes": 49428864,
"domNodes": -265,
"jsHeapTotalBytes": 15069184,
"scriptDurationMs": 128.233,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1779.8699999999599,
"styleRecalcs": 72,
"styleRecalcDurationMs": 35.125,
"layouts": 12,
"layoutDurationMs": 3.013,
"taskDurationMs": 749.966,
"heapDeltaBytes": -801068,
"heapUsedBytes": 47786552,
"domNodes": -265,
"jsHeapTotalBytes": 16379904,
"scriptDurationMs": 120.46300000000001,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1819.613000000004,
"styleRecalcs": 72,
"styleRecalcDurationMs": 34.92000000000001,
"layouts": 12,
"layoutDurationMs": 3.409,
"taskDurationMs": 769.506,
"heapDeltaBytes": -815740,
"heapUsedBytes": 47811876,
"domNodes": -264,
"jsHeapTotalBytes": 16642048,
"scriptDurationMs": 121.95100000000001,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1732.9829999999902,
"styleRecalcs": 31,
"styleRecalcDurationMs": 16.814000000000004,
"layouts": 6,
"layoutDurationMs": 0.5239999999999999,
"taskDurationMs": 284.75,
"heapDeltaBytes": 128228,
"heapUsedBytes": 48499048,
"domNodes": 76,
"jsHeapTotalBytes": 14680064,
"scriptDurationMs": 17.423000000000002,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1717.525999999964,
"styleRecalcs": 31,
"styleRecalcDurationMs": 17.217999999999996,
"layouts": 6,
"layoutDurationMs": 0.6619999999999998,
"taskDurationMs": 280.37899999999996,
"heapDeltaBytes": 211220,
"heapUsedBytes": 48885860,
"domNodes": 75,
"jsHeapTotalBytes": 14942208,
"scriptDurationMs": 17.845,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1736.9529999999713,
"styleRecalcs": 32,
"styleRecalcDurationMs": 17.468,
"layouts": 6,
"layoutDurationMs": 0.6330000000000001,
"taskDurationMs": 291.432,
"heapDeltaBytes": 238044,
"heapUsedBytes": 48861804,
"domNodes": 77,
"jsHeapTotalBytes": 14942208,
"scriptDurationMs": 18.009999999999998,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 592.316000000011,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.278000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 370.20400000000006,
"heapDeltaBytes": 9316648,
"heapUsedBytes": 58037008,
"domNodes": 18,
"jsHeapTotalBytes": 15728640,
"scriptDurationMs": 65.661,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "dom-widget-clipping",
"durationMs": 509.6080000000143,
"styleRecalcs": 11,
"styleRecalcDurationMs": 8.206999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 324.573,
"heapDeltaBytes": 9101436,
"heapUsedBytes": 57409308,
"domNodes": 18,
"jsHeapTotalBytes": 14942208,
"scriptDurationMs": 57.28199999999999,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "dom-widget-clipping",
"durationMs": 518.6019999999871,
"styleRecalcs": 11,
"styleRecalcDurationMs": 8.105,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 323.299,
"heapDeltaBytes": 8760856,
"heapUsedBytes": 57414692,
"domNodes": 18,
"jsHeapTotalBytes": 15466496,
"scriptDurationMs": 57.235,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "large-graph-idle",
"durationMs": 2058.349000000021,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.9,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 541.015,
"heapDeltaBytes": 13611968,
"heapUsedBytes": 71996000,
"domNodes": -265,
"jsHeapTotalBytes": 290816,
"scriptDurationMs": 92.472,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2008.4140000000161,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.909999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 522.466,
"heapDeltaBytes": 9533924,
"heapUsedBytes": 67791636,
"domNodes": -265,
"jsHeapTotalBytes": 815104,
"scriptDurationMs": 83.195,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2010.6319999999869,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.813999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 508.71200000000005,
"heapDeltaBytes": 6748628,
"heapUsedBytes": 65294832,
"domNodes": -264,
"jsHeapTotalBytes": 290816,
"scriptDurationMs": 84.51299999999999,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2138.0809999999997,
"styleRecalcs": 68,
"styleRecalcDurationMs": 17.266999999999996,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1067.404,
"heapDeltaBytes": -1013928,
"heapUsedBytes": 58524480,
"domNodes": -267,
"jsHeapTotalBytes": 1282048,
"scriptDurationMs": 389.173,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2135.3320000000053,
"styleRecalcs": 69,
"styleRecalcDurationMs": 17.976999999999997,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1100.7659999999998,
"heapDeltaBytes": 9490524,
"heapUsedBytes": 68945252,
"domNodes": -260,
"jsHeapTotalBytes": 495616,
"scriptDurationMs": 398.714,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2130.4179999999633,
"styleRecalcs": 69,
"styleRecalcDurationMs": 18.495,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1058.095,
"heapDeltaBytes": 23309048,
"heapUsedBytes": 82672092,
"domNodes": -264,
"jsHeapTotalBytes": 5738496,
"scriptDurationMs": 390.779,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-zoom",
"durationMs": 3181.5460000000257,
"styleRecalcs": 65,
"styleRecalcDurationMs": 17.835,
"layouts": 60,
"layoutDurationMs": 7.444999999999999,
"taskDurationMs": 1342.0939999999998,
"heapDeltaBytes": 9080588,
"heapUsedBytes": 69749848,
"domNodes": -268,
"jsHeapTotalBytes": 552960,
"scriptDurationMs": 490.28399999999993,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3135.264999999947,
"styleRecalcs": 65,
"styleRecalcDurationMs": 17.996,
"layouts": 60,
"layoutDurationMs": 7.194000000000001,
"taskDurationMs": 1321.764,
"heapDeltaBytes": -6614280,
"heapUsedBytes": 54112604,
"domNodes": -269,
"jsHeapTotalBytes": 552960,
"scriptDurationMs": 487.08699999999993,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3134.3009999999367,
"styleRecalcs": 66,
"styleRecalcDurationMs": 18.027,
"layouts": 60,
"layoutDurationMs": 6.952,
"taskDurationMs": 1282.054,
"heapDeltaBytes": 8327504,
"heapUsedBytes": 69233740,
"domNodes": -266,
"jsHeapTotalBytes": 4747264,
"scriptDurationMs": 472.35699999999997,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2011.000000000024,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.841000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 515.355,
"heapDeltaBytes": 3812836,
"heapUsedBytes": 64158816,
"domNodes": -264,
"jsHeapTotalBytes": 552960,
"scriptDurationMs": 86.714,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "minimap-idle",
"durationMs": 2012.8290000000106,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.4719999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 510.72700000000003,
"heapDeltaBytes": 3549720,
"heapUsedBytes": 63248160,
"domNodes": -266,
"jsHeapTotalBytes": 552960,
"scriptDurationMs": 84.44399999999999,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2020.748000000026,
"styleRecalcs": 7,
"styleRecalcDurationMs": 6.0440000000000005,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 496.2199999999999,
"heapDeltaBytes": 6088064,
"heapUsedBytes": 66551016,
"domNodes": -267,
"jsHeapTotalBytes": 1077248,
"scriptDurationMs": 83.148,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 530.5890000000204,
"styleRecalcs": 46,
"styleRecalcDurationMs": 10.92,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 349.46099999999996,
"heapDeltaBytes": 8861292,
"heapUsedBytes": 57533488,
"domNodes": 18,
"jsHeapTotalBytes": 15466496,
"scriptDurationMs": 119.36099999999999,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 513.1980000000453,
"styleRecalcs": 47,
"styleRecalcDurationMs": 11.306000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 354.042,
"heapDeltaBytes": 9548396,
"heapUsedBytes": 58053444,
"domNodes": 19,
"jsHeapTotalBytes": 15728640,
"scriptDurationMs": 119.60200000000002,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 566.2740000000213,
"styleRecalcs": 45,
"styleRecalcDurationMs": 10.153999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 363.88800000000003,
"heapDeltaBytes": 9014028,
"heapUsedBytes": 57716724,
"domNodes": 16,
"jsHeapTotalBytes": 15728640,
"scriptDurationMs": 128.99699999999999,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 1999.7869999999978,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.562,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 364.043,
"heapDeltaBytes": -5216616,
"heapUsedBytes": 43326624,
"domNodes": 18,
"jsHeapTotalBytes": 14544896,
"scriptDurationMs": 13.492999999999999,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2000.7509999999797,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.461,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 359.182,
"heapDeltaBytes": -5215612,
"heapUsedBytes": 43347496,
"domNodes": 18,
"jsHeapTotalBytes": 15331328,
"scriptDurationMs": 13.423999999999998,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 1989.4299999999703,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.826,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 317.455,
"heapDeltaBytes": 22531752,
"heapUsedBytes": 71252420,
"domNodes": 18,
"jsHeapTotalBytes": 14942208,
"scriptDurationMs": 13.019000000000002,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1707.4029999999993,
"styleRecalcs": 74,
"styleRecalcDurationMs": 36.189,
"layouts": 16,
"layoutDurationMs": 4.252000000000001,
"taskDurationMs": 657.497,
"heapDeltaBytes": 14876660,
"heapUsedBytes": 63029448,
"domNodes": 60,
"jsHeapTotalBytes": 14680064,
"scriptDurationMs": 93.10300000000001,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1691.10500000005,
"styleRecalcs": 75,
"styleRecalcDurationMs": 35.458999999999996,
"layouts": 16,
"layoutDurationMs": 3.7819999999999996,
"taskDurationMs": 682.5269999999999,
"heapDeltaBytes": -1592524,
"heapUsedBytes": 47042040,
"domNodes": -261,
"jsHeapTotalBytes": 15069184,
"scriptDurationMs": 90.12799999999999,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1738.301999999976,
"styleRecalcs": 75,
"styleRecalcDurationMs": 36.790000000000006,
"layouts": 16,
"layoutDurationMs": 4.26,
"taskDurationMs": 685.263,
"heapDeltaBytes": -2491776,
"heapUsedBytes": 46350928,
"domNodes": -262,
"jsHeapTotalBytes": 14807040,
"scriptDurationMs": 92.308,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "viewport-pan-sweep",
"durationMs": 8199.689999999975,
"styleRecalcs": 251,
"styleRecalcDurationMs": 51.471000000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3894.0550000000003,
"heapDeltaBytes": 16679640,
"heapUsedBytes": 73806180,
"domNodes": -261,
"jsHeapTotalBytes": 8884224,
"scriptDurationMs": 1450.5140000000001,
"eventListeners": -113,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8164.13,
"styleRecalcs": 249,
"styleRecalcDurationMs": 50.358000000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3648.6749999999997,
"heapDeltaBytes": 10526848,
"heapUsedBytes": 68858916,
"domNodes": -263,
"jsHeapTotalBytes": 7311360,
"scriptDurationMs": 1243.3939999999998,
"eventListeners": -113,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8147.922999999992,
"styleRecalcs": 250,
"styleRecalcDurationMs": 50.932,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3690.5909999999994,
"heapDeltaBytes": 13997232,
"heapUsedBytes": 69856528,
"domNodes": -259,
"jsHeapTotalBytes": 8097792,
"scriptDurationMs": 1308.7949999999998,
"eventListeners": -113,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.80000000000109
},
{
"name": "vue-large-graph-idle",
"durationMs": 12284.96100000001,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12265.722999999998,
"heapDeltaBytes": -31206548,
"heapUsedBytes": 172713524,
"domNodes": -8331,
"jsHeapTotalBytes": 25751552,
"scriptDurationMs": 617.283,
"eventListeners": -16464,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 11825.895999999942,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 11810.464,
"heapDeltaBytes": -47163360,
"heapUsedBytes": 151366816,
"domNodes": -8331,
"jsHeapTotalBytes": 14741504,
"scriptDurationMs": 568.875,
"eventListeners": -16464,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 12027.885999999966,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12018.012000000002,
"heapDeltaBytes": -52742704,
"heapUsedBytes": 158591680,
"domNodes": -8331,
"jsHeapTotalBytes": -17502208,
"scriptDurationMs": 577.458,
"eventListeners": -16464,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 14491.194000000007,
"styleRecalcs": 67,
"styleRecalcDurationMs": 17.22900000000005,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14468.146,
"heapDeltaBytes": -42491232,
"heapUsedBytes": 152438448,
"domNodes": -8335,
"jsHeapTotalBytes": 61440,
"scriptDurationMs": 910.4340000000001,
"eventListeners": -16488,
"totalBlockingTimeMs": 66,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-pan",
"durationMs": 14607.94599999997,
"styleRecalcs": 69,
"styleRecalcDurationMs": 19.181999999999977,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14587.408,
"heapDeltaBytes": -21037952,
"heapUsedBytes": 174619312,
"domNodes": -8331,
"jsHeapTotalBytes": -3170304,
"scriptDurationMs": 885.288,
"eventListeners": -16488,
"totalBlockingTimeMs": 56,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-pan",
"durationMs": 14097.100999999951,
"styleRecalcs": 64,
"styleRecalcDurationMs": 15.514000000000028,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14074.882999999998,
"heapDeltaBytes": -18392152,
"heapUsedBytes": 191307336,
"domNodes": -8331,
"jsHeapTotalBytes": 25927680,
"scriptDurationMs": 854.9830000000001,
"eventListeners": -16464,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 456.3089999999761,
"styleRecalcs": 17,
"styleRecalcDurationMs": 25.498000000000005,
"layouts": 5,
"layoutDurationMs": 1.4150000000000003,
"taskDurationMs": 119.13099999999999,
"heapDeltaBytes": 5244648,
"heapUsedBytes": 55442008,
"domNodes": 164,
"jsHeapTotalBytes": 262144,
"scriptDurationMs": 24.586,
"eventListeners": 69,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "workflow-execution",
"durationMs": 447.4969999999985,
"styleRecalcs": 16,
"styleRecalcDurationMs": 21.753,
"layouts": 5,
"layoutDurationMs": 1.302,
"taskDurationMs": 111.34700000000001,
"heapDeltaBytes": 5014492,
"heapUsedBytes": 55304568,
"domNodes": 153,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 22.47199999999999,
"eventListeners": 69,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "workflow-execution",
"durationMs": 463.8809999999012,
"styleRecalcs": 16,
"styleRecalcDurationMs": 21.935,
"layouts": 5,
"layoutDurationMs": 1.2489999999999999,
"taskDurationMs": 117.61100000000002,
"heapDeltaBytes": 5045664,
"heapUsedBytes": 56144996,
"domNodes": 155,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 24.728,
"eventListeners": 69,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.799999999999727
}
]
} |
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
scripts/prepare-workflow-validation.js (1)
1-82:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftConvert this new build helper to TypeScript to satisfy repo policy.
This PR adds a new
.jsfile underscripts/, but this repository’s rules
disallow introducing JavaScript files.As per coding guidelines
**/*.js: Use TypeScript exclusively, no new JavaScript files.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/prepare-workflow-validation.js` around lines 1 - 82, The new script scripts/prepare-workflow-validation.js must be converted to TypeScript to comply with the repo policy: create scripts/prepare-workflow-validation.ts (remove the .js file), keep the same logic and identifiers (__dirname, repoRoot, packageDir, distDir, workspaceCatalog, resolveCatalog, distPackage), convert CommonJS-ish patterns to valid TS module code (import fs and path with proper types, type the resolveCatalog function signature and return values where useful), ensure JSON.parse and regex usages have correct typings, and update tsconfig.json (or project build includes) so scripts/*.ts is compiled or allowed; run the script build/path changes if any CI references the .js path.
🧹 Nitpick comments (1)
packages/workflow-validation/src/linkTopology.test.ts (1)
10-74: ⚡ Quick winCover the object-form link contract in this suite.
The new package surface includes both tuple and object link shapes, but every
validateLinkTopology()case here exercises only tuple links. Adding one object-form happy-path and one object-form failure case would keep that published branch from drifting untested.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/workflow-validation/src/linkTopology.test.ts` around lines 10 - 74, Tests only exercise tuple-form links; add two cases using object-form links to cover the new contract: (1) a happy-path where makeGraph receives links as objects (e.g., { id, originId, originSlot, targetId, targetSlot, type }) and assert validateLinkTopology(graph) returns [] and describeTopologyError works as expected; (2) a failure case (e.g., target-slot-out-of-bounds or missing-origin-node) constructed with an object-form link and assert the same error kind and payload shape as the tuple tests. Update linkTopology.test.ts to include these two new it(...) cases alongside the existing tuple tests, referencing validateLinkTopology, makeGraph, and describeTopologyError so the object-shape code paths are exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/workflow-validation/src/linkRepair.ts`:
- Around line 492-496: The code is calling splice with idx even when findIndex
returned -1, which removes the last element; update the block around idx (in
linkRepair.ts where you reference data.deletedLinks[i], idx, and graph.links) to
bail out when idx === -1: log the not-found condition (using logger.log or
logger.error) and skip the splice (e.g., continue or return) so you do not call
(graph.links as Array<unknown>).splice(idx, 1) with a negative index.
- Around line 113-116: The exported repairLinks function currently only accepts
SerialisedGraph but its body still branches on isLiveGraph() and handles
record-shaped graph.links, breaking callers that pass live graphs at compile
time; update the public signature to accept both SerialisedGraph and the live
graph type (e.g., SerialisedGraph | LiveGraph or the shared Graph union used
elsewhere) so callers of repairLinks and fixBadLinks compile, and ensure any
type narrowing inside the function still uses isLiveGraph(graph) before
accessing record-shaped graph.links.
In `@packages/workflow-validation/src/linkTopology.ts`:
- Around line 121-136: The origin-slot bounds check in validateLinkTopology()
bails out with continue after pushing an 'origin-slot-out-of-bounds' error,
which prevents also reporting 'target-slot-out-of-bounds' when both
link.originSlot and link.targetSlot are invalid; update the logic around the
checks that reference link.originSlot, outputs, target.inputs and
link.targetSlot to evaluate both bounds before bailing: test origin bounds and
push the origin-slot-out-of-bounds error if needed, test target bounds and push
the target-slot-out-of-bounds error if needed, then only continue if either
error was emitted so both errors can be recorded for the same link.
In `@scripts/generate-json-schema.ts`:
- Line 57: The final success message in scripts/generate-json-schema.ts
currently uses console.warn('JSON Schemas generated successfully!') which emits
to stderr; change that call to console.log('JSON Schemas generated
successfully!') so successful runs print to stdout instead of appearing as a
warning to CI and callers—update the lone logging statement in the script
accordingly.
In `@scripts/prepare-workflow-validation.js`:
- Around line 13-17: The regex that extracts the catalog into workspaceCatalog
fails when "catalog:" is the last top-level YAML block; update the match used in
scripts/prepare-workflow-validation.js so the capture for workspaceCatalog
allows the block to end at EOF as well as before the next top-level key (e.g.,
use a terminating condition that matches either a following non-indented key or
end-of-file). Change the match expression that assigns workspaceCatalog (the
.match(...) call) to use a pattern that accepts end-of-file (for example by
replacing the final `\n\S` requirement with a non-capturing group that allows
`\n\S` OR end-of-input) so resolveCatalog(...) receives the actual block
content.
In `@src/platform/workflow/validation/composables/useWorkflowValidation.ts`:
- Around line 82-96: The catch block in useWorkflowValidation (handling
repairLinks) currently swallows all exceptions and returns { graph: graphData,
aborted: true }; change it so only LinkRepairAbortedError is handled/converted
to an aborted result (with the existing toastStore and console.error behavior),
while any other unexpected error is re-thrown to propagate the failure to
callers (do not return aborted for non-LinkRepairAbortedError). Ensure you
reference the same symbols (LinkRepairAbortedError, toastStore, graphData,
useWorkflowValidation) and preserve existing logging for the known abort case
before re-throwing other errors.
- Around line 22-29: Replace all hard-coded user-facing strings in
useWorkflowValidation.ts with vue-i18n keys: call useI18n() in the composable
and use t('...') for the overflow text in summariseTopologyErrors (currently
using TOPOLOGY_TOAST_LIMIT), the error count message in reportTopology, and the
toast summary/detail strings used around reportTopology and
describeTopologyError; add corresponding keys and English text to
src/locales/en/main.json (e.g. topology.toast.overflow,
topology.toast.errorCount, topology.toast.summary.*, topology.toast.detail.*) so
the messages are routed through i18n rather than embedded literals.
---
Outside diff comments:
In `@scripts/prepare-workflow-validation.js`:
- Around line 1-82: The new script scripts/prepare-workflow-validation.js must
be converted to TypeScript to comply with the repo policy: create
scripts/prepare-workflow-validation.ts (remove the .js file), keep the same
logic and identifiers (__dirname, repoRoot, packageDir, distDir,
workspaceCatalog, resolveCatalog, distPackage), convert CommonJS-ish patterns to
valid TS module code (import fs and path with proper types, type the
resolveCatalog function signature and return values where useful), ensure
JSON.parse and regex usages have correct typings, and update tsconfig.json (or
project build includes) so scripts/*.ts is compiled or allowed; run the script
build/path changes if any CI references the .js path.
---
Nitpick comments:
In `@packages/workflow-validation/src/linkTopology.test.ts`:
- Around line 10-74: Tests only exercise tuple-form links; add two cases using
object-form links to cover the new contract: (1) a happy-path where makeGraph
receives links as objects (e.g., { id, originId, originSlot, targetId,
targetSlot, type }) and assert validateLinkTopology(graph) returns [] and
describeTopologyError works as expected; (2) a failure case (e.g.,
target-slot-out-of-bounds or missing-origin-node) constructed with an
object-form link and assert the same error kind and payload shape as the tuple
tests. Update linkTopology.test.ts to include these two new it(...) cases
alongside the existing tuple tests, referencing validateLinkTopology, makeGraph,
and describeTopologyError so the object-shape code paths are exercised.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 3ac522b9-cb22-4d81-ad6f-0b40672d8379
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (18)
.gitignoreeslint.config.tspackage.jsonpackages/workflow-validation/package.jsonpackages/workflow-validation/src/index.tspackages/workflow-validation/src/linkRepair.tspackages/workflow-validation/src/linkTopology.test.tspackages/workflow-validation/src/linkTopology.tspackages/workflow-validation/src/serialised.tspackages/workflow-validation/src/workflowSchema.tspackages/workflow-validation/tsconfig.jsonpackages/workflow-validation/vite.config.mtsscripts/generate-json-schema.tsscripts/prepare-workflow-validation.jssrc/platform/workflow/validation/composables/useWorkflowValidation.tssrc/utils/linkFixer.test.tstsconfig.jsonvite.config.mts
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #11864 +/- ##
===========================================
- Coverage 71.13% 55.94% -15.19%
===========================================
Files 1490 1384 -106
Lines 86009 70722 -15287
Branches 23344 19702 -3642
===========================================
- Hits 61182 39566 -21616
- Misses 23677 30629 +6952
+ Partials 1150 527 -623
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 999 files with indirect coverage changes 🚀 New features to boost your workflow:
|
- linkRepair: `continue` on `idx === -1` so we don't `splice(-1, 1)` and clobber the last unrelated link. - linkTopology: report origin- and target-slot-out-of-bounds errors independently for the same link instead of bailing after the first. - useWorkflowValidation: re-throw non-`LinkRepairAbortedError` failures so unrelated bugs don't get silently downgraded to an aborted load. - useWorkflowValidation: route every toast string through `vue-i18n` (`validation.topology.*` keys in `src/locales/en/main.json`). - Convert `scripts/prepare-workflow-validation.js` to TypeScript per repo policy and run it via `tsx`. Make the catalog regex robust by appending a synthetic terminator instead of changing the existing `\\n\\S` semantics, which broke for the multiline catalog block. - Add object-form link tests covering the both-shapes contract.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/platform/workflow/validation/composables/useWorkflowValidation.ts (1)
27-38:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winLocalize the topology detail text too.
Line 30 feeds
describeTopologyError()directly into the toast body, and Line 99 surfaceserr.messagefrom the shared package. That means non-English locales will still get English detail text even though the surrounding toast chrome now goes throughvue-i18n. MapTopologyError.kindand the abort case to local keys in this composable instead of rendering library strings directly.As per coding guidelines, "Use vue-i18n for ALL user-facing strings" and "Provide user-friendly and actionable error messages".
Also applies to: 96-100
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/platform/workflow/validation/composables/useWorkflowValidation.ts`:
- Around line 63-70: The code calls repairLinks(graphData as SerialisedGraph,
...) which can mutate its input, but validateWorkflow() later treats that same
graph as the untouched fallback; to fix, create a clone of the serialised graph
(e.g., via a deep-clone utility or structuredClone/JSON deep copy) and pass the
clone into repairLinks so the original graphData remains intact for the
abort/fallback path; update the call site in useWorkflowValidation.ts (the
repairLinks invocation) to use the cloned SerialisedGraph.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2a78249c-84a5-42f2-a83e-4209aca47099
📒 Files selected for processing (7)
packages/workflow-validation/package.jsonpackages/workflow-validation/src/linkRepair.tspackages/workflow-validation/src/linkTopology.test.tspackages/workflow-validation/src/linkTopology.tsscripts/prepare-workflow-validation.tssrc/locales/en/main.jsonsrc/platform/workflow/validation/composables/useWorkflowValidation.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/workflow-validation/src/linkTopology.test.ts
- packages/workflow-validation/src/linkTopology.ts
- packages/workflow-validation/src/linkRepair.ts
…lback repairLinks() mutates its input as it patches links, and can throw LinkRepairAbortedError after partial mutation. The caller relies on the original graphData being untouched as the fallback when validateWorkflow() returns null on abort. Clone via structuredClone before passing to repairLinks so the fallback contract holds. Addresses review feedback: #11864 (comment)
…il strings - Clone the validated graph before passing it to `repairLinks`. Repair is mutating, and after the new re-throw path, an aborted repair would leave the caller's graph half-mutated even though `validateWorkflow` returns `null` to signal fallback. - Render every `TopologyError` via `vue-i18n` keys (`validation.topology.tuple` plus per-kind templates) in the composable, including the abort toast detail and the per-link summary lines. The package keeps `describeTopologyError` as the structured English form for console logs, CI scripts, and the future backend validator.
0155a00 to
ea2e0fe
Compare
…ok config Storybook's vite config does not inherit aliases from vite.config.mts. Add the @/utils/linkFixer and @/platform/workflow/validation/schemas/workflowSchema redirects that the package extraction relies on, matching the pattern already used for shared-frontend-utils. Fixes the storybook-build CI failure on this branch: https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/25272069987/job/74096058783
PR Created by the Glary-Bot Agent
Addresses the broken seedance template thread. Three architectural changes plus a package extraction.
What changed
1. Better linkFixer errors. The four
'Error. Expected node to match patched data.'invariant throws are replaced withLinkRepairAbortedError, which carries a structuredTopologyErrordescribing the offending[linkId, src, srcSlot, tgt, tgtSlot]. Next time a template likeapi_seedance2_0_r2v_real_human.jsonships with links pointing into thin air, the message names the link instead of looking like an internal bug.2. Topology validation split out from repair. A new pure
validateLinkTopology(graph): TopologyError[]walks the link table and reports out-of-bounds slots, missing nodes, and endpoint mismatches as a structured union. Catches the seedance breakage (links targeting slots that don't exist on the node) which the schema cannot detect because cross-references aren't expressible in zod tuples.repairLinks()(the renamedfixBadLinks) keeps the existing rgthree-derived auto-repair algorithm; both are independently callable.3. Topology errors surface in the UI when ValidateWorkflows is on. Today they're swallowed by
console.error. NowuseWorkflowValidationraises a single warn toast listing up to five offenders (the rest go to the console) when the setting is enabled, plus an error toast on unrepairable workflows.validateWorkflowreturnsnullon abort so the caller falls back to the original graph.4. New
@comfyorg/workflow-validationnx package underpackages/. Owns the workflow zod schemas, topology validator, repair, and minimal serialised JSON shapes. Has zero litegraph coupling so a follow-up PR can wireComfy-Org/workflow_templatesCI to consume the published package and validate every template's topology before merge — the second half of Christian's "let me think about how to put this into CI."Migration uses tsconfig + vite path aliases mirroring the existing
@/utils/formatUtilprecedent, so the 76 importers ofworkflowSchemaand the lone importer oflinkFixercontinue to compile without touching any of them.Verification
pnpm typecheckcleanpnpm lintclean (1 pre-existing unrelated warning inuseWorkspaceBilling.test.ts)pnpm knipcleanpnpm test:unit9890/9890 passing including newvalidateLinkTopologytests with a seedance-style fixturepnpm --dir packages/workflow-validation run buildproducesdist/index.js+ per-file.d.tsplus a publish-readydist/package.jsonDeferred follow-ups
release-npm-workflow-validation.yaml) was authored but had to be dropped from this PR because the bot pushing the branch lacks the GitHub Appworkflowspermission. A maintainer can add it in a follow-up — it's a near-clone of the existingrelease-npm-types.yaml.Comfy-Org/workflow_templatesto consume it for CI validation will follow.Architecture notes
Per Oracle review, the package owns the plain serialised workflow contract:
SerialisedGraph,SerialisedNode, link tuple/object shapes are defined locally rather than imported from@/lib/litegraph/.... This keeps the package free of frontend coupling, so Node CI scripts and a future backend validator can use the same validator that runs in the UI.┆Issue is synchronized with this Notion page by Unito