@mountainpass/addressr 2.0.3 → 2.1.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.
@@ -1,82 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * c4-generate.mjs — Regenerate C4 architecture diagrams from source code.
5
- * Portable, self-contained (no npm deps). Run via: node c4-generate.mjs
6
- */
7
- "use strict";
8
-
9
- var _nodeFs = _interopRequireDefault(require("node:fs"));
10
- var _nodePath = _interopRequireDefault(require("node:path"));
11
- var _c4Lib = require("./c4-lib.mjs");
12
- function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
13
- const ROOT = process.cwd();
14
- const OUT_DIR = _nodePath.default.join(ROOT, "docs", "architecture", "generated");
15
- const OUT_JSON = _nodePath.default.join(OUT_DIR, "components.json");
16
- const OUT_MERMAID = _nodePath.default.join(OUT_DIR, "components.mmd");
17
- const C4_MODEL = _nodePath.default.join(ROOT, "docs", "architecture", "C4_MODEL.md");
18
- const C3_START = "<!-- c3:generated:start -->";
19
- const C3_END = "<!-- c3:generated:end -->";
20
- const C4_START = "<!-- c4:generated:start -->";
21
- const C4_END = "<!-- c4:generated:end -->";
22
- const C4_SCAFFOLD = `# C4 Architecture Model
23
-
24
- This repo uses a hybrid C4 approach:
25
- - C1/C2 are curated for intent and business context.
26
- - C3/C4 are generated from code to reduce drift.
27
-
28
- ## C3: Component View (Generated)
29
-
30
- ${C3_START}
31
-
32
- ${C3_END}
33
-
34
- ## C4: Code View (Generated)
35
-
36
- File-level dependency diagrams per component. Dashed arrows indicate cross-component imports. Grey nodes are external files.
37
-
38
- ${C4_START}
39
-
40
- ${C4_END}
41
-
42
- Regenerate: \`/c4\`
43
- Check freshness: \`/c4-check\`
44
- `;
45
- function inlineGenerated(startMarker, endMarker, content) {
46
- if (!_nodeFs.default.existsSync(C4_MODEL)) return;
47
- const doc = _nodeFs.default.readFileSync(C4_MODEL, "utf8");
48
- const startIdx = doc.indexOf(startMarker);
49
- const endIdx = doc.indexOf(endMarker);
50
- if (startIdx === -1 || endIdx === -1) return;
51
- const before = doc.slice(0, startIdx + startMarker.length);
52
- const after = doc.slice(endIdx);
53
- const updated = `${before}\n\n${content}\n\n${after}`;
54
- _nodeFs.default.writeFileSync(C4_MODEL, updated);
55
- }
56
- function main() {
57
- const srcRoot = (0, _c4Lib.detectSourceRoot)(ROOT);
58
- const model = (0, _c4Lib.buildModel)(srcRoot);
59
- const json = (0, _c4Lib.toJson)(model);
60
- const c3Mermaid = (0, _c4Lib.toC3Mermaid)(model);
61
- const c4Mermaid = (0, _c4Lib.toC4Mermaid)(model);
62
- _nodeFs.default.mkdirSync(OUT_DIR, {
63
- recursive: true
64
- });
65
-
66
- // Create scaffold if C4_MODEL.md doesn't exist
67
- if (!_nodeFs.default.existsSync(C4_MODEL)) {
68
- _nodeFs.default.mkdirSync(_nodePath.default.dirname(C4_MODEL), {
69
- recursive: true
70
- });
71
- _nodeFs.default.writeFileSync(C4_MODEL, C4_SCAFFOLD);
72
- }
73
- _nodeFs.default.writeFileSync(OUT_JSON, json);
74
- _nodeFs.default.writeFileSync(OUT_MERMAID, c3Mermaid);
75
- inlineGenerated(C3_START, C3_END, `\`\`\`mermaid\n${c3Mermaid.trimEnd()}\n\`\`\``);
76
- inlineGenerated(C4_START, C4_END, c4Mermaid);
77
- console.log("PASS: C4 artifacts generated:");
78
- console.log(`- ${_nodePath.default.relative(ROOT, OUT_JSON)}`);
79
- console.log(`- ${_nodePath.default.relative(ROOT, OUT_MERMAID)}`);
80
- console.log(`- ${_nodePath.default.relative(ROOT, C4_MODEL)} (C3 + C4 sections updated)`);
81
- }
82
- main();
@@ -1,250 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.buildModel = buildModel;
7
- exports.detectSourceRoot = detectSourceRoot;
8
- exports.toC3Mermaid = toC3Mermaid;
9
- exports.toC4Mermaid = toC4Mermaid;
10
- exports.toJson = toJson;
11
- var _nodeFs = _interopRequireDefault(require("node:fs"));
12
- var _nodePath = _interopRequireDefault(require("node:path"));
13
- function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
- /**
15
- * c4-lib.mjs — Portable C4 model builder (pure Node.js, no npm deps).
16
- * Shared by c4-generate.mjs and c4-check.mjs.
17
- */
18
-
19
- // ---------------------------------------------------------------------------
20
- // Source root detection
21
- // ---------------------------------------------------------------------------
22
-
23
- function detectSourceRoot(projectRoot) {
24
- // 1. Try tsconfig.json
25
- const tsconfigPath = _nodePath.default.join(projectRoot, "tsconfig.json");
26
- if (_nodeFs.default.existsSync(tsconfigPath)) {
27
- try {
28
- const raw = _nodeFs.default.readFileSync(tsconfigPath, "utf8");
29
- // Strip single-line comments for lenient JSON parse
30
- const stripped = raw.replace(/\/\/.*$/gm, "");
31
- const tsconfig = JSON.parse(stripped);
32
- const rootDir = tsconfig?.compilerOptions?.rootDir;
33
- if (rootDir) {
34
- const candidate = _nodePath.default.resolve(projectRoot, rootDir);
35
- if (_nodeFs.default.existsSync(candidate)) return candidate;
36
- }
37
- const includes = tsconfig?.include;
38
- if (Array.isArray(includes) && includes.length > 0) {
39
- // Strip glob suffixes like /**/*
40
- const first = includes[0].replace(/\/\*.*$/, "");
41
- const candidate = _nodePath.default.resolve(projectRoot, first);
42
- if (_nodeFs.default.existsSync(candidate)) return candidate;
43
- }
44
- } catch {
45
- // Fall through to probing
46
- }
47
- }
48
-
49
- // 2. Probe common directories
50
- for (const probe of ["app/src", "src", "lib"]) {
51
- const candidate = _nodePath.default.join(projectRoot, probe);
52
- if (_nodeFs.default.existsSync(candidate)) return candidate;
53
- }
54
-
55
- // 3. Fall back to project root
56
- const fallback = projectRoot;
57
-
58
- // 4. Verify .ts files exist somewhere
59
- if (!hasFilesWithExtension(fallback, ".ts")) {
60
- for (const [ext, lang] of [[".py", "Python"], [".go", "Go"], [".rs", "Rust"], [".java", "Java"]]) {
61
- if (hasFilesWithExtension(fallback, ext)) {
62
- throw new Error(`C4 generation does not yet support ${lang} projects`);
63
- }
64
- }
65
- throw new Error("No TypeScript source files found");
66
- }
67
- return fallback;
68
- }
69
- function hasFilesWithExtension(dir, ext) {
70
- try {
71
- const entries = _nodeFs.default.readdirSync(dir, {
72
- withFileTypes: true
73
- });
74
- for (const entry of entries) {
75
- const full = _nodePath.default.join(dir, entry.name);
76
- if (entry.isDirectory()) {
77
- if (hasFilesWithExtension(full, ext)) return true;
78
- } else if (entry.isFile() && entry.name.endsWith(ext)) {
79
- return true;
80
- }
81
- }
82
- } catch {
83
- // Directory not readable
84
- }
85
- return false;
86
- }
87
-
88
- // ---------------------------------------------------------------------------
89
- // File walking
90
- // ---------------------------------------------------------------------------
91
-
92
- function walk(dir, out) {
93
- const entries = _nodeFs.default.readdirSync(dir, {
94
- withFileTypes: true
95
- });
96
- for (const entry of entries) {
97
- const full = _nodePath.default.join(dir, entry.name);
98
- if (entry.isDirectory()) {
99
- walk(full, out);
100
- continue;
101
- }
102
- if (!entry.isFile()) continue;
103
- if (!entry.name.endsWith(".ts") || entry.name.endsWith(".test.ts")) continue;
104
- out.push(full);
105
- }
106
- }
107
-
108
- // ---------------------------------------------------------------------------
109
- // Import parsing & resolution
110
- // ---------------------------------------------------------------------------
111
-
112
- function parseImports(text) {
113
- const specs = [];
114
- const importRe = /import\s+[^"']*?["']([^"']+)["']/g;
115
- const dynamicRe = /import\(\s*["']([^"']+)["']\s*\)/g;
116
- const requireRe = /require\(\s*["']([^"']+)["']\s*\)/g;
117
- let match;
118
- while ((match = importRe.exec(text)) !== null) specs.push(match[1]);
119
- while ((match = dynamicRe.exec(text)) !== null) specs.push(match[1]);
120
- while ((match = requireRe.exec(text)) !== null) specs.push(match[1]);
121
- return specs;
122
- }
123
- function resolveImport(fromFile, spec, srcRoot) {
124
- if (!spec.startsWith(".")) return null;
125
- const stripped = spec.replace(/\.js$/, "");
126
- const base = _nodePath.default.resolve(_nodePath.default.dirname(fromFile), stripped);
127
- const candidates = [base, `${base}.ts`, _nodePath.default.join(base, "index.ts")];
128
- for (const candidate of candidates) {
129
- if (_nodeFs.default.existsSync(candidate) && _nodeFs.default.statSync(candidate).isFile()) {
130
- return candidate;
131
- }
132
- }
133
- return null;
134
- }
135
-
136
- // ---------------------------------------------------------------------------
137
- // Model building
138
- // ---------------------------------------------------------------------------
139
-
140
- function relToSrc(absPath, srcRoot) {
141
- return _nodePath.default.relative(srcRoot, absPath).split(_nodePath.default.sep).join("/");
142
- }
143
- function componentIdForRel(rel) {
144
- const [first] = rel.split("/");
145
- if (!first || !rel.includes("/")) return "app";
146
- return first;
147
- }
148
- function buildModel(srcRoot) {
149
- const files = [];
150
- walk(srcRoot, files);
151
- const componentFiles = new Map();
152
- const dependencies = new Map();
153
- const fileDeps = [];
154
- for (const absFile of files) {
155
- const fromRel = relToSrc(absFile, srcRoot);
156
- const fromComp = componentIdForRel(fromRel);
157
- if (!componentFiles.has(fromComp)) componentFiles.set(fromComp, new Set());
158
- componentFiles.get(fromComp).add(fromRel);
159
- if (!dependencies.has(fromComp)) dependencies.set(fromComp, new Set());
160
- const text = _nodeFs.default.readFileSync(absFile, "utf8");
161
- const specs = parseImports(text);
162
- for (const spec of specs) {
163
- const resolved = resolveImport(absFile, spec, srcRoot);
164
- if (!resolved) continue;
165
- const toRel = relToSrc(resolved, srcRoot);
166
- const toComp = componentIdForRel(toRel);
167
- fileDeps.push({
168
- from: fromRel,
169
- to: toRel
170
- });
171
- if (toComp !== fromComp) dependencies.get(fromComp).add(toComp);
172
- }
173
- }
174
- const components = [...componentFiles.keys()].sort().map(id => ({
175
- id,
176
- name: id === "app" ? "app-entry" : id,
177
- kind: "generated",
178
- files: [...(componentFiles.get(id) || [])].sort(),
179
- depends_on: [...(dependencies.get(id) || [])].sort()
180
- }));
181
- return {
182
- generator_version: "1",
183
- source_root: _nodePath.default.relative(process.cwd(), srcRoot).split(_nodePath.default.sep).join("/") || ".",
184
- components,
185
- fileDeps
186
- };
187
- }
188
-
189
- // ---------------------------------------------------------------------------
190
- // Mermaid generation
191
- // ---------------------------------------------------------------------------
192
-
193
- function toC3Mermaid(model) {
194
- const lines = ["flowchart LR"];
195
- for (const component of model.components) {
196
- lines.push(` ${component.id}["${component.name}"]`);
197
- }
198
- for (const component of model.components) {
199
- for (const to of component.depends_on) {
200
- lines.push(` ${component.id} --> ${to}`);
201
- }
202
- }
203
- lines.push("");
204
- return `${lines.join("\n")}\n`;
205
- }
206
- function fileNodeId(relPath) {
207
- return relPath.replace(/[/.\\-]/g, "_").replace(/\.ts$/, "");
208
- }
209
- function fileLabel(relPath) {
210
- return _nodePath.default.basename(relPath, ".ts");
211
- }
212
- function toC4Mermaid(model) {
213
- const sections = [];
214
- for (const component of model.components) {
215
- const lines = ["flowchart LR"];
216
- const fileSet = new Set(component.files);
217
- for (const file of component.files) {
218
- lines.push(` ${fileNodeId(file)}["${fileLabel(file)}"]`);
219
- }
220
- const externalNodes = new Set();
221
- const edges = new Set();
222
- for (const dep of model.fileDeps) {
223
- if (!fileSet.has(dep.from)) continue;
224
- const edgeKey = `${dep.from}|${dep.to}`;
225
- if (edges.has(edgeKey)) continue;
226
- edges.add(edgeKey);
227
- if (fileSet.has(dep.to)) {
228
- lines.push(` ${fileNodeId(dep.from)} --> ${fileNodeId(dep.to)}`);
229
- } else {
230
- const toCompId = componentIdForRel(dep.to);
231
- const toComp = toCompId === "app" ? "app-entry" : toCompId;
232
- const extId = fileNodeId(dep.to);
233
- if (!externalNodes.has(dep.to)) {
234
- externalNodes.add(dep.to);
235
- lines.push(` ${extId}["${toComp}/${fileLabel(dep.to)}"]:::ext`);
236
- }
237
- lines.push(` ${fileNodeId(dep.from)} -.-> ${extId}`);
238
- }
239
- }
240
- if (externalNodes.size > 0) {
241
- lines.push(` classDef ext fill:#f0f0f0,stroke:#999,stroke-dasharray:5 5`);
242
- }
243
- lines.push("");
244
- sections.push(`### ${component.name}\n\n\`\`\`mermaid\n${lines.join("\n")}\n\`\`\``);
245
- }
246
- return sections.join("\n\n");
247
- }
248
- function toJson(model) {
249
- return `${JSON.stringify(model, null, 2)}\n`;
250
- }
@@ -1,74 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * c4-check.mjs — Check whether C4 architecture diagrams are up to date.
5
- * Portable, self-contained (no npm deps). Run via: node c4-check.mjs
6
- */
7
- "use strict";
8
-
9
- var _nodeFs = _interopRequireDefault(require("node:fs"));
10
- var _nodePath = _interopRequireDefault(require("node:path"));
11
- var _c4Lib = require("../../c4/scripts/c4-lib.mjs");
12
- function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
13
- const ROOT = process.cwd();
14
- const COMPONENTS_FILE = _nodePath.default.join(ROOT, "docs", "architecture", "generated", "components.json");
15
- const POLICY_FILE = _nodePath.default.join(ROOT, "governance", "architecture-conformance-policy.json");
16
- function main() {
17
- const srcRoot = (0, _c4Lib.detectSourceRoot)(ROOT);
18
- const model = (0, _c4Lib.buildModel)(srcRoot);
19
- const freshJson = (0, _c4Lib.toJson)(model);
20
- const failures = [];
21
-
22
- // 1. Compare JSON against existing components.json
23
- if (!_nodeFs.default.existsSync(COMPONENTS_FILE)) {
24
- failures.push("missing generated architecture model: docs/architecture/generated/components.json");
25
- } else {
26
- const existingJson = _nodeFs.default.readFileSync(COMPONENTS_FILE, "utf8");
27
- if (existingJson !== freshJson) {
28
- failures.push("C4 model is stale — run /c4 to regenerate");
29
- }
30
- }
31
-
32
- // 2. Conformance policy check (if policy file exists)
33
- if (_nodeFs.default.existsSync(POLICY_FILE)) {
34
- const policy = JSON.parse(_nodeFs.default.readFileSync(POLICY_FILE, "utf8"));
35
- const components = new Map();
36
- for (const component of model.components) {
37
- components.set(component.id, new Set(component.depends_on || []));
38
- }
39
- for (const id of policy.required_components || []) {
40
- if (!components.has(id)) {
41
- failures.push(`missing required component: ${id}`);
42
- }
43
- }
44
- for (const rule of policy.forbidden_dependencies || []) {
45
- const deps = components.get(rule.from);
46
- if (!deps) {
47
- failures.push(`forbidden dependency rule references unknown component: ${rule.from}`);
48
- continue;
49
- }
50
- if (deps.has(rule.to)) {
51
- failures.push(`forbidden dependency: ${rule.from} -> ${rule.to}${rule.reason ? ` (${rule.reason})` : ""}`);
52
- }
53
- }
54
- for (const rule of policy.required_dependencies || []) {
55
- const deps = components.get(rule.from);
56
- if (!deps) {
57
- failures.push(`required dependency rule references unknown component: ${rule.from}`);
58
- continue;
59
- }
60
- if (!deps.has(rule.to)) {
61
- failures.push(`missing required dependency: ${rule.from} -> ${rule.to}${rule.reason ? ` (${rule.reason})` : ""}`);
62
- }
63
- }
64
- }
65
- if (failures.length > 0) {
66
- console.error("FAIL: C4 architecture check:");
67
- for (const failure of failures) {
68
- console.error(`- ${failure}`);
69
- }
70
- process.exit(1);
71
- }
72
- console.log("PASS: C4 architecture diagrams are up to date.");
73
- }
74
- main();
@@ -1,191 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Converts an OWM (Online Wardley Mapping) file to SVG and PNG.
4
- *
5
- * Usage: node owm-to-svg.mjs [input.owm] [output.svg]
6
- *
7
- * Defaults:
8
- * input: docs/wardley-map.owm
9
- * output: docs/wardley-map.svg (+ .png via sips)
10
- */
11
- "use strict";
12
-
13
- var _fs = require("fs");
14
- var _child_process = require("child_process");
15
- var _path = require("path");
16
- const inputPath = (0, _path.resolve)(process.argv[2] || 'docs/wardley-map.owm');
17
- const outputSvg = (0, _path.resolve)(process.argv[3] || inputPath.replace(/\.owm$/, '.svg'));
18
- const outputPng = outputSvg.replace(/\.svg$/, '.png');
19
- const raw = (0, _fs.readFileSync)(inputPath, 'utf8');
20
- const lines = raw.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('//'));
21
-
22
- // Parse
23
- let title = '';
24
- const components = [];
25
- const links = [];
26
- const evolves = [];
27
- for (const line of lines) {
28
- const titleMatch = line.match(/^title\s+(.+)/);
29
- if (titleMatch) {
30
- title = titleMatch[1];
31
- continue;
32
- }
33
- const anchorMatch = line.match(/^anchor\s+(.+?)\s+\[([0-9.]+),\s*([0-9.]+)\]/);
34
- if (anchorMatch) {
35
- components.push({
36
- name: anchorMatch[1],
37
- vis: parseFloat(anchorMatch[2]),
38
- evo: parseFloat(anchorMatch[3]),
39
- isAnchor: true
40
- });
41
- continue;
42
- }
43
- const compMatch = line.match(/^component\s+(.+?)\s+\[([0-9.]+),\s*([0-9.]+)\]/);
44
- if (compMatch) {
45
- components.push({
46
- name: compMatch[1],
47
- vis: parseFloat(compMatch[2]),
48
- evo: parseFloat(compMatch[3]),
49
- isAnchor: false
50
- });
51
- continue;
52
- }
53
- const evolveMatch = line.match(/^evolve\s+(.+?)\s+([0-9.]+)/);
54
- if (evolveMatch) {
55
- evolves.push({
56
- name: evolveMatch[1],
57
- targetEvo: parseFloat(evolveMatch[2])
58
- });
59
- continue;
60
- }
61
- const linkMatch = line.match(/^(.+?)->(.+)/);
62
- if (linkMatch) {
63
- links.push({
64
- from: linkMatch[1].trim(),
65
- to: linkMatch[2].trim()
66
- });
67
- continue;
68
- }
69
- }
70
-
71
- // Layout constants
72
- const W = 1200;
73
- const H = 800;
74
- const PAD_LEFT = 80;
75
- const PAD_RIGHT = 60;
76
- const PAD_TOP = 60;
77
- const PAD_BOTTOM = 80;
78
- const CHART_W = W - PAD_LEFT - PAD_RIGHT;
79
- const CHART_H = H - PAD_TOP - PAD_BOTTOM;
80
- function evoToX(evo) {
81
- return PAD_LEFT + evo * CHART_W;
82
- }
83
- function visToY(vis) {
84
- return PAD_TOP + (1 - vis) * CHART_H;
85
- }
86
-
87
- // Build SVG
88
- const svgParts = [];
89
- svgParts.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">`);
90
- svgParts.push(`<defs>
91
- <marker id="evolve-arrow" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
92
- <polygon points="0,0 8,3 0,6" fill="#c44"/>
93
- </marker>
94
- </defs>`);
95
- svgParts.push(`<style>
96
- text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; }
97
- .title { font-size: 20px; font-weight: 700; }
98
- .axis-label { font-size: 12px; fill: #666; }
99
- .phase-label { font-size: 11px; fill: #999; }
100
- .comp-label { font-size: 13px; fill: #222; }
101
- .anchor-label { font-size: 13px; fill: #222; font-weight: 600; }
102
- .evolve-label { font-size: 11px; fill: #c44; }
103
- </style>`);
104
-
105
- // Background
106
- svgParts.push(`<rect width="${W}" height="${H}" fill="white"/>`);
107
-
108
- // Title
109
- svgParts.push(`<text x="${PAD_LEFT}" y="35" class="title">${title}</text>`);
110
-
111
- // Axes
112
- svgParts.push(`<line x1="${PAD_LEFT}" y1="${PAD_TOP}" x2="${PAD_LEFT}" y2="${H - PAD_BOTTOM}" stroke="#333" stroke-width="1.5"/>`);
113
- svgParts.push(`<line x1="${PAD_LEFT}" y1="${H - PAD_BOTTOM}" x2="${W - PAD_RIGHT}" y2="${H - PAD_BOTTOM}" stroke="#333" stroke-width="1.5"/>`);
114
-
115
- // Arrow on evolution axis
116
- const arrowX = W - PAD_RIGHT;
117
- const arrowY = H - PAD_BOTTOM;
118
- svgParts.push(`<polygon points="${arrowX},${arrowY} ${arrowX - 8},${arrowY - 4} ${arrowX - 8},${arrowY + 4}" fill="#333"/>`);
119
-
120
- // Axis titles
121
- svgParts.push(`<text x="${PAD_LEFT - 10}" y="${PAD_TOP + CHART_H / 2}" class="axis-label" transform="rotate(-90 ${PAD_LEFT - 10} ${PAD_TOP + CHART_H / 2})" text-anchor="middle">Value Chain</text>`);
122
- svgParts.push(`<text x="${PAD_LEFT + CHART_W / 2}" y="${H - 15}" class="axis-label" text-anchor="middle">Evolution</text>`);
123
-
124
- // Phase dividers and labels
125
- const phases = [{
126
- boundary: 0.17,
127
- label: 'Genesis'
128
- }, {
129
- boundary: 0.37,
130
- label: 'Custom-Built'
131
- }, {
132
- boundary: 0.63,
133
- label: 'Product'
134
- }, {
135
- boundary: 1.0,
136
- label: 'Commodity'
137
- }];
138
- let prevBound = 0;
139
- for (const phase of phases) {
140
- const midEvo = (prevBound + phase.boundary) / 2;
141
- svgParts.push(`<text x="${evoToX(midEvo)}" y="${H - PAD_BOTTOM + 25}" class="phase-label" text-anchor="middle">${phase.label}</text>`);
142
- if (phase.boundary < 1.0) {
143
- const dx = evoToX(phase.boundary);
144
- svgParts.push(`<line x1="${dx}" y1="${PAD_TOP}" x2="${dx}" y2="${H - PAD_BOTTOM}" stroke="#ddd" stroke-width="1" stroke-dasharray="4,4"/>`);
145
- }
146
- prevBound = phase.boundary;
147
- }
148
-
149
- // Links
150
- const compMap = new Map(components.map(c => [c.name, c]));
151
- for (const link of links) {
152
- const from = compMap.get(link.from);
153
- const to = compMap.get(link.to);
154
- if (!from || !to) continue;
155
- svgParts.push(`<line x1="${evoToX(from.evo)}" y1="${visToY(from.vis)}" x2="${evoToX(to.evo)}" y2="${visToY(to.vis)}" stroke="#aaa" stroke-width="1.5"/>`);
156
- }
157
-
158
- // Evolution arrows
159
- for (const ev of evolves) {
160
- const comp = compMap.get(ev.name);
161
- if (!comp) continue;
162
- const x1 = evoToX(comp.evo);
163
- const x2 = evoToX(ev.targetEvo);
164
- const y = visToY(comp.vis);
165
- svgParts.push(`<line x1="${x1 + 8}" y1="${y}" x2="${x2 - 2}" y2="${y}" stroke="#c44" stroke-width="2" stroke-dasharray="6,3" marker-end="url(#evolve-arrow)"/>`);
166
- svgParts.push(`<circle cx="${x2}" cy="${y}" r="5" fill="none" stroke="#c44" stroke-width="1.5" stroke-dasharray="3,2"/>`);
167
- }
168
-
169
- // Components
170
- for (const comp of components) {
171
- const cx = evoToX(comp.evo);
172
- const cy = visToY(comp.vis);
173
- const r = comp.isAnchor ? 0 : 6;
174
- const labelClass = comp.isAnchor ? 'anchor-label' : 'comp-label';
175
- if (!comp.isAnchor) {
176
- svgParts.push(`<circle cx="${cx}" cy="${cy}" r="${r}" fill="white" stroke="#333" stroke-width="1.5"/>`);
177
- }
178
- svgParts.push(`<text x="${cx + 10}" y="${cy - 10}" class="${labelClass}">${comp.name}</text>`);
179
- }
180
- svgParts.push('</svg>');
181
- const svg = svgParts.join('\n');
182
- (0, _fs.writeFileSync)(outputSvg, svg);
183
- console.log(`SVG: ${outputSvg}`);
184
-
185
- // Convert to PNG via sips (macOS)
186
- try {
187
- (0, _child_process.execSync)(`sips -s format png "${outputSvg}" --out "${outputPng}" 2>/dev/null`);
188
- console.log(`PNG: ${outputPng}`);
189
- } catch {
190
- console.log('PNG: skipped (sips not available, macOS only)');
191
- }