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.
- package/README.md +1 -1
- package/THE-ARKAOS-GUIDE.md +1 -1
- package/VERSION +1 -1
- package/arka/SKILL.md +1 -1
- package/arka/skills/forge/references/workflows.md +10 -0
- package/config/claude-agents/architect.md +1 -1
- package/config/claude-agents/backend-dev.md +1 -1
- package/config/claude-agents/devops-eng.md +1 -1
- package/config/claude-agents/frontend-dev.md +1 -1
- package/config/claude-agents/ops-lead.md +1 -1
- package/config/claude-agents/pm-director.md +1 -1
- package/config/claude-agents/strategy-director.md +1 -1
- package/config/hooks/session-start.sh +1 -1
- package/config/skills-curated.yaml +2 -2
- package/config/skills-provenance.yaml +7 -0
- package/config/statusline.sh +6 -1
- package/core/hooks/session_start.py +69 -28
- package/departments/dev/agents/architect.yaml +1 -0
- package/departments/dev/agents/backend-dev.yaml +1 -0
- package/departments/dev/agents/devops-eng.yaml +1 -0
- package/departments/dev/agents/frontend-dev.yaml +1 -0
- package/departments/dev/agents/tech-lead.yaml +1 -0
- package/departments/dev/skills/diagram/SKILL.md +175 -0
- package/departments/dev/skills/diagram/examples/archify-repo-grid.architecture.json +57 -0
- package/departments/dev/skills/diagram/vendor/LICENSE +22 -0
- package/departments/dev/skills/diagram/vendor/SKILL.md +367 -0
- package/departments/dev/skills/diagram/vendor/assets/template.html +11470 -0
- package/departments/dev/skills/diagram/vendor/bin/archify.mjs +600 -0
- package/departments/dev/skills/diagram/vendor/bin/open-artifact.mjs +67 -0
- package/departments/dev/skills/diagram/vendor/examples/agent-run.lifecycle.json +71 -0
- package/departments/dev/skills/diagram/vendor/examples/agent-tool-call.workflow.json +111 -0
- package/departments/dev/skills/diagram/vendor/examples/async-job-roundtrip.sequence.json +62 -0
- package/departments/dev/skills/diagram/vendor/examples/cache-miss-request.sequence.json +82 -0
- package/departments/dev/skills/diagram/vendor/examples/deployment-release.lifecycle.json +50 -0
- package/departments/dev/skills/diagram/vendor/examples/event-stream.dataflow.json +58 -0
- package/departments/dev/skills/diagram/vendor/examples/incident-response.workflow.json +65 -0
- package/departments/dev/skills/diagram/vendor/examples/product-analytics.dataflow.json +77 -0
- package/departments/dev/skills/diagram/vendor/examples/production-deployment.architecture.json +70 -0
- package/departments/dev/skills/diagram/vendor/examples/release-delivery.workflow.json +63 -0
- package/departments/dev/skills/diagram/vendor/examples/web-app.architecture.json +47 -0
- package/departments/dev/skills/diagram/vendor/package-lock.json +80 -0
- package/departments/dev/skills/diagram/vendor/package.json +28 -0
- package/departments/dev/skills/diagram/vendor/recipes/scenarios.mjs +350 -0
- package/departments/dev/skills/diagram/vendor/renderers/architecture/grid.mjs +62 -0
- package/departments/dev/skills/diagram/vendor/renderers/architecture/render-architecture.mjs +490 -0
- package/departments/dev/skills/diagram/vendor/renderers/dataflow/README.md +93 -0
- package/departments/dev/skills/diagram/vendor/renderers/dataflow/render-dataflow.mjs +396 -0
- package/departments/dev/skills/diagram/vendor/renderers/lifecycle/README.md +103 -0
- package/departments/dev/skills/diagram/vendor/renderers/lifecycle/render-lifecycle.mjs +482 -0
- package/departments/dev/skills/diagram/vendor/renderers/sequence/README.md +96 -0
- package/departments/dev/skills/diagram/vendor/renderers/sequence/render-sequence.mjs +357 -0
- package/departments/dev/skills/diagram/vendor/renderers/shared/cli.mjs +176 -0
- package/departments/dev/skills/diagram/vendor/renderers/shared/generated-validators.mjs +13 -0
- package/departments/dev/skills/diagram/vendor/renderers/shared/geometry.mjs +992 -0
- package/departments/dev/skills/diagram/vendor/renderers/shared/layout-report.mjs +40 -0
- package/departments/dev/skills/diagram/vendor/renderers/shared/utils.mjs +156 -0
- package/departments/dev/skills/diagram/vendor/renderers/shared/validator.mjs +40 -0
- package/departments/dev/skills/diagram/vendor/renderers/workflow/README.md +114 -0
- package/departments/dev/skills/diagram/vendor/renderers/workflow/render-workflow.mjs +598 -0
- package/departments/dev/skills/diagram/vendor/schemas/README.md +93 -0
- package/departments/dev/skills/diagram/vendor/schemas/architecture.schema.json +125 -0
- package/departments/dev/skills/diagram/vendor/schemas/common.schema.json +62 -0
- package/departments/dev/skills/diagram/vendor/schemas/dataflow.schema.json +240 -0
- package/departments/dev/skills/diagram/vendor/schemas/lifecycle.schema.json +260 -0
- package/departments/dev/skills/diagram/vendor/schemas/sequence.schema.json +205 -0
- package/departments/dev/skills/diagram/vendor/schemas/workflow.schema.json +351 -0
- package/departments/dev/skills/diagram/vendor/scripts/check-render-output.mjs +774 -0
- package/departments/dev/skills/diagram/vendor/scripts/generate-validators.mjs +66 -0
- package/departments/dev/skills/diagram/vendor/scripts/render-examples.mjs +26 -0
- package/departments/dev/skills/spec/SKILL.md +19 -0
- package/departments/ops/agents/ops-lead.yaml +1 -0
- package/departments/pm/agents/pm-director.yaml +1 -0
- package/departments/strategy/agents/strategy-director.yaml +1 -0
- package/harness/codex/AGENTS.md +1 -1
- package/harness/copilot/copilot-instructions.md +1 -1
- package/harness/cursor/rules/arkaos.mdc +2 -2
- package/harness/gemini/GEMINI.md +1 -1
- package/harness/opencode/AGENTS.md +1 -1
- package/harness/zed/.rules +1 -1
- package/installer/autoupdate.js +249 -0
- package/installer/cli.js +7 -0
- package/installer/core-snapshot.js +5 -1
- package/installer/doctor.js +11 -0
- package/installer/index.js +11 -0
- package/installer/update.js +11 -0
- package/knowledge/agents-registry-v2.json +9 -1
- package/knowledge/skills-manifest.json +14 -1
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/scripts/auto-update.sh +188 -0
- package/scripts/skill_validator.py +10 -2
- package/scripts/tools/docs_stats.py +8 -2
|
@@ -0,0 +1,396 @@
|
|
|
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
|
+
polylinePath,
|
|
23
|
+
routePointsValue,
|
|
24
|
+
labelPoint,
|
|
25
|
+
componentFill,
|
|
26
|
+
componentText,
|
|
27
|
+
arrowClassMap,
|
|
28
|
+
variantAccent
|
|
29
|
+
} from '../shared/geometry.mjs';
|
|
30
|
+
|
|
31
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const { diagram: dataflow, template, outPath } = loadDiagram({
|
|
33
|
+
rendererDir: __dirname,
|
|
34
|
+
diagramType: 'dataflow',
|
|
35
|
+
defaultExample: 'product-analytics.dataflow.json'
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const viewBox = dataflow.meta?.viewBox || [940, 720];
|
|
39
|
+
const layout = {
|
|
40
|
+
stageY: 46,
|
|
41
|
+
stageH: 36,
|
|
42
|
+
stageBottomPad: 74,
|
|
43
|
+
leftX: 100,
|
|
44
|
+
colGap: 215,
|
|
45
|
+
stageW: 168,
|
|
46
|
+
nodeW: 112,
|
|
47
|
+
nodeH: 58,
|
|
48
|
+
rowYs: [128, 242, 356, 470, 584],
|
|
49
|
+
labelH: 16
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function flowLabelSize(flow) {
|
|
53
|
+
const longestLine = Math.max(textUnits(flow.label), textUnits(flow.classification || ''));
|
|
54
|
+
return {
|
|
55
|
+
width: Math.round(Math.max(34, longestLine * 4.9 + 12) * 10) / 10,
|
|
56
|
+
height: flow.classification ? 27 : layout.labelH,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function stageX(index) {
|
|
61
|
+
return layout.leftX + index * layout.colGap;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function stageFrame(stage, index) {
|
|
65
|
+
return {
|
|
66
|
+
id: index,
|
|
67
|
+
label: stage.label,
|
|
68
|
+
kind: 'stage',
|
|
69
|
+
x: stageX(index) - layout.stageW / 2,
|
|
70
|
+
y: layout.stageY,
|
|
71
|
+
width: layout.stageW,
|
|
72
|
+
height: viewBox[1] - layout.stageY - layout.stageBottomPad,
|
|
73
|
+
radius: 10,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const compositionFrames = asArray(dataflow.stages).map(stageFrame);
|
|
78
|
+
|
|
79
|
+
function measureNode(node) {
|
|
80
|
+
const width = node.width || layout.nodeW;
|
|
81
|
+
const height = node.height || layout.nodeH;
|
|
82
|
+
const cx = stageX(node.stage);
|
|
83
|
+
const y = layout.rowYs[node.row] + (node.yOffset || 0);
|
|
84
|
+
return {
|
|
85
|
+
...node,
|
|
86
|
+
width,
|
|
87
|
+
height,
|
|
88
|
+
cx,
|
|
89
|
+
cy: y + height / 2,
|
|
90
|
+
x: cx - width / 2,
|
|
91
|
+
y
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const nodes = new Map(asArray(dataflow.nodes).map((node) => [node.id, measureNode(node)]));
|
|
96
|
+
const nodeSteps = new Map();
|
|
97
|
+
for (const [index, flow] of asArray(dataflow.flows).entries()) {
|
|
98
|
+
if (!nodeSteps.has(flow.from)) nodeSteps.set(flow.from, index);
|
|
99
|
+
if (!nodeSteps.has(flow.to)) nodeSteps.set(flow.to, index + 1);
|
|
100
|
+
}
|
|
101
|
+
for (const [index, node] of asArray(dataflow.nodes).entries()) {
|
|
102
|
+
if (!nodeSteps.has(node.id)) nodeSteps.set(node.id, index);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function validateDataflow() {
|
|
106
|
+
const problems = [];
|
|
107
|
+
if (dataflow.schema_version !== 1) problems.push('Data-flow files must set "schema_version": 1.');
|
|
108
|
+
if (dataflow.diagram_type !== 'dataflow') problems.push('Data-flow files must set "diagram_type": "dataflow".');
|
|
109
|
+
if (!dataflow.meta?.title) problems.push('Data-flow files must include meta.title.');
|
|
110
|
+
if (!Array.isArray(dataflow.stages) || dataflow.stages.length < 2) {
|
|
111
|
+
problems.push('Data-flow diagrams need at least two stages.');
|
|
112
|
+
}
|
|
113
|
+
if (!Array.isArray(dataflow.nodes) || dataflow.nodes.length < 2) {
|
|
114
|
+
problems.push('Data-flow diagrams need at least two nodes.');
|
|
115
|
+
}
|
|
116
|
+
if (!Array.isArray(dataflow.flows)) problems.push('Data-flow diagrams must include a flows array.');
|
|
117
|
+
if (dataflow.cards !== undefined && !Array.isArray(dataflow.cards)) problems.push('Data-flow "cards" must be an array.');
|
|
118
|
+
if (nodes.size !== asArray(dataflow.nodes).length) problems.push('Node ids must be unique.');
|
|
119
|
+
|
|
120
|
+
const stageCount = asArray(dataflow.stages).length;
|
|
121
|
+
for (const node of nodes.values()) {
|
|
122
|
+
if (typeof node.stage !== 'number' || node.stage < 0 || node.stage >= stageCount) {
|
|
123
|
+
problems.push(`Node "${node.id}" uses invalid stage ${node.stage} — valid stages are 0..${stageCount - 1}.`);
|
|
124
|
+
}
|
|
125
|
+
if (typeof node.row !== 'number' || node.row < 0 || node.row >= layout.rowYs.length) {
|
|
126
|
+
problems.push(`Node "${node.id}" uses invalid row ${node.row} — valid rows are 0..${layout.rowYs.length - 1}.`);
|
|
127
|
+
}
|
|
128
|
+
if (!isFinitePoint(node.x, node.y, node.cx, node.cy)) {
|
|
129
|
+
problems.push(`Node "${node.id}" produced non-finite coordinates — check stage, row, width, height, and yOffset are numbers.`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (node.x < 24 || node.x + node.width > viewBox[0] - 24) {
|
|
133
|
+
problems.push(`Node "${node.id}" exceeds the horizontal bounds of the viewBox — reduce node.width or increase meta.viewBox[0].`);
|
|
134
|
+
}
|
|
135
|
+
if (node.y < layout.stageY + layout.stageH + 22 || node.y + node.height > viewBox[1] - layout.stageBottomPad) {
|
|
136
|
+
problems.push(`Node "${node.id}" exceeds the readable diagram area — keep y between ${layout.stageY + layout.stageH + 22} and ${viewBox[1] - layout.stageBottomPad} (adjust row/yOffset or increase meta.viewBox[1]).`);
|
|
137
|
+
}
|
|
138
|
+
const estLabelW = textUnits(node.label) * 6.2;
|
|
139
|
+
if (estLabelW > node.width + 6) {
|
|
140
|
+
problems.push(`Label "${node.label}" (~${Math.round(estLabelW)}px) is wider than node "${node.id}" (${node.width}px) — shorten the label, move detail to sublabel, or increase node.width.`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const nodeList = asArray(dataflow.nodes);
|
|
145
|
+
for (let i = 0; i < nodeList.length; i += 1) {
|
|
146
|
+
for (let j = i + 1; j < nodeList.length; j += 1) {
|
|
147
|
+
const a = nodes.get(nodeList[i].id);
|
|
148
|
+
const b = nodes.get(nodeList[j].id);
|
|
149
|
+
if (rectsOverlap(a, b, 10)) {
|
|
150
|
+
problems.push(`Nodes "${a.id}" and "${b.id}" are less than 10px apart — move one to another stage/row or adjust yOffset.`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const flow of asArray(dataflow.flows)) {
|
|
156
|
+
if (!nodes.has(flow.from)) problems.push(`Flow "${flow.label || flow.from}" references unknown source "${flow.from}".`);
|
|
157
|
+
if (!nodes.has(flow.to)) problems.push(`Flow "${flow.label || flow.to}" references unknown target "${flow.to}".`);
|
|
158
|
+
if (!flow.label) problems.push(`Flow "${flow.from}" -> "${flow.to}" must include a short data label.`);
|
|
159
|
+
if (nodes.has(flow.from) && nodes.has(flow.to)) {
|
|
160
|
+
const routed = pathFor(flow);
|
|
161
|
+
const [start, end] = [routed.points[0], routed.points[routed.points.length - 1]];
|
|
162
|
+
const distance = Math.hypot(end[0] - start[0], end[1] - start[1]);
|
|
163
|
+
if (distance < 34) problems.push(`Flow "${flow.label}" is too short (${Math.round(distance)}px; minimum 34px) — route it through a channel or spread its nodes.`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
problems.push(...cleanFlowProblems({
|
|
168
|
+
relations: dataflow.flows,
|
|
169
|
+
endpointIds: new Set(nodes.keys()),
|
|
170
|
+
obstacles: nodes.values(),
|
|
171
|
+
pathFor,
|
|
172
|
+
diagramType: 'dataflow',
|
|
173
|
+
relationCollection: 'flows',
|
|
174
|
+
obstacleKind: 'node',
|
|
175
|
+
profile: dataflow.meta?.quality_profile,
|
|
176
|
+
routeHint: 'adjust fromSide/toSide, set route/via or channelX/channelY, or move the node to another stage/row'
|
|
177
|
+
}));
|
|
178
|
+
problems.push(...cleanCrossingProblems({
|
|
179
|
+
relations: dataflow.flows,
|
|
180
|
+
endpointIds: new Set(nodes.keys()),
|
|
181
|
+
pathFor,
|
|
182
|
+
diagramType: 'dataflow',
|
|
183
|
+
relationCollection: 'flows',
|
|
184
|
+
profile: dataflow.meta?.quality_profile,
|
|
185
|
+
routeHint: 'adjust route/via or channelX/channelY so the flows use separate stage corridors'
|
|
186
|
+
}));
|
|
187
|
+
problems.push(...cleanAmbiguousCorridorProblems({
|
|
188
|
+
relations: dataflow.flows,
|
|
189
|
+
endpointIds: new Set(nodes.keys()),
|
|
190
|
+
pathFor,
|
|
191
|
+
diagramType: 'dataflow',
|
|
192
|
+
relationCollection: 'flows',
|
|
193
|
+
profile: dataflow.meta?.quality_profile,
|
|
194
|
+
routeHint: 'adjust route/via or channelX/channelY so unrelated flows do not visually merge'
|
|
195
|
+
}));
|
|
196
|
+
problems.push(...cleanBorderRunProblems({
|
|
197
|
+
relations: dataflow.flows,
|
|
198
|
+
endpointIds: new Set(nodes.keys()),
|
|
199
|
+
frames: compositionFrames,
|
|
200
|
+
pathFor,
|
|
201
|
+
diagramType: 'dataflow',
|
|
202
|
+
relationCollection: 'flows',
|
|
203
|
+
profile: dataflow.meta?.quality_profile,
|
|
204
|
+
routeHint: 'adjust route/via or channelX/channelY so the flow crosses the stage perpendicularly instead of following its border'
|
|
205
|
+
}));
|
|
206
|
+
problems.push(...cleanRouteRhythmProblems({
|
|
207
|
+
relations: dataflow.flows,
|
|
208
|
+
endpointIds: new Set(nodes.keys()),
|
|
209
|
+
pathFor,
|
|
210
|
+
diagramType: 'dataflow',
|
|
211
|
+
relationCollection: 'flows',
|
|
212
|
+
profile: dataflow.meta?.quality_profile,
|
|
213
|
+
routeHint: 'adjust route/via or channelX/channelY so each turn uses a clear inter-stage corridor'
|
|
214
|
+
}));
|
|
215
|
+
|
|
216
|
+
const labelRects = [];
|
|
217
|
+
for (const [flowIndex, flow] of asArray(dataflow.flows).entries()) {
|
|
218
|
+
if (!flow.label || !nodes.has(flow.from) || !nodes.has(flow.to)) continue;
|
|
219
|
+
const [lx, ly] = labelPoint(flow, pathFor(flow).points);
|
|
220
|
+
const { width, height } = flowLabelSize(flow);
|
|
221
|
+
labelRects.push({ relation: flow, relationIndex: flowIndex, label: flow.label, x: lx - width / 2, y: ly - 11, width, height, lx, ly });
|
|
222
|
+
}
|
|
223
|
+
for (const rect of labelRects) {
|
|
224
|
+
for (const node of nodes.values()) {
|
|
225
|
+
if (rectsOverlap(rect, node, -2)) {
|
|
226
|
+
problems.push(`Label "${rect.label}" overlaps node "${node.id}" — adjust labelDx/labelDy/labelSegment or set labelAt.\n${suggestLabelObstacleFix(rect, rect.lx, rect.ly, node, 'node')}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
for (let i = 0; i < labelRects.length; i += 1) {
|
|
231
|
+
for (let j = i + 1; j < labelRects.length; j += 1) {
|
|
232
|
+
if (rectsOverlap(labelRects[i], labelRects[j], -2)) {
|
|
233
|
+
problems.push(`Labels "${labelRects[i].label}" and "${labelRects[j].label}" overlap — adjust labelDx/labelDy.\n${suggestLabelPairFix(labelRects[i], labelRects[j])}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
problems.push(...cleanLabelRouteClearanceProblems({
|
|
238
|
+
relations: dataflow.flows,
|
|
239
|
+
labels: labelRects,
|
|
240
|
+
endpointIds: new Set(nodes.keys()),
|
|
241
|
+
pathFor,
|
|
242
|
+
diagramType: 'dataflow',
|
|
243
|
+
relationCollection: 'flows',
|
|
244
|
+
profile: dataflow.meta?.quality_profile,
|
|
245
|
+
routeHint: 'adjust labelAt, labelDx, labelDy, or labelSegment; otherwise adjust the other flow route/via/channelX/channelY'
|
|
246
|
+
}));
|
|
247
|
+
|
|
248
|
+
const lastStageX = stageX(asArray(dataflow.stages).length - 1);
|
|
249
|
+
if (lastStageX + layout.stageW / 2 > viewBox[0] - 24) {
|
|
250
|
+
problems.push(`Stages exceed viewBox width — set meta.viewBox[0] to at least ${Math.ceil(lastStageX + layout.stageW / 2 + 24)}.`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (problems.length) {
|
|
254
|
+
throw new Error(`Data-flow layout validation failed:\n- ${problems.join('\n- ')}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function routeVia(flow, from, to, start, end) {
|
|
259
|
+
if (flow.via) return flow.via;
|
|
260
|
+
switch (flow.route || 'auto') {
|
|
261
|
+
case 'straight':
|
|
262
|
+
return [];
|
|
263
|
+
case 'vertical-channel': {
|
|
264
|
+
const x = flow.channelX ?? start[0] + (end[0] > start[0] ? 44 : -44);
|
|
265
|
+
return [[x, start[1]], [x, end[1]]];
|
|
266
|
+
}
|
|
267
|
+
case 'bottom-channel': {
|
|
268
|
+
const y = flow.channelY ?? Math.max(from.y + from.height, to.y + to.height) + 26;
|
|
269
|
+
return [[start[0], y], [end[0], y]];
|
|
270
|
+
}
|
|
271
|
+
case 'top-channel': {
|
|
272
|
+
const y = flow.channelY ?? Math.min(from.y, to.y) - 24;
|
|
273
|
+
return [[start[0], y], [end[0], y]];
|
|
274
|
+
}
|
|
275
|
+
case 'auto':
|
|
276
|
+
default: {
|
|
277
|
+
if (Math.abs(start[1] - end[1]) < 4) return [];
|
|
278
|
+
const midX = start[0] + (end[0] - start[0]) / 2;
|
|
279
|
+
return [[midX, start[1]], [midX, end[1]]];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const pathCache = new Map();
|
|
285
|
+
const automaticPorts = automaticPortSpread(dataflow.flows, nodes);
|
|
286
|
+
|
|
287
|
+
function pathFor(flow) {
|
|
288
|
+
if (pathCache.has(flow)) return pathCache.get(flow);
|
|
289
|
+
const from = nodes.get(flow.from);
|
|
290
|
+
const to = nodes.get(flow.to);
|
|
291
|
+
const ports = automaticPorts.get(flow);
|
|
292
|
+
const start = ports?.from || anchor(from, chosenSide(flow.fromSide, defaultFromSide(from, to)));
|
|
293
|
+
const end = ports?.to || anchor(to, chosenSide(flow.toSide, defaultToSide(from, to)));
|
|
294
|
+
const points = [start, ...routeVia(flow, from, to, start, end), end];
|
|
295
|
+
const routed = { d: polylinePath(points), points };
|
|
296
|
+
pathCache.set(flow, routed);
|
|
297
|
+
return routed;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function renderStage(stage, index) {
|
|
301
|
+
const frame = compositionFrames[index];
|
|
302
|
+
const cx = stageX(index);
|
|
303
|
+
return ` <rect data-graph-role="structural-frame" data-composition-frame-kind="stage" data-composition-frame-id="${index}" x="${frame.x}" y="${frame.y}" width="${frame.width}" height="${frame.height}" rx="${frame.radius}" class="c-lane" stroke-width="1"/>
|
|
304
|
+
<text x="${cx}" y="${layout.stageY + 22}" class="t-dim" font-size="9" font-weight="600" text-anchor="middle">${String(index + 1).padStart(2, '0')} / ${esc(stage.label)}</text>`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function renderNode(node) {
|
|
308
|
+
const fill = componentFill[node.type] || 'c-external';
|
|
309
|
+
const accent = componentText[node.type] || 't-muted';
|
|
310
|
+
const hasSub = node.sublabel != null && node.sublabel !== '';
|
|
311
|
+
const sub = hasSub
|
|
312
|
+
? `\n <text data-detail="context" x="${node.cx}" y="${node.y + 37}" class="t-muted" font-size="7" text-anchor="middle">${esc(node.sublabel)}</text>`
|
|
313
|
+
: '';
|
|
314
|
+
const tag = node.tag
|
|
315
|
+
? `\n <text data-detail="fine" x="${node.cx}" y="${node.y + node.height - 11}" class="${accent}" font-size="7" text-anchor="middle">${esc(node.tag)}</text>`
|
|
316
|
+
: '';
|
|
317
|
+
const stage = asArray(dataflow.stages)[node.stage];
|
|
318
|
+
const context = stage ? `${String(node.stage + 1).padStart(2, '0')} / ${stage.label}` : 'Data-flow node';
|
|
319
|
+
const passport = { kind: node.type, sublabel: node.sublabel, tag: node.tag, context };
|
|
320
|
+
return ` <g ${focusNodeAttrs(node.id, node.label, passport)}>
|
|
321
|
+
${focusNodeTitle(node.label, passport)}
|
|
322
|
+
<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="6" class="c-mask"/>
|
|
323
|
+
<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="6" class="${fill}"${animateAttr(dataflow.meta, 'node', nodeSteps.get(node.id))} stroke-width="1.5"/>
|
|
324
|
+
${renderSemanticSigil(node.type, { x: node.x + 6, y: node.y + 6 })}
|
|
325
|
+
<text${hasSub ? ' data-detail-anchor' : ''} x="${node.cx}" y="${node.y + 21}" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">${esc(node.label)}</text>${sub}${tag}
|
|
326
|
+
</g>`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function renderFlowPath(flow, index) {
|
|
330
|
+
const [cls, marker] = arrowClassMap[flow.variant || 'default'] || arrowClassMap.default;
|
|
331
|
+
const routed = pathFor(flow);
|
|
332
|
+
const strokeWidth = flow.width || (flow.variant === 'emphasis' ? 1.8 : 1.4);
|
|
333
|
+
return ` <path ${focusEdgeAttrs(flow.from, flow.to, flow.label, index, flow.id)} data-composition-points="${routePointsValue(routed.points)}" d="${routed.d}" class="${cls}"${animateAttr(dataflow.meta, 'edge', index)} stroke-width="${strokeWidth}" marker-end="url(#${marker})"/>`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function renderFlowLabel(flow, index) {
|
|
337
|
+
const routed = pathFor(flow);
|
|
338
|
+
const [lx, ly] = labelPoint(flow, routed.points);
|
|
339
|
+
const { width: labelW, height: labelH } = flowLabelSize(flow);
|
|
340
|
+
const classification = flow.classification
|
|
341
|
+
? `\n <text data-detail="fine" x="${lx}" y="${ly + 11}" class="t-dim" font-size="7" text-anchor="middle">${esc(flow.classification)}</text>`
|
|
342
|
+
: '';
|
|
343
|
+
return ` <g data-detail="context" ${focusEdgeAttrs(flow.from, flow.to, flow.label, index, flow.id)}>
|
|
344
|
+
<rect x="${lx - labelW / 2}" y="${ly - 11}" width="${labelW}" height="${labelH}" rx="4" class="c-mask"/>
|
|
345
|
+
<text x="${lx}" y="${ly}" class="${variantAccent(flow.variant)}" font-size="8" text-anchor="middle">${esc(flow.label)}</text>${classification}
|
|
346
|
+
</g>`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function renderLegend() {
|
|
350
|
+
const y = viewBox[1] - 36;
|
|
351
|
+
return ` <text x="214" y="${y - 20}" class="t-primary" font-size="10" font-weight="600">Legend</text>
|
|
352
|
+
<path d="M 214 ${y} L 248 ${y}" class="a-emphasis" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
|
|
353
|
+
<text x="257" y="${y + 3}" class="t-muted" font-size="8">primary data</text>
|
|
354
|
+
<path d="M 340 ${y} L 374 ${y}" class="a-security" stroke-width="1.4" marker-end="url(#arrowhead-security)"/>
|
|
355
|
+
<text x="383" y="${y + 3}" class="t-muted" font-size="8">policy / PII</text>
|
|
356
|
+
<path d="M 480 ${y} L 514 ${y}" class="a-dashed" stroke-width="1.4" marker-end="url(#arrowhead-dashed)"/>
|
|
357
|
+
<text x="523" y="${y + 3}" class="t-muted" font-size="8">async batch</text>
|
|
358
|
+
<rect x="625" y="${y - 8}" width="14" height="9" rx="2" class="c-database" stroke-width="1"/>
|
|
359
|
+
<text x="646" y="${y}" class="t-muted" font-size="8">data store</text>`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function renderSvg() {
|
|
363
|
+
return ` <svg viewBox="0 0 ${viewBox[0]} ${viewBox[1]}" ${svgRootAttrs(dataflow.meta, 'data-flow diagram')}>
|
|
364
|
+
${svgAccessibleText(dataflow.meta, 'data-flow diagram')}
|
|
365
|
+
${renderDefinitions()}
|
|
366
|
+
|
|
367
|
+
<!-- Background Grid -->
|
|
368
|
+
<rect width="100%" height="100%" fill="url(#grid)" />
|
|
369
|
+
|
|
370
|
+
<!-- Data Stages -->
|
|
371
|
+
${dataflow.stages.map(renderStage).join('\n\n')}
|
|
372
|
+
|
|
373
|
+
<!-- Flow paths -->
|
|
374
|
+
${asArray(dataflow.flows).map(renderFlowPath).join('\n')}
|
|
375
|
+
|
|
376
|
+
<!-- Nodes -->
|
|
377
|
+
${[...nodes.values()].map(renderNode).join('\n\n')}
|
|
378
|
+
|
|
379
|
+
<!-- Flow labels -->
|
|
380
|
+
${asArray(dataflow.flows).map(renderFlowLabel).join('\n')}
|
|
381
|
+
|
|
382
|
+
<!-- Legend -->
|
|
383
|
+
${renderLegend()}
|
|
384
|
+
</svg>`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
validateDataflow();
|
|
388
|
+
writeDiagram({
|
|
389
|
+
outPath,
|
|
390
|
+
template,
|
|
391
|
+
diagramType: 'dataflow',
|
|
392
|
+
meta: dataflow.meta,
|
|
393
|
+
footerLabel: 'Data-flow diagram',
|
|
394
|
+
svg: renderSvg(),
|
|
395
|
+
cards: dataflow.cards,
|
|
396
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Lifecycle Renderer
|
|
2
|
+
|
|
3
|
+
Render `diagram_type: "lifecycle"` JSON files into the standard Archify HTML
|
|
4
|
+
template.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
node archify/renderers/lifecycle/render-lifecycle.mjs input.lifecycle.json output.html
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The renderer validates input against `archify/schemas/lifecycle.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 `lifecycle.html` in the current working directory.
|
|
15
|
+
|
|
16
|
+
## Input
|
|
17
|
+
|
|
18
|
+
Lifecycle JSON files must set:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"schema_version": 1,
|
|
23
|
+
"diagram_type": "lifecycle",
|
|
24
|
+
"meta": {
|
|
25
|
+
"title": "Agent Run Lifecycle",
|
|
26
|
+
"subtitle": "Lifecycle phases, interruptions, recovery, and terminal exits",
|
|
27
|
+
"viewBox": [980, 660]
|
|
28
|
+
},
|
|
29
|
+
"lanes": [],
|
|
30
|
+
"states": [],
|
|
31
|
+
"transitions": [],
|
|
32
|
+
"cards": []
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Lane ids are semantic and reserved: a lane with id `main` is required and maps
|
|
37
|
+
to the top phase band; `terminal` maps to the bottom outcome band; every other
|
|
38
|
+
lane id (up to 4 lanes total) shares the single middle event band. The three
|
|
39
|
+
band headers render from your lane labels — the middle band joins the labels of
|
|
40
|
+
all event lanes with ` + `. A complete worked example lives at
|
|
41
|
+
`archify/examples/agent-run.lifecycle.json`.
|
|
42
|
+
|
|
43
|
+
The schema lives at:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
archify/schemas/lifecycle.schema.json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Layout budget
|
|
50
|
+
|
|
51
|
+
| Band | Lane id | Top y | Column centers | Default state |
|
|
52
|
+
|------|---------|-------|----------------|---------------|
|
|
53
|
+
| Phase | `main` (required) | 126 | `col` 0–4 → x = 94, 248, 402, 556, 710 | 118×62 |
|
|
54
|
+
| Event | any other id | 278 | `col` 0–2 → x = 402, 556, 710 | 126×58 |
|
|
55
|
+
| Outcome | `terminal` | 450 | `col` 0–2 → x = 402, 556, 710 | 118×58 |
|
|
56
|
+
|
|
57
|
+
| Constant | Value |
|
|
58
|
+
|----------|-------|
|
|
59
|
+
| viewBox | default `[980, 660]`; schema minimum `[420, 360]` |
|
|
60
|
+
| State area | x within `[32, width − 32]`; y within `[64, legend y − 24]` |
|
|
61
|
+
| State spacing | ≥10px between any two states — checked across lanes, because all event lanes share one band; separate same-band states with `col` or `yOffset` |
|
|
62
|
+
| Transition length | ≥32px between endpoints |
|
|
63
|
+
| Legend row | y = height − 98 |
|
|
64
|
+
|
|
65
|
+
The primary lifecycle rail runs along the phase band and extends to the
|
|
66
|
+
furthest occupied phase column. Route presets for transitions: `straight`,
|
|
67
|
+
`drop` (bend at `channelY`, defaulting to the vertical midpoint),
|
|
68
|
+
`bottom-channel`, `top-channel`, `right-channel`, `left-channel`, explicit
|
|
69
|
+
`via` points, or the default `auto`. Multi-segment transitions get rounded
|
|
70
|
+
corners; tune them with `cornerRadius` (default 10, `0` for sharp bends).
|
|
71
|
+
|
|
72
|
+
## Design Rules
|
|
73
|
+
|
|
74
|
+
- Treat lifecycle diagrams as a phase map, not a dense state-transition graph.
|
|
75
|
+
- Put the primary lifecycle on one horizontal rail using the `main` lane.
|
|
76
|
+
- Use `step` labels for ordered phases, such as `01`, `02`, and `03`.
|
|
77
|
+
- Use lower lanes only for interruptions, recovery, and terminal exits.
|
|
78
|
+
- Keep transition labels out of the main SVG unless the label is essential;
|
|
79
|
+
prefer node labels, tags, legend entries, and summary cards.
|
|
80
|
+
- Avoid diagonal and crossing lines. Terminal exits should drop vertically from
|
|
81
|
+
their source event whenever possible.
|
|
82
|
+
- Use `success` for completion, `failure` for failure/terminal exits,
|
|
83
|
+
`waiting` for pauses, and `decision` for quality gates.
|
|
84
|
+
|
|
85
|
+
Schema violations exit non-zero with path-prefixed messages annotated with the
|
|
86
|
+
element's id or label. The renderer additionally fails when it can detect
|
|
87
|
+
layout problems, including a missing `main` lane, duplicate state IDs, unknown
|
|
88
|
+
lanes, unknown transition endpoints, states outside the lifecycle area,
|
|
89
|
+
overlapping states (including across lanes), labels colliding with states or
|
|
90
|
+
other labels, labels wider than their state, unreadably short transitions, or
|
|
91
|
+
transitions crossing unrelated states (2px Clean Flow clearance). Lifecycle
|
|
92
|
+
bands remain intentional pass-through containers.
|
|
93
|
+
Text width is estimated CJK-aware: fullwidth glyphs count as two units.
|
|
94
|
+
|
|
95
|
+
Set `meta.quality_profile` to `showcase` for polished delivery. Unrelated proper
|
|
96
|
+
X crossings then fail with `composition/proper-crossing`; default `standard`
|
|
97
|
+
keeps them as artifact-receipt warnings. The final artifact check samples
|
|
98
|
+
rounded `Q` corners. Collinear corridors remain outside the proper-X rule, but
|
|
99
|
+
a separate gate warns in `standard` and fails in `showcase` when unrelated
|
|
100
|
+
transitions overlap for at least 8px. Shared semantic endpoints, point touches,
|
|
101
|
+
and shorter overlaps remain valid. Showcase also rejects any route segment
|
|
102
|
+
below 8px and any interior turn segment below 16px; ordinary 8–15px endpoint
|
|
103
|
+
stubs remain valid.
|