@mountainpass/addressr 1.1.3 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/.claude/skills/c4/scripts/c4-generate.js +84 -0
- package/lib/.claude/skills/c4/scripts/c4-lib.js +252 -0
- package/lib/.claude/skills/c4-check/scripts/c4-check.js +76 -0
- package/lib/.claude/skills/wardley/owm-to-svg.js +191 -0
- package/lib/src/waycharterServer.js +19 -1
- package/lib/version.js +1 -1
- package/package.json +8 -4
|
@@ -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(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
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(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
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(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
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
|
+
}
|
|
@@ -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/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mountainpass/addressr",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
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",
|
|
@@ -120,7 +121,10 @@
|
|
|
120
121
|
"test:performance": "k6 run --out csv=target/stress.csv test/k6/script.js",
|
|
121
122
|
"add-changeset": "changeset add --open",
|
|
122
123
|
"ci:version": "[ \"$CI\" = true ] && changeset version || echo \"Dry run: changeset version\"",
|
|
123
|
-
"ci:publish": "[ \"$CI\" = true ] && changeset publish || echo \"Dry run: changeset publish\""
|
|
124
|
+
"ci:publish": "[ \"$CI\" = true ] && changeset publish || echo \"Dry run: changeset publish\"",
|
|
125
|
+
"push:watch": "bash scripts/push-and-watch.sh",
|
|
126
|
+
"release:watch": "bash scripts/release-watch.sh",
|
|
127
|
+
"test:hooks": "bats .claude/hooks/test/"
|
|
124
128
|
},
|
|
125
129
|
"bin": {
|
|
126
130
|
"addressr-loader": "lib/bin/addressr-loader.js",
|
|
@@ -192,6 +196,7 @@
|
|
|
192
196
|
"babel-eslint": "^10.0.2",
|
|
193
197
|
"babel-plugin-istanbul": "^6.0.0",
|
|
194
198
|
"babel-preset-env": "^1.7.0",
|
|
199
|
+
"bats": "^1.13.0",
|
|
195
200
|
"chai": "^4.2.0",
|
|
196
201
|
"cucumber": "^5.1.0",
|
|
197
202
|
"eslint": "^7.9.0",
|
|
@@ -212,7 +217,6 @@
|
|
|
212
217
|
"istanbul-middleware": "^0.2.2",
|
|
213
218
|
"license-checker": "^25.0.1",
|
|
214
219
|
"lint-staged": "^11.0.0",
|
|
215
|
-
"ngrok": "^4.0.1",
|
|
216
220
|
"nodemon": "^2.0.4",
|
|
217
221
|
"npm-check": "^5.9.0",
|
|
218
222
|
"npm-run-all": "^4.1.5",
|