@stacksjs/defaults 0.74.41 → 0.74.42
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.
- package/ai/skills/stacks-cms/SKILL.md +4 -4
- package/ai/skills/stacks-config/SKILL.md +1 -1
- package/ai/skills/stacks-technical-diagrams/LICENSE +1 -1
- package/ai/skills/stacks-technical-diagrams/SKILL.md +257 -229
- package/ai/skills/stacks-technical-diagrams/THIRD_PARTY_NOTICES.md +69 -0
- package/ai/skills/stacks-technical-diagrams/assets/JetBrainsMono-OFL.txt +93 -0
- package/ai/skills/stacks-technical-diagrams/assets/template.html +14588 -929
- package/ai/skills/stacks-technical-diagrams/bin/diagrams +10 -0
- package/ai/skills/stacks-technical-diagrams/bin/open-artifact.mjs +86 -0
- package/ai/skills/stacks-technical-diagrams/bin/preview.mjs +654 -0
- package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +1898 -89
- package/ai/skills/stacks-technical-diagrams/bin/visual-check.mjs +829 -0
- package/ai/skills/stacks-technical-diagrams/brand-marks/README.md +31 -0
- package/ai/skills/stacks-technical-diagrams/brand-marks/catalog.json +131 -0
- package/ai/skills/stacks-technical-diagrams/delta/architecture-delta.mjs +1221 -0
- package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +18 -22
- package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +58 -52
- package/ai/skills/stacks-technical-diagrams/examples/async-job-roundtrip.sequence.json +61 -0
- package/ai/skills/stacks-technical-diagrams/examples/brand-aware-delivery.architecture.json +47 -0
- package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +30 -23
- package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.base.architecture.json +31 -0
- package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.head.architecture.json +31 -0
- package/ai/skills/stacks-technical-diagrams/examples/deployment-release.lifecycle.json +49 -0
- package/ai/skills/stacks-technical-diagrams/examples/event-stream.dataflow.json +57 -0
- package/ai/skills/stacks-technical-diagrams/examples/incident-response.workflow.json +64 -0
- package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +22 -16
- package/ai/skills/stacks-technical-diagrams/examples/production-deployment.architecture.json +71 -0
- package/ai/skills/stacks-technical-diagrams/examples/release-delivery.workflow.json +62 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +16 -11
- package/ai/skills/stacks-technical-diagrams/migrations/workflow-v2.mjs +279 -0
- package/ai/skills/stacks-technical-diagrams/recipes/scenarios.mjs +391 -0
- package/ai/skills/stacks-technical-diagrams/references/authoring-contract.md +243 -0
- package/ai/skills/stacks-technical-diagrams/references/brand-marks.md +65 -0
- package/ai/skills/stacks-technical-diagrams/references/delivery-contract.md +120 -0
- package/ai/skills/stacks-technical-diagrams/references/viewer-runtime.md +45 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +780 -73
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +25 -3
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +240 -52
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +31 -7
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +227 -50
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +36 -6
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +270 -63
- package/ai/skills/stacks-technical-diagrams/renderers/shared/brand-marks.mjs +563 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/bun-runtime.mjs +20 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +186 -10
- package/ai/skills/stacks-technical-diagrams/renderers/shared/desktop-readability.mjs +26 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/diagnostics.mjs +127 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/engineering-profiles.mjs +157 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-brand-marks.mjs +2003 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +3 -3
- package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +1195 -2
- package/ai/skills/stacks-technical-diagrams/renderers/shared/i18n.mjs +595 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/legend.mjs +217 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/output-path.mjs +340 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-evidence.mjs +238 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-location.mjs +58 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/text-fit.mjs +49 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +163 -19
- package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +51 -5
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +137 -17
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +24 -470
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-compiler.mjs +4400 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-migration-geometry.mjs +144 -0
- package/ai/skills/stacks-technical-diagrams/schemas/README.md +154 -11
- package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +61 -4
- package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +72 -0
- package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +40 -18
- package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +43 -18
- package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +41 -4
- package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +97 -1
- package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +551 -12
- package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +3 -4
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
|
@@ -1,481 +1,35 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
asArray,
|
|
7
|
-
isFinitePoint,
|
|
8
|
-
rectsOverlap,
|
|
9
|
-
segmentIntersectsRect,
|
|
10
|
-
suggestLabelObstacleFix,
|
|
11
|
-
suggestLabelPairFix,
|
|
12
|
-
anchor,
|
|
13
|
-
defaultFromSide,
|
|
14
|
-
defaultToSide,
|
|
15
|
-
chosenSide,
|
|
16
|
-
polylinePath,
|
|
17
|
-
labelPoint,
|
|
18
|
-
componentFill,
|
|
19
|
-
componentText,
|
|
20
|
-
arrowClassMap,
|
|
21
|
-
variantAccent
|
|
22
|
-
} from '../shared/geometry.mjs';
|
|
3
|
+
import { loadDiagramWithBrandMarks, writeDiagram } from '../shared/cli.mjs';
|
|
4
|
+
import { throwDiagnosticError } from '../shared/diagnostics.mjs';
|
|
5
|
+
import { compileWorkflow } from './workflow-compiler.mjs';
|
|
23
6
|
|
|
24
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
-
const { diagram: workflow, template, outPath } =
|
|
8
|
+
const { diagram: workflow, template, outPath } = await loadDiagramWithBrandMarks({
|
|
26
9
|
rendererDir: __dirname,
|
|
27
10
|
diagramType: 'workflow',
|
|
28
11
|
defaultExample: 'agent-tool-call.workflow.json'
|
|
29
12
|
});
|
|
30
13
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
laneH: 104,
|
|
36
|
-
laneGap: 20,
|
|
37
|
-
laneTitleH: 30,
|
|
38
|
-
colXs: [88, 220, 300, 430, 500, 625],
|
|
39
|
-
nodeW: 92,
|
|
40
|
-
nodeH: 52
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// Content is 680px wide (laneX + laneW); auto height fits the lanes plus legend.
|
|
44
|
-
const autoHeight = layout.laneY
|
|
45
|
-
+ (workflow.lanes?.length || 1) * layout.laneH
|
|
46
|
-
+ ((workflow.lanes?.length || 1) - 1) * layout.laneGap
|
|
47
|
-
+ 124;
|
|
48
|
-
const viewBox = workflow.meta?.viewBox || [720, autoHeight];
|
|
49
|
-
|
|
50
|
-
const laneIndex = new Map(asArray(workflow.lanes).map((lane, index) => [lane.id, index]));
|
|
51
|
-
|
|
52
|
-
function laneTop(id) {
|
|
53
|
-
return layout.laneY + laneIndex.get(id) * (layout.laneH + layout.laneGap);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function lastLaneBottom() {
|
|
57
|
-
return layout.laneY + workflow.lanes.length * layout.laneH + (workflow.lanes.length - 1) * layout.laneGap;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function legendY() {
|
|
61
|
-
return lastLaneBottom() + 44;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function measureNode(node) {
|
|
65
|
-
const width = node.width || layout.nodeW;
|
|
66
|
-
const height = node.height || (node.tag ? 68 : layout.nodeH);
|
|
67
|
-
const cx = layout.colXs[node.col];
|
|
68
|
-
const contentH = layout.laneH - layout.laneTitleH;
|
|
69
|
-
const y = laneTop(node.lane) + layout.laneTitleH + (contentH - height) / 2 + (node.yOffset || 0);
|
|
70
|
-
return {
|
|
71
|
-
...node,
|
|
72
|
-
width,
|
|
73
|
-
height,
|
|
74
|
-
x: cx - width / 2,
|
|
75
|
-
y,
|
|
76
|
-
cx,
|
|
77
|
-
cy: y + height / 2
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const nodes = new Map(asArray(workflow.nodes).map((node) => [node.id, measureNode(node)]));
|
|
82
|
-
|
|
83
|
-
const mainPathSteps = new Map(asArray(workflow.mainPath).map((id, index) => [id, index]));
|
|
84
|
-
const edgeSteps = new Map(asArray(workflow.edges).map((edge, index) => {
|
|
85
|
-
const fromStep = mainPathSteps.get(edge.from);
|
|
86
|
-
const toStep = mainPathSteps.get(edge.to);
|
|
87
|
-
const mainStep = Number.isInteger(fromStep) && toStep === fromStep + 1 ? fromStep : null;
|
|
88
|
-
return [edge, mainStep ?? asArray(workflow.mainPath).length + index];
|
|
89
|
-
}));
|
|
90
|
-
|
|
91
|
-
function nodeStep(node) {
|
|
92
|
-
return mainPathSteps.get(node.id) ?? asArray(workflow.mainPath).length + asArray(workflow.nodes).findIndex((item) => item.id === node.id);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function validateWorkflow() {
|
|
96
|
-
const problems = [];
|
|
97
|
-
if (workflow.schema_version !== 1) {
|
|
98
|
-
problems.push('Workflow files must set "schema_version": 1.');
|
|
99
|
-
}
|
|
100
|
-
if (workflow.diagram_type !== 'workflow') {
|
|
101
|
-
problems.push(`Unsupported diagram_type "${workflow.diagram_type}". Expected "workflow".`);
|
|
102
|
-
}
|
|
103
|
-
if (!workflow.meta || !workflow.meta.title) {
|
|
104
|
-
problems.push('Workflow files must include meta.title.');
|
|
105
|
-
}
|
|
106
|
-
if (!Array.isArray(workflow.lanes) || !workflow.lanes.length) {
|
|
107
|
-
problems.push('Workflow files must include at least one lane.');
|
|
108
|
-
}
|
|
109
|
-
if (!Array.isArray(workflow.nodes)) {
|
|
110
|
-
problems.push('Workflow files must include a nodes array.');
|
|
111
|
-
}
|
|
112
|
-
if (!Array.isArray(workflow.edges)) {
|
|
113
|
-
problems.push('Workflow files must include an edges array.');
|
|
114
|
-
}
|
|
115
|
-
if (workflow.phases !== undefined && !Array.isArray(workflow.phases)) {
|
|
116
|
-
problems.push('Workflow "phases" must be an array.');
|
|
117
|
-
}
|
|
118
|
-
if (workflow.groups !== undefined && !Array.isArray(workflow.groups)) {
|
|
119
|
-
problems.push('Workflow "groups" must be an array.');
|
|
120
|
-
}
|
|
121
|
-
if (workflow.mainPath !== undefined && !Array.isArray(workflow.mainPath)) {
|
|
122
|
-
problems.push('Workflow "mainPath" must be an array of node ids.');
|
|
123
|
-
}
|
|
124
|
-
if (workflow.cards !== undefined && !Array.isArray(workflow.cards)) {
|
|
125
|
-
problems.push('Workflow "cards" must be an array.');
|
|
126
|
-
}
|
|
127
|
-
if (problems.length) {
|
|
128
|
-
throw new Error(`Workflow layout validation failed:\n- ${problems.join('\n- ')}`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const laneIds = new Set(workflow.lanes.map((lane) => lane.id));
|
|
132
|
-
if (laneIds.size !== workflow.lanes.length) {
|
|
133
|
-
problems.push('Lane ids must be unique.');
|
|
134
|
-
}
|
|
135
|
-
if (nodes.size !== workflow.nodes.length) {
|
|
136
|
-
problems.push('Node ids must be unique.');
|
|
137
|
-
}
|
|
138
|
-
const phaseIds = new Set(asArray(workflow.phases).map((phase) => phase.id));
|
|
139
|
-
if (phaseIds.size !== asArray(workflow.phases).length) {
|
|
140
|
-
problems.push('Phase ids must be unique.');
|
|
141
|
-
}
|
|
142
|
-
const groupIds = new Set(asArray(workflow.groups).map((group) => group.id));
|
|
143
|
-
if (groupIds.size !== asArray(workflow.groups).length) {
|
|
144
|
-
problems.push('Group ids must be unique.');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
for (const node of nodes.values()) {
|
|
148
|
-
if (!laneIds.has(node.lane)) {
|
|
149
|
-
problems.push(`Node "${node.id}" uses unknown lane "${node.lane}".`);
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
if (!Number.isInteger(node.col) || node.col < 0 || node.col >= layout.colXs.length) {
|
|
153
|
-
problems.push(`Node "${node.id}" uses column ${node.col}, but valid columns are integers 0..${layout.colXs.length - 1}.`);
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (!isFinitePoint(node.x, node.y, node.cx, node.cy)) {
|
|
157
|
-
problems.push(`Node "${node.id}" produced non-finite coordinates - check col, width, height, and yOffset are numbers.`);
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
const estLabelW = textUnits(node.label) * 6.8;
|
|
161
|
-
if (estLabelW > node.width + 6) {
|
|
162
|
-
problems.push(`Label "${node.label}" (~${Math.round(estLabelW)}px) is wider than node "${node.id}" (${node.width}px) - shorten the label, move detail to sublabel, or increase node.width.`);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const top = laneTop(node.lane);
|
|
166
|
-
const contentTop = top + layout.laneTitleH;
|
|
167
|
-
const laneRight = layout.laneX + layout.laneW;
|
|
168
|
-
if (node.x < layout.laneX || node.x + node.width > laneRight) {
|
|
169
|
-
problems.push(`Node "${node.id}" exceeds the horizontal bounds of lane "${node.lane}".`);
|
|
170
|
-
}
|
|
171
|
-
if (node.y < contentTop || node.y + node.height > top + layout.laneH) {
|
|
172
|
-
problems.push(`Node "${node.id}" collides with the title or boundary of lane "${node.lane}".`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
for (const phase of asArray(workflow.phases)) {
|
|
177
|
-
if (!Number.isInteger(phase.fromCol) || !Number.isInteger(phase.toCol)) {
|
|
178
|
-
problems.push(`Phase "${phase.id}" must use integer fromCol/toCol values.`);
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
if (phase.fromCol < 0 || phase.toCol >= layout.colXs.length || phase.fromCol > phase.toCol) {
|
|
182
|
-
problems.push(`Phase "${phase.id}" uses invalid columns ${phase.fromCol}..${phase.toCol}; use an ordered range within 0..${layout.colXs.length - 1}.`);
|
|
183
|
-
}
|
|
184
|
-
const estLabelW = textUnits(phase.label) * 5.6;
|
|
185
|
-
const width = spanForCols(phase.fromCol, phase.toCol).width;
|
|
186
|
-
if (estLabelW > width + 8) {
|
|
187
|
-
problems.push(`Phase label "${phase.label}" (~${Math.round(estLabelW)}px) is wider than its ${Math.round(width)}px span - shorten the label or widen the phase range.`);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
for (const group of asArray(workflow.groups)) {
|
|
192
|
-
if (!laneIds.has(group.lane)) {
|
|
193
|
-
problems.push(`Group "${group.id}" uses unknown lane "${group.lane}".`);
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
if (!Number.isInteger(group.fromCol) || !Number.isInteger(group.toCol)) {
|
|
197
|
-
problems.push(`Group "${group.id}" must use integer fromCol/toCol values.`);
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
if (group.fromCol < 0 || group.toCol >= layout.colXs.length || group.fromCol > group.toCol) {
|
|
201
|
-
problems.push(`Group "${group.id}" uses invalid columns ${group.fromCol}..${group.toCol}; use an ordered range within 0..${layout.colXs.length - 1}.`);
|
|
202
|
-
}
|
|
203
|
-
const contained = [...nodes.values()].some((node) => node.lane === group.lane && node.col >= group.fromCol && node.col <= group.toCol);
|
|
204
|
-
if (!contained) {
|
|
205
|
-
problems.push(`Group "${group.id}" does not contain any nodes - align its lane/columns with the parallel or branch work it frames.`);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const byLane = new Map();
|
|
210
|
-
for (const node of nodes.values()) {
|
|
211
|
-
byLane.set(node.lane, [...(byLane.get(node.lane) || []), node]);
|
|
212
|
-
}
|
|
213
|
-
for (const [lane, laneNodes] of byLane) {
|
|
214
|
-
for (let i = 0; i < laneNodes.length; i += 1) {
|
|
215
|
-
for (let j = i + 1; j < laneNodes.length; j += 1) {
|
|
216
|
-
if (rectsOverlap(laneNodes[i], laneNodes[j], 8)) {
|
|
217
|
-
problems.push(`Nodes "${laneNodes[i].id}" and "${laneNodes[j].id}" are less than 8px apart in lane "${lane}" - move one to another col, adjust yOffset, or reduce width/height.`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
for (const edge of workflow.edges) {
|
|
224
|
-
if (!nodes.has(edge.from)) problems.push(`Edge "${edge.label || edge.from}" references unknown source "${edge.from}".`);
|
|
225
|
-
if (!nodes.has(edge.to)) problems.push(`Edge "${edge.label || edge.to}" references unknown target "${edge.to}".`);
|
|
226
|
-
if (nodes.has(edge.from) && nodes.has(edge.to)) {
|
|
227
|
-
const routed = pathFor(edge);
|
|
228
|
-
if (routed.points.length === 2) {
|
|
229
|
-
const [start, end] = routed.points;
|
|
230
|
-
const segmentLength = Math.hypot(end[0] - start[0], end[1] - start[1]);
|
|
231
|
-
if (segmentLength < 28) {
|
|
232
|
-
problems.push(`Edge "${edge.from}" -> "${edge.to}" is too short (${Math.round(segmentLength)}px; minimum 28px) - drop its label or route it through a channel.`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
const segments = [];
|
|
236
|
-
for (let i = 1; i < routed.points.length; i += 1) {
|
|
237
|
-
segments.push({ start: routed.points[i - 1], end: routed.points[i] });
|
|
238
|
-
}
|
|
239
|
-
for (const node of nodes.values()) {
|
|
240
|
-
if (node.id === edge.from || node.id === edge.to) continue;
|
|
241
|
-
if (segments.some((segment) => segmentIntersectsRect(segment, node, 2))) {
|
|
242
|
-
problems.push(`Edge "${edge.from}" -> "${edge.to}" crosses node "${node.id}" - adjust fromSide/toSide, route it through a channel, or move one node to a clearer lane/column.`);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (Array.isArray(workflow.mainPath)) {
|
|
249
|
-
for (const id of workflow.mainPath) {
|
|
250
|
-
if (!nodes.has(id)) {
|
|
251
|
-
problems.push(`mainPath references unknown node "${id}".`);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
for (let i = 0; i < workflow.mainPath.length - 1; i += 1) {
|
|
255
|
-
const fromId = workflow.mainPath[i];
|
|
256
|
-
const toId = workflow.mainPath[i + 1];
|
|
257
|
-
const from = nodes.get(fromId);
|
|
258
|
-
const to = nodes.get(toId);
|
|
259
|
-
if (!from || !to) continue;
|
|
260
|
-
const linked = workflow.edges.some((edge) => edge.from === fromId && edge.to === toId);
|
|
261
|
-
if (!linked) {
|
|
262
|
-
problems.push(`mainPath step "${fromId}" -> "${toId}" has no matching edge - add the edge or remove the pair from mainPath.`);
|
|
263
|
-
}
|
|
264
|
-
if (to.col < from.col) {
|
|
265
|
-
problems.push(`mainPath step "${fromId}" -> "${toId}" moves backward from col ${from.col} to ${to.col} - use a return edge outside mainPath for loops.`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const labelRects = [];
|
|
271
|
-
for (const edge of workflow.edges) {
|
|
272
|
-
if (!edge.label || !nodes.has(edge.from) || !nodes.has(edge.to)) continue;
|
|
273
|
-
const [lx, ly] = labelPoint(edge, pathFor(edge).points);
|
|
274
|
-
const width = Math.max(30, textUnits(edge.label) * 4.8 + 10);
|
|
275
|
-
labelRects.push({ label: edge.label, x: lx - width / 2, y: ly - 10, width, height: 14, lx, ly });
|
|
276
|
-
}
|
|
277
|
-
for (const rect of labelRects) {
|
|
278
|
-
for (const node of nodes.values()) {
|
|
279
|
-
if (rectsOverlap(rect, node, -2)) {
|
|
280
|
-
problems.push(`Label "${rect.label}" overlaps node "${node.id}" - adjust labelDx/labelDy/labelSegment or set labelAt.\n${suggestLabelObstacleFix(rect, rect.lx, rect.ly, node, 'node')}`);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
for (let i = 0; i < labelRects.length; i += 1) {
|
|
285
|
-
for (let j = i + 1; j < labelRects.length; j += 1) {
|
|
286
|
-
if (rectsOverlap(labelRects[i], labelRects[j], -2)) {
|
|
287
|
-
problems.push(`Labels "${labelRects[i].label}" and "${labelRects[j].label}" overlap - adjust labelDx/labelDy or remove one label.\n${suggestLabelPairFix(labelRects[i], labelRects[j])}`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (viewBox[0] < layout.laneX + layout.laneW + 16) {
|
|
293
|
-
problems.push(`viewBox width ${viewBox[0]} clips the ${layout.laneW}px lanes - set meta.viewBox[0] to at least ${layout.laneX + layout.laneW + 16}.`);
|
|
294
|
-
}
|
|
295
|
-
if (legendY() + 18 > viewBox[1]) {
|
|
296
|
-
problems.push(`Legend exceeds viewBox height ${viewBox[1]} - set meta.viewBox[1] to at least ${legendY() + 18}.`);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (problems.length) {
|
|
300
|
-
throw new Error(`Workflow layout validation failed:\n- ${problems.join('\n- ')}`);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function gapYBetween(fromLane, toLane, bias = 0.5) {
|
|
305
|
-
const a = laneTop(fromLane) + layout.laneH;
|
|
306
|
-
const b = laneTop(toLane);
|
|
307
|
-
return a + (b - a) * bias;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function spanForCols(fromCol, toCol, pad = 46) {
|
|
311
|
-
const start = layout.colXs[fromCol] - pad;
|
|
312
|
-
const end = layout.colXs[toCol] + pad;
|
|
313
|
-
return { x: start, width: end - start, cx: (start + end) / 2 };
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function sameLaneAutoVia(start, end) {
|
|
317
|
-
if (start[0] === end[0] || start[1] === end[1]) return [];
|
|
318
|
-
const midX = (start[0] + end[0]) / 2;
|
|
319
|
-
return [[midX, start[1]], [midX, end[1]]];
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function routeVia(edge, from, to, start, end) {
|
|
323
|
-
if (edge.via) return edge.via;
|
|
324
|
-
switch (edge.route || 'auto') {
|
|
325
|
-
case 'straight':
|
|
326
|
-
return [];
|
|
327
|
-
case 'drop': {
|
|
328
|
-
const y = gapYBetween(from.lane, to.lane, edge.bias ?? 0.5);
|
|
329
|
-
return [[start[0], y], [end[0], y]];
|
|
330
|
-
}
|
|
331
|
-
case 'outside-right': {
|
|
332
|
-
const x = edge.channelX ?? layout.laneX + layout.laneW + 12;
|
|
333
|
-
return [[x, start[1]], [x, end[1]]];
|
|
334
|
-
}
|
|
335
|
-
case 'return-left': {
|
|
336
|
-
const x = edge.channelX ?? Math.min(from.x, to.x) - 28;
|
|
337
|
-
return [[x, start[1]], [x, end[1]]];
|
|
338
|
-
}
|
|
339
|
-
case 'bottom-channel': {
|
|
340
|
-
const y = edge.channelY ?? Math.max(from.y + from.height, to.y + to.height) + 32;
|
|
341
|
-
return [[start[0], y], [end[0], y]];
|
|
342
|
-
}
|
|
343
|
-
case 'up-channel': {
|
|
344
|
-
const y = edge.channelY ?? Math.min(from.y, to.y) - 28;
|
|
345
|
-
return [[start[0], y], [end[0], y]];
|
|
346
|
-
}
|
|
347
|
-
case 'auto':
|
|
348
|
-
default: {
|
|
349
|
-
if (from.lane === to.lane) return sameLaneAutoVia(start, end);
|
|
350
|
-
const y = gapYBetween(from.lane, to.lane, edge.bias ?? 0.5);
|
|
351
|
-
return [[start[0], y], [end[0], y]];
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const pathCache = new Map();
|
|
357
|
-
|
|
358
|
-
function pathFor(edge) {
|
|
359
|
-
if (pathCache.has(edge)) return pathCache.get(edge);
|
|
360
|
-
const from = nodes.get(edge.from);
|
|
361
|
-
const to = nodes.get(edge.to);
|
|
362
|
-
const start = anchor(from, chosenSide(edge.fromSide, defaultFromSide(from, to)));
|
|
363
|
-
const end = anchor(to, chosenSide(edge.toSide, defaultToSide(from, to)));
|
|
364
|
-
const points = [start, ...routeVia(edge, from, to, start, end), end];
|
|
365
|
-
const routed = { d: polylinePath(points), points };
|
|
366
|
-
pathCache.set(edge, routed);
|
|
367
|
-
return routed;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function renderLane(lane, index) {
|
|
371
|
-
const y = layout.laneY + index * (layout.laneH + layout.laneGap);
|
|
372
|
-
const exception = lane.variant === 'exception'
|
|
373
|
-
? `\n <rect x="${layout.laneX + 6}" y="${y + 6}" width="${layout.laneW - 12}" height="${layout.laneH - 12}" rx="8" class="c-security-group" stroke-width="1"/>`
|
|
374
|
-
: '';
|
|
375
|
-
const labelClass = lane.variant === 'exception' ? 't-security' : 't-dim';
|
|
376
|
-
const prefix = lane.variant === 'exception' ? 'EX' : String(index + 1).padStart(2, '0');
|
|
377
|
-
return ` <rect x="${layout.laneX}" y="${y}" width="${layout.laneW}" height="${layout.laneH}" rx="10" class="c-lane" stroke-width="1"/>${exception}
|
|
378
|
-
<text x="${layout.laneX + 14}" y="${y + 22}" class="${labelClass}" font-size="10" font-weight="600">${prefix} / ${esc(lane.label)}</text>`;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function renderPhase(phase) {
|
|
382
|
-
const span = spanForCols(phase.fromCol, phase.toCol, 46);
|
|
383
|
-
const accent = variantAccent(phase.variant);
|
|
384
|
-
const [lineClass] = arrowClassMap[phase.variant || 'default'] || arrowClassMap.default;
|
|
385
|
-
return ` <line x1="${span.x}" y1="35" x2="${span.x + span.width}" y2="35" class="${lineClass}" stroke-width="1.1"/>
|
|
386
|
-
<rect x="${span.x}" y="27" width="${span.width}" height="16" rx="4" class="c-mask"/>
|
|
387
|
-
<text x="${span.cx}" y="39" class="${accent}" font-size="8" font-weight="600" text-anchor="middle">${esc(phase.label)}</text>`;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function renderGroup(group) {
|
|
391
|
-
const span = spanForCols(group.fromCol, group.toCol, 50);
|
|
392
|
-
const y = laneTop(group.lane) + layout.laneTitleH + 8;
|
|
393
|
-
const height = layout.laneH - layout.laneTitleH - 16;
|
|
394
|
-
const cls = group.variant === 'security' ? 'c-security-group' : 'c-lane';
|
|
395
|
-
const textClass = variantAccent(group.variant);
|
|
396
|
-
return ` <rect x="${span.x}" y="${y}" width="${span.width}" height="${height}" rx="9" class="${cls}" stroke-width="1"/>
|
|
397
|
-
<text x="${span.x + 10}" y="${y + 14}" class="${textClass}" font-size="7" font-weight="600">${esc(group.label)}</text>`;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function renderNode(node) {
|
|
401
|
-
const fill = componentFill[node.type] || 'c-external';
|
|
402
|
-
const accent = componentText[node.type] || 't-muted';
|
|
403
|
-
const tag = node.tag
|
|
404
|
-
? `\n <text x="${node.cx}" y="${node.y + node.height - 12}" class="${accent}" font-size="7" text-anchor="middle">${esc(node.tag)}</text>`
|
|
405
|
-
: '';
|
|
406
|
-
return ` <rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="6" class="c-mask"/>
|
|
407
|
-
<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="6" class="${fill}"${animateAttr(workflow.meta, 'node', nodeStep(node))} stroke-width="1.5"/>
|
|
408
|
-
<text x="${node.cx}" y="${node.y + 21}" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">${esc(node.label)}</text>
|
|
409
|
-
<text x="${node.cx}" y="${node.y + 38}" class="t-muted" font-size="8" text-anchor="middle">${esc(node.sublabel || '')}</text>${tag}`;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function renderEdgePath(edge) {
|
|
413
|
-
const [cls, marker] = arrowClassMap[edge.variant || 'default'] || arrowClassMap.default;
|
|
414
|
-
const routed = pathFor(edge);
|
|
415
|
-
const strokeWidth = edge.width || (edge.variant === 'emphasis' ? 1.8 : 1.4);
|
|
416
|
-
return ` <path d="${routed.d}" class="${cls}"${animateAttr(workflow.meta, 'edge', edgeSteps.get(edge))} stroke-width="${strokeWidth}" marker-end="url(#${marker})"/>`;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function renderEdgeLabel(edge) {
|
|
420
|
-
if (!edge.label) return '';
|
|
421
|
-
const routed = pathFor(edge);
|
|
422
|
-
const [lx, ly] = labelPoint(edge, routed.points);
|
|
423
|
-
const labelW = Math.max(30, textUnits(edge.label) * 4.8 + 10);
|
|
424
|
-
return ` <rect x="${lx - labelW / 2}" y="${ly - 10}" width="${labelW}" height="14" rx="3" class="c-mask"/>
|
|
425
|
-
<text x="${lx}" y="${ly}" class="${variantAccent(edge.variant, { dashed: 't-database' })}" font-size="8" text-anchor="middle">${esc(edge.label)}</text>`;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function renderLegend() {
|
|
429
|
-
const y = legendY();
|
|
430
|
-
return ` <text x="175" y="${y - 20}" class="t-primary" font-size="10" font-weight="600">Legend</text>
|
|
431
|
-
<rect x="175" y="${y - 8}" width="14" height="9" rx="2" class="c-frontend" stroke-width="1"/>
|
|
432
|
-
<text x="195" y="${y}" class="t-muted" font-size="7">User UI</text>
|
|
433
|
-
<rect x="260" y="${y - 8}" width="14" height="9" rx="2" class="c-backend" stroke-width="1"/>
|
|
434
|
-
<text x="280" y="${y}" class="t-muted" font-size="7">Agent logic</text>
|
|
435
|
-
<rect x="370" y="${y - 8}" width="14" height="9" rx="2" class="c-security" stroke-width="1"/>
|
|
436
|
-
<text x="390" y="${y}" class="t-muted" font-size="7">Policy</text>
|
|
437
|
-
<rect x="455" y="${y - 8}" width="14" height="9" rx="2" class="c-messagebus" stroke-width="1"/>
|
|
438
|
-
<text x="475" y="${y}" class="t-muted" font-size="7">Tool action</text>
|
|
439
|
-
<rect x="565" y="${y - 8}" width="14" height="9" rx="2" class="c-database" stroke-width="1"/>
|
|
440
|
-
<text x="585" y="${y}" class="t-muted" font-size="7">Context / trace</text>`;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function renderSvg() {
|
|
444
|
-
return ` <svg viewBox="0 0 ${viewBox[0]} ${viewBox[1]}" ${svgRootAttrs(workflow.meta, 'workflow diagram')}>
|
|
445
|
-
${renderDefinitions()}
|
|
446
|
-
|
|
447
|
-
<!-- Background Grid -->
|
|
448
|
-
<rect width="100%" height="100%" fill="url(#grid)" />
|
|
449
|
-
|
|
450
|
-
<!-- Swimlanes -->
|
|
451
|
-
${workflow.lanes.map(renderLane).join('\n\n')}
|
|
452
|
-
|
|
453
|
-
<!-- Phase headers -->
|
|
454
|
-
${asArray(workflow.phases).map(renderPhase).join('\n')}
|
|
455
|
-
|
|
456
|
-
<!-- Workflow groups -->
|
|
457
|
-
${asArray(workflow.groups).map(renderGroup).join('\n')}
|
|
458
|
-
|
|
459
|
-
<!-- Edge paths -->
|
|
460
|
-
${workflow.edges.map(renderEdgePath).join('\n')}
|
|
461
|
-
|
|
462
|
-
<!-- Nodes -->
|
|
463
|
-
${[...nodes.values()].map(renderNode).join('\n\n')}
|
|
464
|
-
|
|
465
|
-
<!-- Edge labels -->
|
|
466
|
-
${workflow.edges.map(renderEdgeLabel).join('\n')}
|
|
14
|
+
const compiled = compileWorkflow({
|
|
15
|
+
workflow,
|
|
16
|
+
qualityProfile: process.env.ARCHIFY_QUALITY_PROFILE || workflow.meta?.quality_profile,
|
|
17
|
+
});
|
|
467
18
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
19
|
+
const layoutJson = process.argv.includes('--layout-json');
|
|
20
|
+
|
|
21
|
+
if (layoutJson) {
|
|
22
|
+
process.stdout.write(`${JSON.stringify(compiled.receipt, null, 2)}\n`);
|
|
23
|
+
if (!compiled.ok) process.exitCode = 1;
|
|
24
|
+
} else if (!compiled.ok) {
|
|
25
|
+
throwDiagnosticError(compiled.error || 'Workflow compilation failed.', compiled.diagnostics);
|
|
26
|
+
} else {
|
|
27
|
+
writeDiagram({
|
|
28
|
+
outPath,
|
|
29
|
+
template,
|
|
30
|
+
diagramType: 'workflow',
|
|
31
|
+
meta: workflow.meta,
|
|
32
|
+
svg: compiled.svg,
|
|
33
|
+
cards: workflow.cards,
|
|
34
|
+
});
|
|
471
35
|
}
|
|
472
|
-
|
|
473
|
-
validateWorkflow();
|
|
474
|
-
writeDiagram({
|
|
475
|
-
outPath,
|
|
476
|
-
template,
|
|
477
|
-
meta: workflow.meta,
|
|
478
|
-
footerLabel: 'Workflow diagram',
|
|
479
|
-
svg: renderSvg(),
|
|
480
|
-
cards: workflow.cards,
|
|
481
|
-
});
|