@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.
Files changed (74) hide show
  1. package/ai/skills/stacks-cms/SKILL.md +4 -4
  2. package/ai/skills/stacks-config/SKILL.md +1 -1
  3. package/ai/skills/stacks-technical-diagrams/LICENSE +1 -1
  4. package/ai/skills/stacks-technical-diagrams/SKILL.md +257 -229
  5. package/ai/skills/stacks-technical-diagrams/THIRD_PARTY_NOTICES.md +69 -0
  6. package/ai/skills/stacks-technical-diagrams/assets/JetBrainsMono-OFL.txt +93 -0
  7. package/ai/skills/stacks-technical-diagrams/assets/template.html +14588 -929
  8. package/ai/skills/stacks-technical-diagrams/bin/diagrams +10 -0
  9. package/ai/skills/stacks-technical-diagrams/bin/open-artifact.mjs +86 -0
  10. package/ai/skills/stacks-technical-diagrams/bin/preview.mjs +654 -0
  11. package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +1898 -89
  12. package/ai/skills/stacks-technical-diagrams/bin/visual-check.mjs +829 -0
  13. package/ai/skills/stacks-technical-diagrams/brand-marks/README.md +31 -0
  14. package/ai/skills/stacks-technical-diagrams/brand-marks/catalog.json +131 -0
  15. package/ai/skills/stacks-technical-diagrams/delta/architecture-delta.mjs +1221 -0
  16. package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +18 -22
  17. package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +58 -52
  18. package/ai/skills/stacks-technical-diagrams/examples/async-job-roundtrip.sequence.json +61 -0
  19. package/ai/skills/stacks-technical-diagrams/examples/brand-aware-delivery.architecture.json +47 -0
  20. package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +30 -23
  21. package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.base.architecture.json +31 -0
  22. package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.head.architecture.json +31 -0
  23. package/ai/skills/stacks-technical-diagrams/examples/deployment-release.lifecycle.json +49 -0
  24. package/ai/skills/stacks-technical-diagrams/examples/event-stream.dataflow.json +57 -0
  25. package/ai/skills/stacks-technical-diagrams/examples/incident-response.workflow.json +64 -0
  26. package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +22 -16
  27. package/ai/skills/stacks-technical-diagrams/examples/production-deployment.architecture.json +71 -0
  28. package/ai/skills/stacks-technical-diagrams/examples/release-delivery.workflow.json +62 -0
  29. package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +16 -11
  30. package/ai/skills/stacks-technical-diagrams/migrations/workflow-v2.mjs +279 -0
  31. package/ai/skills/stacks-technical-diagrams/recipes/scenarios.mjs +391 -0
  32. package/ai/skills/stacks-technical-diagrams/references/authoring-contract.md +243 -0
  33. package/ai/skills/stacks-technical-diagrams/references/brand-marks.md +65 -0
  34. package/ai/skills/stacks-technical-diagrams/references/delivery-contract.md +120 -0
  35. package/ai/skills/stacks-technical-diagrams/references/viewer-runtime.md +45 -0
  36. package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +780 -73
  37. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +25 -3
  38. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +240 -52
  39. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +31 -7
  40. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +227 -50
  41. package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +36 -6
  42. package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +270 -63
  43. package/ai/skills/stacks-technical-diagrams/renderers/shared/brand-marks.mjs +563 -0
  44. package/ai/skills/stacks-technical-diagrams/renderers/shared/bun-runtime.mjs +20 -0
  45. package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +186 -10
  46. package/ai/skills/stacks-technical-diagrams/renderers/shared/desktop-readability.mjs +26 -0
  47. package/ai/skills/stacks-technical-diagrams/renderers/shared/diagnostics.mjs +127 -0
  48. package/ai/skills/stacks-technical-diagrams/renderers/shared/engineering-profiles.mjs +157 -0
  49. package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-brand-marks.mjs +2003 -0
  50. package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +3 -3
  51. package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +1195 -2
  52. package/ai/skills/stacks-technical-diagrams/renderers/shared/i18n.mjs +595 -0
  53. package/ai/skills/stacks-technical-diagrams/renderers/shared/legend.mjs +217 -0
  54. package/ai/skills/stacks-technical-diagrams/renderers/shared/output-path.mjs +340 -0
  55. package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-evidence.mjs +238 -0
  56. package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-location.mjs +58 -0
  57. package/ai/skills/stacks-technical-diagrams/renderers/shared/text-fit.mjs +49 -0
  58. package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +163 -19
  59. package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +51 -5
  60. package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +137 -17
  61. package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +24 -470
  62. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-compiler.mjs +4400 -0
  63. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-migration-geometry.mjs +144 -0
  64. package/ai/skills/stacks-technical-diagrams/schemas/README.md +154 -11
  65. package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +61 -4
  66. package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +72 -0
  67. package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +40 -18
  68. package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +43 -18
  69. package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +41 -4
  70. package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +97 -1
  71. package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +551 -12
  72. package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +3 -4
  73. package/ide/vscode/package.json +1 -1
  74. package/package.json +2 -2
@@ -2,11 +2,18 @@
2
2
 
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
+ import { collectAmbiguousCorridors, collectBorderRuns, collectLabelRouteClearance, collectRouteRhythmIssues, routeBudgetMetrics } from '../renderers/shared/geometry.mjs';
6
+ import {
7
+ DESKTOP_READABILITY_VIEWPORT,
8
+ DESKTOP_READER_DIAGRAM_WIDTH,
9
+ MIN_PROJECTED_NODE_TEXT_PX,
10
+ projectedNodeTextPx,
11
+ } from '../renderers/shared/desktop-readability.mjs';
5
12
 
6
13
  const input = process.argv[2];
7
14
 
8
15
  if (!input || input === '-h' || input === '--help') {
9
- console.error('Usage: bun scripts/check-render-output.mjs <diagram.html>');
16
+ console.error('Usage: node scripts/check-render-output.mjs <diagram.html>');
10
17
  process.exit(input ? 0 : 2);
11
18
  }
12
19
 
@@ -24,6 +31,33 @@ try {
24
31
  }
25
32
 
26
33
  const checks = [];
34
+ let composition = {
35
+ schemaVersion: 1,
36
+ profile: 'standard',
37
+ status: 'pass',
38
+ summary: { errors: 0, warnings: 0 },
39
+ metrics: {
40
+ properCrossings: 0,
41
+ ambiguousCorridors: 0,
42
+ containerBorderRuns: 0,
43
+ labelRouteClearanceIssues: 0,
44
+ minLabelRouteClearance: null,
45
+ maxBends: 0,
46
+ routesOverSuggestedBends: 0,
47
+ maxStretch: null,
48
+ routesOverSuggestedStretch: 0,
49
+ minSegmentPx: null,
50
+ minInteriorSegmentPx: null,
51
+ shortSegmentCount: 0,
52
+ shortEndpointSegmentCount: 0,
53
+ shortInteriorSegmentCount: 0,
54
+ microSegmentCount: 0,
55
+ desktopReadabilityIssues: 0,
56
+ minProjectedNodeTextPx: null,
57
+ },
58
+ suggestedLimits: { bendsPerRelationship: 2, stretch: 1.35, segmentPx: 16, microSegmentPx: 8 },
59
+ issues: [],
60
+ };
27
61
 
28
62
  function addCheck(name, ok, details = []) {
29
63
  checks.push({ name, ok, details });
@@ -34,15 +68,193 @@ addCheck('single_svg', svgMatches.length === 1, [`found ${svgMatches.length} <sv
34
68
 
35
69
  if (svgMatches.length === 1) {
36
70
  const svg = svgMatches[0][0];
71
+ const svgRoot = svg.match(/<svg\b[^>]*>/i)?.[0] || '';
72
+ const svgAttrs = parseAttrs(svgRoot);
73
+ const qualityProfile = svgAttrs['data-quality-profile'] || 'standard';
74
+ const qualityGatesEnforced = svgAttrs['data-quality-gates'] !== 'advisory';
37
75
  addCheck('finite_svg', !/\b(?:NaN|undefined|Infinity|-Infinity)\b/.test(svg));
38
76
  const legendStart = svg.indexOf('<!-- Legend -->');
39
77
  const beforeLegend = legendStart >= 0 ? svg.slice(0, legendStart) : svg;
78
+ const desktopReadabilityIssue = collectDesktopReadability(svgAttrs, beforeLegend);
40
79
  const arrows = collectArrows(beforeLegend);
41
- const diagonal = arrows.filter((arrow) => isTwoPointDiagonal(arrow));
80
+ const diagonal = arrows.flatMap((arrow) => diagonalStraightSegments(arrow).map((segment) => ({ arrow, ...segment })));
42
81
  addCheck(
43
82
  'orthogonal_arrows',
44
83
  diagonal.length === 0,
45
- diagonal.map((arrow) => `${arrow.kind} ${arrow.index}: ${arrow.raw}`),
84
+ diagonal.map(({ arrow, segmentIndex }) => `${arrow.kind} ${arrow.index} segment ${segmentIndex + 1}: ${arrow.raw}`),
85
+ );
86
+ const relationshipCrossings = collectRelationshipCrossings(arrows);
87
+ const compositionFrames = collectCompositionFrames(beforeLegend);
88
+ const containerBorderRuns = collectBorderRuns({
89
+ routedRelations: arrows
90
+ .filter((arrow) => arrow.from && arrow.to && arrow.borderSegments.length)
91
+ .map((arrow) => ({
92
+ relation: arrow,
93
+ relationIndex: arrow.index,
94
+ segments: arrow.borderSegments,
95
+ })),
96
+ frames: compositionFrames,
97
+ });
98
+ const routedRelationships = arrows
99
+ .filter((arrow) => arrow.from && arrow.to && arrow.routePoints.length)
100
+ .map((arrow) => ({ relation: arrow, relationIndex: arrow.index, points: arrow.routePoints }));
101
+ const routeMetrics = routeBudgetMetrics({ routedRelations: routedRelationships });
102
+ const routeRhythmIssues = collectRouteRhythmIssues({ routedRelations: routedRelationships });
103
+ const ambiguousCorridors = collectAmbiguousCorridors({ routedRelations: routedRelationships });
104
+ const relationshipLabels = collectRelationshipLabelMasks(beforeLegend, arrows);
105
+ const labelClearanceThreshold = qualityProfile === 'showcase' ? 4 : 2;
106
+ const labelRouteMeasurements = collectLabelRouteClearance({
107
+ labels: relationshipLabels,
108
+ routedRelations: arrows.map((arrow) => ({ relation: arrow, relationIndex: arrow.index, points: arrow.routePoints })),
109
+ threshold: Number.MAX_VALUE,
110
+ });
111
+ const labelRouteClearance = collectLabelRouteClearance({
112
+ labels: relationshipLabels,
113
+ routedRelations: arrows.map((arrow) => ({ relation: arrow, relationIndex: arrow.index, points: arrow.routePoints })),
114
+ threshold: labelClearanceThreshold,
115
+ });
116
+ const crossingIsError = qualityProfile === 'showcase';
117
+ const corridorIsError = qualityProfile === 'showcase';
118
+ const rhythmIsError = qualityProfile === 'showcase';
119
+ const labelClearanceIsError = qualityProfile === 'showcase';
120
+ const desktopReadabilityIsError = qualityProfile === 'showcase';
121
+ const compositionErrors = (qualityGatesEnforced ? containerBorderRuns.length : 0)
122
+ + (crossingIsError ? relationshipCrossings.length : 0)
123
+ + (corridorIsError ? ambiguousCorridors.length : 0)
124
+ + (labelClearanceIsError ? labelRouteClearance.length : 0)
125
+ + (rhythmIsError ? routeRhythmIssues.length : 0)
126
+ + (desktopReadabilityIsError && desktopReadabilityIssue ? 1 : 0);
127
+ const compositionWarnings = (qualityGatesEnforced ? 0 : containerBorderRuns.length)
128
+ + (crossingIsError ? 0 : relationshipCrossings.length)
129
+ + (corridorIsError ? 0 : ambiguousCorridors.length)
130
+ + (labelClearanceIsError ? 0 : labelRouteClearance.length)
131
+ + (rhythmIsError ? 0 : routeRhythmIssues.length)
132
+ + (desktopReadabilityIsError || !desktopReadabilityIssue ? 0 : 1);
133
+ composition = {
134
+ schemaVersion: 1,
135
+ profile: qualityProfile,
136
+ status: compositionErrors ? 'fail' : 'pass',
137
+ summary: {
138
+ errors: compositionErrors,
139
+ warnings: compositionWarnings,
140
+ },
141
+ metrics: {
142
+ properCrossings: relationshipCrossings.length,
143
+ ambiguousCorridors: ambiguousCorridors.length,
144
+ containerBorderRuns: containerBorderRuns.length,
145
+ labelRouteClearanceIssues: labelRouteClearance.length,
146
+ minLabelRouteClearance: labelRouteMeasurements.length
147
+ ? Math.round(Math.min(...labelRouteMeasurements.map((hit) => hit.clearance)) * 10) / 10
148
+ : null,
149
+ desktopReadabilityIssues: desktopReadabilityIssue ? 1 : 0,
150
+ minProjectedNodeTextPx: desktopReadabilityIssue?.projectedFontPx ?? null,
151
+ ...roundedRouteMetrics(routeMetrics),
152
+ },
153
+ suggestedLimits: { bendsPerRelationship: 2, stretch: 1.35, segmentPx: 16, microSegmentPx: 8 },
154
+ issues: [
155
+ ...containerBorderRuns.map((hit) => ({
156
+ severity: qualityGatesEnforced ? 'error' : 'warning',
157
+ code: 'composition/container-border-run',
158
+ relationship: relationshipRecord(hit.relation),
159
+ frame: frameRecord(hit.frame),
160
+ side: hit.side,
161
+ segmentIndex: hit.segmentIndex,
162
+ overlapLength: Math.round(hit.overlapLength * 10) / 10,
163
+ from: hit.overlapStart.map((value) => Math.round(value * 10) / 10),
164
+ to: hit.overlapEnd.map((value) => Math.round(value * 10) / 10),
165
+ })),
166
+ ...labelRouteClearance.map((hit) => ({
167
+ severity: labelClearanceIsError ? 'error' : 'warning',
168
+ code: 'composition/label-route-clearance',
169
+ label: hit.label?.label || hit.labelRelation?.label || '',
170
+ labelRelationship: relationshipRecord(hit.labelRelation),
171
+ otherRelationship: relationshipRecord(hit.otherRelation),
172
+ segmentIndex: hit.segmentIndex,
173
+ labelRect: roundedRect(hit.rect),
174
+ clearance: Math.round(hit.clearance * 10) / 10,
175
+ intersectionLength: Math.round((hit.intersectionLength || 0) * 10) / 10,
176
+ threshold: hit.threshold,
177
+ from: hit.start.map((value) => Math.round(value * 10) / 10),
178
+ to: hit.end.map((value) => Math.round(value * 10) / 10),
179
+ })),
180
+ ...relationshipCrossings.map((hit) => ({
181
+ severity: crossingIsError ? 'error' : 'warning',
182
+ code: 'composition/proper-crossing',
183
+ relationship: relationshipRecord(hit.left),
184
+ otherRelationship: relationshipRecord(hit.right),
185
+ point: hit.point.map((value) => Math.round(value * 10) / 10),
186
+ })),
187
+ ...ambiguousCorridors.map((hit) => ({
188
+ severity: corridorIsError ? 'error' : 'warning',
189
+ code: 'composition/ambiguous-corridor',
190
+ relationship: relationshipRecord(hit.left.relation),
191
+ otherRelationship: relationshipRecord(hit.right.relation),
192
+ segmentIndex: hit.leftSegment,
193
+ otherSegmentIndex: hit.rightSegment,
194
+ overlapLength: Math.round(hit.overlapLength * 10) / 10,
195
+ from: hit.overlapStart.map((value) => Math.round(value * 10) / 10),
196
+ to: hit.overlapEnd.map((value) => Math.round(value * 10) / 10),
197
+ })),
198
+ ...routeRhythmIssues.map((hit) => ({
199
+ severity: rhythmIsError ? 'error' : 'warning',
200
+ code: hit.code,
201
+ relationship: relationshipRecord(hit.relation),
202
+ segmentIndex: hit.segmentIndex,
203
+ position: hit.position,
204
+ length: Math.round(hit.length * 10) / 10,
205
+ from: hit.start.map((value) => Math.round(value * 10) / 10),
206
+ to: hit.end.map((value) => Math.round(value * 10) / 10),
207
+ })),
208
+ ...(desktopReadabilityIssue ? [{
209
+ severity: desktopReadabilityIsError ? 'error' : 'warning',
210
+ code: 'composition/desktop-readability',
211
+ viewportWidth: DESKTOP_READABILITY_VIEWPORT.width,
212
+ viewportHeight: DESKTOP_READABILITY_VIEWPORT.height,
213
+ availableDiagramWidth: DESKTOP_READER_DIAGRAM_WIDTH,
214
+ viewBoxWidth: desktopReadabilityIssue.viewBoxWidth,
215
+ scale: desktopReadabilityIssue.scale,
216
+ text: desktopReadabilityIssue.text,
217
+ detail: desktopReadabilityIssue.detail,
218
+ sourceFontPx: desktopReadabilityIssue.sourceFontPx,
219
+ projectedFontPx: desktopReadabilityIssue.projectedFontPx,
220
+ minimumProjectedFontPx: MIN_PROJECTED_NODE_TEXT_PX,
221
+ }] : []),
222
+ ],
223
+ };
224
+ addCheck(
225
+ 'label_route_clearance',
226
+ !labelClearanceIsError || labelRouteClearance.length === 0,
227
+ labelRouteClearance.map((hit) => (
228
+ `[composition/label-route-clearance] ${qualityProfile} label "${hit.label?.label || hit.labelRelation?.label || ''}" on ${relationshipName(hit.labelRelation)} is ${Math.round(hit.clearance * 10) / 10}px from ${relationshipName(hit.otherRelation)} segment ${hit.segmentIndex} [${formatPoint(hit.start)}] -> [${formatPoint(hit.end)}]${hit.intersectionLength > 0 ? ` with ${Math.round(hit.intersectionLength * 10) / 10}px hidden by the mask` : ''} (minimum ${hit.threshold}px) - use renderer-supported label controls (message y for sequence; otherwise labelAt, labelDx, labelDy, or labelSegment), or adjust the other relationship route/via/channel.`
229
+ )),
230
+ );
231
+ addCheck(
232
+ 'relationship_crossings',
233
+ !crossingIsError || relationshipCrossings.length === 0,
234
+ relationshipCrossings.map((hit) => (
235
+ `[composition/proper-crossing] ${qualityProfile} ${relationshipName(hit.left)} crosses ${relationshipName(hit.right)} at [${formatPoint(hit.point)}]`
236
+ )),
237
+ );
238
+ addCheck(
239
+ 'relationship_corridors',
240
+ !corridorIsError || ambiguousCorridors.length === 0,
241
+ ambiguousCorridors.map((hit) => (
242
+ `[composition/ambiguous-corridor] ${qualityProfile} ${relationshipName(hit.left.relation)} shares a ${Math.round(hit.overlapLength * 10) / 10}px corridor with ${relationshipName(hit.right.relation)} at [${formatPoint(hit.overlapStart)}] -> [${formatPoint(hit.overlapEnd)}]`
243
+ )),
244
+ );
245
+ addCheck(
246
+ 'container_border_runs',
247
+ !qualityGatesEnforced || containerBorderRuns.length === 0,
248
+ containerBorderRuns.map((hit) => (
249
+ `[composition/container-border-run] ${relationshipName(hit.relation)} follows ${frameName(hit.frame)} ${hit.side} border for ${Math.round(hit.overlapLength * 10) / 10}px on segment ${hit.segmentIndex} [${formatPoint(hit.overlapStart)}] -> [${formatPoint(hit.overlapEnd)}]`
250
+ )),
251
+ );
252
+ addCheck(
253
+ 'route_rhythm',
254
+ !rhythmIsError || routeRhythmIssues.length === 0,
255
+ routeRhythmIssues.map((hit) => (
256
+ `[${hit.code}] ${qualityProfile} ${relationshipName(hit.relation)} has a ${Math.round(hit.length * 10) / 10}px ${hit.position} segment ${hit.segmentIndex} [${formatPoint(hit.start)}] -> [${formatPoint(hit.end)}]`
257
+ )),
46
258
  );
47
259
 
48
260
  if (legendStart >= 0) {
@@ -59,9 +271,10 @@ if (svgMatches.length === 1) {
59
271
  }
60
272
  }
61
273
 
62
- const ok = checks.every((check) => check.ok);
63
- console.log(JSON.stringify({ ok, file: htmlPath, checks }, null, 2));
64
- process.exit(ok ? 0 : 1);
274
+ const ok = checks.every((check) => check.ok) && composition.status !== 'fail';
275
+ console.log(JSON.stringify({ ok, file: htmlPath, checks, composition }, null, 2));
276
+ // Let pending stdout writes drain: large receipts are asynchronous when piped.
277
+ process.exitCode = ok ? 0 : 1;
65
278
 
66
279
  function collectArrows(fragment) {
67
280
  const arrows = [];
@@ -75,12 +288,184 @@ function collectArrows(fragment) {
75
288
  const segments = tag[1].toLowerCase() === 'line'
76
289
  ? lineSegments(attrs)
77
290
  : pathSegments(attrs.d || '');
78
- arrows.push({ kind: tag[1].toLowerCase(), index: index += 1, raw, segments });
291
+ const borderSegments = tag[1].toLowerCase() === 'line'
292
+ ? segments
293
+ : straightPathSegments(attrs.d || '');
294
+ arrows.push({
295
+ kind: tag[1].toLowerCase(),
296
+ index: index += 1,
297
+ raw,
298
+ segments,
299
+ borderSegments,
300
+ routePoints: parseRoutePoints(attrs['data-composition-points']) || (
301
+ borderSegments.length ? [borderSegments[0].start, ...borderSegments.map((segment) => segment.end)] : []
302
+ ),
303
+ from: attrs['data-edge-from'] || attrs['data-composition-edge-from'],
304
+ to: attrs['data-edge-to'] || attrs['data-composition-edge-to'],
305
+ id: attrs['data-edge-id'] || attrs['data-composition-edge-id'],
306
+ key: attrs['data-edge-key'],
307
+ label: attrs['data-edge-label'],
308
+ offset: tag.index,
309
+ });
79
310
  }
80
311
 
81
312
  return arrows;
82
313
  }
83
314
 
315
+ function collectRelationshipLabelMasks(fragment, arrows) {
316
+ const labels = [];
317
+ for (const match of fragment.matchAll(/<g\b[^>]*\bdata-edge-(?:key|id|from)="[^"]*"[^>]*>[\s\S]*?<\/g>/gi)) {
318
+ const group = match[0];
319
+ const groupAttrs = parseAttrs(group.match(/<g\b[^>]*>/i)?.[0] || '');
320
+ const rectTag = [...group.matchAll(/<rect\b[^>]*>/gi)]
321
+ .map((item) => item[0])
322
+ .find((tag) => /\bclass="[^"]*\bc-mask\b/.test(tag));
323
+ if (!rectTag) continue;
324
+ const attrs = parseAttrs(rectTag);
325
+ const rect = {
326
+ x: numberAttr(attrs, 'x'),
327
+ y: numberAttr(attrs, 'y'),
328
+ width: numberAttr(attrs, 'width'),
329
+ height: numberAttr(attrs, 'height'),
330
+ };
331
+ if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite)) continue;
332
+ const groupStart = match.index;
333
+ const groupEnd = groupStart + group.length;
334
+ const containedOwner = arrows.find((arrow) => (
335
+ arrow.offset > groupStart
336
+ && arrow.offset < groupEnd
337
+ && arrow.from === groupAttrs['data-edge-from']
338
+ && arrow.to === groupAttrs['data-edge-to']
339
+ && (!groupAttrs['data-edge-id'] || !arrow.id || arrow.id === groupAttrs['data-edge-id'])
340
+ ));
341
+ const owner = arrows.find((arrow) => (
342
+ groupAttrs['data-edge-key'] !== undefined && arrow.key === groupAttrs['data-edge-key']
343
+ )) || containedOwner || arrows.find((arrow) => (
344
+ arrow.id === groupAttrs['data-edge-id']
345
+ && arrow.from === groupAttrs['data-edge-from']
346
+ && arrow.to === groupAttrs['data-edge-to']
347
+ ));
348
+ if (!owner) continue;
349
+ if (owner.key === undefined && groupAttrs['data-edge-key'] !== undefined) owner.key = groupAttrs['data-edge-key'];
350
+ if (!owner.id && groupAttrs['data-edge-id']) owner.id = groupAttrs['data-edge-id'];
351
+ if (!owner.label && groupAttrs['data-edge-label']) owner.label = groupAttrs['data-edge-label'];
352
+ labels.push({
353
+ relation: owner,
354
+ relationIndex: owner.index,
355
+ label: groupAttrs['data-edge-label'] || '',
356
+ rect,
357
+ });
358
+ }
359
+ return labels;
360
+ }
361
+
362
+ function roundedRect(rect) {
363
+ return Object.fromEntries(Object.entries(rect).map(([key, value]) => [key, Math.round(value * 10) / 10]));
364
+ }
365
+
366
+ function roundedRouteMetrics(metrics) {
367
+ return {
368
+ ...metrics,
369
+ maxStretch: metrics.maxStretch == null ? null : Math.round(metrics.maxStretch * 1000) / 1000,
370
+ minSegmentPx: metrics.minSegmentPx == null ? null : Math.round(metrics.minSegmentPx * 10) / 10,
371
+ minInteriorSegmentPx: metrics.minInteriorSegmentPx == null ? null : Math.round(metrics.minInteriorSegmentPx * 10) / 10,
372
+ };
373
+ }
374
+
375
+ function parseRoutePoints(value) {
376
+ if (!value) return null;
377
+ const points = value.split(';').map((pair) => pair.split(',').map(Number));
378
+ return points.length >= 2 && points.every(isPoint) ? points : null;
379
+ }
380
+
381
+ function collectRelationshipCrossings(arrows) {
382
+ const relationships = arrows.filter((arrow) => arrow.from && arrow.to && arrow.segments.length);
383
+ const crossings = [];
384
+ for (let leftIndex = 0; leftIndex < relationships.length; leftIndex += 1) {
385
+ const left = relationships[leftIndex];
386
+ for (let rightIndex = leftIndex + 1; rightIndex < relationships.length; rightIndex += 1) {
387
+ const right = relationships[rightIndex];
388
+ if ([left.from, left.to].some((id) => id === right.from || id === right.to)) continue;
389
+ let point = null;
390
+ for (const leftSegment of left.segments) {
391
+ for (const rightSegment of right.segments) {
392
+ point = properSegmentIntersection(leftSegment.start, leftSegment.end, rightSegment.start, rightSegment.end);
393
+ if (point) break;
394
+ }
395
+ if (point) break;
396
+ }
397
+ if (point) crossings.push({ left, right, point });
398
+ }
399
+ }
400
+ return crossings;
401
+ }
402
+
403
+ function relationshipName(arrow) {
404
+ return arrow.id
405
+ ? `relationship id "${arrow.id}" ("${arrow.from}" -> "${arrow.to}")`
406
+ : `relationship "${arrow.from}" -> "${arrow.to}"`;
407
+ }
408
+
409
+ function relationshipRecord(arrow) {
410
+ const stableIndex = Number(arrow.key);
411
+ return {
412
+ id: arrow.id,
413
+ from: arrow.from,
414
+ to: arrow.to,
415
+ label: arrow.label || '',
416
+ collectionIndex: Number.isInteger(stableIndex) && stableIndex >= 0 ? stableIndex : arrow.index - 1,
417
+ artifactIndex: arrow.index,
418
+ };
419
+ }
420
+
421
+ function collectCompositionFrames(fragment) {
422
+ const frames = [];
423
+ for (const match of fragment.matchAll(/<(rect|path|line)\b[^>]*>/gi)) {
424
+ const attrs = parseAttrs(match[0]);
425
+ const kind = attrs['data-composition-frame-kind'];
426
+ if (!kind) continue;
427
+ const identity = attrs['data-composition-frame-id'] || frames.length;
428
+ if (match[1].toLowerCase() === 'rect') {
429
+ const frame = {
430
+ kind,
431
+ id: identity,
432
+ x: numberAttr(attrs, 'x'),
433
+ y: numberAttr(attrs, 'y'),
434
+ width: numberAttr(attrs, 'width'),
435
+ height: numberAttr(attrs, 'height'),
436
+ radius: numberAttr(attrs, 'rx') || 0,
437
+ };
438
+ if ([frame.x, frame.y, frame.width, frame.height].every(Number.isFinite)) frames.push(frame);
439
+ continue;
440
+ }
441
+ const segments = match[1].toLowerCase() === 'line'
442
+ ? lineSegments(attrs)
443
+ : pathSegments(attrs.d || '');
444
+ for (const [segmentIndex, segment] of segments.entries()) {
445
+ frames.push({
446
+ kind,
447
+ id: segments.length > 1 ? `${identity}:${segmentIndex}` : identity,
448
+ shape: 'line',
449
+ start: segment.start,
450
+ end: segment.end,
451
+ });
452
+ }
453
+ }
454
+ return frames;
455
+ }
456
+
457
+ function frameName(frame) {
458
+ return `${frame.kind || 'frame'} "${frame.id}"`;
459
+ }
460
+
461
+ function frameRecord(frame) {
462
+ return { kind: frame.kind, id: frame.id };
463
+ }
464
+
465
+ function formatPoint(point) {
466
+ return point.map((value) => Math.round(value * 10) / 10).join(', ');
467
+ }
468
+
84
469
  function lineSegments(attrs) {
85
470
  const start = [numberAttr(attrs, 'x1'), numberAttr(attrs, 'y1')];
86
471
  const end = [numberAttr(attrs, 'x2'), numberAttr(attrs, 'y2')];
@@ -97,10 +482,85 @@ function pathSegments(d) {
97
482
  return segments;
98
483
  }
99
484
 
100
- function isTwoPointDiagonal(arrow) {
101
- if (arrow.segments.length !== 1) return false;
102
- const { start, end } = arrow.segments[0];
103
- return Math.abs(start[0] - end[0]) > 0.01 && Math.abs(start[1] - end[1]) > 0.01;
485
+ // Border runs use exact visible primitives. Non-collinear Q curves are never
486
+ // flattened into chords here: a tangent or sampled near-horizontal curve is
487
+ // not a structural border run. A fully collinear Q remains a straight visible
488
+ // primitive and is included.
489
+ function straightPathSegments(d) {
490
+ const tokens = d.match(/[MLHVQZmlhvqz]|[-+]?(?:\d*\.)?\d+(?:e[-+]?\d+)?/g) || [];
491
+ const segments = [];
492
+ let i = 0;
493
+ let command = '';
494
+ let current = [0, 0];
495
+ let start = null;
496
+ while (i < tokens.length) {
497
+ if (isCommand(tokens[i])) command = tokens[i++];
498
+ if (!command) break;
499
+ const absolute = command === command.toUpperCase();
500
+ switch (command.toUpperCase()) {
501
+ case 'M':
502
+ case 'L': {
503
+ let first = true;
504
+ while (i + 1 < tokens.length && !isCommand(tokens[i])) {
505
+ const point = [Number.parseFloat(tokens[i++]), Number.parseFloat(tokens[i++])];
506
+ if (!point.every(Number.isFinite)) break;
507
+ const next = absolute ? point : [current[0] + point[0], current[1] + point[1]];
508
+ if (command.toUpperCase() === 'L' || !first) segments.push({ start: current, end: next });
509
+ current = next;
510
+ if (!start) start = current;
511
+ first = false;
512
+ }
513
+ break;
514
+ }
515
+ case 'H': {
516
+ while (i < tokens.length && !isCommand(tokens[i])) {
517
+ const value = Number.parseFloat(tokens[i++]);
518
+ if (!Number.isFinite(value)) break;
519
+ const next = [absolute ? value : current[0] + value, current[1]];
520
+ segments.push({ start: current, end: next });
521
+ current = next;
522
+ }
523
+ break;
524
+ }
525
+ case 'V': {
526
+ while (i < tokens.length && !isCommand(tokens[i])) {
527
+ const value = Number.parseFloat(tokens[i++]);
528
+ if (!Number.isFinite(value)) break;
529
+ const next = [current[0], absolute ? value : current[1] + value];
530
+ segments.push({ start: current, end: next });
531
+ current = next;
532
+ }
533
+ break;
534
+ }
535
+ case 'Q': {
536
+ while (i + 3 < tokens.length && !isCommand(tokens[i])) {
537
+ const values = [0, 0, 0, 0].map(() => Number.parseFloat(tokens[i++]));
538
+ if (!values.every(Number.isFinite)) break;
539
+ const control = absolute ? values.slice(0, 2) : [current[0] + values[0], current[1] + values[1]];
540
+ const end = absolute ? values.slice(2, 4) : [current[0] + values[2], current[1] + values[3]];
541
+ if (Math.abs(crossProduct(current, control, end)) <= 1e-9) segments.push({ start: current, end });
542
+ current = end;
543
+ }
544
+ break;
545
+ }
546
+ case 'Z':
547
+ if (start) segments.push({ start: current, end: start });
548
+ current = start || current;
549
+ command = '';
550
+ break;
551
+ default:
552
+ return [];
553
+ }
554
+ }
555
+ return segments.filter(({ start: a, end: b }) => isPoint(a) && isPoint(b));
556
+ }
557
+
558
+ function diagonalStraightSegments(arrow) {
559
+ return arrow.borderSegments.flatMap(({ start, end }, segmentIndex) => (
560
+ Math.abs(start[0] - end[0]) > 0.01 && Math.abs(start[1] - end[1]) > 0.01
561
+ ? [{ segmentIndex, start, end }]
562
+ : []
563
+ ));
104
564
  }
105
565
 
106
566
  function collectLegendBoxes(fragment) {
@@ -159,6 +619,37 @@ function textBox(attrs, text) {
159
619
  };
160
620
  }
161
621
 
622
+ function collectDesktopReadability(svgAttrs, fragment) {
623
+ const viewBox = String(svgAttrs.viewBox || '').trim().split(/[\s,]+/).map(Number);
624
+ const viewBoxWidth = viewBox.length === 4 ? viewBox[2] : Number.NaN;
625
+ if (!Number.isFinite(viewBoxWidth) || viewBoxWidth <= 0) return null;
626
+ const scale = Math.min(1, DESKTOP_READER_DIAGRAM_WIDTH / viewBoxWidth);
627
+ let worst = null;
628
+ for (const match of fragment.matchAll(/<text\b([^>]*)>([\s\S]*?)<\/text>/gi)) {
629
+ const primary = /\bdata-node-label(?:\s*=|\s|$)/i.test(match[1]);
630
+ const boundary = /\bdata-boundary-label(?:\s*=|\s|$)/i.test(match[1]);
631
+ const context = /\bdata-detail\s*=\s*"context"/i.test(match[1]);
632
+ if (!primary && !boundary && !context) continue;
633
+ const attrs = parseAttrs(match[1]);
634
+ const fontSize = Number.parseFloat(attrs['font-size'] || '');
635
+ if (!Number.isFinite(fontSize)) continue;
636
+ const projected = projectedNodeTextPx(fontSize, viewBoxWidth);
637
+ if (projected >= MIN_PROJECTED_NODE_TEXT_PX) continue;
638
+ const candidate = {
639
+ viewBoxWidth,
640
+ scale,
641
+ text: stripTags(match[2]).trim(),
642
+ detail: primary
643
+ ? 'primary'
644
+ : boundary ? 'boundary' : 'context',
645
+ sourceFontPx: fontSize,
646
+ projectedFontPx: projected,
647
+ };
648
+ if (!worst || candidate.projectedFontPx < worst.projectedFontPx) worst = candidate;
649
+ }
650
+ return worst;
651
+ }
652
+
162
653
  function estimatedTextWidth(text, fontSize) {
163
654
  let units = 0;
164
655
  for (const char of text) units += char.charCodeAt(0) > 255 ? 1.8 : 0.62;
@@ -166,7 +657,7 @@ function estimatedTextWidth(text, fontSize) {
166
657
  }
167
658
 
168
659
  function pointsFromPath(d) {
169
- const tokens = d.match(/[MLHVZmlhvz]|[-+]?(?:\d*\.)?\d+(?:e[-+]?\d+)?/g) || [];
660
+ const tokens = d.match(/[MLHVQZmlhvqz]|[-+]?(?:\d*\.)?\d+(?:e[-+]?\d+)?/g) || [];
170
661
  const points = [];
171
662
  let i = 0;
172
663
  let command = '';
@@ -209,6 +700,32 @@ function pointsFromPath(d) {
209
700
  }
210
701
  break;
211
702
  }
703
+ case 'Q': {
704
+ while (i + 3 < tokens.length && !isCommand(tokens[i])) {
705
+ const controlX = Number.parseFloat(tokens[i++]);
706
+ const controlY = Number.parseFloat(tokens[i++]);
707
+ const endX = Number.parseFloat(tokens[i++]);
708
+ const endY = Number.parseFloat(tokens[i++]);
709
+ if (![controlX, controlY, endX, endY].every(Number.isFinite)) break;
710
+ const control = absolute
711
+ ? [controlX, controlY]
712
+ : [current[0] + controlX, current[1] + controlY];
713
+ const end = absolute
714
+ ? [endX, endY]
715
+ : [current[0] + endX, current[1] + endY];
716
+ const startPoint = current;
717
+ for (let step = 1; step <= 8; step += 1) {
718
+ const amount = step / 8;
719
+ const remaining = 1 - amount;
720
+ points.push([
721
+ remaining * remaining * startPoint[0] + 2 * remaining * amount * control[0] + amount * amount * end[0],
722
+ remaining * remaining * startPoint[1] + 2 * remaining * amount * control[1] + amount * amount * end[1],
723
+ ]);
724
+ }
725
+ current = end;
726
+ }
727
+ break;
728
+ }
212
729
  case 'Z': {
213
730
  if (start) points.push(start);
214
731
  break;
@@ -221,6 +738,28 @@ function pointsFromPath(d) {
221
738
  return points.filter(isPoint);
222
739
  }
223
740
 
741
+ function properSegmentIntersection(a, b, c, d) {
742
+ const abC = crossProduct(a, b, c);
743
+ const abD = crossProduct(a, b, d);
744
+ const cdA = crossProduct(c, d, a);
745
+ const cdB = crossProduct(c, d, b);
746
+ const epsilon = 1e-9;
747
+ const opposite = (left, right) => (left > epsilon && right < -epsilon) || (left < -epsilon && right > epsilon);
748
+ if (!opposite(abC, abD) || !opposite(cdA, cdB)) return null;
749
+ const denominator = (a[0] - b[0]) * (c[1] - d[1]) - (a[1] - b[1]) * (c[0] - d[0]);
750
+ if (Math.abs(denominator) < epsilon) return null;
751
+ const ab = a[0] * b[1] - a[1] * b[0];
752
+ const cd = c[0] * d[1] - c[1] * d[0];
753
+ return [
754
+ (ab * (c[0] - d[0]) - (a[0] - b[0]) * cd) / denominator,
755
+ (ab * (c[1] - d[1]) - (a[1] - b[1]) * cd) / denominator,
756
+ ];
757
+ }
758
+
759
+ function crossProduct(a, b, c) {
760
+ return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
761
+ }
762
+
224
763
  function segmentIntersectsBox(segment, box) {
225
764
  const { start, end } = segment;
226
765
  if (pointInsideBox(start, box) || pointInsideBox(end, box)) return true;
@@ -4,6 +4,7 @@
4
4
  import { execFileSync } from 'node:child_process';
5
5
  import path from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
+ import { runtimeArgs } from '../renderers/shared/bun-runtime.mjs';
7
8
 
8
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
10
  const skillRoot = path.resolve(__dirname, '..');
@@ -18,11 +19,9 @@ const TARGETS = [
18
19
  ];
19
20
 
20
21
  for (const [mode, input, output] of TARGETS) {
21
- execFileSync(process.execPath, [
22
- `--config=${path.join(skillRoot, 'bunfig.toml')}`,
23
- '--no-env-file',
22
+ execFileSync(process.execPath, runtimeArgs([
24
23
  path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
25
24
  path.join(skillRoot, 'examples', input),
26
25
  path.join(outputRoot, output),
27
- ], { stdio: 'inherit' });
26
+ ]), { stdio: 'inherit' });
28
27
  }
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.74.41",
5
+ "version": "0.74.42",
6
6
  "description": "A modern Stacks development environment.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",