astro-archify 0.3.4

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 (30) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/astro-archify-integration.d.ts +100 -0
  4. package/astro-archify-integration.js +0 -0
  5. package/package.json +64 -0
  6. package/vendor/archify/LICENSE +22 -0
  7. package/vendor/archify/NOTICE.md +48 -0
  8. package/vendor/archify/assets/template.html +14787 -0
  9. package/vendor/archify/renderers/architecture/grid.mjs +62 -0
  10. package/vendor/archify/renderers/architecture/render-architecture.mjs +1089 -0
  11. package/vendor/archify/renderers/dataflow/render-dataflow.mjs +482 -0
  12. package/vendor/archify/renderers/lifecycle/render-lifecycle.mjs +570 -0
  13. package/vendor/archify/renderers/sequence/render-sequence.mjs +468 -0
  14. package/vendor/archify/renderers/shared/brand-marks.mjs +563 -0
  15. package/vendor/archify/renderers/shared/cli.mjs +220 -0
  16. package/vendor/archify/renderers/shared/desktop-readability.mjs +26 -0
  17. package/vendor/archify/renderers/shared/diagnostics.mjs +116 -0
  18. package/vendor/archify/renderers/shared/engineering-profiles.mjs +157 -0
  19. package/vendor/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
  20. package/vendor/archify/renderers/shared/generated-validators.mjs +13 -0
  21. package/vendor/archify/renderers/shared/geometry.mjs +1334 -0
  22. package/vendor/archify/renderers/shared/i18n.mjs +594 -0
  23. package/vendor/archify/renderers/shared/layout-report.mjs +40 -0
  24. package/vendor/archify/renderers/shared/legend.mjs +217 -0
  25. package/vendor/archify/renderers/shared/output-path.mjs +321 -0
  26. package/vendor/archify/renderers/shared/repository-evidence.mjs +235 -0
  27. package/vendor/archify/renderers/shared/text-fit.mjs +49 -0
  28. package/vendor/archify/renderers/shared/utils.mjs +232 -0
  29. package/vendor/archify/renderers/shared/validator.mjs +86 -0
  30. package/vendor/archify/renderers/workflow/render-workflow.mjs +749 -0
@@ -0,0 +1,468 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { esc, renderDefinitions, renderSemanticSigil, textUnits } from '../shared/utils.mjs';
4
+ import { animateAttr, focusEdgeAttrs, focusNodeAttrs, focusNodeTitle, loadDiagramWithBrandMarks, writeDiagram, svgAccessibleText, svgRootAttrs } from '../shared/cli.mjs';
5
+ import { throwDiagnosticProblems } from '../shared/diagnostics.mjs';
6
+ import { resolveLegend, renderLegend as renderResolvedLegend } from '../shared/legend.mjs';
7
+ import { componentFill, arrowClassMap, rectsOverlap, cleanFlowProblems, cleanCrossingProblems, cleanAmbiguousCorridorProblems, cleanBorderRunProblems, cleanRouteRhythmProblems, cleanLabelRouteClearanceProblems, routePointsValue, asArray, isFinitePoint } from '../shared/geometry.mjs';
8
+ import { availableNodeTextWidth, fittedNodeFontSize, minimumNodeTextWidth } from '../shared/text-fit.mjs';
9
+ import { brandLabelFitWidth, brandMetadataFor, brandTopRailProblem, renderBrandMark } from '../shared/brand-marks.mjs';
10
+ import { translateMessage as i18nText } from '../shared/i18n.mjs';
11
+
12
+ const participantTextFit = {
13
+ sublabelPreferred: 7,
14
+ sublabelMinimum: 6,
15
+ };
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+ const { diagram: sequence, template, outPath } = await loadDiagramWithBrandMarks({
19
+ rendererDir: __dirname,
20
+ diagramType: 'sequence',
21
+ defaultExample: 'cache-miss-request.sequence.json'
22
+ });
23
+
24
+ const viewBox = sequence.meta?.viewBox || [920, 760];
25
+ // The timeline scales with viewBox height: a taller viewBox gains message room,
26
+ // a shorter one shrinks the readable band (validated below) instead of clipping.
27
+ // `column_fit: "spread"` widens the lanes with the viewBox instead of keeping
28
+ // the fixed 108px gap, so a wide canvas gains column distance and label room
29
+ // rather than dead space on the right. The default stays "fixed" so existing
30
+ // diagrams keep their coordinates.
31
+ const columnFit = sequence.meta?.column_fit === 'spread' ? 'spread' : 'fixed';
32
+ const participantCount = Math.max(1, asArray(sequence.participants).length);
33
+ const sideMargin = 62;
34
+ const participantW = columnFit === 'spread'
35
+ ? Math.max(86, Math.min(190, Math.round((viewBox[0] - sideMargin * 2) / participantCount) - 24))
36
+ : 86;
37
+ const colGap = columnFit === 'spread' && participantCount > 1
38
+ ? Math.max(108, (viewBox[0] - 40 - sideMargin - participantW) / (participantCount - 1))
39
+ : 108;
40
+
41
+ const layout = {
42
+ topY: 72,
43
+ participantW,
44
+ participantH: 54,
45
+ lifelineTop: 142,
46
+ lifelineBottom: viewBox[1] - 65,
47
+ legendY: viewBox[1] - 54,
48
+ leftX: columnFit === 'spread' ? sideMargin + participantW / 2 : sideMargin,
49
+ colGap,
50
+ labelH: 16
51
+ };
52
+
53
+ const participantBoxWidthNote = columnFit === 'spread'
54
+ ? `participant boxes are ${participantW}px for this viewBox width and ${participantCount} participants`
55
+ : `participant boxes are a fixed ${participantW}px unless meta.column_fit is "spread"`;
56
+
57
+ const arrowClass = {
58
+ ...arrowClassMap,
59
+ return: ['a-default', 'arrowhead']
60
+ };
61
+
62
+ function participantX(index) {
63
+ return layout.leftX + index * layout.colGap;
64
+ }
65
+
66
+ const participants = new Map(asArray(sequence.participants).map((participant, index) => [
67
+ participant.id,
68
+ {
69
+ ...participant,
70
+ index,
71
+ cx: participantX(index),
72
+ x: participantX(index) - layout.participantW / 2,
73
+ y: layout.topY,
74
+ width: layout.participantW,
75
+ height: layout.participantH,
76
+ cy: layout.topY + layout.participantH / 2
77
+ }
78
+ ]));
79
+
80
+ function messageGeometry(message) {
81
+ const from = participants.get(message.from);
82
+ const to = participants.get(message.to);
83
+ if (!from || !to || typeof message.y !== 'number') return null;
84
+ const direction = to.cx > from.cx ? 1 : -1;
85
+ const start = from.cx + direction * 7;
86
+ const end = to.cx - direction * 7;
87
+ return { start, end, center: (start + end) / 2 };
88
+ }
89
+
90
+ function messageLabelBox(message, relationIndex = null) {
91
+ const geometry = messageGeometry(message);
92
+ if (!geometry) return null;
93
+ const width = Math.max(34, textUnits(message.label) * 5.2 + 12);
94
+ return {
95
+ relation: message,
96
+ relationIndex,
97
+ label: message.label,
98
+ x: geometry.center - width / 2,
99
+ y: message.y - 20,
100
+ width,
101
+ height: layout.labelH,
102
+ };
103
+ }
104
+
105
+ function messageRouteBox(message) {
106
+ const geometry = messageGeometry(message);
107
+ if (!geometry) return null;
108
+ return {
109
+ x: Math.min(geometry.start, geometry.end),
110
+ y: message.y - 2,
111
+ width: Math.abs(geometry.end - geometry.start),
112
+ height: 4,
113
+ };
114
+ }
115
+
116
+ const compositionFrames = asArray(sequence.segments).map((segment, index) => ({
117
+ id: index,
118
+ label: segment.label,
119
+ kind: 'segment',
120
+ x: 48,
121
+ y: segment.from,
122
+ width: viewBox[0] - 96,
123
+ height: segment.to - segment.from,
124
+ radius: 10,
125
+ }));
126
+
127
+ function messagePath(message) {
128
+ return {
129
+ points: participants.has(message.from) && participants.has(message.to)
130
+ ? [[participants.get(message.from).cx, message.y], [participants.get(message.to).cx, message.y]]
131
+ : []
132
+ };
133
+ }
134
+
135
+ function validateSequence() {
136
+ const problems = [];
137
+ if (sequence.schema_version !== 1) problems.push('Sequence files must set "schema_version": 1.');
138
+ if (sequence.diagram_type !== 'sequence') problems.push('Sequence files must set "diagram_type": "sequence".');
139
+ if (!sequence.meta?.title) problems.push('Sequence files must include meta.title.');
140
+ if (!Array.isArray(sequence.participants) || sequence.participants.length < 2) {
141
+ problems.push('Sequence diagrams need at least two participants.');
142
+ }
143
+ if (participants.size !== asArray(sequence.participants).length) problems.push('Participant ids must be unique.');
144
+ if (!Array.isArray(sequence.messages) || sequence.messages.length < 1) {
145
+ problems.push('Sequence diagrams need at least one message.');
146
+ }
147
+ if (sequence.cards !== undefined && !Array.isArray(sequence.cards)) problems.push('Sequence "cards" must be an array.');
148
+ for (const arr of ['segments', 'activations']) {
149
+ if (sequence[arr] !== undefined && !Array.isArray(sequence[arr])) problems.push(`Sequence "${arr}" must be an array.`);
150
+ }
151
+
152
+ if (layout.lifelineBottom - layout.lifelineTop < 120) {
153
+ problems.push(`viewBox height ${viewBox[1]} leaves under 120px of timeline — set meta.viewBox[1] to at least ${layout.lifelineTop + 120 + 65}.`);
154
+ }
155
+
156
+ for (const participant of participants.values()) {
157
+ const estLabelW = textUnits(participant.label) * 6.8;
158
+ if (estLabelW > layout.participantW + 6) {
159
+ problems.push(`Label "${participant.label}" (~${Math.round(estLabelW)}px) is wider than the ${layout.participantW}px participant box — shorten it.`);
160
+ }
161
+ const brandRailProblem = brandTopRailProblem(participant, layout.participantW, 8, 'Participant');
162
+ if (brandRailProblem) problems.push(brandRailProblem);
163
+ // sublabel renders as a single unwrapped <text>; shrink-to-fit handles the
164
+ // ordinary case, this rejects what it cannot rescue.
165
+ if (participant.sublabel) {
166
+ const availableTextW = availableNodeTextWidth(layout.participantW);
167
+ const minimumW = minimumNodeTextWidth(participant.sublabel, participantTextFit.sublabelMinimum);
168
+ if (minimumW > availableTextW) {
169
+ problems.push(`Sublabel "${participant.sublabel}" needs ~${Math.ceil(minimumW)}px at the ${participantTextFit.sublabelMinimum}px legible minimum, but participant "${participant.id}" provides ${availableTextW}px — shorten the sublabel (${participantBoxWidthNote}).`);
170
+ }
171
+ }
172
+ }
173
+
174
+ for (const message of asArray(sequence.messages)) {
175
+ if (!participants.has(message.from)) problems.push(`Message "${message.label}" references unknown source "${message.from}".`);
176
+ if (!participants.has(message.to)) problems.push(`Message "${message.label}" references unknown target "${message.to}".`);
177
+ if (typeof message.y !== 'number') problems.push(`Message "${message.label}" must provide a numeric y.`);
178
+ if (message.y < layout.lifelineTop + 18 || message.y > layout.lifelineBottom - 18) {
179
+ problems.push(`Message "${message.label}" sits outside the readable timeline — keep y between ${layout.lifelineTop + 18} and ${layout.lifelineBottom - 18}.`);
180
+ }
181
+ if (participants.has(message.from) && participants.has(message.to)) {
182
+ const distance = Math.abs(participants.get(message.to).cx - participants.get(message.from).cx);
183
+ if (distance < 60) problems.push(`Message "${message.label}" spans ${Math.round(distance)}px (minimum 60px) — give its participants more column distance.`);
184
+ }
185
+ }
186
+
187
+ // Participant headers are opaque nodes. Lifelines, activation bars, and
188
+ // segment bands remain intentional pass-through geometry and are excluded.
189
+ problems.push(...cleanFlowProblems({
190
+ relations: sequence.messages,
191
+ endpointIds: new Set(participants.keys()),
192
+ obstacles: participants.values(),
193
+ pathFor: messagePath,
194
+ diagramType: 'sequence',
195
+ relationCollection: 'messages',
196
+ obstacleKind: 'participant header',
197
+ profile: sequence.meta?.quality_profile,
198
+ clearance: 0,
199
+ routeHint: 'move the message y below the participant headers or reorder participants'
200
+ }));
201
+ problems.push(...cleanCrossingProblems({
202
+ relations: sequence.messages,
203
+ endpointIds: new Set(participants.keys()),
204
+ pathFor: messagePath,
205
+ diagramType: 'sequence',
206
+ relationCollection: 'messages',
207
+ profile: sequence.meta?.quality_profile,
208
+ routeHint: 'separate the message y values; lifeline crossings remain allowed'
209
+ }));
210
+ problems.push(...cleanAmbiguousCorridorProblems({
211
+ relations: sequence.messages,
212
+ endpointIds: new Set(participants.keys()),
213
+ pathFor: messagePath,
214
+ diagramType: 'sequence',
215
+ relationCollection: 'messages',
216
+ profile: sequence.meta?.quality_profile,
217
+ routeHint: 'separate the message y values so unrelated messages do not visually merge'
218
+ }));
219
+ problems.push(...cleanBorderRunProblems({
220
+ relations: sequence.messages,
221
+ endpointIds: new Set(participants.keys()),
222
+ frames: compositionFrames,
223
+ pathFor: messagePath,
224
+ diagramType: 'sequence',
225
+ relationCollection: 'messages',
226
+ profile: sequence.meta?.quality_profile,
227
+ routeHint: 'move the message y so it crosses a segment boundary perpendicularly or stays clearly inside the segment'
228
+ }));
229
+ problems.push(...cleanRouteRhythmProblems({
230
+ relations: sequence.messages,
231
+ endpointIds: new Set(participants.keys()),
232
+ pathFor: messagePath,
233
+ diagramType: 'sequence',
234
+ relationCollection: 'messages',
235
+ profile: sequence.meta?.quality_profile,
236
+ routeHint: 'increase participant spacing or simplify message routing so every turn has room to read'
237
+ }));
238
+
239
+ // Vertical crowding only matters when the arrows share horizontal space;
240
+ // disjoint arrows may legitimately run in parallel rows.
241
+ const placed = asArray(sequence.messages)
242
+ .filter((m) => participants.has(m.from) && participants.has(m.to))
243
+ .map((m) => ({
244
+ label: m.label,
245
+ y: m.y,
246
+ x1: Math.min(participants.get(m.from).cx, participants.get(m.to).cx),
247
+ x2: Math.max(participants.get(m.from).cx, participants.get(m.to).cx)
248
+ }))
249
+ .sort((a, b) => a.y - b.y);
250
+ for (let i = 0; i < placed.length; i += 1) {
251
+ for (let j = i + 1; j < placed.length && placed[j].y - placed[i].y < 28; j += 1) {
252
+ if (placed[i].x1 < placed[j].x2 && placed[j].x1 < placed[i].x2) {
253
+ problems.push(`Messages "${placed[i].label}" and "${placed[j].label}" are less than 28px apart and share horizontal space — spread their y values.`);
254
+ }
255
+ }
256
+ }
257
+
258
+ // Label masks can extend well past the arrow span, so check the actual
259
+ // label rectangles too — tangent arrows with long labels still collide.
260
+ const labelRects = asArray(sequence.messages)
261
+ .map((m, messageIndex) => messageLabelBox(m, messageIndex))
262
+ .filter(Boolean);
263
+ for (let i = 0; i < labelRects.length; i += 1) {
264
+ for (let j = i + 1; j < labelRects.length; j += 1) {
265
+ if (rectsOverlap(labelRects[i], labelRects[j], -2)) {
266
+ problems.push(`Labels "${labelRects[i].label}" and "${labelRects[j].label}" overlap — spread their message y values or shorten the labels.`);
267
+ }
268
+ }
269
+ }
270
+ problems.push(...cleanLabelRouteClearanceProblems({
271
+ relations: sequence.messages,
272
+ labels: labelRects,
273
+ endpointIds: new Set(participants.keys()),
274
+ pathFor: messagePath,
275
+ diagramType: 'sequence',
276
+ relationCollection: 'messages',
277
+ profile: sequence.meta?.quality_profile,
278
+ routeHint: 'spread the message y values, shorten the label, or reorder participants so the adjacent route stays visible'
279
+ }));
280
+
281
+ for (const segment of asArray(sequence.segments)) {
282
+ if (segment.to <= segment.from) {
283
+ problems.push(`Segment "${segment.label}" has invalid y range (from ${segment.from} to ${segment.to}) — "to" must be greater than "from".`);
284
+ }
285
+ if (segment.from < layout.topY || segment.to > layout.lifelineBottom + 20) {
286
+ problems.push(`Segment "${segment.label}" extends outside the canvas — keep its y range between ${layout.topY} and ${layout.lifelineBottom + 20}.`);
287
+ }
288
+ }
289
+
290
+ for (const activation of asArray(sequence.activations)) {
291
+ if (!participants.has(activation.participant)) problems.push(`Activation references unknown participant "${activation.participant}".`);
292
+ if (activation.to <= activation.from) problems.push(`Activation for "${activation.participant}" has invalid time range — "to" must be greater than "from".`);
293
+ }
294
+
295
+ const lastParticipant = asArray(sequence.participants)[asArray(sequence.participants).length - 1];
296
+ if (lastParticipant && participants.get(lastParticipant.id).cx + layout.participantW / 2 > viewBox[0] - 40) {
297
+ const requiredWidth = Math.ceil(participants.get(lastParticipant.id).cx + layout.participantW / 2 + 40);
298
+ problems.push(`Participants exceed viewBox width — set meta.viewBox[0] to at least ${requiredWidth} or remove a participant.`);
299
+ }
300
+
301
+ if (problems.length) {
302
+ throwDiagnosticProblems('Sequence layout validation failed', problems, {
303
+ subject: { diagramType: 'sequence' },
304
+ });
305
+ }
306
+ }
307
+
308
+ function renderParticipant(participant) {
309
+ const fill = componentFill[participant.type] || 'c-external';
310
+ const hasSub = participant.sublabel != null && participant.sublabel !== '';
311
+ const sub = hasSub
312
+ ? `\n <text data-detail="context" x="${participant.cx}" y="${layout.topY + 39}" class="t-muted" font-size="${fittedNodeFontSize(participant.sublabel, layout.participantW, participantTextFit.sublabelPreferred, participantTextFit.sublabelMinimum)}" text-anchor="middle">${esc(participant.sublabel)}</text>`
313
+ : '';
314
+ const brand = renderBrandMark(participant, { x: participant.x + layout.participantW - 22, y: layout.topY + 6 });
315
+ const labelFontSize = fittedNodeFontSize(participant.label, brandLabelFitWidth(participant, layout.participantW), 11, 8);
316
+ const passport = {
317
+ kind: participant.type,
318
+ sublabel: participant.sublabel,
319
+ context: i18nText(sequence.meta.locale, 'node.context.sequence'),
320
+ ...brandMetadataFor(participant),
321
+ };
322
+ return ` <g ${focusNodeAttrs(participant.id, participant.label, passport, sequence.meta.locale)}>
323
+ ${focusNodeTitle(participant.label, passport)}
324
+ <rect x="${participant.x}" y="${layout.topY}" width="${layout.participantW}" height="${layout.participantH}" rx="6" class="c-mask"/>
325
+ <rect x="${participant.x}" y="${layout.topY}" width="${layout.participantW}" height="${layout.participantH}" rx="6" class="${fill}"${animateAttr(sequence.meta, 'node', participant.index)} stroke-width="1.5"/>
326
+ ${renderSemanticSigil(participant.type, { x: participant.x + 6, y: layout.topY + 6 })}${brand ? `\n ${brand}` : ''}
327
+ <text data-node-label=""${hasSub ? ' data-detail-anchor=""' : ''} x="${participant.cx}" y="${layout.topY + 22}" class="t-primary" font-size="${labelFontSize}" font-weight="600" text-anchor="middle">${esc(participant.label)}</text>${sub}
328
+ </g>`;
329
+ }
330
+
331
+ function renderLifeline(participant) {
332
+ return ` <path d="M ${participant.cx} ${layout.lifelineTop} L ${participant.cx} ${layout.lifelineBottom}" class="a-default" stroke-width="0.8" stroke-dasharray="3,7"/>`;
333
+ }
334
+
335
+ function renderSegment(segment, index) {
336
+ return ` <rect data-graph-role="structural-frame" data-composition-frame-kind="segment" data-composition-frame-id="${index}" x="48" y="${segment.from}" width="${viewBox[0] - 96}" height="${segment.to - segment.from}" rx="10" class="c-lane" stroke-width="1"/>`;
337
+ }
338
+
339
+ function renderSegmentLabel(segment, index) {
340
+ const labelW = Math.max(42, textUnits(segment.label) * 5.2 + 14);
341
+ const occupied = asArray(sequence.messages)
342
+ .flatMap((message) => [messageLabelBox(message), messageRouteBox(message)])
343
+ .filter(Boolean);
344
+ const label = { x: 56, y: segment.from - 22, width: labelW, height: 18 };
345
+ for (let attempt = 0; attempt < 4; attempt += 1) {
346
+ if (!occupied.some((rect) => rectsOverlap(label, rect, 2))) break;
347
+ label.y -= 22;
348
+ }
349
+ return ` <g data-graph-role="segment-label" data-segment-id="${index}">
350
+ <rect x="${label.x}" y="${label.y}" width="${label.width}" height="${label.height}" rx="3" class="c-mask"/>
351
+ <text x="${label.x + 6}" y="${label.y + 13}" class="t-dim" font-size="9" font-weight="600">${esc(segment.label)}</text>
352
+ </g>`;
353
+ }
354
+
355
+ function renderActivation(activation) {
356
+ const participant = participants.get(activation.participant);
357
+ const fill = componentFill[activation.type] || componentFill[participant.type] || 'c-external';
358
+ const x = participant.cx - 5;
359
+ const height = activation.to - activation.from;
360
+ return ` <rect x="${x}" y="${activation.from}" width="10" height="${height}" rx="3" class="c-mask"/>
361
+ <rect x="${x}" y="${activation.from}" width="10" height="${height}" rx="3" class="${fill}" stroke-width="1"/>`;
362
+ }
363
+
364
+ function messageLabel(message, x1, x2) {
365
+ const box = messageLabelBox(message);
366
+ const center = box ? box.x + box.width / 2 : (x1 + x2) / 2;
367
+ const y = message.y - 10;
368
+ const labelW = box?.width || Math.max(34, textUnits(message.label) * 5.2 + 12);
369
+ const accent = message.variant === 'security'
370
+ ? 't-security'
371
+ : message.variant === 'dashed'
372
+ ? 't-messagebus'
373
+ : message.variant === 'return'
374
+ ? 't-muted'
375
+ : 't-backend';
376
+ return ` <g data-detail="context">
377
+ <rect x="${center - labelW / 2}" y="${y - 10}" width="${labelW}" height="${layout.labelH}" rx="3" class="c-mask"/>
378
+ <text x="${center}" y="${y}" class="${accent}" font-size="9" text-anchor="middle">${esc(message.label)}</text>
379
+ </g>`;
380
+ }
381
+
382
+ function renderMessage(message, index) {
383
+ const { start, end } = messageGeometry(message);
384
+ const [cls, marker] = arrowClass[message.variant || 'default'] || arrowClass.default;
385
+ const strokeWidth = message.variant === 'emphasis' ? 1.8 : 1.4;
386
+ const dash = message.variant === 'return' ? ' stroke-dasharray="3,5"' : '';
387
+ const note = message.note
388
+ ? `\n <text data-detail="fine" x="${Math.min(start, end) + 12}" y="${message.y + 18}" class="t-dim" font-size="7">${esc(message.note)}</text>`
389
+ : '';
390
+ return ` <g ${focusEdgeAttrs(message.from, message.to, message.label, index, message.id)}>
391
+ <path data-composition-edge-from="${esc(message.from)}" data-composition-edge-to="${esc(message.to)}"${message.id ? ` data-composition-edge-id="${esc(message.id)}"` : ''} data-composition-points="${routePointsValue([[start, message.y], [end, message.y]])}" d="M ${start} ${message.y} L ${end} ${message.y}" class="${cls}"${animateAttr(sequence.meta, 'edge', index)} stroke-width="${strokeWidth}"${dash} marker-end="url(#${marker})"/>
392
+ ${messageLabel(message, start, end)}${note}
393
+ </g>`;
394
+ }
395
+
396
+ const LEGEND_CATALOG = [
397
+ { kind: 'emphasis', className: 'a-emphasis', marker: 'arrowhead-emphasis', strokeWidth: 1.8 },
398
+ { kind: 'return', className: 'a-default', marker: 'arrowhead', dash: '3,5' },
399
+ { kind: 'security', className: 'a-security', marker: 'arrowhead-security' },
400
+ { kind: 'dashed', className: 'a-dashed', marker: 'arrowhead-dashed' },
401
+ { kind: 'default', className: 'a-default', marker: 'arrowhead' },
402
+ ].map((entry) => ({
403
+ ...entry,
404
+ interactive: false,
405
+ swatchWidth: 34,
406
+ swatchGap: 9,
407
+ label: i18nText(sequence.meta.locale, `legend.sequence.${entry.kind}`),
408
+ }));
409
+
410
+ function renderLegend() {
411
+ const presentKinds = new Set(asArray(sequence.messages).map((message) => message.variant || 'default'));
412
+ const entries = resolveLegend(sequence.meta?.legend, LEGEND_CATALOG, presentKinds);
413
+ return renderResolvedLegend({
414
+ entries,
415
+ locale: sequence.meta.locale,
416
+ layout: {
417
+ x: 40,
418
+ baselineY: layout.legendY,
419
+ width: viewBox[0] - 80,
420
+ minTitleY: layout.legendY - 30,
421
+ unfit: sequence.meta?.legend === undefined ? 'hide' : 'error',
422
+ diagramType: 'sequence',
423
+ },
424
+ renderSwatch: (entry) => `<path d="M ${entry.x} ${entry.baseline - 3} L ${entry.x + 34} ${entry.baseline - 3}" class="${entry.className}" stroke-width="${entry.strokeWidth || 1.4}"${entry.dash ? ` stroke-dasharray="${entry.dash}"` : ''} marker-end="url(#${entry.marker})"/>`,
425
+ });
426
+ }
427
+
428
+ function renderSvg() {
429
+ const participantList = [...participants.values()];
430
+ return ` <svg viewBox="0 0 ${viewBox[0]} ${viewBox[1]}" ${svgRootAttrs(sequence.meta, 'sequence diagram')}>
431
+ ${svgAccessibleText(sequence.meta, 'sequence')}
432
+ ${renderDefinitions()}
433
+
434
+ <!-- Background Grid -->
435
+ <rect width="100%" height="100%" fill="url(#grid)" />
436
+
437
+ <!-- Time Segments -->
438
+ ${asArray(sequence.segments).map(renderSegment).join('\n\n')}
439
+
440
+ <!-- Lifelines -->
441
+ ${participantList.map(renderLifeline).join('\n')}
442
+
443
+ <!-- Activations -->
444
+ ${asArray(sequence.activations).map(renderActivation).join('\n')}
445
+
446
+ <!-- Messages -->
447
+ ${asArray(sequence.messages).map(renderMessage).join('\n\n')}
448
+
449
+ <!-- Segment Labels -->
450
+ ${asArray(sequence.segments).map(renderSegmentLabel).join('\n')}
451
+
452
+ <!-- Participants -->
453
+ ${participantList.map(renderParticipant).join('\n\n')}
454
+
455
+ <!-- Legend -->
456
+ ${renderLegend()}
457
+ </svg>`;
458
+ }
459
+
460
+ validateSequence();
461
+ writeDiagram({
462
+ outPath,
463
+ template,
464
+ diagramType: 'sequence',
465
+ meta: sequence.meta,
466
+ svg: renderSvg(),
467
+ cards: sequence.cards,
468
+ });