@mountainpass/addressr 1.1.3 → 1.1.5

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.
@@ -0,0 +1,84 @@
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 = require("node:fs");
10
+ var _nodeFs2 = _interopRequireDefault(_nodeFs);
11
+ var _nodePath = require("node:path");
12
+ var _nodePath2 = _interopRequireDefault(_nodePath);
13
+ var _c4Lib = require("./c4-lib.mjs");
14
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
15
+ const ROOT = process.cwd();
16
+ const OUT_DIR = _nodePath2.default.join(ROOT, "docs", "architecture", "generated");
17
+ const OUT_JSON = _nodePath2.default.join(OUT_DIR, "components.json");
18
+ const OUT_MERMAID = _nodePath2.default.join(OUT_DIR, "components.mmd");
19
+ const C4_MODEL = _nodePath2.default.join(ROOT, "docs", "architecture", "C4_MODEL.md");
20
+ const C3_START = "<!-- c3:generated:start -->";
21
+ const C3_END = "<!-- c3:generated:end -->";
22
+ const C4_START = "<!-- c4:generated:start -->";
23
+ const C4_END = "<!-- c4:generated:end -->";
24
+ const C4_SCAFFOLD = `# C4 Architecture Model
25
+
26
+ This repo uses a hybrid C4 approach:
27
+ - C1/C2 are curated for intent and business context.
28
+ - C3/C4 are generated from code to reduce drift.
29
+
30
+ ## C3: Component View (Generated)
31
+
32
+ ${C3_START}
33
+
34
+ ${C3_END}
35
+
36
+ ## C4: Code View (Generated)
37
+
38
+ File-level dependency diagrams per component. Dashed arrows indicate cross-component imports. Grey nodes are external files.
39
+
40
+ ${C4_START}
41
+
42
+ ${C4_END}
43
+
44
+ Regenerate: \`/c4\`
45
+ Check freshness: \`/c4-check\`
46
+ `;
47
+ function inlineGenerated(startMarker, endMarker, content) {
48
+ if (!_nodeFs2.default.existsSync(C4_MODEL)) return;
49
+ const doc = _nodeFs2.default.readFileSync(C4_MODEL, "utf8");
50
+ const startIdx = doc.indexOf(startMarker);
51
+ const endIdx = doc.indexOf(endMarker);
52
+ if (startIdx === -1 || endIdx === -1) return;
53
+ const before = doc.slice(0, startIdx + startMarker.length);
54
+ const after = doc.slice(endIdx);
55
+ const updated = `${before}\n\n${content}\n\n${after}`;
56
+ _nodeFs2.default.writeFileSync(C4_MODEL, updated);
57
+ }
58
+ function main() {
59
+ const srcRoot = (0, _c4Lib.detectSourceRoot)(ROOT);
60
+ const model = (0, _c4Lib.buildModel)(srcRoot);
61
+ const json = (0, _c4Lib.toJson)(model);
62
+ const c3Mermaid = (0, _c4Lib.toC3Mermaid)(model);
63
+ const c4Mermaid = (0, _c4Lib.toC4Mermaid)(model);
64
+ _nodeFs2.default.mkdirSync(OUT_DIR, {
65
+ recursive: true
66
+ });
67
+
68
+ // Create scaffold if C4_MODEL.md doesn't exist
69
+ if (!_nodeFs2.default.existsSync(C4_MODEL)) {
70
+ _nodeFs2.default.mkdirSync(_nodePath2.default.dirname(C4_MODEL), {
71
+ recursive: true
72
+ });
73
+ _nodeFs2.default.writeFileSync(C4_MODEL, C4_SCAFFOLD);
74
+ }
75
+ _nodeFs2.default.writeFileSync(OUT_JSON, json);
76
+ _nodeFs2.default.writeFileSync(OUT_MERMAID, c3Mermaid);
77
+ inlineGenerated(C3_START, C3_END, `\`\`\`mermaid\n${c3Mermaid.trimEnd()}\n\`\`\``);
78
+ inlineGenerated(C4_START, C4_END, c4Mermaid);
79
+ console.log("PASS: C4 artifacts generated:");
80
+ console.log(`- ${_nodePath2.default.relative(ROOT, OUT_JSON)}`);
81
+ console.log(`- ${_nodePath2.default.relative(ROOT, OUT_MERMAID)}`);
82
+ console.log(`- ${_nodePath2.default.relative(ROOT, C4_MODEL)} (C3 + C4 sections updated)`);
83
+ }
84
+ main();
@@ -0,0 +1,252 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.detectSourceRoot = detectSourceRoot;
7
+ exports.buildModel = buildModel;
8
+ exports.toC3Mermaid = toC3Mermaid;
9
+ exports.toC4Mermaid = toC4Mermaid;
10
+ exports.toJson = toJson;
11
+ var _nodeFs = require("node:fs");
12
+ var _nodeFs2 = _interopRequireDefault(_nodeFs);
13
+ var _nodePath = require("node:path");
14
+ var _nodePath2 = _interopRequireDefault(_nodePath);
15
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
16
+ /**
17
+ * c4-lib.mjs — Portable C4 model builder (pure Node.js, no npm deps).
18
+ * Shared by c4-generate.mjs and c4-check.mjs.
19
+ */
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Source root detection
23
+ // ---------------------------------------------------------------------------
24
+
25
+ function detectSourceRoot(projectRoot) {
26
+ // 1. Try tsconfig.json
27
+ const tsconfigPath = _nodePath2.default.join(projectRoot, "tsconfig.json");
28
+ if (_nodeFs2.default.existsSync(tsconfigPath)) {
29
+ try {
30
+ const raw = _nodeFs2.default.readFileSync(tsconfigPath, "utf8");
31
+ // Strip single-line comments for lenient JSON parse
32
+ const stripped = raw.replace(/\/\/.*$/gm, "");
33
+ const tsconfig = JSON.parse(stripped);
34
+ const rootDir = tsconfig?.compilerOptions?.rootDir;
35
+ if (rootDir) {
36
+ const candidate = _nodePath2.default.resolve(projectRoot, rootDir);
37
+ if (_nodeFs2.default.existsSync(candidate)) return candidate;
38
+ }
39
+ const includes = tsconfig?.include;
40
+ if (Array.isArray(includes) && includes.length > 0) {
41
+ // Strip glob suffixes like /**/*
42
+ const first = includes[0].replace(/\/\*.*$/, "");
43
+ const candidate = _nodePath2.default.resolve(projectRoot, first);
44
+ if (_nodeFs2.default.existsSync(candidate)) return candidate;
45
+ }
46
+ } catch {
47
+ // Fall through to probing
48
+ }
49
+ }
50
+
51
+ // 2. Probe common directories
52
+ for (const probe of ["app/src", "src", "lib"]) {
53
+ const candidate = _nodePath2.default.join(projectRoot, probe);
54
+ if (_nodeFs2.default.existsSync(candidate)) return candidate;
55
+ }
56
+
57
+ // 3. Fall back to project root
58
+ const fallback = projectRoot;
59
+
60
+ // 4. Verify .ts files exist somewhere
61
+ if (!hasFilesWithExtension(fallback, ".ts")) {
62
+ for (const [ext, lang] of [[".py", "Python"], [".go", "Go"], [".rs", "Rust"], [".java", "Java"]]) {
63
+ if (hasFilesWithExtension(fallback, ext)) {
64
+ throw new Error(`C4 generation does not yet support ${lang} projects`);
65
+ }
66
+ }
67
+ throw new Error("No TypeScript source files found");
68
+ }
69
+ return fallback;
70
+ }
71
+ function hasFilesWithExtension(dir, ext) {
72
+ try {
73
+ const entries = _nodeFs2.default.readdirSync(dir, {
74
+ withFileTypes: true
75
+ });
76
+ for (const entry of entries) {
77
+ const full = _nodePath2.default.join(dir, entry.name);
78
+ if (entry.isDirectory()) {
79
+ if (hasFilesWithExtension(full, ext)) return true;
80
+ } else if (entry.isFile() && entry.name.endsWith(ext)) {
81
+ return true;
82
+ }
83
+ }
84
+ } catch {
85
+ // Directory not readable
86
+ }
87
+ return false;
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // File walking
92
+ // ---------------------------------------------------------------------------
93
+
94
+ function walk(dir, out) {
95
+ const entries = _nodeFs2.default.readdirSync(dir, {
96
+ withFileTypes: true
97
+ });
98
+ for (const entry of entries) {
99
+ const full = _nodePath2.default.join(dir, entry.name);
100
+ if (entry.isDirectory()) {
101
+ walk(full, out);
102
+ continue;
103
+ }
104
+ if (!entry.isFile()) continue;
105
+ if (!entry.name.endsWith(".ts") || entry.name.endsWith(".test.ts")) continue;
106
+ out.push(full);
107
+ }
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Import parsing & resolution
112
+ // ---------------------------------------------------------------------------
113
+
114
+ function parseImports(text) {
115
+ const specs = [];
116
+ const importRe = /import\s+[^"']*?["']([^"']+)["']/g;
117
+ const dynamicRe = /import\(\s*["']([^"']+)["']\s*\)/g;
118
+ const requireRe = /require\(\s*["']([^"']+)["']\s*\)/g;
119
+ let match;
120
+ while ((match = importRe.exec(text)) !== null) specs.push(match[1]);
121
+ while ((match = dynamicRe.exec(text)) !== null) specs.push(match[1]);
122
+ while ((match = requireRe.exec(text)) !== null) specs.push(match[1]);
123
+ return specs;
124
+ }
125
+ function resolveImport(fromFile, spec, srcRoot) {
126
+ if (!spec.startsWith(".")) return null;
127
+ const stripped = spec.replace(/\.js$/, "");
128
+ const base = _nodePath2.default.resolve(_nodePath2.default.dirname(fromFile), stripped);
129
+ const candidates = [base, `${base}.ts`, _nodePath2.default.join(base, "index.ts")];
130
+ for (const candidate of candidates) {
131
+ if (_nodeFs2.default.existsSync(candidate) && _nodeFs2.default.statSync(candidate).isFile()) {
132
+ return candidate;
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Model building
140
+ // ---------------------------------------------------------------------------
141
+
142
+ function relToSrc(absPath, srcRoot) {
143
+ return _nodePath2.default.relative(srcRoot, absPath).split(_nodePath2.default.sep).join("/");
144
+ }
145
+ function componentIdForRel(rel) {
146
+ const [first] = rel.split("/");
147
+ if (!first || !rel.includes("/")) return "app";
148
+ return first;
149
+ }
150
+ function buildModel(srcRoot) {
151
+ const files = [];
152
+ walk(srcRoot, files);
153
+ const componentFiles = new Map();
154
+ const dependencies = new Map();
155
+ const fileDeps = [];
156
+ for (const absFile of files) {
157
+ const fromRel = relToSrc(absFile, srcRoot);
158
+ const fromComp = componentIdForRel(fromRel);
159
+ if (!componentFiles.has(fromComp)) componentFiles.set(fromComp, new Set());
160
+ componentFiles.get(fromComp).add(fromRel);
161
+ if (!dependencies.has(fromComp)) dependencies.set(fromComp, new Set());
162
+ const text = _nodeFs2.default.readFileSync(absFile, "utf8");
163
+ const specs = parseImports(text);
164
+ for (const spec of specs) {
165
+ const resolved = resolveImport(absFile, spec, srcRoot);
166
+ if (!resolved) continue;
167
+ const toRel = relToSrc(resolved, srcRoot);
168
+ const toComp = componentIdForRel(toRel);
169
+ fileDeps.push({
170
+ from: fromRel,
171
+ to: toRel
172
+ });
173
+ if (toComp !== fromComp) dependencies.get(fromComp).add(toComp);
174
+ }
175
+ }
176
+ const components = [...componentFiles.keys()].sort().map(id => ({
177
+ id,
178
+ name: id === "app" ? "app-entry" : id,
179
+ kind: "generated",
180
+ files: [...(componentFiles.get(id) || [])].sort(),
181
+ depends_on: [...(dependencies.get(id) || [])].sort()
182
+ }));
183
+ return {
184
+ generator_version: "1",
185
+ source_root: _nodePath2.default.relative(process.cwd(), srcRoot).split(_nodePath2.default.sep).join("/") || ".",
186
+ components,
187
+ fileDeps
188
+ };
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Mermaid generation
193
+ // ---------------------------------------------------------------------------
194
+
195
+ function toC3Mermaid(model) {
196
+ const lines = ["flowchart LR"];
197
+ for (const component of model.components) {
198
+ lines.push(` ${component.id}["${component.name}"]`);
199
+ }
200
+ for (const component of model.components) {
201
+ for (const to of component.depends_on) {
202
+ lines.push(` ${component.id} --> ${to}`);
203
+ }
204
+ }
205
+ lines.push("");
206
+ return `${lines.join("\n")}\n`;
207
+ }
208
+ function fileNodeId(relPath) {
209
+ return relPath.replace(/[/.\\-]/g, "_").replace(/\.ts$/, "");
210
+ }
211
+ function fileLabel(relPath) {
212
+ return _nodePath2.default.basename(relPath, ".ts");
213
+ }
214
+ function toC4Mermaid(model) {
215
+ const sections = [];
216
+ for (const component of model.components) {
217
+ const lines = ["flowchart LR"];
218
+ const fileSet = new Set(component.files);
219
+ for (const file of component.files) {
220
+ lines.push(` ${fileNodeId(file)}["${fileLabel(file)}"]`);
221
+ }
222
+ const externalNodes = new Set();
223
+ const edges = new Set();
224
+ for (const dep of model.fileDeps) {
225
+ if (!fileSet.has(dep.from)) continue;
226
+ const edgeKey = `${dep.from}|${dep.to}`;
227
+ if (edges.has(edgeKey)) continue;
228
+ edges.add(edgeKey);
229
+ if (fileSet.has(dep.to)) {
230
+ lines.push(` ${fileNodeId(dep.from)} --> ${fileNodeId(dep.to)}`);
231
+ } else {
232
+ const toCompId = componentIdForRel(dep.to);
233
+ const toComp = toCompId === "app" ? "app-entry" : toCompId;
234
+ const extId = fileNodeId(dep.to);
235
+ if (!externalNodes.has(dep.to)) {
236
+ externalNodes.add(dep.to);
237
+ lines.push(` ${extId}["${toComp}/${fileLabel(dep.to)}"]:::ext`);
238
+ }
239
+ lines.push(` ${fileNodeId(dep.from)} -.-> ${extId}`);
240
+ }
241
+ }
242
+ if (externalNodes.size > 0) {
243
+ lines.push(` classDef ext fill:#f0f0f0,stroke:#999,stroke-dasharray:5 5`);
244
+ }
245
+ lines.push("");
246
+ sections.push(`### ${component.name}\n\n\`\`\`mermaid\n${lines.join("\n")}\n\`\`\``);
247
+ }
248
+ return sections.join("\n\n");
249
+ }
250
+ function toJson(model) {
251
+ return `${JSON.stringify(model, null, 2)}\n`;
252
+ }
@@ -0,0 +1,76 @@
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 = require("node:fs");
10
+ var _nodeFs2 = _interopRequireDefault(_nodeFs);
11
+ var _nodePath = require("node:path");
12
+ var _nodePath2 = _interopRequireDefault(_nodePath);
13
+ var _c4Lib = require("../../c4/scripts/c4-lib.mjs");
14
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
15
+ const ROOT = process.cwd();
16
+ const COMPONENTS_FILE = _nodePath2.default.join(ROOT, "docs", "architecture", "generated", "components.json");
17
+ const POLICY_FILE = _nodePath2.default.join(ROOT, "governance", "architecture-conformance-policy.json");
18
+ function main() {
19
+ const srcRoot = (0, _c4Lib.detectSourceRoot)(ROOT);
20
+ const model = (0, _c4Lib.buildModel)(srcRoot);
21
+ const freshJson = (0, _c4Lib.toJson)(model);
22
+ const failures = [];
23
+
24
+ // 1. Compare JSON against existing components.json
25
+ if (!_nodeFs2.default.existsSync(COMPONENTS_FILE)) {
26
+ failures.push("missing generated architecture model: docs/architecture/generated/components.json");
27
+ } else {
28
+ const existingJson = _nodeFs2.default.readFileSync(COMPONENTS_FILE, "utf8");
29
+ if (existingJson !== freshJson) {
30
+ failures.push("C4 model is stale — run /c4 to regenerate");
31
+ }
32
+ }
33
+
34
+ // 2. Conformance policy check (if policy file exists)
35
+ if (_nodeFs2.default.existsSync(POLICY_FILE)) {
36
+ const policy = JSON.parse(_nodeFs2.default.readFileSync(POLICY_FILE, "utf8"));
37
+ const components = new Map();
38
+ for (const component of model.components) {
39
+ components.set(component.id, new Set(component.depends_on || []));
40
+ }
41
+ for (const id of policy.required_components || []) {
42
+ if (!components.has(id)) {
43
+ failures.push(`missing required component: ${id}`);
44
+ }
45
+ }
46
+ for (const rule of policy.forbidden_dependencies || []) {
47
+ const deps = components.get(rule.from);
48
+ if (!deps) {
49
+ failures.push(`forbidden dependency rule references unknown component: ${rule.from}`);
50
+ continue;
51
+ }
52
+ if (deps.has(rule.to)) {
53
+ failures.push(`forbidden dependency: ${rule.from} -> ${rule.to}${rule.reason ? ` (${rule.reason})` : ""}`);
54
+ }
55
+ }
56
+ for (const rule of policy.required_dependencies || []) {
57
+ const deps = components.get(rule.from);
58
+ if (!deps) {
59
+ failures.push(`required dependency rule references unknown component: ${rule.from}`);
60
+ continue;
61
+ }
62
+ if (!deps.has(rule.to)) {
63
+ failures.push(`missing required dependency: ${rule.from} -> ${rule.to}${rule.reason ? ` (${rule.reason})` : ""}`);
64
+ }
65
+ }
66
+ }
67
+ if (failures.length > 0) {
68
+ console.error("FAIL: C4 architecture check:");
69
+ for (const failure of failures) {
70
+ console.error(`- ${failure}`);
71
+ }
72
+ process.exit(1);
73
+ }
74
+ console.log("PASS: C4 architecture diagrams are up to date.");
75
+ }
76
+ main();
@@ -0,0 +1,191 @@
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
+ }
@@ -30,7 +30,7 @@ var _envPaths = require("env-paths");
30
30
  var _envPaths2 = _interopRequireDefault(_envPaths);
31
31
  var _fs = require("fs");
32
32
  var _fs2 = _interopRequireDefault(_fs);
33
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
33
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
34
34
  const cacheDir = `${(0, _envPaths2.default)("", {
35
35
  suffix: ""
36
36
  }).cache}/dagger`;
@@ -9,7 +9,7 @@ exports.initIndex = initIndex;
9
9
  exports.esConnect = esConnect;
10
10
  var _debug = require("debug");
11
11
  var _debug2 = _interopRequireDefault(_debug);
12
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
13
13
  const waitPort = require('wait-port');
14
14
  const elasticsearch = require('@opensearch-project/opensearch');
15
15
  const logger = (0, _debug2.default)('api');
@@ -9,7 +9,7 @@ var _debug = require("debug");
9
9
  var _debug2 = _interopRequireDefault(_debug);
10
10
  var _addressService = require("../service/address-service");
11
11
  var _writer = require("../utils/writer.js");
12
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
13
13
  var logger = (0, _debug2.default)('api');
14
14
  function getAddress(request, response) {
15
15
  logger('IN getAddress');
package/lib/loader.js CHANGED
@@ -5,7 +5,7 @@ var _debug2 = _interopRequireDefault(_debug);
5
5
  var _elasticsearch = require("./client/elasticsearch");
6
6
  var _addressService = require("./service/address-service");
7
7
  var _printVersion = require("./service/printVersion");
8
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
8
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
9
9
  const logger = (0, _debug2.default)('api');
10
10
  const error = (0, _debug2.default)('error');
11
11
  if (process.env.DEBUG == undefined) {
package/lib/server.js CHANGED
@@ -5,7 +5,7 @@ var _debug2 = _interopRequireDefault(_debug);
5
5
  var _elasticsearch = require("./client/elasticsearch");
6
6
  var _printVersion = require("./service/printVersion");
7
7
  var _swagger = require("./swagger");
8
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
8
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
9
9
  const logger = (0, _debug2.default)('api');
10
10
  (0, _swagger.startServer)().then(() => {
11
11
  logger('connecting es client');
@@ -9,7 +9,7 @@ var _debug2 = _interopRequireDefault(_debug);
9
9
  var _httpLinkHeader = require("http-link-header");
10
10
  var _httpLinkHeader2 = _interopRequireDefault(_httpLinkHeader);
11
11
  var _setLinkOptions = require("./setLinkOptions");
12
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
13
13
  var logger = (0, _debug2.default)('api');
14
14
 
15
15
  /**
@@ -42,7 +42,7 @@ var _crypto = require("crypto");
42
42
  var _crypto2 = _interopRequireDefault(_crypto);
43
43
  var _globPromise = require("glob-promise");
44
44
  var _globPromise2 = _interopRequireDefault(_globPromise);
45
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
45
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
46
46
  /* eslint-disable eslint-comments/disable-enable-pair */
47
47
  /* eslint-disable security/detect-non-literal-regexp */
48
48
  /* eslint-disable security/detect-object-injection */
@@ -7,7 +7,7 @@ exports.printVersion = printVersion;
7
7
  var _dotenv = require("dotenv");
8
8
  var _dotenv2 = _interopRequireDefault(_dotenv);
9
9
  var _version = require("../version");
10
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
10
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
11
  _dotenv2.default.config();
12
12
  function printVersion() {
13
13
  let environment = process.env.NODE_ENV || 'development';
@@ -5,7 +5,7 @@ var _debug2 = _interopRequireDefault(_debug);
5
5
  var _elasticsearch = require("../client/elasticsearch");
6
6
  var _printVersion = require("../service/printVersion");
7
7
  var _waycharterServer = require("./waycharterServer");
8
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
8
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
9
9
  const logger = (0, _debug2.default)('api');
10
10
  (0, _waycharterServer.startRest2Server)().then(() => {
11
11
  logger('connecting es client');
@@ -15,7 +15,7 @@ var _addressService = require("../service/address-service");
15
15
  var _version = require("../version");
16
16
  var _crypto = require("crypto");
17
17
  var _crypto2 = _interopRequireDefault(_crypto);
18
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
18
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
19
19
  //import connect from 'connect';
20
20
 
21
21
  var app = (0, _express2.default)();
@@ -111,12 +111,30 @@ function startRest2Server() {
111
111
  parameters: ['q']
112
112
  }]
113
113
  });
114
+ const healthType = waycharter.registerResourceType({
115
+ path: '/health',
116
+ loader: async () => {
117
+ return {
118
+ body: {
119
+ status: 'healthy',
120
+ version: _version.version,
121
+ timestamp: new Date().toISOString()
122
+ },
123
+ headers: {
124
+ 'cache-control': 'no-cache'
125
+ }
126
+ };
127
+ }
128
+ });
114
129
  const index = waycharter.registerResourceType({
115
130
  path: '/',
116
131
  loader: async () => {
117
132
  return {
118
133
  body: {},
119
- links: addressesType.additionalPaths,
134
+ links: [...addressesType.additionalPaths, {
135
+ rel: 'https://addressr.io/rels/health',
136
+ path: '/health'
137
+ }],
120
138
  headers: {
121
139
  etag: `"${_version.version}"`,
122
140
  'cache-control': `public, max-age=${ONE_WEEK}`
package/lib/swagger.js CHANGED
@@ -17,7 +17,7 @@ var _jsYaml = require("js-yaml");
17
17
  var _path = require("path");
18
18
  var _path2 = _interopRequireDefault(_path);
19
19
  var _swaggerTools = require("swagger-tools");
20
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
20
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
21
21
  //import connect from 'connect';
22
22
 
23
23
  var app = (0, _express2.default)();
@@ -2,7 +2,7 @@
2
2
 
3
3
  var _progress = require("progress");
4
4
  var _progress2 = _interopRequireDefault(_progress);
5
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
5
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
6
6
  const {
7
7
  parse
8
8
  } = require('url');
package/lib/version.js CHANGED
@@ -3,5 +3,5 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- // generated by genversion
7
- const version = exports.version = '1.1.3';
6
+ // Generated by genversion.
7
+ const version = exports.version = '1.1.5';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mountainpass/addressr",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Australian Address Validation, Search and Autocomplete",
5
5
  "author": {
6
6
  "name": "Mountain Pass",
@@ -75,7 +75,7 @@
75
75
  "npm-check-unused": "npm-check",
76
76
  "lint": "eslint . --fix",
77
77
  "test": "NO_STRICT=' ' npm-run-all --serial test:nogeo test:geo",
78
- "test:nogeo": "NO_STRICT=' ' npm-run-all --serial test:nodejs:nogeo test:rest:nogeo test:cli:nogeo",
78
+ "test:nogeo": "NO_STRICT=' ' npm-run-all --serial test:nodejs:nogeo test:rest:nogeo test:rest2:nogeo test:cli:nogeo",
79
79
  "test:geo": "NO_STRICT=' ' npm-run-all --serial test:nodejs:geo test:rest:geo test:cli:geo",
80
80
  "watch:test": "nodemon -V --ext \"*.feature, *.js, *.json, *.css, *.yaml\" -x npm -- run test",
81
81
  "genversion": "genversion --es6 --semi version.js",
@@ -89,6 +89,7 @@
89
89
  "cover:rest:nogeo": "nyc --report-dir coverage/rest --temp-dir coverage/rest/.nyc_output npm run test:rest:nogeo",
90
90
  "test:rest:geo": "PORT=$npm_package_config_localport ADDRESSR_ENABLE_GEO=1 ES_INDEX_NAME=test-geo COVERED_STATES=OT DEBUG=error,api,express:*,swagger-tools*,test,es TEST_PROFILE=rest cucumber-js -p rest -- --harmony_async_iteration",
91
91
  "cover:rest:geo": "nyc --report-dir coverage/rest-geo --temp-dir coverage/rest-geo/.nyc_output npm run test:rest:geo",
92
+ "test:rest2:nogeo": "PORT=$npm_package_config_localport ES_INDEX_NAME=test COVERED_STATES=OT DEBUG=error,api,express:*,swagger-tools*,test,es,waychaser,waycharter TEST_PROFILE=rest2 cucumber-js -p rest2 -- --harmony_async_iteration",
92
93
  "test:rest2:geo": "PORT=$npm_package_config_localport ADDRESSR_ENABLE_GEO=1 ES_INDEX_NAME=test-geo COVERED_STATES=OT DEBUG=error,api,express:*,swagger-tools*,test,es,waychaser,waycharter TEST_PROFILE=rest2 cucumber-js -p rest2 -- --harmony_async_iteration",
93
94
  "watch:test:rest2:geo": "nodemon -V -x npm -- run ${npm_lifecycle_event#watch:}",
94
95
  "watch:test:rest:nogeo": "nodemon -V --ext \"*.feature, *.js, *.json, *.css, *.yaml\" -x npm -- run test:rest:nogeo",
@@ -117,10 +118,14 @@
117
118
  "postdocker:push": "docker push \"mountainpass/addressr:latest\"",
118
119
  "check-licenses": "license-checker --production --onlyAllow 'MIT;Apache-2.0;ISC;Custom: http://github.com/substack/node-bufferlist;Unlicense;BSD-2-Clause;BSD-3-Clause;WTFPL;0BSD;MIT*' --summary",
119
120
  "pre-commit": "lint-staged && npm run check-licenses",
121
+ "check-deps": "dry-aged-deps --check",
120
122
  "test:performance": "k6 run --out csv=target/stress.csv test/k6/script.js",
121
123
  "add-changeset": "changeset add --open",
122
124
  "ci:version": "[ \"$CI\" = true ] && changeset version || echo \"Dry run: changeset version\"",
123
- "ci:publish": "[ \"$CI\" = true ] && changeset publish || echo \"Dry run: changeset publish\""
125
+ "ci:publish": "[ \"$CI\" = true ] && changeset publish || echo \"Dry run: changeset publish\"",
126
+ "push:watch": "bash scripts/push-and-watch.sh",
127
+ "release:watch": "bash scripts/release-watch.sh",
128
+ "test:hooks": "bats .claude/hooks/test/"
124
129
  },
125
130
  "bin": {
126
131
  "addressr-loader": "lib/bin/addressr-loader.js",
@@ -192,8 +197,10 @@
192
197
  "babel-eslint": "^10.0.2",
193
198
  "babel-plugin-istanbul": "^6.0.0",
194
199
  "babel-preset-env": "^1.7.0",
200
+ "bats": "^1.13.0",
195
201
  "chai": "^4.2.0",
196
202
  "cucumber": "^5.1.0",
203
+ "dry-aged-deps": "^2.6.0",
197
204
  "eslint": "^7.9.0",
198
205
  "eslint-config-prettier": "^8.0.0",
199
206
  "eslint-plugin-chai-friendly": "^0.7.1",
@@ -212,7 +219,6 @@
212
219
  "istanbul-middleware": "^0.2.2",
213
220
  "license-checker": "^25.0.1",
214
221
  "lint-staged": "^11.0.0",
215
- "ngrok": "^4.0.1",
216
222
  "nodemon": "^2.0.4",
217
223
  "npm-check": "^5.9.0",
218
224
  "npm-run-all": "^4.1.5",
@@ -248,7 +254,8 @@
248
254
  },
249
255
  "husky": {
250
256
  "hooks": {
251
- "pre-commit": "npm run pre-commit"
257
+ "pre-commit": "npm run pre-commit",
258
+ "pre-push": "npm run check-deps"
252
259
  }
253
260
  }
254
- }
261
+ }