@work-graph/cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/README.md +31 -0
  2. package/bin/work-graph.mjs +238 -0
  3. package/package.json +38 -0
  4. package/vendor/packages/design-tokens/generated/gripe-dark-default.css +67 -0
  5. package/vendor/packages/design-tokens/generated/marketplace-default.css +67 -0
  6. package/vendor/packages/design-tokens/generated/workgraph-dark.css +67 -0
  7. package/vendor/packages/workgraph-mcp/README.md +28 -0
  8. package/vendor/packages/workgraph-mcp/bin/workgraph-mcp.mjs +21 -0
  9. package/vendor/packages/workgraph-mcp/package.json +37 -0
  10. package/vendor/packages/workgraph-mcp/src/handlers.mjs +761 -0
  11. package/vendor/packages/workgraph-mcp/src/index.mjs +638 -0
  12. package/vendor/packages/workgraph-mcp/src/prompts.mjs +162 -0
  13. package/vendor/public/assets/workgraph-logo.svg +11 -0
  14. package/vendor/public/fonts/GraphikLCG/GraphikLCG-Medium.woff2 +0 -0
  15. package/vendor/public/fonts/GraphikLCG/GraphikLCG-Regular.woff2 +0 -0
  16. package/vendor/public/fonts/GraphikLCG/GraphikLCG-Semibold.woff2 +0 -0
  17. package/vendor/public/fonts/GraphikLCG/stylesheet.css +25 -0
  18. package/vendor/public/graph-canvas-lit-flow.css +154 -0
  19. package/vendor/public/graph-canvas-lit-flow.css.map +7 -0
  20. package/vendor/public/graph-canvas-lit-flow.js +8530 -0
  21. package/vendor/public/graph-canvas-lit-flow.js.map +7 -0
  22. package/vendor/src/agentBehaviorRulesAudit.mjs +168 -0
  23. package/vendor/src/agentBehaviorRulesBundle.mjs +144 -0
  24. package/vendor/src/agentRunApi.mjs +136 -0
  25. package/vendor/src/agentToolLoopGuard.mjs +88 -0
  26. package/vendor/src/agentWorkerClaudeProvider.mjs +288 -0
  27. package/vendor/src/agentWorkerCursorSdkProvider.mjs +156 -0
  28. package/vendor/src/agentWorkerLiveLoop.mjs +455 -0
  29. package/vendor/src/agentWorkerLocalCliProvider.mjs +217 -0
  30. package/vendor/src/agentWorkerLocalRunner.mjs +246 -0
  31. package/vendor/src/agentWorkerOpenAiProvider.mjs +459 -0
  32. package/vendor/src/analyticsPanelProjection.mjs +212 -0
  33. package/vendor/src/analyticsRecordStore.mjs +165 -0
  34. package/vendor/src/analyticsRecordWorkItems.mjs +104 -0
  35. package/vendor/src/architectureL1Canon.mjs +419 -0
  36. package/vendor/src/architectureLayout.mjs +229 -0
  37. package/vendor/src/architectureSnapshot.mjs +490 -0
  38. package/vendor/src/architectureViewsProjection.mjs +116 -0
  39. package/vendor/src/atomInspector.mjs +253 -0
  40. package/vendor/src/atomInspectorApi.mjs +130 -0
  41. package/vendor/src/auditGapMatrixRefresh.mjs +121 -0
  42. package/vendor/src/backlogSchemaLint.mjs +176 -0
  43. package/vendor/src/blockedOnebaseGoPreflightEval.mjs +100 -0
  44. package/vendor/src/bracketIrTraceSignal.mjs +93 -0
  45. package/vendor/src/bvcAtomParser.mjs +210 -0
  46. package/vendor/src/bvcDialectRegistry.mjs +86 -0
  47. package/vendor/src/bvcFileFormat.mjs +218 -0
  48. package/vendor/src/bvcFormatCli.mjs +55 -0
  49. package/vendor/src/bvcLintCli.mjs +48 -0
  50. package/vendor/src/bvcNewWritePolicy.mjs +70 -0
  51. package/vendor/src/charterPreflightPromoteGate.mjs +194 -0
  52. package/vendor/src/claimNoEligibleEval.mjs +205 -0
  53. package/vendor/src/closingAnalysisSuggest.mjs +59 -0
  54. package/vendor/src/codeGapAnalyzer.mjs +308 -0
  55. package/vendor/src/codeGapBacklogFeeder.mjs +82 -0
  56. package/vendor/src/codeGapDraftIntakeApi.mjs +307 -0
  57. package/vendor/src/codeGapOperatorProjection.mjs +60 -0
  58. package/vendor/src/codeSyntaxHighlight.mjs +123 -0
  59. package/vendor/src/codegenEvidence.mjs +187 -0
  60. package/vendor/src/compilerRoundTripCli.mjs +164 -0
  61. package/vendor/src/dagreGraphLayout.mjs +78 -0
  62. package/vendor/src/draftIntakePromotionRules.mjs +205 -0
  63. package/vendor/src/epicWorkScope.mjs +85 -0
  64. package/vendor/src/evalLiveLlmEnv.mjs +63 -0
  65. package/vendor/src/evidenceReadModel.mjs +167 -0
  66. package/vendor/src/gfsOverlayProjectPassport.mjs +235 -0
  67. package/vendor/src/globalStepPathToBvcReferences.mjs +196 -0
  68. package/vendor/src/goldenPath.mjs +69 -0
  69. package/vendor/src/graphCanvasLayout.mjs +464 -0
  70. package/vendor/src/graphCanvasLitFlow/client/graphCanvasMinimap.ts +261 -0
  71. package/vendor/src/graphCanvasLitFlow/client/graphCanvasSvgEdges.ts +259 -0
  72. package/vendor/src/graphCanvasLitFlow/client/graphCanvasTheme.css +152 -0
  73. package/vendor/src/graphCanvasLitFlow/client/graphCardNode.ts +328 -0
  74. package/vendor/src/graphCanvasLitFlow/client/mountGraphCanvasLitFlow.ts +322 -0
  75. package/vendor/src/graphCanvasLitFlow/graphCanvasEdgeLabels.mjs +58 -0
  76. package/vendor/src/graphCanvasLitFlow/graphCanvasEdgeRouter.mjs +142 -0
  77. package/vendor/src/graphCanvasLitFlow/graphCanvasLayoutProfile.mjs +32 -0
  78. package/vendor/src/graphCanvasLitFlow/graphCanvasNodeMetrics.mjs +45 -0
  79. package/vendor/src/graphCanvasLitFlow/graphCanvasProjection.mjs +115 -0
  80. package/vendor/src/graphCanvasLitFlow/graphCanvasProjectionToFlow.mjs +133 -0
  81. package/vendor/src/graphCanvasLitFlow/graphCanvasTraversal.mjs +77 -0
  82. package/vendor/src/graphCanvasLitFlow/layoutIntentRoadmapWorkStack.mjs +73 -0
  83. package/vendor/src/graphCanvasLitFlow/resolveGraphCanvasOverlaps.mjs +77 -0
  84. package/vendor/src/graphRagContextSlice.mjs +461 -0
  85. package/vendor/src/gvmVerifyWorkerGate.mjs +95 -0
  86. package/vendor/src/homeSnapshotApi.mjs +131 -0
  87. package/vendor/src/homeSnapshotProjection.mjs +275 -0
  88. package/vendor/src/inboxEventStream.mjs +140 -0
  89. package/vendor/src/intentComposerApi.mjs +245 -0
  90. package/vendor/src/intentGraphGbcSliceBoundary.mjs +258 -0
  91. package/vendor/src/intentGraphProjection.mjs +208 -0
  92. package/vendor/src/intentHierarchy.mjs +241 -0
  93. package/vendor/src/intentNodeLint.mjs +107 -0
  94. package/vendor/src/intentNodeRuntime.mjs +185 -0
  95. package/vendor/src/intentRoadmapCanvas.mjs +393 -0
  96. package/vendor/src/intentRoadmapEpicProjection.mjs +122 -0
  97. package/vendor/src/intentRoadmapMermaid.mjs +165 -0
  98. package/vendor/src/intentRoadmapProjection.mjs +85 -0
  99. package/vendor/src/intentTreeLint.mjs +114 -0
  100. package/vendor/src/intentTreeMigration.mjs +150 -0
  101. package/vendor/src/intentTreeWorkItems.mjs +227 -0
  102. package/vendor/src/kanbanBoardProjection.mjs +58 -0
  103. package/vendor/src/languageAdapterRegistry.mjs +180 -0
  104. package/vendor/src/languageAdapters/goAdapter.mjs +62 -0
  105. package/vendor/src/languageAdapters/jsTsAdapter.mjs +60 -0
  106. package/vendor/src/languageAdapters/jsonYamlAdapter.mjs +103 -0
  107. package/vendor/src/languageAdapters/onebaseOsAdapter.mjs +55 -0
  108. package/vendor/src/languageAdapters/plaintextAdapter.mjs +36 -0
  109. package/vendor/src/languageAdapters/shared.mjs +68 -0
  110. package/vendor/src/languageAdapters/stepAdapter.mjs +81 -0
  111. package/vendor/src/lintPlanWorkAlignment.mjs +136 -0
  112. package/vendor/src/loopHintRepeatToolEval.mjs +153 -0
  113. package/vendor/src/lowcodeScaffoldCli.mjs +386 -0
  114. package/vendor/src/markdownDocumentRender.mjs +208 -0
  115. package/vendor/src/memoryPanelProjection.mjs +116 -0
  116. package/vendor/src/memoryRecordWriter.mjs +243 -0
  117. package/vendor/src/memoryWorkerSlice.mjs +238 -0
  118. package/vendor/src/migrateStepToBvc.mjs +133 -0
  119. package/vendor/src/missionControlServerHandlers.mjs +195 -0
  120. package/vendor/src/missionControlUiClient.mjs +278 -0
  121. package/vendor/src/onebaseCliCapabilityProbe.mjs +107 -0
  122. package/vendor/src/onebaseCliRunner.mjs +145 -0
  123. package/vendor/src/onebaseGrossProfitStaticVerify.mjs +98 -0
  124. package/vendor/src/onebaseParityEvidenceSync.mjs +88 -0
  125. package/vendor/src/onebasePvrgGraphNodes.mjs +257 -0
  126. package/vendor/src/onebaseRestEvidenceAdapter.mjs +216 -0
  127. package/vendor/src/onebaseVectorDslCodegenReadiness.mjs +137 -0
  128. package/vendor/src/onebaseWorkItemTemplate.mjs +154 -0
  129. package/vendor/src/onebaseWorkerTools.mjs +586 -0
  130. package/vendor/src/operatorShellProjection.mjs +102 -0
  131. package/vendor/src/pipelineProseRender.mjs +180 -0
  132. package/vendor/src/pipelineStageLint.mjs +118 -0
  133. package/vendor/src/promptRulesEditorApi.mjs +174 -0
  134. package/vendor/src/promptRulesProjection.mjs +134 -0
  135. package/vendor/src/pvrg/bladeAdapter.mjs +40 -0
  136. package/vendor/src/pvrgTaskScope.mjs +152 -0
  137. package/vendor/src/releaseGateMatrix.mjs +188 -0
  138. package/vendor/src/schematicView.mjs +305 -0
  139. package/vendor/src/seedAnalyticsRecord.mjs +217 -0
  140. package/vendor/src/semanticSearchBm25.mjs +103 -0
  141. package/vendor/src/semanticSearchExcerpts.mjs +68 -0
  142. package/vendor/src/semanticSearchTfidfVector.mjs +86 -0
  143. package/vendor/src/semanticSearchWorkflow.mjs +366 -0
  144. package/vendor/src/stepAtomFormatter.mjs +413 -0
  145. package/vendor/src/stepGraphSlice.mjs +318 -0
  146. package/vendor/src/ui/atoms/badge.mjs +40 -0
  147. package/vendor/src/ui/atoms/badgeClient.mjs +32 -0
  148. package/vendor/src/ui/atoms/button.mjs +114 -0
  149. package/vendor/src/ui/atoms/buttonClient.mjs +49 -0
  150. package/vendor/src/ui/atoms/icon.mjs +23 -0
  151. package/vendor/src/ui/atoms/input.mjs +38 -0
  152. package/vendor/src/ui/atoms/modal.mjs +44 -0
  153. package/vendor/src/ui/atoms/select.mjs +98 -0
  154. package/vendor/src/ui/backlogShellButtons.mjs +238 -0
  155. package/vendor/src/ui/htmlEscape.mjs +11 -0
  156. package/vendor/src/ui/molecules/rating.mjs +48 -0
  157. package/vendor/src/ui/molecules/tabs.mjs +70 -0
  158. package/vendor/src/ui/organisms/modal.mjs +1 -0
  159. package/vendor/src/ui/pages/uiKitPage.mjs +147 -0
  160. package/vendor/src/ui/workItemStatusTone.mjs +36 -0
  161. package/vendor/src/unifiedLinkageProjection.mjs +264 -0
  162. package/vendor/src/verificationLoop.mjs +206 -0
  163. package/vendor/src/workGraphBacklogPersist.mjs +234 -0
  164. package/vendor/src/workGraphBacklogUiServer.mjs +9192 -0
  165. package/vendor/src/workGraphBoundedTargetFileRead.mjs +178 -0
  166. package/vendor/src/workGraphCycleSlice.mjs +184 -0
  167. package/vendor/src/workGraphDaemonTick.mjs +307 -0
  168. package/vendor/src/workGraphDaemonWatch.mjs +157 -0
  169. package/vendor/src/workGraphEngineRoot.mjs +136 -0
  170. package/vendor/src/workGraphInstallLayout.mjs +65 -0
  171. package/vendor/src/workGraphLlmUsefulnessEval.mjs +611 -0
  172. package/vendor/src/workGraphPhasePromoteReadyQueue.mjs +159 -0
  173. package/vendor/src/workGraphProjectHost.mjs +149 -0
  174. package/vendor/src/workGraphProjectInit.mjs +392 -0
  175. package/vendor/src/workGraphPromoteReadyApi.mjs +115 -0
  176. package/vendor/src/workGraphRecoveryPolicy.mjs +124 -0
  177. package/vendor/src/workGraphRunnerQueueProjection.mjs +187 -0
  178. package/vendor/src/workGraphRuntime.mjs +1008 -0
  179. package/vendor/src/workGraphToolSurfaceAudit.mjs +372 -0
  180. package/vendor/src/workGraphToolTransportRuntime.mjs +195 -0
  181. package/vendor/src/workGraphWorkerProvider.mjs +600 -0
  182. package/vendor/src/workItemBvcQuality.mjs +262 -0
  183. package/vendor/src/workItemCreateAnalysis.mjs +157 -0
  184. package/vendor/src/workItemDecisionPipeline.mjs +278 -0
  185. package/vendor/src/workItemEpicCascade.mjs +176 -0
  186. package/vendor/src/workItemExecutionGate.mjs +78 -0
  187. package/vendor/src/workItemHierarchy.mjs +226 -0
  188. package/vendor/src/workItemProseLint.mjs +133 -0
  189. package/vendor/src/workItemTextRusify.mjs +794 -0
  190. package/vendor/src/workItemTraceEnvelope.mjs +158 -0
  191. package/vendor/src/workItemUiReferences.mjs +272 -0
  192. package/vendor/src/workflowEpicGrouping.mjs +67 -0
  193. package/vendor/src/workflowTreeProjection.mjs +53 -0
  194. package/vendor/src/workspaceRegistry.mjs +150 -0
@@ -0,0 +1,464 @@
1
+ /** @typedef {'layered-dag-v1' | 'pipeline-v1'} LayoutProfileKind */
2
+
3
+ /**
4
+ * @typedef {{
5
+ * profile: LayoutProfileKind,
6
+ * ranks?: Record<string, number>,
7
+ * manualOverrides?: Record<string, { col?: number, row?: number, colSpan?: number }>,
8
+ * pipelineNodeIds?: string[],
9
+ * }} LayoutProfile
10
+ */
11
+
12
+ /**
13
+ * @typedef {{
14
+ * nodeWidth?: number,
15
+ * nodeMinHeight?: number,
16
+ * colGap?: number,
17
+ * rowGap?: number,
18
+ * offsetX?: number,
19
+ * offsetY?: number,
20
+ * }} GridLayoutConfig
21
+ */
22
+
23
+ export const LAYOUT_PROFILE_LAYERED_DAG_V1 = 'layered-dag-v1';
24
+ export const LAYOUT_PROFILE_PIPELINE_V1 = 'pipeline-v1';
25
+
26
+ export const GRAPH_CANVAS_VIEW_FULL = 'full';
27
+ export const GRAPH_CANVAS_VIEW_PIPELINE = 'pipeline';
28
+
29
+ const compareText = (left, right) => String(left).localeCompare(String(right), 'en', { sensitivity: 'variant' });
30
+
31
+ /** @type {LayoutProfile} */
32
+ export const ARCHITECTURE_LAYOUT_PROFILE = {
33
+ profile: LAYOUT_PROFILE_LAYERED_DAG_V1,
34
+ ranks: {
35
+ 'step-canon': 0,
36
+ 'work-graph': 0,
37
+ 'agent-runtime': 0,
38
+ 'trace-evidence': 0,
39
+ 'project-memory': 0,
40
+ 'domains': 1,
41
+ 'derived-projections': 1,
42
+ },
43
+ manualOverrides: {
44
+ 'step-canon': { col: 0, row: 0 },
45
+ 'work-graph': { col: 1, row: 0 },
46
+ 'agent-runtime': { col: 2, row: 0 },
47
+ 'trace-evidence': { col: 3, row: 0 },
48
+ 'project-memory': { col: 4, row: 0 },
49
+ 'domains': { col: 1, row: 1, colSpan: 2 },
50
+ 'derived-projections': { col: 3, row: 1 },
51
+ },
52
+ };
53
+
54
+ /** @type {LayoutProfile} */
55
+ export const ARCHITECTURE_PIPELINE_LAYOUT_PROFILE = {
56
+ profile: LAYOUT_PROFILE_PIPELINE_V1,
57
+ pipelineNodeIds: [
58
+ 'step-canon',
59
+ 'work-graph',
60
+ 'agent-runtime',
61
+ 'trace-evidence',
62
+ 'project-memory',
63
+ ],
64
+ };
65
+
66
+ /** @type {LayoutProfile} */
67
+ export const SCHEMATIC_LAYOUT_PROFILE = {
68
+ profile: LAYOUT_PROFILE_LAYERED_DAG_V1,
69
+ ranks: {
70
+ 'intent-tree': 0,
71
+ 'work-graph': 0,
72
+ 'runner': 0,
73
+ 'evidence': 0,
74
+ 'memory': 1,
75
+ 'graph-rag': 1,
76
+ 'ui': 1,
77
+ 'domains': 1,
78
+ 'storage': 2,
79
+ },
80
+ manualOverrides: {
81
+ 'intent-tree': { col: 0, row: 0 },
82
+ 'work-graph': { col: 1, row: 0 },
83
+ 'runner': { col: 2, row: 0 },
84
+ 'evidence': { col: 3, row: 0 },
85
+ 'memory': { col: 0, row: 1 },
86
+ 'graph-rag': { col: 1, row: 1 },
87
+ 'ui': { col: 2, row: 1 },
88
+ 'domains': { col: 3, row: 1, colSpan: 2 },
89
+ 'storage': { col: 1, row: 2, colSpan: 2 },
90
+ },
91
+ };
92
+
93
+ /** @type {LayoutProfile} */
94
+ export const SCHEMATIC_PIPELINE_LAYOUT_PROFILE = {
95
+ profile: LAYOUT_PROFILE_PIPELINE_V1,
96
+ pipelineNodeIds: [
97
+ 'intent-tree',
98
+ 'work-graph',
99
+ 'runner',
100
+ 'evidence',
101
+ 'memory',
102
+ 'graph-rag',
103
+ 'ui',
104
+ ],
105
+ };
106
+
107
+ /**
108
+ * @param {unknown} profile
109
+ * @returns {LayoutProfile}
110
+ */
111
+ export function normalizeLayoutProfile(profile) {
112
+ if (!profile || typeof profile !== 'object') {
113
+ throw new TypeError('layoutProfile must be an object');
114
+ }
115
+
116
+ const candidate = /** @type {LayoutProfile} */ (profile);
117
+ if (candidate.profile !== LAYOUT_PROFILE_LAYERED_DAG_V1 && candidate.profile !== LAYOUT_PROFILE_PIPELINE_V1) {
118
+ throw new RangeError(`Unknown layout profile: ${String(candidate.profile)}`);
119
+ }
120
+
121
+ if (candidate.profile === LAYOUT_PROFILE_PIPELINE_V1) {
122
+ const pipelineNodeIds = [...(candidate.pipelineNodeIds ?? [])].map(String).filter(Boolean);
123
+ if (pipelineNodeIds.length === 0) {
124
+ throw new RangeError('pipeline-v1 layoutProfile requires pipelineNodeIds');
125
+ }
126
+ return {
127
+ profile: LAYOUT_PROFILE_PIPELINE_V1,
128
+ pipelineNodeIds,
129
+ };
130
+ }
131
+
132
+ return {
133
+ profile: LAYOUT_PROFILE_LAYERED_DAG_V1,
134
+ ranks: candidate.ranks ? { ...candidate.ranks } : {},
135
+ manualOverrides: candidate.manualOverrides ? { ...candidate.manualOverrides } : {},
136
+ };
137
+ }
138
+
139
+ /**
140
+ * @param {string} viewMode
141
+ * @param {{ full?: LayoutProfile, pipeline?: LayoutProfile }} profiles
142
+ * @returns {LayoutProfile}
143
+ */
144
+ export function resolveLayoutProfileForViewMode(viewMode, profiles) {
145
+ if (viewMode === GRAPH_CANVAS_VIEW_PIPELINE) {
146
+ return normalizeLayoutProfile(profiles.pipeline);
147
+ }
148
+ return normalizeLayoutProfile(profiles.full);
149
+ }
150
+
151
+ /**
152
+ * @param {{ nodeIds: string[], edges: Array<{ from: string, to: string, upstream?: boolean }>, layoutProfile: LayoutProfile }} input
153
+ * @returns {Map<string, { col: number, row: number, colSpan?: number }>}
154
+ */
155
+ export function computeNodeSlots({ nodeIds, edges, layoutProfile }) {
156
+ const profile = normalizeLayoutProfile(layoutProfile);
157
+ const slots = new Map();
158
+
159
+ if (profile.profile === LAYOUT_PROFILE_PIPELINE_V1) {
160
+ const orderedIds = profile.pipelineNodeIds.filter((id) => nodeIds.includes(id));
161
+ orderedIds.forEach((id, index) => {
162
+ slots.set(id, { col: index, row: 0, colSpan: 1 });
163
+ });
164
+ for (const id of nodeIds) {
165
+ if (!slots.has(id)) {
166
+ slots.set(id, { col: orderedIds.length, row: 0, colSpan: 1 });
167
+ }
168
+ }
169
+ return slots;
170
+ }
171
+
172
+ const forwardEdges = edges.filter((edge) => !edge.upstream);
173
+ const rankById = new Map(nodeIds.map((id) => [id, profile.ranks?.[id] ?? null]));
174
+
175
+ for (const id of nodeIds) {
176
+ if (rankById.get(id) === null || rankById.get(id) === undefined) {
177
+ rankById.set(id, inferRankFromForwardEdges(id, forwardEdges, rankById, nodeIds));
178
+ }
179
+ }
180
+
181
+ const nodesByRow = new Map();
182
+ for (const id of [...nodeIds].sort(compareText)) {
183
+ const override = profile.manualOverrides?.[id];
184
+ if (override && Number.isInteger(override.row) && Number.isInteger(override.col)) {
185
+ slots.set(id, {
186
+ col: override.col,
187
+ row: override.row,
188
+ colSpan: override.colSpan ?? 1,
189
+ });
190
+ continue;
191
+ }
192
+
193
+ const row = rankById.get(id) ?? 0;
194
+ const rowNodes = nodesByRow.get(row) ?? [];
195
+ rowNodes.push(id);
196
+ nodesByRow.set(row, rowNodes);
197
+ }
198
+
199
+ for (const [row, ids] of nodesByRow.entries()) {
200
+ ids.sort(compareText).forEach((id, index) => {
201
+ if (slots.has(id)) {
202
+ return;
203
+ }
204
+ const override = profile.manualOverrides?.[id];
205
+ slots.set(id, {
206
+ col: Number.isInteger(override?.col) ? override.col : index,
207
+ row,
208
+ colSpan: override?.colSpan ?? 1,
209
+ });
210
+ });
211
+ }
212
+
213
+ for (const id of nodeIds) {
214
+ if (!slots.has(id)) {
215
+ slots.set(id, { col: 0, row: 0, colSpan: 1 });
216
+ }
217
+ }
218
+
219
+ return slots;
220
+ }
221
+
222
+ /**
223
+ * @param {string} nodeId
224
+ * @param {Array<{ from: string, to: string }>} forwardEdges
225
+ * @param {Map<string, number | null>} rankById
226
+ * @param {string[]} nodeIds
227
+ */
228
+ function inferRankFromForwardEdges(nodeId, forwardEdges, rankById, nodeIds) {
229
+ const incoming = forwardEdges.filter((edge) => edge.to === nodeId);
230
+ if (incoming.length === 0) {
231
+ return 0;
232
+ }
233
+
234
+ let maxParentRank = 0;
235
+ for (const edge of incoming) {
236
+ if (!nodeIds.includes(edge.from)) {
237
+ continue;
238
+ }
239
+ const parentRank = rankById.get(edge.from);
240
+ const resolvedParentRank = parentRank === null || parentRank === undefined
241
+ ? inferRankFromForwardEdges(edge.from, forwardEdges, rankById, nodeIds)
242
+ : parentRank;
243
+ rankById.set(edge.from, resolvedParentRank);
244
+ maxParentRank = Math.max(maxParentRank, resolvedParentRank + 1);
245
+ }
246
+
247
+ return maxParentRank;
248
+ }
249
+
250
+ /**
251
+ * @param {Array<{ row: number, height: number }>} nodes
252
+ * @param {GridLayoutConfig} config
253
+ */
254
+ export function buildGraphCanvasRowTops(nodes, config) {
255
+ const rowGap = config.rowGap ?? 96;
256
+ const offsetY = config.offsetY ?? 32;
257
+ const nodeMinHeight = config.nodeMinHeight ?? 112;
258
+ const rowHeights = new Map();
259
+
260
+ for (const node of nodes) {
261
+ rowHeights.set(node.row, Math.max(rowHeights.get(node.row) ?? 0, node.height));
262
+ }
263
+
264
+ const rowTops = new Map();
265
+ let y = offsetY;
266
+ const maxRow = Math.max(...nodes.map((node) => node.row), 0);
267
+ for (let row = 0; row <= maxRow; row += 1) {
268
+ rowTops.set(row, y);
269
+ y += (rowHeights.get(row) ?? nodeMinHeight) + rowGap;
270
+ }
271
+
272
+ return rowTops;
273
+ }
274
+
275
+ /**
276
+ * @param {{
277
+ * items: Array<{ id: string, [key: string]: unknown }>,
278
+ * edges: Array<{ from: string, to: string, upstream?: boolean }>,
279
+ * layoutProfile: LayoutProfile,
280
+ * estimateSize: (item: object, slot: { colSpan?: number }) => { width: number, height: number },
281
+ * config?: GridLayoutConfig,
282
+ * }} input
283
+ */
284
+ export function placeGraphCanvasNodes({ items, edges, layoutProfile, estimateSize, config = {} }) {
285
+ const nodeWidth = config.nodeWidth ?? 228;
286
+ const colGap = config.colGap ?? 72;
287
+ const offsetX = config.offsetX ?? 32;
288
+ const offsetY = config.offsetY ?? 32;
289
+
290
+ const nodeIds = items.map((item) => item.id);
291
+ const slots = computeNodeSlots({ nodeIds, edges, layoutProfile });
292
+
293
+ const nodes = items.map((item) => {
294
+ const slot = slots.get(item.id) ?? { col: 0, row: 0, colSpan: 1 };
295
+ const colSpan = slot.colSpan ?? 1;
296
+ const { width, height } = estimateSize(item, slot);
297
+ return {
298
+ item,
299
+ id: item.id,
300
+ x: offsetX + slot.col * (nodeWidth + colGap),
301
+ y: offsetY,
302
+ width: colSpan > 1 ? nodeWidth * colSpan + colGap * (colSpan - 1) : width,
303
+ height,
304
+ row: slot.row,
305
+ col: slot.col,
306
+ colSpan,
307
+ };
308
+ });
309
+
310
+ const rowTops = buildGraphCanvasRowTops(nodes, config);
311
+ for (const node of nodes) {
312
+ node.y = rowTops.get(node.row) ?? offsetY;
313
+ }
314
+
315
+ return nodes;
316
+ }
317
+
318
+ /**
319
+ * @param {{
320
+ * nodes: Array<{ id: string, x: number, y: number, width: number, height: number }>,
321
+ * nodeIds: string[],
322
+ * edges: Array<{ from: string, to: string, upstream?: boolean }>,
323
+ * }} input
324
+ */
325
+ export function filterGraphForViewMode({ nodes, nodeIds, edges, viewMode, pipelineNodeIds }) {
326
+ if (viewMode !== GRAPH_CANVAS_VIEW_PIPELINE) {
327
+ return {
328
+ nodes,
329
+ edges,
330
+ nodeIds,
331
+ };
332
+ }
333
+
334
+ const allowed = new Set(pipelineNodeIds.filter((id) => nodeIds.includes(id)));
335
+ return {
336
+ nodes: nodes.filter((node) => allowed.has(node.id)),
337
+ edges: edges.filter((edge) => allowed.has(edge.from) && allowed.has(edge.to) && !edge.upstream),
338
+ nodeIds: pipelineNodeIds.filter((id) => allowed.has(id)),
339
+ };
340
+ }
341
+
342
+ function segmentIntersects(a1, a2, b1, b2) {
343
+ const det = (a2.x - a1.x) * (b2.y - b1.y) - (a2.y - a1.y) * (b2.x - b1.x);
344
+ if (Math.abs(det) < 1e-9) {
345
+ return false;
346
+ }
347
+
348
+ const lambda = ((b2.y - b1.y) * (b2.x - a1.x) + (b1.x - b2.x) * (b2.y - a1.y)) / det;
349
+ const gamma = ((a1.y - a2.y) * (b2.x - a1.x) + (a2.x - a1.x) * (b2.y - a1.y)) / det;
350
+ return lambda > 0.01 && lambda < 0.99 && gamma > 0.01 && gamma < 0.99;
351
+ }
352
+
353
+ function edgeSegment(edge) {
354
+ const geometry = edge.geometry;
355
+ if (!geometry) {
356
+ return null;
357
+ }
358
+ return {
359
+ a: { x: geometry.startX, y: geometry.startY },
360
+ b: { x: geometry.endX, y: geometry.endY },
361
+ };
362
+ }
363
+
364
+ function nodeBoxes(nodes) {
365
+ return nodes.map((node) => ({
366
+ id: node.id,
367
+ left: node.x,
368
+ top: node.y,
369
+ right: node.x + node.width,
370
+ bottom: node.y + node.height,
371
+ }));
372
+ }
373
+
374
+ function pointInBox(x, y, box, padding = 4) {
375
+ return x >= box.left - padding
376
+ && x <= box.right + padding
377
+ && y >= box.top - padding
378
+ && y <= box.bottom + padding;
379
+ }
380
+
381
+ function minGapBetweenNodes(nodes) {
382
+ let minGap = Number.POSITIVE_INFINITY;
383
+ for (let i = 0; i < nodes.length; i += 1) {
384
+ for (let j = i + 1; j < nodes.length; j += 1) {
385
+ const a = nodes[i];
386
+ const b = nodes[j];
387
+ const gapX = Math.max(0, Math.max(a.x - (b.x + b.width), b.x - (a.x + a.width)));
388
+ const gapY = Math.max(0, Math.max(a.y - (b.y + b.height), b.y - (a.y + a.height)));
389
+ const gap = Math.max(gapX, gapY);
390
+ if (gap < minGap) {
391
+ minGap = gap;
392
+ }
393
+ }
394
+ }
395
+ return Number.isFinite(minGap) ? minGap : 0;
396
+ }
397
+
398
+ /**
399
+ * @param {{
400
+ * nodes: Array<{ id: string, x: number, y: number, width: number, height: number }>,
401
+ * edges: Array<{ from: string, to: string, geometry?: { labelX?: number, labelY?: number, startX?: number, startY?: number, endX?: number, endY?: number } }>,
402
+ * }} layout
403
+ */
404
+ export function computeLayoutQualityMetrics(layout) {
405
+ const boxes = nodeBoxes(layout.nodes);
406
+ const segments = layout.edges.map(edgeSegment).filter(Boolean);
407
+
408
+ let edgeCrossings = 0;
409
+ for (let i = 0; i < segments.length; i += 1) {
410
+ for (let j = i + 1; j < segments.length; j += 1) {
411
+ if (segmentIntersects(segments[i].a, segments[i].b, segments[j].a, segments[j].b)) {
412
+ edgeCrossings += 1;
413
+ }
414
+ }
415
+ }
416
+
417
+ let labelsUnderNodes = 0;
418
+ for (const edge of layout.edges) {
419
+ const geometry = edge.geometry;
420
+ if (!geometry || !Number.isFinite(geometry.labelX) || !Number.isFinite(geometry.labelY)) {
421
+ continue;
422
+ }
423
+ for (const box of boxes) {
424
+ if (box.id === edge.from || box.id === edge.to) {
425
+ continue;
426
+ }
427
+ if (pointInBox(geometry.labelX, geometry.labelY, box)) {
428
+ labelsUnderNodes += 1;
429
+ break;
430
+ }
431
+ }
432
+ }
433
+
434
+ return {
435
+ edgeCrossings,
436
+ minGap: minGapBetweenNodes(layout.nodes),
437
+ labelsUnderNodes,
438
+ };
439
+ }
440
+
441
+ /** @type {{ edgeCrossings: number, minGap: number, labelsUnderNodes: number }} */
442
+ export const LAYOUT_QUALITY_THRESHOLDS = {
443
+ edgeCrossings: 12,
444
+ minGap: 24,
445
+ labelsUnderNodes: 0,
446
+ };
447
+
448
+ /**
449
+ * @param {{ edgeCrossings: number, minGap: number, labelsUnderNodes: number }} metrics
450
+ * @param {{ edgeCrossings?: number, minGap?: number, labelsUnderNodes?: number }} [thresholds]
451
+ */
452
+ export function assertLayoutQuality(metrics, thresholds = LAYOUT_QUALITY_THRESHOLDS) {
453
+ const failures = [];
454
+ if (metrics.edgeCrossings > (thresholds.edgeCrossings ?? LAYOUT_QUALITY_THRESHOLDS.edgeCrossings)) {
455
+ failures.push(`edge_crossings ${metrics.edgeCrossings} > ${thresholds.edgeCrossings ?? LAYOUT_QUALITY_THRESHOLDS.edgeCrossings}`);
456
+ }
457
+ if (metrics.minGap < (thresholds.minGap ?? LAYOUT_QUALITY_THRESHOLDS.minGap)) {
458
+ failures.push(`min_gap ${metrics.minGap} < ${thresholds.minGap ?? LAYOUT_QUALITY_THRESHOLDS.minGap}`);
459
+ }
460
+ if (metrics.labelsUnderNodes > (thresholds.labelsUnderNodes ?? LAYOUT_QUALITY_THRESHOLDS.labelsUnderNodes)) {
461
+ failures.push(`labels_under_nodes ${metrics.labelsUnderNodes} > ${thresholds.labelsUnderNodes ?? LAYOUT_QUALITY_THRESHOLDS.labelsUnderNodes}`);
462
+ }
463
+ return failures;
464
+ }