arkaos 4.32.0 → 4.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +1 -1
  2. package/THE-ARKAOS-GUIDE.md +1 -1
  3. package/VERSION +1 -1
  4. package/arka/SKILL.md +1 -1
  5. package/arka/skills/forge/references/workflows.md +10 -0
  6. package/config/claude-agents/architect.md +1 -1
  7. package/config/claude-agents/backend-dev.md +1 -1
  8. package/config/claude-agents/devops-eng.md +1 -1
  9. package/config/claude-agents/frontend-dev.md +1 -1
  10. package/config/claude-agents/ops-lead.md +1 -1
  11. package/config/claude-agents/pm-director.md +1 -1
  12. package/config/claude-agents/strategy-director.md +1 -1
  13. package/config/hooks/session-start.sh +1 -1
  14. package/config/skills-curated.yaml +2 -2
  15. package/config/skills-provenance.yaml +7 -0
  16. package/config/statusline.sh +6 -1
  17. package/core/hooks/session_start.py +69 -28
  18. package/departments/dev/agents/architect.yaml +1 -0
  19. package/departments/dev/agents/backend-dev.yaml +1 -0
  20. package/departments/dev/agents/devops-eng.yaml +1 -0
  21. package/departments/dev/agents/frontend-dev.yaml +1 -0
  22. package/departments/dev/agents/tech-lead.yaml +1 -0
  23. package/departments/dev/skills/diagram/SKILL.md +175 -0
  24. package/departments/dev/skills/diagram/examples/archify-repo-grid.architecture.json +57 -0
  25. package/departments/dev/skills/diagram/vendor/LICENSE +22 -0
  26. package/departments/dev/skills/diagram/vendor/SKILL.md +367 -0
  27. package/departments/dev/skills/diagram/vendor/assets/template.html +11470 -0
  28. package/departments/dev/skills/diagram/vendor/bin/archify.mjs +600 -0
  29. package/departments/dev/skills/diagram/vendor/bin/open-artifact.mjs +67 -0
  30. package/departments/dev/skills/diagram/vendor/examples/agent-run.lifecycle.json +71 -0
  31. package/departments/dev/skills/diagram/vendor/examples/agent-tool-call.workflow.json +111 -0
  32. package/departments/dev/skills/diagram/vendor/examples/async-job-roundtrip.sequence.json +62 -0
  33. package/departments/dev/skills/diagram/vendor/examples/cache-miss-request.sequence.json +82 -0
  34. package/departments/dev/skills/diagram/vendor/examples/deployment-release.lifecycle.json +50 -0
  35. package/departments/dev/skills/diagram/vendor/examples/event-stream.dataflow.json +58 -0
  36. package/departments/dev/skills/diagram/vendor/examples/incident-response.workflow.json +65 -0
  37. package/departments/dev/skills/diagram/vendor/examples/product-analytics.dataflow.json +77 -0
  38. package/departments/dev/skills/diagram/vendor/examples/production-deployment.architecture.json +70 -0
  39. package/departments/dev/skills/diagram/vendor/examples/release-delivery.workflow.json +63 -0
  40. package/departments/dev/skills/diagram/vendor/examples/web-app.architecture.json +47 -0
  41. package/departments/dev/skills/diagram/vendor/package-lock.json +80 -0
  42. package/departments/dev/skills/diagram/vendor/package.json +28 -0
  43. package/departments/dev/skills/diagram/vendor/recipes/scenarios.mjs +350 -0
  44. package/departments/dev/skills/diagram/vendor/renderers/architecture/grid.mjs +62 -0
  45. package/departments/dev/skills/diagram/vendor/renderers/architecture/render-architecture.mjs +490 -0
  46. package/departments/dev/skills/diagram/vendor/renderers/dataflow/README.md +93 -0
  47. package/departments/dev/skills/diagram/vendor/renderers/dataflow/render-dataflow.mjs +396 -0
  48. package/departments/dev/skills/diagram/vendor/renderers/lifecycle/README.md +103 -0
  49. package/departments/dev/skills/diagram/vendor/renderers/lifecycle/render-lifecycle.mjs +482 -0
  50. package/departments/dev/skills/diagram/vendor/renderers/sequence/README.md +96 -0
  51. package/departments/dev/skills/diagram/vendor/renderers/sequence/render-sequence.mjs +357 -0
  52. package/departments/dev/skills/diagram/vendor/renderers/shared/cli.mjs +176 -0
  53. package/departments/dev/skills/diagram/vendor/renderers/shared/generated-validators.mjs +13 -0
  54. package/departments/dev/skills/diagram/vendor/renderers/shared/geometry.mjs +992 -0
  55. package/departments/dev/skills/diagram/vendor/renderers/shared/layout-report.mjs +40 -0
  56. package/departments/dev/skills/diagram/vendor/renderers/shared/utils.mjs +156 -0
  57. package/departments/dev/skills/diagram/vendor/renderers/shared/validator.mjs +40 -0
  58. package/departments/dev/skills/diagram/vendor/renderers/workflow/README.md +114 -0
  59. package/departments/dev/skills/diagram/vendor/renderers/workflow/render-workflow.mjs +598 -0
  60. package/departments/dev/skills/diagram/vendor/schemas/README.md +93 -0
  61. package/departments/dev/skills/diagram/vendor/schemas/architecture.schema.json +125 -0
  62. package/departments/dev/skills/diagram/vendor/schemas/common.schema.json +62 -0
  63. package/departments/dev/skills/diagram/vendor/schemas/dataflow.schema.json +240 -0
  64. package/departments/dev/skills/diagram/vendor/schemas/lifecycle.schema.json +260 -0
  65. package/departments/dev/skills/diagram/vendor/schemas/sequence.schema.json +205 -0
  66. package/departments/dev/skills/diagram/vendor/schemas/workflow.schema.json +351 -0
  67. package/departments/dev/skills/diagram/vendor/scripts/check-render-output.mjs +774 -0
  68. package/departments/dev/skills/diagram/vendor/scripts/generate-validators.mjs +66 -0
  69. package/departments/dev/skills/diagram/vendor/scripts/render-examples.mjs +26 -0
  70. package/departments/dev/skills/spec/SKILL.md +19 -0
  71. package/departments/ops/agents/ops-lead.yaml +1 -0
  72. package/departments/pm/agents/pm-director.yaml +1 -0
  73. package/departments/strategy/agents/strategy-director.yaml +1 -0
  74. package/harness/codex/AGENTS.md +1 -1
  75. package/harness/copilot/copilot-instructions.md +1 -1
  76. package/harness/cursor/rules/arkaos.mdc +2 -2
  77. package/harness/gemini/GEMINI.md +1 -1
  78. package/harness/opencode/AGENTS.md +1 -1
  79. package/harness/zed/.rules +1 -1
  80. package/installer/autoupdate.js +249 -0
  81. package/installer/cli.js +7 -0
  82. package/installer/core-snapshot.js +5 -1
  83. package/installer/doctor.js +11 -0
  84. package/installer/index.js +11 -0
  85. package/installer/update.js +11 -0
  86. package/knowledge/agents-registry-v2.json +9 -1
  87. package/knowledge/skills-manifest.json +14 -1
  88. package/package.json +3 -2
  89. package/pyproject.toml +1 -1
  90. package/scripts/auto-update.sh +188 -0
  91. package/scripts/skill_validator.py +10 -2
  92. package/scripts/tools/docs_stats.py +8 -2
@@ -0,0 +1,482 @@
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, loadDiagram, writeDiagram, svgAccessibleText, svgRootAttrs } from '../shared/cli.mjs';
5
+ import {
6
+ asArray,
7
+ isFinitePoint,
8
+ rectsOverlap,
9
+ cleanFlowProblems,
10
+ cleanCrossingProblems,
11
+ cleanAmbiguousCorridorProblems,
12
+ cleanBorderRunProblems,
13
+ cleanRouteRhythmProblems,
14
+ cleanLabelRouteClearanceProblems,
15
+ suggestLabelObstacleFix,
16
+ suggestLabelPairFix,
17
+ anchor,
18
+ automaticPortSpread,
19
+ defaultFromSide,
20
+ defaultToSide,
21
+ chosenSide,
22
+ roundedPath,
23
+ routePointsValue,
24
+ labelPoint,
25
+ arrowClassMap,
26
+ variantAccent
27
+ } from '../shared/geometry.mjs';
28
+
29
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
30
+ const { diagram: lifecycle, template, outPath } = loadDiagram({
31
+ rendererDir: __dirname,
32
+ diagramType: 'lifecycle',
33
+ defaultExample: 'agent-run.lifecycle.json'
34
+ });
35
+
36
+ const viewBox = lifecycle.meta?.viewBox || [980, 660];
37
+ const layout = {
38
+ phaseY: 126,
39
+ eventY: 278,
40
+ outcomeY: 450,
41
+ phaseW: 118,
42
+ phaseH: 62,
43
+ eventW: 126,
44
+ eventH: 58,
45
+ outcomeW: 118,
46
+ outcomeH: 58,
47
+ phaseXs: [94, 248, 402, 556, 710],
48
+ eventXs: [402, 556, 710],
49
+ outcomeXs: [402, 556, 710]
50
+ };
51
+
52
+ const typeClass = {
53
+ start: 'c-frontend',
54
+ active: 'c-backend',
55
+ waiting: 'c-cloud',
56
+ decision: 'c-security',
57
+ success: 'c-database',
58
+ failure: 'c-security',
59
+ neutral: 'c-external',
60
+ external: 'c-external'
61
+ };
62
+
63
+ const textClass = {
64
+ start: 't-frontend',
65
+ active: 't-backend',
66
+ waiting: 't-cloud',
67
+ decision: 't-security',
68
+ success: 't-database',
69
+ failure: 't-security',
70
+ neutral: 't-muted',
71
+ external: 't-muted'
72
+ };
73
+
74
+ function legendY() {
75
+ return viewBox[1] - 98;
76
+ }
77
+
78
+ // Lane semantics are fixed: lane id "main" maps to the top phase band, lane id
79
+ // "terminal" maps to the bottom outcome band, and every other lane shares the
80
+ // middle event band (separated visually via yOffset).
81
+ function bandFor(lane) {
82
+ if (lane === 'main') return 'phase';
83
+ if (lane === 'terminal') return 'outcome';
84
+ return 'event';
85
+ }
86
+
87
+ function measureState(state) {
88
+ const isPhase = bandFor(state.lane) === 'phase';
89
+ const isOutcome = bandFor(state.lane) === 'outcome';
90
+ const width = state.width || (isPhase ? layout.phaseW : isOutcome ? layout.outcomeW : layout.eventW);
91
+ const height = state.height || (isPhase ? layout.phaseH : isOutcome ? layout.outcomeH : layout.eventH);
92
+ const xs = isPhase ? layout.phaseXs : isOutcome ? layout.outcomeXs : layout.eventXs;
93
+ const cx = xs[state.col] ?? xs[xs.length - 1];
94
+ const y = (
95
+ isPhase ? layout.phaseY :
96
+ isOutcome ? layout.outcomeY :
97
+ layout.eventY
98
+ ) + (state.yOffset || 0);
99
+ return {
100
+ ...state,
101
+ width,
102
+ height,
103
+ x: cx - width / 2,
104
+ y,
105
+ cx,
106
+ cy: y + height / 2
107
+ };
108
+ }
109
+
110
+ const states = new Map(asArray(lifecycle.states).map((state) => [state.id, measureState(state)]));
111
+ const laneLabels = new Map(asArray(lifecycle.lanes).map((lane) => [lane.id, lane.label]));
112
+ const stateSteps = new Map();
113
+ for (const [index, transition] of asArray(lifecycle.transitions).entries()) {
114
+ if (!stateSteps.has(transition.from)) stateSteps.set(transition.from, index);
115
+ if (!stateSteps.has(transition.to)) stateSteps.set(transition.to, index + 1);
116
+ }
117
+ for (const [index, state] of asArray(lifecycle.states).entries()) {
118
+ if (!stateSteps.has(state.id)) stateSteps.set(state.id, index);
119
+ }
120
+
121
+ function validateLifecycle() {
122
+ const problems = [];
123
+ if (lifecycle.schema_version !== 1) problems.push('Lifecycle files must set "schema_version": 1.');
124
+ if (lifecycle.diagram_type !== 'lifecycle') problems.push('Lifecycle files must set "diagram_type": "lifecycle".');
125
+ if (!lifecycle.meta?.title) problems.push('Lifecycle files must include meta.title.');
126
+ if (!Array.isArray(lifecycle.lanes) || lifecycle.lanes.length < 1) problems.push('Lifecycle diagrams need at least one lane.');
127
+ if (!Array.isArray(lifecycle.states) || lifecycle.states.length < 2) problems.push('Lifecycle diagrams need at least two states.');
128
+ if (!Array.isArray(lifecycle.transitions)) problems.push('Lifecycle diagrams must include a transitions array.');
129
+ if (lifecycle.cards !== undefined && !Array.isArray(lifecycle.cards)) problems.push('Lifecycle "cards" must be an array.');
130
+ if (states.size !== asArray(lifecycle.states).length) problems.push('State ids must be unique.');
131
+
132
+ // The three bands are fixed at y=112/264/436; the legend (viewBox[1] - 98)
133
+ // must clear the outcome band's header zone.
134
+ if (legendY() - 20 < 448) {
135
+ problems.push(`viewBox height ${viewBox[1]} is too short for the fixed band layout — set meta.viewBox[1] to at least 566.`);
136
+ }
137
+
138
+ const laneIds = new Set(asArray(lifecycle.lanes).map((lane) => lane.id));
139
+ if (laneIds.size !== asArray(lifecycle.lanes).length) problems.push('Lane ids must be unique.');
140
+ if (!laneIds.has('main')) {
141
+ problems.push('Lifecycle diagrams need a lane with id "main" (the phase rail). Lane ids "main" and "terminal" are reserved: "main" maps to the top phase band, "terminal" to the bottom outcome band, and all other lanes share the middle event band.');
142
+ }
143
+
144
+ for (const state of states.values()) {
145
+ if (!laneIds.has(state.lane)) {
146
+ problems.push(`State "${state.id}" uses unknown lane "${state.lane}".`);
147
+ continue;
148
+ }
149
+ const band = bandFor(state.lane);
150
+ const maxCol = band === 'phase'
151
+ ? layout.phaseXs.length
152
+ : band === 'outcome'
153
+ ? layout.outcomeXs.length
154
+ : layout.eventXs.length;
155
+ if (!Number.isInteger(state.col) || state.col < 0 || state.col >= maxCol) {
156
+ problems.push(`State "${state.id}" uses invalid column ${state.col} — the ${band} band has integer columns 0..${maxCol - 1}.`);
157
+ continue;
158
+ }
159
+ if (!isFinitePoint(state.x, state.y, state.cx, state.cy)) {
160
+ problems.push(`State "${state.id}" produced non-finite coordinates — check col, width, height, and yOffset are numbers.`);
161
+ continue;
162
+ }
163
+ if (state.x < 32 || state.x + state.width > viewBox[0] - 32) {
164
+ problems.push(`State "${state.id}" exceeds the horizontal bounds of the diagram — reduce state.width or increase meta.viewBox[0].`);
165
+ }
166
+ if (state.y < 64 || state.y + state.height > legendY() - 24) {
167
+ problems.push(`State "${state.id}" exceeds the vertical lifecycle area — keep y between 64 and ${legendY() - 24} (adjust yOffset or increase meta.viewBox[1]).`);
168
+ }
169
+ const estLabelW = textUnits(state.label) * 6.2;
170
+ if (estLabelW > state.width + 6) {
171
+ problems.push(`Label "${state.label}" (~${Math.round(estLabelW)}px) is wider than state "${state.id}" (${state.width}px) — shorten the label, move detail to sublabel, or increase state.width.`);
172
+ }
173
+ }
174
+
175
+ // All non-main/non-terminal lanes share the same y band, so the overlap
176
+ // check must run across lanes — not per-lane.
177
+ const allStates = [...states.values()];
178
+ for (let i = 0; i < allStates.length; i += 1) {
179
+ for (let j = i + 1; j < allStates.length; j += 1) {
180
+ if (rectsOverlap(allStates[i], allStates[j], 10)) {
181
+ problems.push(`States "${allStates[i].id}" and "${allStates[j].id}" are less than 10px apart — move one to another col or separate them with yOffset (lanes other than "main"/"terminal" share one band).`);
182
+ }
183
+ }
184
+ }
185
+
186
+ for (const transition of asArray(lifecycle.transitions)) {
187
+ if (!states.has(transition.from)) problems.push(`Transition "${transition.label || transition.from}" references unknown source "${transition.from}".`);
188
+ if (!states.has(transition.to)) problems.push(`Transition "${transition.label || transition.to}" references unknown target "${transition.to}".`);
189
+ if (states.has(transition.from) && states.has(transition.to)) {
190
+ const routed = pathFor(transition);
191
+ const [start, end] = [routed.points[0], routed.points[routed.points.length - 1]];
192
+ const distance = Math.hypot(end[0] - start[0], end[1] - start[1]);
193
+ if (distance < 32) problems.push(`Transition "${transition.label || `${transition.from}->${transition.to}`}" is too short (${Math.round(distance)}px; minimum 32px) — route it through a channel or drop its label.`);
194
+ }
195
+ }
196
+
197
+ problems.push(...cleanFlowProblems({
198
+ relations: lifecycle.transitions,
199
+ endpointIds: new Set(states.keys()),
200
+ obstacles: states.values(),
201
+ pathFor,
202
+ diagramType: 'lifecycle',
203
+ relationCollection: 'transitions',
204
+ obstacleKind: 'state',
205
+ profile: lifecycle.meta?.quality_profile,
206
+ routeHint: 'adjust fromSide/toSide, set route/via or channelX/channelY, or move the state with col/yOffset'
207
+ }));
208
+ problems.push(...cleanCrossingProblems({
209
+ relations: lifecycle.transitions,
210
+ endpointIds: new Set(states.keys()),
211
+ pathFor,
212
+ diagramType: 'lifecycle',
213
+ relationCollection: 'transitions',
214
+ profile: lifecycle.meta?.quality_profile,
215
+ routeHint: 'adjust route/via or channelX/channelY so the transitions use separate lifecycle corridors'
216
+ }));
217
+ problems.push(...cleanAmbiguousCorridorProblems({
218
+ relations: lifecycle.transitions,
219
+ endpointIds: new Set(states.keys()),
220
+ pathFor,
221
+ diagramType: 'lifecycle',
222
+ relationCollection: 'transitions',
223
+ profile: lifecycle.meta?.quality_profile,
224
+ routeHint: 'adjust route/via or channelX/channelY so unrelated transitions do not visually merge'
225
+ }));
226
+ // Lifecycle bands are dashed reading guides, not closed containers. Keep the
227
+ // shared contract wired with an explicit empty frame set so future typed
228
+ // lifecycle containers cannot accidentally inherit presentation geometry.
229
+ problems.push(...cleanBorderRunProblems({
230
+ relations: lifecycle.transitions,
231
+ endpointIds: new Set(states.keys()),
232
+ frames: [],
233
+ pathFor,
234
+ diagramType: 'lifecycle',
235
+ relationCollection: 'transitions',
236
+ profile: lifecycle.meta?.quality_profile
237
+ }));
238
+ problems.push(...cleanRouteRhythmProblems({
239
+ relations: lifecycle.transitions,
240
+ endpointIds: new Set(states.keys()),
241
+ pathFor,
242
+ diagramType: 'lifecycle',
243
+ relationCollection: 'transitions',
244
+ profile: lifecycle.meta?.quality_profile,
245
+ routeHint: 'move route/via or channel coordinates so each lifecycle turn has a readable run-up'
246
+ }));
247
+
248
+ const labelRects = [];
249
+ for (const [transitionIndex, transition] of asArray(lifecycle.transitions).entries()) {
250
+ if (!transition.label || !states.has(transition.from) || !states.has(transition.to)) continue;
251
+ const [lx, ly] = labelPoint(transition, pathFor(transition).points);
252
+ const longestLine = Math.max(textUnits(transition.label), textUnits(transition.note || ''));
253
+ const width = Math.max(32, longestLine * 4.9 + 12);
254
+ const height = transition.note ? 27 : 16;
255
+ labelRects.push({ relation: transition, relationIndex: transitionIndex, label: transition.label, x: lx - width / 2, y: ly - 11, width, height, lx, ly });
256
+ }
257
+ for (const rect of labelRects) {
258
+ for (const state of states.values()) {
259
+ if (rectsOverlap(rect, state, -2)) {
260
+ problems.push(`Label "${rect.label}" overlaps state "${state.id}" — adjust labelDx/labelDy/labelSegment or set labelAt.\n${suggestLabelObstacleFix(rect, rect.lx, rect.ly, state, 'state')}`);
261
+ }
262
+ }
263
+ }
264
+ for (let i = 0; i < labelRects.length; i += 1) {
265
+ for (let j = i + 1; j < labelRects.length; j += 1) {
266
+ if (rectsOverlap(labelRects[i], labelRects[j], -2)) {
267
+ problems.push(`Labels "${labelRects[i].label}" and "${labelRects[j].label}" overlap — adjust labelDx/labelDy.\n${suggestLabelPairFix(labelRects[i], labelRects[j])}`);
268
+ }
269
+ }
270
+ }
271
+ problems.push(...cleanLabelRouteClearanceProblems({
272
+ relations: lifecycle.transitions,
273
+ labels: labelRects,
274
+ endpointIds: new Set(states.keys()),
275
+ pathFor,
276
+ diagramType: 'lifecycle',
277
+ relationCollection: 'transitions',
278
+ profile: lifecycle.meta?.quality_profile,
279
+ }));
280
+
281
+ if (problems.length) {
282
+ throw new Error(`Lifecycle layout validation failed:\n- ${problems.join('\n- ')}`);
283
+ }
284
+ }
285
+
286
+ function routeVia(transition, from, to, start, end) {
287
+ if (transition.via) return transition.via;
288
+ switch (transition.route || 'auto') {
289
+ case 'straight':
290
+ return [];
291
+ case 'drop': {
292
+ const y = transition.channelY ?? (start[1] + end[1]) / 2;
293
+ return [[start[0], y], [end[0], y]];
294
+ }
295
+ case 'bottom-channel': {
296
+ const y = transition.channelY ?? Math.max(from.y + from.height, to.y + to.height) + 34;
297
+ return [[start[0], y], [end[0], y]];
298
+ }
299
+ case 'top-channel': {
300
+ const y = transition.channelY ?? Math.min(from.y, to.y) - 28;
301
+ return [[start[0], y], [end[0], y]];
302
+ }
303
+ case 'right-channel': {
304
+ const x = transition.channelX ?? Math.max(from.x + from.width, to.x + to.width) + 36;
305
+ return [[x, start[1]], [x, end[1]]];
306
+ }
307
+ case 'left-channel': {
308
+ const x = transition.channelX ?? Math.min(from.x, to.x) - 36;
309
+ return [[x, start[1]], [x, end[1]]];
310
+ }
311
+ case 'auto':
312
+ default: {
313
+ if (from.lane === to.lane) return [];
314
+ const y = transition.channelY ?? (start[1] + end[1]) / 2;
315
+ return [[start[0], y], [end[0], y]];
316
+ }
317
+ }
318
+ }
319
+
320
+ const pathCache = new Map();
321
+ const automaticPorts = automaticPortSpread(lifecycle.transitions, states);
322
+
323
+ function pathFor(transition) {
324
+ if (pathCache.has(transition)) return pathCache.get(transition);
325
+ const from = states.get(transition.from);
326
+ const to = states.get(transition.to);
327
+ const ports = automaticPorts.get(transition);
328
+ const start = ports?.from || anchor(from, chosenSide(transition.fromSide, defaultFromSide(from, to)));
329
+ const end = ports?.to || anchor(to, chosenSide(transition.toSide, defaultToSide(from, to)));
330
+ let via = routeVia(transition, from, to, start, end);
331
+ if (ports && !via.length && Math.abs(start[0] - end[0]) >= 4 && Math.abs(start[1] - end[1]) >= 4) {
332
+ const midX = (start[0] + end[0]) / 2;
333
+ via = [[midX, start[1]], [midX, end[1]]];
334
+ }
335
+ const points = [start, ...via, end];
336
+ const routed = {
337
+ d: roundedPath(points, transition.cornerRadius ?? 10),
338
+ points
339
+ };
340
+ pathCache.set(transition, routed);
341
+ return routed;
342
+ }
343
+
344
+ function bandTitles() {
345
+ const lanes = asArray(lifecycle.lanes);
346
+ const mainLane = lanes.find((lane) => lane.id === 'main');
347
+ const terminalLane = lanes.find((lane) => lane.id === 'terminal');
348
+ const eventLanes = lanes.filter((lane) => lane.id !== 'main' && lane.id !== 'terminal');
349
+ return [
350
+ mainLane?.label || 'Lifecycle phases',
351
+ eventLanes.length ? eventLanes.map((lane) => lane.label).join(' + ') : 'Interruptions + recovery',
352
+ terminalLane?.label || 'Outcomes'
353
+ ];
354
+ }
355
+
356
+ function renderBands() {
357
+ const right = viewBox[0] - 72;
358
+ const titles = bandTitles();
359
+ return ` <path d="M 72 112 L ${right} 112" class="a-default" stroke-width="0.8" stroke-dasharray="3,8"/>
360
+ <text x="72" y="100" class="t-dim" font-size="10" font-weight="600">01 / ${esc(titles[0])}</text>
361
+ <path d="M 72 264 L ${right} 264" class="a-default" stroke-width="0.8" stroke-dasharray="3,8"/>
362
+ <text x="72" y="252" class="t-dim" font-size="10" font-weight="600">02 / ${esc(titles[1])}</text>
363
+ <path d="M 72 436 L ${right} 436" class="a-default" stroke-width="0.8" stroke-dasharray="3,8"/>
364
+ <text x="72" y="424" class="t-dim" font-size="10" font-weight="600">03 / ${esc(titles[2])}</text>`;
365
+ }
366
+
367
+ function renderState(state) {
368
+ const fill = typeClass[state.type] || typeClass.neutral;
369
+ const accent = textClass[state.type] || 't-muted';
370
+ const hasSub = state.sublabel != null && state.sublabel !== '';
371
+ const sub = hasSub
372
+ ? `\n <text data-detail="context" x="${state.cx}" y="${state.y + 37}" class="t-muted" font-size="7" text-anchor="middle">${esc(state.sublabel)}</text>`
373
+ : '';
374
+ const tag = state.tag
375
+ ? `\n <text data-detail="fine" x="${state.cx}" y="${state.y + state.height - 11}" class="${accent}" font-size="7" text-anchor="middle">${esc(state.tag)}</text>`
376
+ : '';
377
+ const step = state.step
378
+ ? `\n <text data-detail="fine" x="${state.x + 10}" y="${state.y + 14}" class="${accent}" font-size="7" font-weight="700">${esc(state.step)}</text>`
379
+ : '';
380
+ const passport = { kind: state.type, sublabel: state.sublabel, tag: state.tag, context: laneLabels.get(state.lane) || 'Lifecycle state' };
381
+ return ` <g ${focusNodeAttrs(state.id, state.label, passport)}>
382
+ ${focusNodeTitle(state.label, passport)}
383
+ <rect x="${state.x}" y="${state.y}" width="${state.width}" height="${state.height}" rx="7" class="c-mask"/>
384
+ <rect x="${state.x}" y="${state.y}" width="${state.width}" height="${state.height}" rx="7" class="${fill}"${animateAttr(lifecycle.meta, 'node', stateSteps.get(state.id))} stroke-width="1.5"/>
385
+ ${renderSemanticSigil(state.type, { x: state.x + state.width - 17, y: state.y + 6 })}${step}
386
+ <text${hasSub ? ' data-detail-anchor' : ''} x="${state.cx}" y="${state.y + 21}" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">${esc(state.label)}</text>${sub}${tag}
387
+ </g>`;
388
+ }
389
+
390
+ function renderTransitionPath(transition, index) {
391
+ const [cls, marker] = arrowClassMap[transition.variant || 'default'] || arrowClassMap.default;
392
+ const routed = pathFor(transition);
393
+ const strokeWidth = transition.width || (transition.variant === 'emphasis' ? 2 : 1.1);
394
+ return ` <path ${focusEdgeAttrs(transition.from, transition.to, transition.label, index, transition.id)} data-composition-points="${routePointsValue(routed.points)}" d="${routed.d}" class="${cls}"${animateAttr(lifecycle.meta, 'edge', index)} stroke-width="${strokeWidth}" marker-end="url(#${marker})"/>`;
395
+ }
396
+
397
+ function renderTransitionLabel(transition, index) {
398
+ if (!transition.label) return '';
399
+ const routed = pathFor(transition);
400
+ const [lx, ly] = labelPoint(transition, routed.points);
401
+ const longestLine = Math.max(textUnits(transition.label), textUnits(transition.note || ''));
402
+ const labelW = Math.max(32, longestLine * 4.9 + 12);
403
+ const labelH = transition.note ? 27 : 16;
404
+ const note = transition.note
405
+ ? `\n <text data-detail="fine" x="${lx}" y="${ly + 11}" class="t-dim" font-size="7" text-anchor="middle">${esc(transition.note)}</text>`
406
+ : '';
407
+ return ` <g data-detail="context" ${focusEdgeAttrs(transition.from, transition.to, transition.label, index, transition.id)}>
408
+ <rect x="${lx - labelW / 2}" y="${ly - 11}" width="${labelW}" height="${labelH}" rx="4" class="c-mask"/>
409
+ <text x="${lx}" y="${ly}" class="${variantAccent(transition.variant)}" font-size="8" text-anchor="middle">${esc(transition.label)}</text>${note}
410
+ </g>`;
411
+ }
412
+
413
+ function renderLegend() {
414
+ const y = legendY();
415
+ return ` <g data-legend-bridge>
416
+ <text x="220" y="${y - 20}" class="t-primary" font-size="10" font-weight="600">Legend</text>
417
+ <g data-legend-kind="active">
418
+ <rect x="220" y="${y - 8}" width="14" height="9" rx="2" class="c-backend" stroke-width="1"/>
419
+ <text x="240" y="${y}" class="t-muted" font-size="7">active state</text>
420
+ </g>
421
+ <g data-legend-kind="waiting">
422
+ <rect x="325" y="${y - 8}" width="14" height="9" rx="2" class="c-cloud" stroke-width="1"/>
423
+ <text x="345" y="${y}" class="t-muted" font-size="7">waiting</text>
424
+ </g>
425
+ <g data-legend-kind="success">
426
+ <rect x="415" y="${y - 8}" width="14" height="9" rx="2" class="c-database" stroke-width="1"/>
427
+ <text x="435" y="${y}" class="t-muted" font-size="7">terminal success</text>
428
+ </g>
429
+ <g data-legend-kind="failure">
430
+ <rect x="560" y="${y - 8}" width="14" height="9" rx="2" class="c-security" stroke-width="1"/>
431
+ <text x="580" y="${y}" class="t-muted" font-size="7">failure / exit</text>
432
+ </g>
433
+ </g>`;
434
+ }
435
+
436
+ function renderLifecycleRail() {
437
+ const mainCols = [...states.values()]
438
+ .filter((state) => bandFor(state.lane) === 'phase')
439
+ .map((state) => state.col);
440
+ if (!mainCols.length) return '';
441
+ const railEnd = layout.phaseXs[Math.max(...mainCols)] + 38;
442
+ return ` <path d="M 154 ${layout.phaseY + 31} L ${railEnd} ${layout.phaseY + 31}" class="a-emphasis" stroke-width="2.2" marker-end="url(#arrowhead-emphasis)"/>`;
443
+ }
444
+
445
+ function renderSvg() {
446
+ return ` <svg viewBox="0 0 ${viewBox[0]} ${viewBox[1]}" ${svgRootAttrs(lifecycle.meta, 'lifecycle diagram')}>
447
+ ${svgAccessibleText(lifecycle.meta, 'lifecycle diagram')}
448
+ ${renderDefinitions()}
449
+
450
+ <!-- Background Grid -->
451
+ <rect width="100%" height="100%" fill="url(#grid)" />
452
+
453
+ <!-- Lifecycle bands -->
454
+ ${renderBands()}
455
+
456
+ <!-- Primary lifecycle rail -->
457
+ ${renderLifecycleRail()}
458
+
459
+ <!-- Transition paths -->
460
+ ${asArray(lifecycle.transitions).map(renderTransitionPath).join('\n')}
461
+
462
+ <!-- States -->
463
+ ${[...states.values()].map(renderState).join('\n\n')}
464
+
465
+ <!-- Transition labels -->
466
+ ${asArray(lifecycle.transitions).map(renderTransitionLabel).join('\n')}
467
+
468
+ <!-- Legend -->
469
+ ${renderLegend()}
470
+ </svg>`;
471
+ }
472
+
473
+ validateLifecycle();
474
+ writeDiagram({
475
+ outPath,
476
+ template,
477
+ diagramType: 'lifecycle',
478
+ meta: lifecycle.meta,
479
+ footerLabel: 'Lifecycle diagram',
480
+ svg: renderSvg(),
481
+ cards: lifecycle.cards,
482
+ });
@@ -0,0 +1,96 @@
1
+ # Sequence Renderer
2
+
3
+ Render `diagram_type: "sequence"` JSON files into the standard Archify HTML
4
+ template.
5
+
6
+ ```bash
7
+ node archify/renderers/sequence/render-sequence.mjs input.sequence.json output.html
8
+ ```
9
+
10
+ The renderer validates input against `archify/schemas/sequence.schema.json`
11
+ with the bundled standalone validator. No dependency installation is required.
12
+
13
+ If `output.html` is omitted, the renderer uses `meta.output` from the JSON file
14
+ or falls back to `sequence.html` in the current working directory.
15
+
16
+ ## Input
17
+
18
+ Sequence JSON files must set:
19
+
20
+ ```json
21
+ {
22
+ "schema_version": 1,
23
+ "diagram_type": "sequence",
24
+ "meta": {
25
+ "title": "Cache Miss Request Sequence",
26
+ "subtitle": "Frontend request path with auth and cache fallback",
27
+ "viewBox": [920, 760]
28
+ },
29
+ "participants": [],
30
+ "segments": [],
31
+ "messages": [],
32
+ "activations": [],
33
+ "cards": []
34
+ }
35
+ ```
36
+
37
+ The timeline scales with the viewBox height: a taller `meta.viewBox` buys more
38
+ message room, a shorter one shrinks the readable band instead of clipping. A
39
+ complete worked example lives at
40
+ `archify/examples/cache-miss-request.sequence.json`.
41
+
42
+ The schema lives at:
43
+
44
+ ```text
45
+ archify/schemas/sequence.schema.json
46
+ ```
47
+
48
+ ## Layout budget
49
+
50
+ | Constant | Value |
51
+ |----------|-------|
52
+ | viewBox | default `[920, 760]`; schema minimum `[480, 480]` |
53
+ | Participant boxes | 86×54 at y 72; centers at x = 62 + index×108 |
54
+ | Participant count | last center + 43 must be ≤ width − 40 (8 fit at width 920) |
55
+ | Lifelines | from y 142 down to height − 65; band must be ≥120px tall |
56
+ | Message `y` range | `[160, height − 83]` |
57
+ | Message spacing | ≥28px vertical between messages that share horizontal space |
58
+ | Arrow span | ≥60px horizontal between the two participants |
59
+ | Segments | y pixel ranges with `to > from`, inside `[72, lifeline bottom + 20]` |
60
+ | Legend row | y = height − 54 |
61
+
62
+ `segments[].from/to` and `activations[].from/to` are y pixel coordinates, not
63
+ participant ids; activations also require `to > from`.
64
+
65
+ ## Design Rules
66
+
67
+ - Put participants across the top, ordered by the story the reader should
68
+ follow.
69
+ - Time moves downward.
70
+ - Use `emphasis` for the main request path.
71
+ - Use `security` for auth, consent, permission, and policy calls.
72
+ - Use `return` for quiet response messages.
73
+ - Use `dashed` for async trace, event, logging, and non-blocking work.
74
+ - Use segments as light background guides; keep segment labels short.
75
+ - Keep labels short enough to fit in narrow previews.
76
+
77
+ Schema violations exit non-zero with path-prefixed messages annotated with the
78
+ element's id or label. The renderer additionally fails when it can detect
79
+ layout problems, including missing participants, duplicate participant IDs,
80
+ participant labels wider than their box, unknown message endpoints, messages
81
+ outside the readable timeline, overly tight vertical spacing between messages
82
+ that overlap horizontally, invalid segment or activation ranges, or
83
+ participants that exceed the viewBox. The shared Clean Flow contract treats
84
+ participant headers as semantic boxes while explicitly allowing messages to
85
+ cross intermediate lifelines, activation bars, and segment frames. Text width is estimated CJK-aware:
86
+ fullwidth glyphs count as two units.
87
+
88
+ Set `meta.quality_profile` to `showcase` for polished delivery. Unrelated proper
89
+ message X crossings then fail with `composition/proper-crossing`; default
90
+ `standard` keeps them as artifact-receipt warnings. Messages may still cross
91
+ intermediate lifelines. Collinear corridors remain outside the proper-X rule,
92
+ but a separate gate warns in `standard` and fails in `showcase` when unrelated
93
+ messages overlap for at least 8px. Shared semantic endpoints, point touches,
94
+ and shorter overlaps remain valid. Showcase also rejects any route segment
95
+ below 8px and any interior turn segment below 16px; ordinary 8–15px endpoint
96
+ stubs remain valid.