hexwright 0.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.
package/dist/cli.js ADDED
@@ -0,0 +1,2223 @@
1
+ #!/usr/bin/env node
2
+ import "./chunk-ZXMDA7VB.js";
3
+
4
+ // src/cli.ts
5
+ import { mkdirSync, writeFileSync } from "fs";
6
+ import { createRequire } from "module";
7
+ import { dirname as dirname2, join as join6, resolve } from "path";
8
+ import { parseArgs } from "util";
9
+
10
+ // src/mcp.ts
11
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import { z } from "zod";
14
+ var ok = (text) => ({ content: [{ type: "text", text }] });
15
+ var brief = (n) => `${n.name} [${n.component}] ${n.domain} \u2014 ${n.file}:${n.line}`;
16
+ var edgeLine = (e, other, dir) => ` ${dir} ${other?.name ?? "?"} [${other?.component}] ${other?.domain}${e.rel === "DEPENDS_ON" ? "" : ` (${e.rel})`}${e.identifierOnly ? " \xB7id" : ""}${e.violation ? ` !! ${e.violation}` : ""}${e.contracts.length ? `
17
+ uses: ${e.contracts.join(", ")}` : ""}`;
18
+ function buildServer(getQuery, _project) {
19
+ const server = new McpServer({ name: "hexwright", version: "0.1.0" });
20
+ const resolveOr = (q, name) => {
21
+ const r = q.resolve(name);
22
+ if (r.node) return { node: r.node, err: null };
23
+ const list = r.candidates.map(brief).join("\n");
24
+ return {
25
+ node: null,
26
+ err: ok(
27
+ r.candidates.length ? `'${name}' is ambiguous or not exact. Candidates:
28
+ ${list}` : `not found: ${name}`
29
+ )
30
+ };
31
+ };
32
+ server.registerTool(
33
+ "check_violations",
34
+ {
35
+ title: "Boundary violations",
36
+ description: "Whether the code breaks its own architecture: cross-domain Entity access, an Entity leaked into a contract, an inbound adapter touching an Entity, an application or domain type referencing an adapter. Run this after finishing a change and before opening a PR \u2014 these are verdicts over the whole graph, so reading files will not surface them. scope 'delta' reports only what this branch introduced, including a relation that already existed but became a violation because a type moved.",
37
+ inputSchema: { scope: z.enum(["all", "delta"]).optional() }
38
+ },
39
+ async ({ scope = "all" }) => {
40
+ const q = getQuery();
41
+ if (scope === "delta" && !q.hasBase) {
42
+ return ok("no base ref \u2014 restart the server with --base <ref> to scope to this branch");
43
+ }
44
+ const vs = q.violations(scope);
45
+ if (!vs.length) return ok(scope === "delta" ? "no new violations" : "no violations");
46
+ return ok(
47
+ [
48
+ `${vs.length} violation(s):`,
49
+ ...vs.map((e) => {
50
+ const s = q.node(e.src);
51
+ const d = q.node(e.dst);
52
+ return ` ${s?.name} \u2192 ${d?.domain}.${d?.name}
53
+ ${e.violation}
54
+ ${s?.file}`;
55
+ })
56
+ ].join("\n")
57
+ );
58
+ }
59
+ );
60
+ server.registerTool(
61
+ "get_delta",
62
+ {
63
+ title: "Branch delta",
64
+ description: "What this branch changed structurally against the base: types added, types whose public contract or relations changed \u2014 and for each, exactly what was added or removed. Use it before opening a PR to confirm the change matches what was agreed, and to describe it from facts rather than memory. Needs the server started with --base."
65
+ },
66
+ async () => {
67
+ const q = getQuery();
68
+ const d = q.delta();
69
+ if (!d.added.length && !d.modified.length) {
70
+ return ok(
71
+ q.hasBase ? "no structural change on this branch" : "no base ref \u2014 restart with --base"
72
+ );
73
+ }
74
+ const out = [
75
+ `added ${d.added.length} types \xB7 ${d.addedEdges.length} edges \xB7 modified ${d.modified.length} types`
76
+ ];
77
+ if (d.added.length) out.push("", "added:", ...d.added.map((n) => ` + ${brief(n)}`));
78
+ if (d.modified.length) {
79
+ out.push("", "modified:");
80
+ for (const n of d.modified) {
81
+ out.push(` ~ ${brief(n)}`);
82
+ const c = n.change;
83
+ if (!c) continue;
84
+ const nameOf = (id) => q.node(id)?.name ?? id;
85
+ for (const s of c.apiRemoved) out.push(` - contract ${s}`);
86
+ for (const s of c.apiAdded) out.push(` + contract ${s}`);
87
+ for (const s of c.propsRemoved) out.push(` - property ${s}`);
88
+ for (const s of c.propsAdded) out.push(` + property ${s}`);
89
+ for (const id of c.depsRemoved) out.push(` - depends ${nameOf(id)}`);
90
+ for (const id of c.depsAdded) out.push(` + depends ${nameOf(id)}`);
91
+ }
92
+ }
93
+ const newViol = q.violations("delta");
94
+ out.push("", newViol.length ? `NEW violations (${newViol.length}):` : "no new violations");
95
+ for (const e of newViol) {
96
+ out.push(` ${q.node(e.src)?.name} \u2192 ${q.node(e.dst)?.name} ${e.violation}`);
97
+ }
98
+ return ok(out.join("\n"));
99
+ }
100
+ );
101
+ server.registerTool(
102
+ "dependencies",
103
+ {
104
+ title: "Dependencies",
105
+ description: "How a type is wired, in either direction. 'out' = what it depends on and which methods it actually calls on each \u2014 including references held only in a local variable, whose type name never appears in the source, so grep cannot find them. 'in' = what depends on it, which of its methods each consumer uses, and which it declares that nobody calls. Use 'in' before adding a method to an existing port or use case: if the consumers already use disjoint subsets, the interface wants splitting rather than growing. Raise hops to see what a change would reach beyond the direct callers.",
106
+ inputSchema: {
107
+ name: z.string().describe("exact type name or FQCN"),
108
+ direction: z.enum(["in", "out", "both"]).optional(),
109
+ hops: z.number().int().min(1).max(4).optional().describe("in-direction only. default 1"),
110
+ includeIdentifiers: z.boolean().optional().describe("include identifier-only references (FamilyId etc). default false")
111
+ }
112
+ },
113
+ async ({ name, direction = "both", hops = 1, includeIdentifiers = false }) => {
114
+ const q = getQuery();
115
+ const { node: n, err } = resolveOr(q, name);
116
+ if (!n) return err;
117
+ const keep = (e) => includeIdentifiers || !e.identifierOnly;
118
+ const out = [`${n.name} [${n.component}] ${n.domain} \u2014 ${n.file}:${n.line}`];
119
+ if (direction !== "in") {
120
+ const es = q.outgoing(n.id).filter(keep);
121
+ out.push("", `depends on (${es.length}):`);
122
+ for (const e of es) out.push(edgeLine(e, q.node(e.dst), "\u2192"));
123
+ }
124
+ if (direction !== "out") {
125
+ const es = q.incoming(n.id).filter(keep);
126
+ out.push("", `depended on by (${es.length}):`);
127
+ for (const e of es) out.push(edgeLine(e, q.node(e.src), "\u2190"));
128
+ const u = q.contractUsage(n.id);
129
+ if (u.declared.length && u.unusedByAll.length) {
130
+ out.push("", `declares ${u.declared.length}, never called (${u.unusedByAll.length}):`);
131
+ for (const s of u.unusedByAll) out.push(` ${s}`);
132
+ }
133
+ if (hops > 1) {
134
+ const reach = q.impact(n.id, hops).filter((r) => r.hop > 1);
135
+ out.push("", `reaches ${reach.length} more type(s) beyond the direct callers:`);
136
+ for (const r of reach) out.push(` hop ${r.hop} ${brief(r.node)}`);
137
+ }
138
+ }
139
+ return ok(out.join("\n"));
140
+ }
141
+ );
142
+ return server;
143
+ }
144
+ async function serveStdio(getQuery, project) {
145
+ const server = buildServer(getQuery, project);
146
+ await server.connect(new StdioServerTransport());
147
+ }
148
+
149
+ // src/output/tsv.ts
150
+ function toTsv(g) {
151
+ const prefix = commonPrefix(g.nodes.map((n) => n.id));
152
+ const s = (id) => prefix && id.startsWith(prefix) ? id.slice(prefix.length) : id;
153
+ const out = [
154
+ `# hexwright project=${g.project} ref=${g.ref} prefix=${prefix}`,
155
+ `# nodes=${g.nodes.length} edges=${g.edges.length}`
156
+ ];
157
+ for (const n of g.nodes) {
158
+ out.push(["N", s(n.id), n.domain, n.component, `${n.file}:${n.line}`].join(" "));
159
+ }
160
+ for (const e of g.edges) {
161
+ const flags = (e.crossDomain ? "x" : "") + (e.identifierOnly ? "i" : "") + (e.violation ? "!" : "");
162
+ out.push(["E", e.rel, s(e.src), s(e.dst), flags].join(" "));
163
+ }
164
+ return `${out.join("\n")}
165
+ `;
166
+ }
167
+ function commonPrefix(ids) {
168
+ if (!ids.length) return "";
169
+ const parts = ids.map((i) => i.split("."));
170
+ const first = parts[0];
171
+ const common = [];
172
+ for (let i = 0; i < first.length - 1; i++) {
173
+ const seg = first[i];
174
+ if (parts.every((p) => p[i] === seg)) common.push(seg);
175
+ else break;
176
+ }
177
+ return common.length ? `${common.join(".")}.` : "";
178
+ }
179
+ function deltaSummary(d, g) {
180
+ const byId = new Map(g.nodes.map((n) => [n.id, n]));
181
+ const label = (id) => {
182
+ const n = byId.get(id);
183
+ return n ? `${n.name} [${n.component}] ${n.domain}` : id;
184
+ };
185
+ const lines = [
186
+ `added ${d.addedNodes.length} nodes \xB7 ${d.addedEdges.length} edges`,
187
+ `modified ${d.modifiedNodes.length} nodes`,
188
+ `removed ${d.removedNodes.length} nodes \xB7 ${d.removedEdges.length} edges`
189
+ ];
190
+ for (const id of d.addedNodes) lines.push(` + ${label(id)}`);
191
+ for (const id of d.modifiedNodes) lines.push(` ~ ${label(id)}`);
192
+ for (const id of d.removedNodes) lines.push(` - ${id}`);
193
+ return lines.join("\n");
194
+ }
195
+
196
+ // src/profile.ts
197
+ import { readFileSync } from "fs";
198
+ import { parse } from "yaml";
199
+ function loadProfile(path, overrides = {}) {
200
+ const p = parse(readFileSync(path, "utf8"));
201
+ p.domain = { ...p.domain, ...overrides };
202
+ return p;
203
+ }
204
+ function layerOf(p, path) {
205
+ for (const [layer, frag] of Object.entries(p.layers)) {
206
+ if (path.includes(frag)) return layer;
207
+ }
208
+ return "other";
209
+ }
210
+ function sublayerOf(p, path) {
211
+ for (const [sub, frag] of Object.entries(p.sublayers)) {
212
+ if (path.includes(frag)) return sub;
213
+ }
214
+ return "";
215
+ }
216
+ function adapterKindOf(p, path) {
217
+ for (const [kind, frag] of Object.entries(p.adapterKinds)) {
218
+ if (path.includes(frag)) return kind;
219
+ }
220
+ return path.includes("/adapter/") ? "in" : "";
221
+ }
222
+ function isExcluded(p, path) {
223
+ return p.exclude.some((frag) => path.includes(frag));
224
+ }
225
+ function componentOf(p, ctx) {
226
+ for (const r of p.components) {
227
+ if (r.layer && r.layer !== ctx.layer) continue;
228
+ if (r.sublayer && r.sublayer !== ctx.sublayer) continue;
229
+ if (r.kinds && !r.kinds.includes(ctx.kind)) continue;
230
+ if (r.structs && !r.structs.some((x) => x.toLowerCase() === ctx.struct.toLowerCase())) continue;
231
+ if (r.nameEnds && !ctx.name.endsWith(r.nameEnds)) continue;
232
+ return r.as;
233
+ }
234
+ return "DTO";
235
+ }
236
+
237
+ // src/project.ts
238
+ import { existsSync, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
239
+ import { join, relative } from "path";
240
+ var GRADLE = ["build.gradle.kts", "build.gradle"];
241
+ var SETTINGS = ["settings.gradle.kts", "settings.gradle"];
242
+ var SKIP = /* @__PURE__ */ new Set(["node_modules", "build", ".git", ".gradle", "dist", "out"]);
243
+ function findGradleModules(root, maxDepth) {
244
+ const found = [];
245
+ const walk2 = (dir, depth) => {
246
+ if (GRADLE.some((f) => existsSync(join(dir, f)))) found.push(dir);
247
+ if (depth >= maxDepth) return;
248
+ for (const e of readdirSync(dir)) {
249
+ if (SKIP.has(e) || e.startsWith(".")) continue;
250
+ const p = join(dir, e);
251
+ if (statSync(p).isDirectory()) walk2(p, depth + 1);
252
+ }
253
+ };
254
+ walk2(root, 0);
255
+ return found;
256
+ }
257
+ function countSubprojects(dir) {
258
+ for (const f of SETTINGS) {
259
+ const p = join(dir, f);
260
+ if (!existsSync(p)) continue;
261
+ const text = readFileSync2(p, "utf8").replace(/\/\/[^\n]*/g, "");
262
+ return (text.match(/\binclude\s*[("']/g) ?? []).length;
263
+ }
264
+ return 0;
265
+ }
266
+ function detectSource(repo, override) {
267
+ if (override) {
268
+ const asModule = join(repo, override, "src", "main", "kotlin");
269
+ if (existsSync(asModule)) {
270
+ return {
271
+ srcAbs: asModule,
272
+ srcRel: join(override, "src/main/kotlin"),
273
+ module: override,
274
+ subprojects: countSubprojects(join(repo, override)) || countSubprojects(repo),
275
+ how: "gradle"
276
+ };
277
+ }
278
+ const abs = join(repo, override);
279
+ if (!existsSync(abs)) throw new Error(`source root not found: ${abs}`);
280
+ return { srcAbs: abs, srcRel: override, module: ".", subprojects: 0, how: "explicit" };
281
+ }
282
+ const modules = findGradleModules(repo, 2).filter(
283
+ (m) => existsSync(join(m, "src", "main", "kotlin"))
284
+ );
285
+ if (modules.length === 1) {
286
+ const mod = modules[0];
287
+ const abs = join(mod, "src", "main", "kotlin");
288
+ return {
289
+ srcAbs: abs,
290
+ srcRel: relative(repo, abs) || ".",
291
+ module: relative(repo, mod) || ".",
292
+ subprojects: countSubprojects(mod) || countSubprojects(repo),
293
+ how: "gradle"
294
+ };
295
+ }
296
+ if (modules.length > 1) {
297
+ const names = modules.map((m) => relative(repo, m) || ".").join(", ");
298
+ throw new Error(
299
+ `found ${modules.length} Gradle modules with Kotlin sources (${names}).
300
+ hexwright assumes a single module \u2014 pass --src to pick one.`
301
+ );
302
+ }
303
+ const fallback = join(repo, "src", "main", "kotlin");
304
+ if (existsSync(fallback)) {
305
+ return {
306
+ srcAbs: fallback,
307
+ srcRel: "src/main/kotlin",
308
+ module: ".",
309
+ subprojects: 0,
310
+ how: "convention"
311
+ };
312
+ }
313
+ throw new Error(
314
+ "no Kotlin source root found. Looked for build.gradle(.kts) with src/main/kotlin up to two levels deep, then ./src/main/kotlin. Pass --src to specify it."
315
+ );
316
+ }
317
+
318
+ // src/view/layout.ts
319
+ var LEVEL = {
320
+ Entity: 0,
321
+ Service: 1,
322
+ Event: 1,
323
+ UseCase: 2,
324
+ Port: 3,
325
+ DTO: 4,
326
+ VO: 4,
327
+ Error: 4,
328
+ Shared: 4,
329
+ Adapter: 5
330
+ };
331
+ var SATURATION = {
332
+ Entity: 95,
333
+ Service: 72,
334
+ Event: 60,
335
+ UseCase: 48,
336
+ Port: 30,
337
+ DTO: 0,
338
+ VO: 0,
339
+ Error: 0,
340
+ Shared: 22,
341
+ Adapter: 18
342
+ };
343
+ var SHAPE = {
344
+ Entity: "barrel",
345
+ Service: "rectangle",
346
+ UseCase: "round-rectangle",
347
+ Port: "cut-rectangle",
348
+ Event: "rhomboid",
349
+ DTO: "round-rectangle",
350
+ VO: "round-rectangle",
351
+ Error: "round-rectangle",
352
+ Shared: "round-rectangle",
353
+ Adapter: "round-rectangle"
354
+ };
355
+ var CORE = ["Entity", "Service", "UseCase", "Port", "Event"];
356
+ var OUTLINE = ["DTO", "VO", "Error"];
357
+ var HUE_FLOOR = 42;
358
+ var HUE_CEIL = 338;
359
+ var HUE_SEP = 15;
360
+ function rawHue(domain) {
361
+ let h = 0;
362
+ for (let i = 0; i < domain.length; i++) h = h * 31 + domain.charCodeAt(i) | 0;
363
+ const frac = Math.abs(h * 0.6180339887 % 1);
364
+ return Math.round(HUE_FLOOR + frac * (HUE_CEIL - HUE_FLOOR));
365
+ }
366
+ var circDist = (a, b) => {
367
+ const d = Math.abs(a - b) % 360;
368
+ return Math.min(d, 360 - d);
369
+ };
370
+ function hueMap(domains) {
371
+ const names = [...new Set(domains)].filter((d) => !isCommon(d)).sort();
372
+ const taken = [];
373
+ const out = /* @__PURE__ */ new Map();
374
+ const span = HUE_CEIL - HUE_FLOOR;
375
+ for (const d of names) {
376
+ const start = rawHue(d);
377
+ let hue = start;
378
+ for (let step = 0; step < span; step++) {
379
+ const cand = HUE_FLOOR + (start - HUE_FLOOR + step) % span;
380
+ if (taken.every((t) => circDist(t, cand) >= HUE_SEP)) {
381
+ hue = cand;
382
+ break;
383
+ }
384
+ }
385
+ taken.push(hue);
386
+ out.set(d, hue);
387
+ }
388
+ return (d) => isCommon(d) ? 0 : out.get(d) ?? rawHue(d);
389
+ }
390
+ var isCommon = (domain) => domain === "common" || domain === "shared";
391
+ var NODE_W = 190;
392
+ var NODE_H = 40;
393
+ var RING_GAP = 230;
394
+ var ARC = 268;
395
+ var DOMAIN_GAP = 130;
396
+ var BOX_PAD = 34;
397
+ var LABEL_H = 30;
398
+ function concentric(nodes) {
399
+ const rings = /* @__PURE__ */ new Map();
400
+ for (const n of nodes) {
401
+ const lv = LEVEL[n.component];
402
+ rings.set(lv, [...rings.get(lv) ?? [], n]);
403
+ }
404
+ const pos = /* @__PURE__ */ new Map();
405
+ let prev = 0;
406
+ for (const lv of [...rings.keys()].sort((a, b) => a - b)) {
407
+ const ring = rings.get(lv);
408
+ const need = ring.length * ARC / (2 * Math.PI);
409
+ const r = lv === 0 && ring.length === 1 ? 0 : Math.max(prev + RING_GAP, need, 170);
410
+ ring.forEach((n, i) => {
411
+ if (r === 0) pos.set(n.id, [0, 0]);
412
+ else {
413
+ const a = 2 * Math.PI * i / ring.length - Math.PI / 2;
414
+ pos.set(n.id, [r * Math.cos(a), r * Math.sin(a)]);
415
+ }
416
+ });
417
+ prev = r;
418
+ }
419
+ let hw = 0;
420
+ let hh = 0;
421
+ for (const [x, y] of pos.values()) {
422
+ hw = Math.max(hw, Math.abs(x));
423
+ hh = Math.max(hh, Math.abs(y));
424
+ }
425
+ return { pos, hw: hw + NODE_W / 2 + BOX_PAD, hh: hh + NODE_H / 2 + BOX_PAD + LABEL_H / 2 };
426
+ }
427
+ function placeDomains(g, box, gap = DOMAIN_GAP) {
428
+ const byId = new Map(g.nodes.map((n) => [n.id, n]));
429
+ const link = /* @__PURE__ */ new Map();
430
+ for (const e of g.edges) {
431
+ const a = byId.get(e.src)?.domain;
432
+ const b = byId.get(e.dst)?.domain;
433
+ if (!a || !b || a === b) continue;
434
+ const k = a < b ? `${a}|${b}` : `${b}|${a}`;
435
+ link.set(k, (link.get(k) ?? 0) + 1);
436
+ }
437
+ const size = (d) => box.get(d) ?? { hw: 0, hh: 0 };
438
+ const area = (d) => size(d).hw * size(d).hh;
439
+ const doms = [...box.keys()].sort((a, b) => area(b) - area(a));
440
+ const P = /* @__PURE__ */ new Map();
441
+ let r = 0;
442
+ let ang = 0;
443
+ doms.forEach((d, i) => {
444
+ if (i === 0) {
445
+ P.set(d, [0, 0]);
446
+ return;
447
+ }
448
+ r += Math.hypot(size(d).hw, size(d).hh) * 0.95;
449
+ ang += 2.399963;
450
+ P.set(d, [r * Math.cos(ang), r * Math.sin(ang)]);
451
+ });
452
+ const separate = (step) => {
453
+ let worst = 0;
454
+ for (let i = 0; i < doms.length; i++) {
455
+ const a = doms[i];
456
+ for (let j = i + 1; j < doms.length; j++) {
457
+ const b = doms[j];
458
+ const pa = P.get(a);
459
+ const pb = P.get(b);
460
+ const dx = pb[0] - pa[0];
461
+ const dy = pb[1] - pa[1];
462
+ const ox = size(a).hw + size(b).hw + gap - Math.abs(dx);
463
+ const oy = size(a).hh + size(b).hh + gap - Math.abs(dy);
464
+ if (ox <= 0 || oy <= 0) continue;
465
+ worst = Math.max(worst, Math.min(ox, oy));
466
+ const shift = Math.min(ox, oy) * step;
467
+ if (ox < oy) {
468
+ const s = dx >= 0 ? 1 : -1;
469
+ pa[0] -= s * shift;
470
+ pb[0] += s * shift;
471
+ } else {
472
+ const s = dy >= 0 ? 1 : -1;
473
+ pa[1] -= s * shift;
474
+ pb[1] += s * shift;
475
+ }
476
+ }
477
+ }
478
+ return worst;
479
+ };
480
+ for (let iter = 0; iter < 400; iter++) {
481
+ for (let i = 0; i < doms.length; i++) {
482
+ const a = doms[i];
483
+ for (let j = i + 1; j < doms.length; j++) {
484
+ const b = doms[j];
485
+ const w = link.get(a < b ? `${a}|${b}` : `${b}|${a}`) ?? 0;
486
+ const pa = P.get(a);
487
+ const pb = P.get(b);
488
+ const dx = pb[0] - pa[0];
489
+ const dy = pb[1] - pa[1];
490
+ const dist = Math.hypot(dx, dy) || 1;
491
+ const ox = size(a).hw + size(b).hw + gap - Math.abs(dx);
492
+ const oy = size(a).hh + size(b).hh + gap - Math.abs(dy);
493
+ if (ox > 0 && oy > 0) continue;
494
+ const slack = -Math.min(ox, oy);
495
+ const pull = Math.min(slack * (w ? 0.06 * Math.log1p(w) : 6e-3), 90);
496
+ P.set(a, [pa[0] + dx / dist * pull, pa[1] + dy / dist * pull]);
497
+ P.set(b, [pb[0] - dx / dist * pull, pb[1] - dy / dist * pull]);
498
+ }
499
+ }
500
+ separate(0.5);
501
+ for (const d of doms) {
502
+ const p = P.get(d);
503
+ p[0] -= p[0] * 4e-3;
504
+ p[1] -= p[1] * 4e-3;
505
+ }
506
+ }
507
+ for (let iter = 0; iter < 600 && separate(0.5) > 0.5; iter++) ;
508
+ return P;
509
+ }
510
+ function hexLayout(g) {
511
+ const byDomain = /* @__PURE__ */ new Map();
512
+ for (const n of g.nodes) byDomain.set(n.domain, [...byDomain.get(n.domain) ?? [], n]);
513
+ const local = /* @__PURE__ */ new Map();
514
+ const box = /* @__PURE__ */ new Map();
515
+ for (const [d, ns] of byDomain) {
516
+ const c = concentric(ns);
517
+ local.set(d, c.pos);
518
+ box.set(d, { hw: c.hw, hh: c.hh });
519
+ }
520
+ const centers = placeDomains(g, box);
521
+ const out = [];
522
+ for (const [d, pos] of local) {
523
+ const [cx, cy] = centers.get(d) ?? [0, 0];
524
+ for (const [id, [x, y]] of pos) out.push({ id, x: Math.round(cx + x), y: Math.round(cy + y) });
525
+ }
526
+ return out;
527
+ }
528
+ var COL_W = NODE_W + 54;
529
+ var ROW_H = 74;
530
+ function blockOf(d, ns) {
531
+ const items = [...ns].sort(
532
+ (a, b) => LEVEL[a.component] - LEVEL[b.component] || a.name.localeCompare(b.name)
533
+ );
534
+ const cols = Math.max(1, Math.min(5, Math.ceil(Math.sqrt(items.length / 2.2))));
535
+ const rows = Math.ceil(items.length / cols);
536
+ return { d, items, cols, w: cols * COL_W + 80, h: rows * ROW_H + 92 };
537
+ }
538
+ function organicLayout(g) {
539
+ const byDomain = /* @__PURE__ */ new Map();
540
+ for (const n of g.nodes) byDomain.set(n.domain, [...byDomain.get(n.domain) ?? [], n]);
541
+ const blocks = [...byDomain].map(([d, ns]) => blockOf(d, ns));
542
+ const box = new Map(blocks.map((b) => [b.d, { hw: b.w / 2, hh: b.h / 2 }]));
543
+ const centers = placeDomains(g, box, 70);
544
+ const out = [];
545
+ for (const b of blocks) {
546
+ const [cx, cy] = centers.get(b.d) ?? [0, 0];
547
+ b.items.forEach((n, i) => {
548
+ out.push({
549
+ id: n.id,
550
+ x: Math.round(cx - b.w / 2 + 40 + i % b.cols * COL_W + NODE_W / 2),
551
+ y: Math.round(cy - b.h / 2 + 52 + Math.floor(i / b.cols) * ROW_H)
552
+ });
553
+ });
554
+ }
555
+ return out;
556
+ }
557
+ function gridLayout(g, maxWidth = 3e3) {
558
+ const byDomain = /* @__PURE__ */ new Map();
559
+ for (const n of g.nodes) byDomain.set(n.domain, [...byDomain.get(n.domain) ?? [], n]);
560
+ const blocks = [...byDomain.entries()].map(([d, ns]) => blockOf(d, ns)).sort((a, b) => b.items.length - a.items.length);
561
+ const MAXW = maxWidth;
562
+ const out = [];
563
+ let y = 0;
564
+ let x = 0;
565
+ let rowH = 0;
566
+ for (const b of blocks) {
567
+ if (x + b.w > MAXW && x > 0) {
568
+ y += rowH + 90;
569
+ x = 0;
570
+ rowH = 0;
571
+ }
572
+ b.items.forEach((n, i) => {
573
+ out.push({
574
+ id: n.id,
575
+ x: Math.round(x + 40 + i % b.cols * (NODE_W + 54) + NODE_W / 2),
576
+ y: Math.round(y + 52 + Math.floor(i / b.cols) * 74)
577
+ });
578
+ });
579
+ x += b.w + 70;
580
+ rowH = Math.max(rowH, b.h);
581
+ }
582
+ return out;
583
+ }
584
+
585
+ // src/render/select.ts
586
+ function select(g, view, showIdentifiers = false) {
587
+ const byId = new Map(g.nodes.map((n) => [n.id, n]));
588
+ const real = g.edges.filter((e) => showIdentifiers || !e.identifierOnly);
589
+ const withNeighbours = (seed) => {
590
+ const out = new Set(seed);
591
+ for (const e of real) {
592
+ if (seed.has(e.src) && byId.has(e.dst)) out.add(e.dst);
593
+ if (seed.has(e.dst) && byId.has(e.src)) out.add(e.src);
594
+ }
595
+ return out;
596
+ };
597
+ const changedSet = () => {
598
+ const seed = new Set(
599
+ g.nodes.filter((n) => n.status === "added" || n.status === "modified").map((n) => n.id)
600
+ );
601
+ if (!seed.size) throw new Error("no structural change on this branch \u2014 nothing to render");
602
+ for (const e of real) {
603
+ if (e.status !== "added") continue;
604
+ if (seed.has(e.src) && byId.has(e.dst)) seed.add(e.dst);
605
+ if (seed.has(e.dst) && byId.has(e.src)) seed.add(e.src);
606
+ }
607
+ return seed;
608
+ };
609
+ const dependents = (seed) => {
610
+ const out = new Set(seed);
611
+ for (const e of real) if (seed.has(e.dst) && byId.has(e.src)) out.add(e.src);
612
+ return out;
613
+ };
614
+ let keep;
615
+ let label;
616
+ if (view === "all") {
617
+ keep = new Set(g.nodes.map((n) => n.id));
618
+ label = "all types";
619
+ } else if (view === "core") {
620
+ keep = new Set(g.nodes.filter((n) => CORE.includes(n.component)).map((n) => n.id));
621
+ label = "core \u2014 Entity \xB7 Service \xB7 UseCase \xB7 Port \xB7 Event";
622
+ } else if (view.startsWith("domain:")) {
623
+ const d = view.slice(7);
624
+ const seed = new Set(g.nodes.filter((n) => n.domain === d).map((n) => n.id));
625
+ if (!seed.size) throw new Error(`no such domain: ${d}`);
626
+ keep = withNeighbours(seed);
627
+ label = `domain ${d} and what it touches`;
628
+ } else if (view === "impact") {
629
+ keep = dependents(changedSet());
630
+ label = "what this branch changed, and everything that depends on it";
631
+ } else {
632
+ keep = changedSet();
633
+ const linked = real.filter((e) => keep.has(e.src) && keep.has(e.dst)).length;
634
+ if (linked) {
635
+ label = "what this branch added and changed";
636
+ } else {
637
+ keep = dependents(keep);
638
+ label = "what this branch changed (no new relations \u2014 showing dependents)";
639
+ }
640
+ }
641
+ const nodes = g.nodes.filter((n) => keep.has(n.id));
642
+ const edges = real.filter((e) => keep.has(e.src) && keep.has(e.dst));
643
+ return { graph: { ...g, nodes, edges }, label };
644
+ }
645
+
646
+ // src/render/svg.ts
647
+ var BG = "#0e1116";
648
+ var FG = "#e6edf3";
649
+ var MUTED = "#8b949e";
650
+ var DIM = "#6e7681";
651
+ var RULE = "#30363d";
652
+ var ADDED = "#f0883e";
653
+ var MODIFIED = "#a371f7";
654
+ var VIOLATION = "#f85149";
655
+ var FONT = "-apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, 'DejaVu Sans', 'Liberation Sans', 'Noto Sans', sans-serif";
656
+ var hsl = (h, s, l) => `hsl(${h},${s}%,${l}%)`;
657
+ var esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
658
+ function fit(text, maxPx, fontPx) {
659
+ const w = (s) => [...s].reduce(
660
+ (a, c) => a + (c.charCodeAt(0) > 11904 ? 1 : /[A-Z]/.test(c) ? 0.62 : 0.5),
661
+ 0
662
+ ) * fontPx;
663
+ if (w(text) <= maxPx) return text;
664
+ let out = text;
665
+ while (out.length > 1 && w(`${out}\u2026`) > maxPx) out = out.slice(0, -1);
666
+ return `${out}\u2026`;
667
+ }
668
+ var sizeOf = (n) => n.component === "Entity" ? { w: 190, h: 36 } : { w: 182, h: 32 };
669
+ function shapePath(p) {
670
+ const { x, y, w, h } = p;
671
+ const l = x - w / 2;
672
+ const t = y - h / 2;
673
+ switch (SHAPE[p.n.component]) {
674
+ case "barrel":
675
+ return `<rect x="${l}" y="${t}" width="${w}" height="${h}" rx="${h / 2}" ry="${h / 2}"/>`;
676
+ case "rectangle":
677
+ return `<rect x="${l}" y="${t}" width="${w}" height="${h}"/>`;
678
+ case "cut-rectangle": {
679
+ const c = 8;
680
+ const pts = [
681
+ [l + c, t],
682
+ [l + w - c, t],
683
+ [l + w, t + c],
684
+ [l + w, t + h - c],
685
+ [l + w - c, t + h],
686
+ [l + c, t + h],
687
+ [l, t + h - c],
688
+ [l, t + c]
689
+ ];
690
+ return `<polygon points="${pts.map(([a, b]) => `${a},${b}`).join(" ")}"/>`;
691
+ }
692
+ case "rhomboid": {
693
+ const s = 9;
694
+ return `<polygon points="${l + s},${t} ${l + w},${t} ${l + w - s},${t + h} ${l},${t + h}"/>`;
695
+ }
696
+ default:
697
+ return `<rect x="${l}" y="${t}" width="${w}" height="${h}" rx="5" ry="5"/>`;
698
+ }
699
+ }
700
+ var qbez = (a, c, b, t) => ({
701
+ x: (1 - t) ** 2 * a.x + 2 * (1 - t) * t * c.x + t ** 2 * b.x,
702
+ y: (1 - t) ** 2 * a.y + 2 * (1 - t) * t * c.y + t ** 2 * b.y
703
+ });
704
+ var inside = (p, b, pad = 0) => Math.abs(p.x - b.x) <= b.w / 2 + pad && Math.abs(p.y - b.y) <= b.h / 2 + pad;
705
+ function trim(a, c, b, ab, bb) {
706
+ const N = 120;
707
+ let t0 = 0;
708
+ let t1 = 1;
709
+ for (let i = 0; i <= N; i++) {
710
+ const t = i / N;
711
+ if (!inside(qbez(a, c, b, t), ab, 1)) {
712
+ t0 = t;
713
+ break;
714
+ }
715
+ }
716
+ for (let i = 0; i <= N; i++) {
717
+ const t = 1 - i / N;
718
+ if (!inside(qbez(a, c, b, t), bb, 3)) {
719
+ t1 = t;
720
+ break;
721
+ }
722
+ }
723
+ return t1 - t0 < 0.02 ? void 0 : { t0, t1 };
724
+ }
725
+ var EDGE = {
726
+ DEPENDS_ON: { width: 1.9, dash: "", bow: 0, arrow: "vee", scale: 0.95 },
727
+ IMPLEMENTS: { width: 2.2, dash: "9 5", bow: 42, arrow: "triangle", scale: 1.35 },
728
+ EXTENDS: { width: 3.4, dash: "3 3", bow: 58, arrow: "triangle", scale: 1.6 }
729
+ };
730
+ function arrowHead(tip, dir, st, color) {
731
+ const len = 11 * st.scale;
732
+ const half = 4.6 * st.scale;
733
+ const nx = -dir.y;
734
+ const ny = dir.x;
735
+ const bx = tip.x - dir.x * len;
736
+ const by = tip.y - dir.y * len;
737
+ const p1 = `${bx + nx * half},${by + ny * half}`;
738
+ const p2 = `${bx - nx * half},${by - ny * half}`;
739
+ if (st.arrow === "vee") {
740
+ return `<path d="M${p1} L${tip.x},${tip.y} L${p2}" fill="none" stroke="${color}" stroke-width="${st.width * 1.3}" stroke-linecap="round"/>`;
741
+ }
742
+ return `<polygon points="${tip.x},${tip.y} ${p1} ${p2}" fill="${BG}" stroke="${color}" stroke-width="${st.width}"/>`;
743
+ }
744
+ function gridWidthFor(g) {
745
+ const TARGET = 4 / 3;
746
+ let best = 3e3;
747
+ let bestErr = Number.POSITIVE_INFINITY;
748
+ for (const w of [900, 1200, 1500, 1900, 2400, 3e3, 3800]) {
749
+ const pos = gridLayout(g, w);
750
+ if (!pos.length) continue;
751
+ const xs = pos.map((p) => p.x);
752
+ const ys = pos.map((p) => p.y);
753
+ const gw = Math.max(...xs) - Math.min(...xs) + 300;
754
+ const gh = Math.max(...ys) - Math.min(...ys) + 200;
755
+ const err = Math.abs(gw / gh - TARGET);
756
+ if (err < bestErr) {
757
+ bestErr = err;
758
+ best = w;
759
+ }
760
+ }
761
+ return best;
762
+ }
763
+ function renderSvg(g, opt = {}) {
764
+ const layout = opt.layout ?? "hex";
765
+ const hueOf = hueMap(g.nodes.map((n) => n.domain));
766
+ const place = layout === "hex" ? hexLayout(g) : layout === "organic" ? organicLayout(g) : gridLayout(g, gridWidthFor(g));
767
+ const coords = new Map(place.map((p) => [p.id, p]));
768
+ const placed = g.nodes.map((n) => {
769
+ const c = coords.get(n.id) ?? { x: 0, y: 0 };
770
+ return { n, x: c.x, y: c.y, ...sizeOf(n) };
771
+ });
772
+ const at = new Map(placed.map((p) => [p.n.id, p]));
773
+ const domains = /* @__PURE__ */ new Map();
774
+ for (const p of placed) {
775
+ const b = domains.get(p.n.domain);
776
+ const x1 = Math.min(b ? b.x : Number.POSITIVE_INFINITY, p.x - p.w / 2);
777
+ const y1 = Math.min(b ? b.y : Number.POSITIVE_INFINITY, p.y - p.h / 2);
778
+ const x2 = Math.max(b ? b.w : Number.NEGATIVE_INFINITY, p.x + p.w / 2);
779
+ const y2 = Math.max(b ? b.h : Number.NEGATIVE_INFINITY, p.y + p.h / 2);
780
+ domains.set(p.n.domain, { x: x1, y: y1, w: x2, h: y2 });
781
+ }
782
+ const PAD = 26;
783
+ const LABEL = 26;
784
+ const boxes = [...domains].map(([d, b]) => ({
785
+ d,
786
+ x: b.x - PAD,
787
+ y: b.y - PAD - LABEL,
788
+ w: b.w - b.x + PAD * 2,
789
+ h: b.h - b.y + PAD * 2 + LABEL
790
+ }));
791
+ const parts = [];
792
+ for (const b of boxes) {
793
+ const hue = hueOf(b.d);
794
+ const sat = isCommon(b.d) ? 0 : 26;
795
+ parts.push(
796
+ `<rect x="${b.x}" y="${b.y}" width="${b.w}" height="${b.h}" rx="10" fill="${hsl(hue, sat, 13)}" fill-opacity="0.5" stroke="${hsl(hue, sat + 19, 42)}" stroke-width="1.5"/>`,
797
+ `<text x="${b.x + 14}" y="${b.y + 20}" font-size="19" font-weight="bold" fill="${hsl(hue, sat + 29, 62)}">${esc(b.d)}</text>`
798
+ );
799
+ }
800
+ const hasDelta = g.nodes.some((n) => n.status === "added" || n.status === "modified");
801
+ const order = (e) => e.violation ? 2 : e.status === "added" ? 1 : 0;
802
+ for (const e of [...g.edges].sort((a, b) => order(a) - order(b))) {
803
+ const s = at.get(e.src);
804
+ const d = at.get(e.dst);
805
+ if (!s || !d) continue;
806
+ const st = EDGE[e.rel] ?? EDGE.DEPENDS_ON;
807
+ const a = { x: s.x, y: s.y };
808
+ const b = { x: d.x, y: d.y };
809
+ const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
810
+ const len = Math.hypot(b.x - a.x, b.y - a.y) || 1;
811
+ const c = st.bow === 0 ? mid : {
812
+ x: mid.x + -(b.y - a.y) / len * st.bow * 2,
813
+ y: mid.y + (b.x - a.x) / len * st.bow * 2
814
+ };
815
+ const cut = trim(a, c, b, s, d);
816
+ if (!cut) continue;
817
+ const hue = hueOf(s.n.domain);
818
+ let color = hsl(hue, e.crossDomain ? 62 : 22, e.crossDomain ? 58 : 46);
819
+ let op = e.crossDomain ? 0.8 : 0.62;
820
+ if (hasDelta && e.status === "existing") op *= 0.45;
821
+ let width = st.width;
822
+ let dash = st.dash;
823
+ if (e.identifierOnly) {
824
+ dash = "1 4";
825
+ op = 0.4;
826
+ }
827
+ if (e.status === "added") {
828
+ color = ADDED;
829
+ op = 1;
830
+ }
831
+ if (e.violation) {
832
+ color = VIOLATION;
833
+ op = 1;
834
+ width = 3.4;
835
+ }
836
+ const N = 26;
837
+ const pts = [];
838
+ for (let i = 0; i <= N; i++) pts.push(qbez(a, c, b, cut.t0 + (cut.t1 - cut.t0) * i / N));
839
+ const path = pts.map((p, i) => `${i ? "L" : "M"}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
840
+ parts.push(
841
+ `<path d="${path}" fill="none" stroke="${color}" stroke-width="${width}"` + (dash ? ` stroke-dasharray="${dash}"` : "") + ` opacity="${op}"/>`
842
+ );
843
+ const tip = pts[N];
844
+ const prev = pts[N - 2];
845
+ const dl = Math.hypot(tip.x - prev.x, tip.y - prev.y) || 1;
846
+ parts.push(
847
+ `<g opacity="${op}">${arrowHead(tip, { x: (tip.x - prev.x) / dl, y: (tip.y - prev.y) / dl }, { ...st, width }, color)}</g>`
848
+ );
849
+ if (e.violation) {
850
+ const m = qbez(a, c, b, (cut.t0 + cut.t1) / 2);
851
+ const tw = e.violation.length * 5.2 + 8;
852
+ parts.push(
853
+ `<rect x="${m.x - tw / 2}" y="${m.y - 8}" width="${tw}" height="15" rx="3" fill="${BG}" fill-opacity="0.85"/>`,
854
+ `<text x="${m.x}" y="${m.y + 3.5}" font-size="10" fill="${VIOLATION}" text-anchor="middle">${esc(e.violation)}</text>`
855
+ );
856
+ }
857
+ }
858
+ for (const p of placed) {
859
+ const n = p.n;
860
+ const hue = hueOf(n.domain);
861
+ const sat = isCommon(n.domain) ? 0 : SATURATION[n.component];
862
+ const outline = OUTLINE.includes(n.component);
863
+ const body = shapePath(p);
864
+ const fill = outline ? "none" : hsl(hue, sat, 46 - sat * 0.07);
865
+ let stroke = hsl(hue, outline ? 48 : sat, outline ? 52 : 70);
866
+ let sw = outline ? 1.4 : n.component === "Entity" ? 2.6 : n.component === "Service" ? 1.8 : 1;
867
+ if (n.status === "added") {
868
+ stroke = ADDED;
869
+ sw = 3;
870
+ } else if (n.status === "modified") {
871
+ stroke = MODIFIED;
872
+ sw = 2.6;
873
+ }
874
+ const dashed = n.status === "modified" ? "7 4" : outline ? "4 3" : "";
875
+ const attrs = ` fill="${fill}" stroke="${stroke}" stroke-width="${sw}"` + (dashed ? ` stroke-dasharray="${dashed}"` : "");
876
+ parts.push(body.replace("/>", `${attrs}/>`));
877
+ const bold = n.component === "Entity";
878
+ const fs = bold ? 12 : 11;
879
+ parts.push(
880
+ `<text x="${p.x}" y="${p.y + fs * 0.36}" font-size="${fs}" text-anchor="middle"` + (bold ? ` font-weight="bold"` : "") + ` fill="${outline ? hsl(hue, hue ? 52 : 0, 72) : "#fff"}">${esc(fit(n.name, p.w - 20, fs))}</text>`
881
+ );
882
+ }
883
+ const xs = boxes.flatMap((b) => [b.x, b.x + b.w]);
884
+ const ys = boxes.flatMap((b) => [b.y, b.y + b.h]);
885
+ const M = 40;
886
+ const minX = Math.min(...xs) - M;
887
+ const minY = Math.min(...ys) - M;
888
+ const gw = Math.max(...xs) - minX + M;
889
+ const gh = Math.max(...ys) - minY + M;
890
+ const head = header(g, opt.viewLabel ?? "", gw);
891
+ const legend = legendBlock(g, gw, head.h + gh, hueOf);
892
+ const total = head.h + gh + legend.h;
893
+ return [
894
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${Math.round(gw)}" height="${Math.round(total)}"`,
895
+ ` viewBox="0 0 ${Math.round(gw)} ${Math.round(total)}" font-family="${FONT}">`,
896
+ `<rect width="100%" height="100%" fill="${BG}"/>`,
897
+ head.svg,
898
+ `<g transform="translate(${-minX},${head.h - minY})">`,
899
+ ...parts,
900
+ "</g>",
901
+ legend.svg,
902
+ "</svg>",
903
+ ""
904
+ ].join("\n");
905
+ }
906
+ function header(g, viewLabel, w) {
907
+ const add = g.nodes.filter((n) => n.status === "added").length;
908
+ const mod = g.nodes.filter((n) => n.status === "modified").length;
909
+ const viol = g.edges.filter((e) => e.violation).length;
910
+ const bits = [`${g.nodes.length} types`, `${g.edges.length} relations`];
911
+ if (add || mod) bits.push(`${add} added \xB7 ${mod} modified`);
912
+ bits.push(viol ? `${viol} violation${viol > 1 ? "s" : ""}` : "no violations");
913
+ return {
914
+ h: 78,
915
+ svg: [
916
+ `<text x="26" y="30" font-size="17" font-weight="bold" fill="${FG}">${esc(g.project)}</text>`,
917
+ `<text x="${esc(String(26 + g.project.length * 10 + 12))}" y="30" font-size="12" fill="${DIM}">${esc(g.ref)}</text>`,
918
+ `<text x="26" y="52" font-size="12" fill="${MUTED}">${esc(viewLabel)}</text>`,
919
+ `<text x="${w - 26}" y="30" font-size="12" text-anchor="end" fill="${viol ? VIOLATION : MUTED}">${esc(bits.join(" \xB7 "))}</text>`,
920
+ `<line x1="0" y1="66" x2="${w}" y2="66" stroke="${RULE}"/>`
921
+ ].join("\n")
922
+ };
923
+ }
924
+ function legendBlock(g, w, top, hueOf) {
925
+ const comps = [...new Set(g.nodes.map((n) => n.component))].sort((a, b) => LEVEL[a] - LEVEL[b]);
926
+ const rels = [...new Set(g.edges.map((e) => e.rel))];
927
+ const doms = [...new Set(g.nodes.map((n) => n.domain))].sort();
928
+ const rows = [`<line x1="0" y1="${top}" x2="${w}" y2="${top}" stroke="${RULE}"/>`];
929
+ let y = top + 26;
930
+ let x = 26;
931
+ rows.push(`<text x="${x}" y="${y + 4}" font-size="11" fill="${DIM}">shape</text>`);
932
+ x += 48;
933
+ for (const c of comps) {
934
+ const p = {
935
+ n: { component: c, domain: "", name: "" },
936
+ x: x + 16,
937
+ y,
938
+ w: 30,
939
+ h: 15
940
+ };
941
+ const outline = OUTLINE.includes(c);
942
+ rows.push(
943
+ shapePath(p).replace(
944
+ "/>",
945
+ ` fill="${outline ? "none" : MUTED}" stroke="${MUTED}" stroke-width="1.2"${outline ? ' stroke-dasharray="3 2"' : ""}/>`
946
+ ),
947
+ `<text x="${x + 38}" y="${y + 4}" font-size="11" fill="${MUTED}">${c}</text>`
948
+ );
949
+ x += 46 + c.length * 6.6;
950
+ }
951
+ y += 26;
952
+ x = 26;
953
+ rows.push(`<text x="${x}" y="${y + 4}" font-size="11" fill="${DIM}">edge</text>`);
954
+ x += 48;
955
+ for (const r of rels) {
956
+ const st = EDGE[r];
957
+ rows.push(
958
+ `<path d="M${x},${y} L${x + 34},${y}" stroke="${MUTED}" stroke-width="${st.width}"${st.dash ? ` stroke-dasharray="${st.dash}"` : ""} fill="none"/>`,
959
+ arrowHead({ x: x + 40, y }, { x: 1, y: 0 }, st, MUTED),
960
+ `<text x="${x + 48}" y="${y + 4}" font-size="11" fill="${MUTED}">${r}</text>`
961
+ );
962
+ x += 60 + r.length * 6.6;
963
+ }
964
+ rows.push(
965
+ `<path d="M${x},${y} L${x + 34},${y}" stroke="${VIOLATION}" stroke-width="3.4" fill="none"/>`,
966
+ `<text x="${x + 40}" y="${y + 4}" font-size="11" fill="${VIOLATION}">boundary violation</text>`
967
+ );
968
+ x += 150;
969
+ rows.push(
970
+ `<rect x="${x}" y="${y - 7}" width="14" height="14" rx="3" fill="none" stroke="${ADDED}" stroke-width="3"/>`,
971
+ `<text x="${x + 20}" y="${y + 4}" font-size="11" fill="${ADDED}">added</text>`,
972
+ `<rect x="${x + 66}" y="${y - 7}" width="14" height="14" rx="3" fill="none" stroke="${MODIFIED}" stroke-width="2.6" stroke-dasharray="7 4"/>`,
973
+ `<text x="${x + 86}" y="${y + 4}" font-size="11" fill="${MODIFIED}">modified</text>`
974
+ );
975
+ y += 26;
976
+ x = 26;
977
+ rows.push(`<text x="${x}" y="${y + 4}" font-size="11" fill="${DIM}">domain</text>`);
978
+ x += 48;
979
+ for (const d of doms) {
980
+ if (x > w - 160) {
981
+ y += 20;
982
+ x = 74;
983
+ }
984
+ rows.push(
985
+ `<rect x="${x}" y="${y - 6}" width="11" height="11" rx="2" fill="${hsl(hueOf(d), isCommon(d) ? 0 : 60, 50)}"/>`,
986
+ `<text x="${x + 16}" y="${y + 4}" font-size="11" fill="${MUTED}">${esc(d)}</text>`
987
+ );
988
+ x += 24 + d.length * 6.6;
989
+ }
990
+ y += 24;
991
+ rows.push(
992
+ `<text x="26" y="${y + 4}" font-size="10" fill="${DIM}">colour = domain \xB7 saturation = position in the hexagon (Entity darkest \u2192 outward lighter) \xB7 coordinates are deterministic, so two branches line up</text>`
993
+ );
994
+ return { svg: rows.join("\n"), h: y + 22 - top };
995
+ }
996
+
997
+ // src/source.ts
998
+ import { readdirSync as readdirSync3, statSync as statSync3 } from "fs";
999
+ import { join as join4 } from "path";
1000
+
1001
+ // src/model.ts
1002
+ var nodeIndex = (g) => new Map(g.nodes.map((n) => [n.id, n]));
1003
+ var edgeKey = (e) => `${e.src}\0${e.dst}\0${e.rel}`;
1004
+
1005
+ // src/delta.ts
1006
+ function diff(base, head) {
1007
+ const b = nodeIndex(base);
1008
+ const h = nodeIndex(head);
1009
+ const addedNodes = [...h.keys()].filter((id) => !b.has(id)).sort();
1010
+ const removedNodes = [...b.keys()].filter((id) => !h.has(id)).sort();
1011
+ const be = new Map(base.edges.map((e) => [edgeKey(e), e]));
1012
+ const he = new Map(head.edges.map((e) => [edgeKey(e), e]));
1013
+ const addedEdges = [...he].filter(([k]) => !be.has(k)).map(([, e]) => e);
1014
+ const removedEdges = [...be].filter(([k]) => !he.has(k)).map(([, e]) => e);
1015
+ const contractChanged = /* @__PURE__ */ new Set();
1016
+ for (const [id, n] of h) {
1017
+ const o = b.get(id);
1018
+ if (!o) continue;
1019
+ if (o.api.join("\n") !== n.api.join("\n") || o.props.join("\n") !== n.props.join("\n")) {
1020
+ contractChanged.add(id);
1021
+ }
1022
+ }
1023
+ const touched = /* @__PURE__ */ new Set();
1024
+ for (const e of [...addedEdges, ...removedEdges]) {
1025
+ touched.add(e.src);
1026
+ touched.add(e.dst);
1027
+ }
1028
+ const added = new Set(addedNodes);
1029
+ const modifiedNodes = [.../* @__PURE__ */ new Set([...contractChanged, ...touched])].filter((id) => h.has(id) && !added.has(id)).sort();
1030
+ const gone = (from, to) => {
1031
+ const s = new Set(to);
1032
+ return from.filter((x) => !s.has(x));
1033
+ };
1034
+ const changes = /* @__PURE__ */ new Map();
1035
+ for (const id of modifiedNodes) {
1036
+ const o = b.get(id);
1037
+ const n = h.get(id);
1038
+ changes.set(id, {
1039
+ apiAdded: gone(n.api, o.api),
1040
+ apiRemoved: gone(o.api, n.api),
1041
+ propsAdded: gone(n.props, o.props),
1042
+ propsRemoved: gone(o.props, n.props),
1043
+ depsAdded: addedEdges.filter((e) => e.src === id).map((e) => e.dst),
1044
+ depsRemoved: removedEdges.filter((e) => e.src === id).map((e) => e.dst)
1045
+ });
1046
+ }
1047
+ const baseViol = new Set(base.edges.filter((e) => e.violation).map(edgeKey));
1048
+ const newViolations = new Set(
1049
+ head.edges.filter((e) => e.violation && !baseViol.has(edgeKey(e))).map(edgeKey)
1050
+ );
1051
+ return {
1052
+ addedNodes,
1053
+ removedNodes,
1054
+ modifiedNodes,
1055
+ addedEdges,
1056
+ removedEdges,
1057
+ changes,
1058
+ newViolations
1059
+ };
1060
+ }
1061
+ function applyStatus(head, d) {
1062
+ const added = new Set(d.addedNodes);
1063
+ const modified = new Set(d.modifiedNodes);
1064
+ const addedE = new Set(d.addedEdges.map(edgeKey));
1065
+ return {
1066
+ ...head,
1067
+ nodes: head.nodes.map((n) => ({
1068
+ ...n,
1069
+ status: added.has(n.id) ? "added" : modified.has(n.id) ? "modified" : "existing",
1070
+ ...d.changes.get(n.id) ? { change: d.changes.get(n.id) } : {}
1071
+ })),
1072
+ edges: head.edges.map((e) => ({
1073
+ ...e,
1074
+ status: addedE.has(edgeKey(e)) ? "added" : "existing",
1075
+ ...d.newViolations.has(edgeKey(e)) ? { newViolation: true } : {}
1076
+ }))
1077
+ };
1078
+ }
1079
+
1080
+ // src/extract/kotlin.ts
1081
+ import { readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1082
+ import { join as join2, relative as relative2 } from "path";
1083
+
1084
+ // src/extract/kotlin-syntax.ts
1085
+ var PACKAGE = /^package\s+([\w.]+)/;
1086
+ var IMPORT = /^import\s+([\w.]+)(?:\s+as\s+(\w+))?/;
1087
+ var MODS = "(?:public |internal |private |protected |open |abstract |sealed |data |value |inner |annotation |enum |final |external |expect |actual |fun )*";
1088
+ var DECL = new RegExp(
1089
+ `^(?<mods>${MODS})(?<kw>class|interface|object)\\s+(?<name>[A-Za-z_]\\w*)`
1090
+ );
1091
+ var FUN_MODS = "(?:override |open |abstract |suspend |operator |private |internal |protected |inline |final )*";
1092
+ var SIG = new RegExp(
1093
+ `^\\s+(?:@\\w+(?:\\([^)]*\\))?\\s*)*(?<mods>${FUN_MODS})fun\\s+(?<name>\\w+)\\s*\\(`
1094
+ );
1095
+ var FUN_LINE = new RegExp(
1096
+ `^\\s+(?:@\\w+(?:\\([^)]*\\))?\\s+)*${FUN_MODS.replace("| final ", "|final ")}fun\\s`
1097
+ );
1098
+ var PROP = /^ {4}(?!private|internal|protected)(?:override |open |const |lateinit )*va[lr]\s+(\w+)\s*:\s*([^=\n]+?)\s*(?:=|$)/;
1099
+ var FIELD = /va[lr]\s+(\w+)\s*:\s*([A-Za-z_]\w*)/g;
1100
+ var LOCAL_CALL = /va[lr]\s+(\w+)\s*=\s*([A-Za-z_]\w*)[.!?]*\.(\w+)\s*\(/g;
1101
+ var LOCAL_CTOR = /va[lr]\s+(\w+)\s*=\s*([A-Z]\w*)\s*\(/g;
1102
+ var CALL = /\b(\w+)[!?]*\.(\w+)\s*\(/g;
1103
+ var TYPENAME = /\b([A-Z]\w*)\b/g;
1104
+ function kindOf(mods, kw) {
1105
+ const m = mods.split(/\s+/);
1106
+ for (const k of ["value", "data", "enum", "sealed", "annotation", "abstract", "fun"]) {
1107
+ if (m.includes(k)) return `${k} ${kw}`;
1108
+ }
1109
+ return kw;
1110
+ }
1111
+ function structOf(kind) {
1112
+ if (kind.includes("interface")) return "Interface";
1113
+ if (kind === "object") return "Object";
1114
+ return "Class";
1115
+ }
1116
+ function stripComments(text) {
1117
+ const out = [];
1118
+ let inBlock = false;
1119
+ let inLine = false;
1120
+ let inStr = false;
1121
+ for (let i = 0; i < text.length; i++) {
1122
+ const c = text[i];
1123
+ if (inLine) {
1124
+ if (c === "\n") {
1125
+ inLine = false;
1126
+ out.push("\n");
1127
+ } else out.push(" ");
1128
+ continue;
1129
+ }
1130
+ if (inBlock) {
1131
+ if (c === "*" && text[i + 1] === "/") {
1132
+ out.push(" ");
1133
+ i++;
1134
+ inBlock = false;
1135
+ continue;
1136
+ }
1137
+ out.push(c === "\n" ? "\n" : " ");
1138
+ continue;
1139
+ }
1140
+ if (!inStr && c === "/" && text[i + 1] === "*") {
1141
+ inBlock = true;
1142
+ out.push(" ");
1143
+ i++;
1144
+ continue;
1145
+ }
1146
+ if (!inStr && c === "/" && text[i + 1] === "/") {
1147
+ inLine = true;
1148
+ out.push(" ");
1149
+ i++;
1150
+ continue;
1151
+ }
1152
+ if (c === '"') inStr = !inStr;
1153
+ else if (c === "\n") inStr = false;
1154
+ out.push(c);
1155
+ }
1156
+ return out.join("");
1157
+ }
1158
+ function cleanSig(name, params, ret) {
1159
+ const r = (ret.split("{")[0] ?? "").split("=")[0]?.trimEnd() ?? "";
1160
+ return `${name}${params}${r}`.replace(/\s+/g, " ").replace(/\( /g, "(").replace(/ \)/g, ")").replace(/ ,/g, ",").trim();
1161
+ }
1162
+ function collectSigs(body) {
1163
+ const out = /* @__PURE__ */ new Map();
1164
+ for (let i = 0; i < body.length; i++) {
1165
+ const m = SIG.exec(body[i]);
1166
+ if (!m?.groups) continue;
1167
+ const name = m.groups.name;
1168
+ const isPublic = !/private|internal|protected/.test(m.groups.mods ?? "");
1169
+ const buf = [];
1170
+ let depth = 0;
1171
+ let started = false;
1172
+ for (let j = i; j < Math.min(i + 30, body.length); j++) {
1173
+ let line = body[j];
1174
+ if (j === i) line = line.slice(line.indexOf("(", line.indexOf("fun")));
1175
+ for (const ch of line) {
1176
+ if (ch === "(") {
1177
+ depth++;
1178
+ started = true;
1179
+ } else if (ch === ")") depth--;
1180
+ buf.push(ch);
1181
+ if (started && depth === 0) break;
1182
+ }
1183
+ if (started && depth === 0) {
1184
+ const parts = body[j].split(")");
1185
+ out.set(name, { params: buf.join(""), ret: parts[parts.length - 1], isPublic });
1186
+ break;
1187
+ }
1188
+ buf.push(" ");
1189
+ }
1190
+ }
1191
+ return out;
1192
+ }
1193
+ function ctorParams(header2) {
1194
+ const i = header2.indexOf("(");
1195
+ if (i < 0) return [];
1196
+ let depth = 0;
1197
+ const buf = [];
1198
+ for (const ch of header2.slice(i)) {
1199
+ if (ch === "(" || ch === "<") depth++;
1200
+ else if (ch === ")" || ch === ">") {
1201
+ depth--;
1202
+ if (depth === 0) break;
1203
+ }
1204
+ buf.push(ch);
1205
+ }
1206
+ const inner = buf.join("").slice(1);
1207
+ const parts = [];
1208
+ let cur = [];
1209
+ depth = 0;
1210
+ for (const ch of inner) {
1211
+ if ("(<[".includes(ch)) depth++;
1212
+ else if (")>]".includes(ch)) depth--;
1213
+ if (ch === "," && depth === 0) {
1214
+ parts.push(cur.join(""));
1215
+ cur = [];
1216
+ } else cur.push(ch);
1217
+ }
1218
+ parts.push(cur.join(""));
1219
+ const res = [];
1220
+ for (let p of parts) {
1221
+ p = (p.split("=")[0] ?? "").replace(/\s+/g, " ").trim();
1222
+ p = p.replace(/^(?:@\w+(?:\([^)]*\))?\s*)*/, "").replace(/^(?:private |internal |protected |override |val |var )*/, "").trim();
1223
+ if (p.includes(":")) res.push(p);
1224
+ }
1225
+ return res;
1226
+ }
1227
+ function enumEntries(body) {
1228
+ const i = body.indexOf("{");
1229
+ if (i < 0) return [];
1230
+ let depth = 0;
1231
+ const out = [];
1232
+ let cur = [];
1233
+ for (const ch of body.slice(i + 1)) {
1234
+ if (ch === "(" || ch === "[") depth++;
1235
+ else if (ch === ")" || ch === "]") depth--;
1236
+ else if (ch === "{") depth++;
1237
+ else if (ch === "}") {
1238
+ if (depth === 0) break;
1239
+ depth--;
1240
+ }
1241
+ if ((ch === "," || ch === ";") && depth === 0) {
1242
+ out.push(cur.join(""));
1243
+ cur = [];
1244
+ if (ch === ";") break;
1245
+ } else cur.push(ch);
1246
+ }
1247
+ out.push(cur.join(""));
1248
+ const res = [];
1249
+ for (const e of out) {
1250
+ const m = /^\s*([A-Z][A-Za-z0-9_]*)/.exec(e);
1251
+ if (m) res.push(m[1]);
1252
+ }
1253
+ return res;
1254
+ }
1255
+ function parseSupertypes(lines, start) {
1256
+ const buf = [];
1257
+ let depth = 0;
1258
+ let seenColon = false;
1259
+ for (let li = start; li < Math.min(start + 80, lines.length); li++) {
1260
+ let line = lines[li];
1261
+ const cpos = line.indexOf("//");
1262
+ if (cpos >= 0) line = line.slice(0, cpos);
1263
+ for (const ch of line) {
1264
+ if ("(<[".includes(ch)) depth++;
1265
+ else if (")>]".includes(ch)) depth = Math.max(0, depth - 1);
1266
+ else if (ch === "{" && depth === 0) return supertypeNames(buf.join(""), seenColon);
1267
+ else if (ch === ":" && depth === 0) seenColon = true;
1268
+ buf.push(ch);
1269
+ }
1270
+ const stripped = buf.join("").trimEnd();
1271
+ if (depth === 0 && stripped) {
1272
+ const last = stripped[stripped.length - 1];
1273
+ if (seenColon) {
1274
+ if (last !== "," && last !== ":") return supertypeNames(buf.join(""), seenColon);
1275
+ } else if (li > start || !stripped.includes("(") || stripped.endsWith(")")) {
1276
+ if (!stripped.endsWith(",")) return [];
1277
+ }
1278
+ }
1279
+ buf.push("\n");
1280
+ }
1281
+ return supertypeNames(buf.join(""), seenColon);
1282
+ }
1283
+ function supertypeNames(text, seenColon) {
1284
+ if (!seenColon) return [];
1285
+ let depth = 0;
1286
+ let idx = -1;
1287
+ for (let i = 0; i < text.length; i++) {
1288
+ const ch = text[i];
1289
+ if ("(<[".includes(ch)) depth++;
1290
+ else if (")>]".includes(ch)) depth = Math.max(0, depth - 1);
1291
+ else if (ch === ":" && depth === 0) {
1292
+ idx = i;
1293
+ break;
1294
+ }
1295
+ }
1296
+ if (idx < 0) return [];
1297
+ const parts = [];
1298
+ let cur = [];
1299
+ depth = 0;
1300
+ for (const ch of text.slice(idx + 1)) {
1301
+ if ("(<[".includes(ch)) depth++;
1302
+ else if (")>]".includes(ch)) depth--;
1303
+ if (ch === "," && depth === 0) {
1304
+ parts.push(cur.join(""));
1305
+ cur = [];
1306
+ } else cur.push(ch);
1307
+ }
1308
+ parts.push(cur.join(""));
1309
+ const names = [];
1310
+ for (const p of parts) {
1311
+ const head = (p.split(/\bby\b/)[0] ?? "").trim();
1312
+ const m = /^([A-Za-z_][\w.]*)/.exec(head);
1313
+ if (m) names.push(m[1].split(".").pop());
1314
+ }
1315
+ return names;
1316
+ }
1317
+
1318
+ // src/extract/kotlin.ts
1319
+ function walk(dir, out = []) {
1320
+ for (const e of readdirSync2(dir)) {
1321
+ const p = join2(dir, e);
1322
+ if (statSync2(p).isDirectory()) walk(p, out);
1323
+ else if (e.endsWith(".kt")) out.push(p);
1324
+ }
1325
+ return out;
1326
+ }
1327
+ function inferBasePackage(pkgs) {
1328
+ if (!pkgs.length) return "";
1329
+ const split = pkgs.map((p) => p.split("."));
1330
+ const first = split[0];
1331
+ const common = [];
1332
+ for (let i = 0; i < first.length; i++) {
1333
+ const seg = first[i];
1334
+ if (split.every((s) => s[i] === seg)) common.push(seg);
1335
+ else break;
1336
+ }
1337
+ return common.join(".");
1338
+ }
1339
+ function extractKotlin(srcRoot, profile, project, ref) {
1340
+ const files = walk(srcRoot).sort();
1341
+ const infos = [];
1342
+ const rawPkgs = [];
1343
+ for (const abs of files) {
1344
+ const rel = relative2(srcRoot, abs);
1345
+ if (isExcluded(profile, `/${rel}`)) continue;
1346
+ const lines = stripComments(readFileSync3(abs, "utf8")).split("\n");
1347
+ let pkg = "";
1348
+ const imports = /* @__PURE__ */ new Map();
1349
+ const decls = [];
1350
+ for (let i = 0; i < lines.length; i++) {
1351
+ const line = lines[i];
1352
+ const pm = PACKAGE.exec(line);
1353
+ if (pm) {
1354
+ pkg = pm[1];
1355
+ continue;
1356
+ }
1357
+ const im = IMPORT.exec(line);
1358
+ if (im) {
1359
+ const fq = im[1];
1360
+ imports.set(im[2] ?? fq.split(".").pop(), fq);
1361
+ continue;
1362
+ }
1363
+ const dm = DECL.exec(line);
1364
+ if (dm?.groups) {
1365
+ decls.push({
1366
+ name: dm.groups.name,
1367
+ line: i,
1368
+ kind: kindOf(dm.groups.mods ?? "", dm.groups.kw)
1369
+ });
1370
+ }
1371
+ }
1372
+ rawPkgs.push(pkg);
1373
+ infos.push({ rel, pkg, imports, decls, lines, layer: layerOf(profile, `/${rel}`) });
1374
+ }
1375
+ const base = profile.domain.base || inferBasePackage(rawPkgs);
1376
+ const domainOf = (pkg) => {
1377
+ const rest = pkg.startsWith(`${base}.`) ? pkg.slice(base.length + 1) : pkg;
1378
+ return rest.split(".")[profile.domain.at] ?? "root";
1379
+ };
1380
+ const types = /* @__PURE__ */ new Map();
1381
+ const byPkg = /* @__PURE__ */ new Map();
1382
+ for (const fi of infos) {
1383
+ if (fi.layer === "other") continue;
1384
+ const path = `/${fi.rel}`;
1385
+ const layer = fi.layer;
1386
+ const sublayer = sublayerOf(profile, path);
1387
+ const adapterKind = layer === "adapter" ? adapterKindOf(profile, path) : "";
1388
+ const domain = domainOf(fi.pkg);
1389
+ for (let di = 0; di < fi.decls.length; di++) {
1390
+ const d = fi.decls[di];
1391
+ const end = di + 1 < fi.decls.length ? fi.decls[di + 1].line : fi.lines.length;
1392
+ const body = fi.lines.slice(d.line, end);
1393
+ const struct = structOf(d.kind);
1394
+ const sigs = collectSigs(body);
1395
+ const api = [];
1396
+ for (const [nm, s] of sigs) if (s.isPublic) api.push(cleanSig(nm, s.params, s.ret));
1397
+ const headerFull = body.slice(0, 40).join("\n");
1398
+ const braceAt = headerFull.indexOf("{");
1399
+ const header2 = braceAt >= 0 ? headerFull.slice(0, braceAt) : headerFull;
1400
+ const props = ctorParams(header2);
1401
+ if (braceAt >= 0) {
1402
+ const rest = body.slice(header2.split("\n").length - 1);
1403
+ const first = rest[0];
1404
+ if (first !== void 0) rest[0] = first.slice(first.indexOf("{") + 1);
1405
+ for (const ln of rest) {
1406
+ const pm = PROP.exec(ln);
1407
+ if (pm) props.push(`${pm[1]}: ${pm[2].trim().replace(/,$/, "")}`);
1408
+ }
1409
+ }
1410
+ if (d.kind.includes("enum")) props.push(...enumEntries(body.join("\n")));
1411
+ const id = `${fi.pkg}.${d.name}`;
1412
+ types.set(id, {
1413
+ id,
1414
+ name: d.name,
1415
+ domain,
1416
+ component: componentOf(profile, { layer, sublayer, kind: d.kind, struct, name: d.name }),
1417
+ layer,
1418
+ sublayer,
1419
+ kind: d.kind,
1420
+ adapterKind,
1421
+ api,
1422
+ props,
1423
+ file: fi.rel,
1424
+ line: d.line + 1,
1425
+ struct,
1426
+ sigs
1427
+ });
1428
+ byPkg.set(fi.pkg, [...byPkg.get(fi.pkg) ?? [], id]);
1429
+ }
1430
+ }
1431
+ const visibleOf = (fi) => {
1432
+ const v = /* @__PURE__ */ new Map();
1433
+ for (const [simple, fq] of fi.imports) if (types.has(fq)) v.set(simple, fq);
1434
+ for (const id of byPkg.get(fi.pkg) ?? []) {
1435
+ const t = types.get(id);
1436
+ if (!v.has(t.name)) v.set(t.name, id);
1437
+ }
1438
+ return v;
1439
+ };
1440
+ const analyzed = new Set(Object.keys(profile.layers));
1441
+ const dep = /* @__PURE__ */ new Map();
1442
+ const supers = [];
1443
+ const layerViol = /* @__PURE__ */ new Set();
1444
+ const key = (s, d) => `${s}\0${d}`;
1445
+ for (const fi of infos) {
1446
+ if (!analyzed.has(fi.layer)) continue;
1447
+ const visible = visibleOf(fi);
1448
+ for (let di = 0; di < fi.decls.length; di++) {
1449
+ const d = fi.decls[di];
1450
+ const id = `${fi.pkg}.${d.name}`;
1451
+ const end = di + 1 < fi.decls.length ? fi.decls[di + 1].line : fi.lines.length;
1452
+ const body = fi.lines.slice(d.line, end).join("\n");
1453
+ for (const sup of parseSupertypes(fi.lines, d.line)) {
1454
+ const tgt = visible.get(sup);
1455
+ if (tgt) supers.push([id, tgt]);
1456
+ }
1457
+ for (const [simple, tgt] of visible) {
1458
+ if (tgt === id) continue;
1459
+ const t = types.get(tgt);
1460
+ const cnt = body.split(new RegExp(`\\b${simple}\\b`, "g")).length - 1;
1461
+ if (!cnt) continue;
1462
+ if (t.layer === "adapter" && (fi.layer === "application" || fi.layer === "domain")) {
1463
+ layerViol.add(key(id, tgt));
1464
+ }
1465
+ dep.set(key(id, tgt), (dep.get(key(id, tgt)) ?? 0) + cnt);
1466
+ }
1467
+ }
1468
+ }
1469
+ const sigTypes = /* @__PURE__ */ new Map();
1470
+ const visCache = /* @__PURE__ */ new Map();
1471
+ for (const fi of infos) visCache.set(fi.rel, visibleOf(fi));
1472
+ for (const [id, t] of types) {
1473
+ const v = visCache.get(t.file) ?? /* @__PURE__ */ new Map();
1474
+ const m = /* @__PURE__ */ new Map();
1475
+ for (const [nm, s] of t.sigs) {
1476
+ const pick = (txt) => {
1477
+ const out = /* @__PURE__ */ new Set();
1478
+ for (const mm of txt.matchAll(TYPENAME)) {
1479
+ const tgt = v.get(mm[1]);
1480
+ if (tgt) out.add(tgt);
1481
+ }
1482
+ return out;
1483
+ };
1484
+ m.set(nm, [pick(s.params), pick(s.ret)]);
1485
+ }
1486
+ sigTypes.set(id, m);
1487
+ }
1488
+ const viaSig = /* @__PURE__ */ new Set();
1489
+ const usedContracts = /* @__PURE__ */ new Map();
1490
+ for (const fi of infos) {
1491
+ if (!analyzed.has(fi.layer)) continue;
1492
+ const visible = visCache.get(fi.rel);
1493
+ for (let di = 0; di < fi.decls.length; di++) {
1494
+ const d = fi.decls[di];
1495
+ const id = `${fi.pkg}.${d.name}`;
1496
+ const end = di + 1 < fi.decls.length ? fi.decls[di + 1].line : fi.lines.length;
1497
+ const body = fi.lines.slice(d.line, end).join("\n");
1498
+ const bind = /* @__PURE__ */ new Map();
1499
+ for (const m of body.matchAll(FIELD)) {
1500
+ const tgt = visible.get(m[2]);
1501
+ if (tgt) bind.set(m[1], tgt);
1502
+ }
1503
+ for (let pass = 0; pass < 3; pass++) {
1504
+ for (const m of body.matchAll(LOCAL_CTOR)) {
1505
+ const tgt = visible.get(m[2]);
1506
+ if (tgt && !bind.has(m[1])) bind.set(m[1], tgt);
1507
+ }
1508
+ for (const m of body.matchAll(LOCAL_CALL)) {
1509
+ const owner = bind.get(m[2]);
1510
+ if (!owner) continue;
1511
+ const rets = sigTypes.get(owner)?.get(m[3])?.[1];
1512
+ if (rets?.size === 1 && !bind.has(m[1])) {
1513
+ bind.set(m[1], [...rets][0]);
1514
+ }
1515
+ }
1516
+ }
1517
+ for (const m of body.matchAll(CALL)) {
1518
+ const owner = bind.get(m[1]);
1519
+ if (!owner) continue;
1520
+ const sg = sigTypes.get(owner)?.get(m[2]);
1521
+ if (!sg) continue;
1522
+ const ck = key(id, owner);
1523
+ usedContracts.set(ck, (usedContracts.get(ck) ?? /* @__PURE__ */ new Set()).add(m[2]));
1524
+ for (const tgt of /* @__PURE__ */ new Set([...sg[0], ...sg[1]])) {
1525
+ if (tgt === id) continue;
1526
+ const k = key(id, tgt);
1527
+ if (!dep.has(k)) {
1528
+ dep.set(k, 1);
1529
+ viaSig.add(k);
1530
+ }
1531
+ }
1532
+ }
1533
+ }
1534
+ }
1535
+ const supSet = new Set(supers.map(([c, p]) => key(c, p)));
1536
+ const edges = [];
1537
+ const push2 = (src, dst, rel, weight) => {
1538
+ const s = types.get(src);
1539
+ const d = types.get(dst);
1540
+ const k = key(src, dst);
1541
+ const sigs = d.sigs;
1542
+ const contracts = [...usedContracts.get(k) ?? []].sort().map((m) => {
1543
+ const sg = sigs.get(m);
1544
+ return sg ? cleanSig(m, sg.params, sg.ret) : m;
1545
+ });
1546
+ edges.push({
1547
+ src,
1548
+ dst,
1549
+ rel,
1550
+ weight,
1551
+ crossDomain: s.domain !== d.domain,
1552
+ identifierOnly: d.component === "VO" && d.sublayer === "model",
1553
+ viaSignature: viaSig.has(k),
1554
+ contracts: rel === "DEPENDS_ON" ? contracts : [],
1555
+ violation: violationOf(profile, s, d, layerViol.has(k))
1556
+ });
1557
+ };
1558
+ for (const [c, p] of supSet.size ? [...supSet].map((k) => k.split("\0")) : []) {
1559
+ const parent = types.get(p);
1560
+ push2(c, p, parent.struct === "Interface" ? "IMPLEMENTS" : "EXTENDS", 1);
1561
+ }
1562
+ for (const [k, w] of dep) {
1563
+ if (supSet.has(k)) continue;
1564
+ const [s, d] = k.split("\0");
1565
+ push2(s, d, "DEPENDS_ON", w);
1566
+ }
1567
+ const nodes = [...types.values()].map(({ struct: _s, sigs: _g, ...n }) => n);
1568
+ nodes.sort((a, b) => a.id.localeCompare(b.id));
1569
+ edges.sort(
1570
+ (a, b) => a.src.localeCompare(b.src) || a.dst.localeCompare(b.dst) || a.rel.localeCompare(b.rel)
1571
+ );
1572
+ return { project, ref, nodes, edges };
1573
+ }
1574
+ function violationOf(p, s, d, layerBack) {
1575
+ if (layerBack) {
1576
+ const rule = p.rules.layering.find((r) => r.from.includes(s.layer) && r.to.includes(d.layer));
1577
+ if (rule) return rule.message;
1578
+ }
1579
+ if (d.component !== "Entity") return "";
1580
+ const ea = p.rules.entityAccess;
1581
+ if (s.domain !== d.domain) {
1582
+ return ea.crossDomain === "deny" ? "cross-domain Entity access" : "";
1583
+ }
1584
+ if (s.layer === "adapter") {
1585
+ return ea.allowAdapterKinds.includes(s.adapterKind) ? "" : "inbound adapter touches Entity";
1586
+ }
1587
+ return ea.allow.includes(s.component) ? "" : `${s.component} exposes Entity`;
1588
+ }
1589
+
1590
+ // src/git.ts
1591
+ import { execFileSync } from "child_process";
1592
+ import { mkdtempSync, rmSync } from "fs";
1593
+ import { tmpdir } from "os";
1594
+ import { join as join3 } from "path";
1595
+ function exportRef(repo, ref, subPath = "") {
1596
+ const dir = mkdtempSync(join3(tmpdir(), "hexwright-"));
1597
+ const args = ["-C", repo, "archive", "--format=tar", ref];
1598
+ if (subPath) args.push("--", subPath);
1599
+ let tar;
1600
+ try {
1601
+ tar = execFileSync("git", args, { maxBuffer: 1 << 30, stdio: ["ignore", "pipe", "ignore"] });
1602
+ } catch {
1603
+ rmSync(dir, { recursive: true, force: true });
1604
+ const remote = /^([^/]+)\/(.+)$/.exec(ref);
1605
+ const [name, dest] = remote ? [remote[2], `refs/remotes/${remote[1]}/${remote[2]}`] : [ref, `refs/heads/${ref}`];
1606
+ throw new Error(
1607
+ `cannot read '${ref}' from ${repo} \u2014 the ref is not in this clone.
1608
+ CI checkouts are shallow by default. Either fetch it:
1609
+ git fetch --no-tags --depth=1 origin ${name}:${dest}
1610
+ or check out with full history (actions/checkout: fetch-depth: 0).`
1611
+ );
1612
+ }
1613
+ execFileSync("tar", ["-x", "-C", dir], { input: tar });
1614
+ return { dir, cleanup: () => rmSync(dir, { recursive: true, force: true }) };
1615
+ }
1616
+ var gitOut = (repo, args) => execFileSync("git", ["-C", repo, ...args], {
1617
+ encoding: "utf8",
1618
+ stdio: ["ignore", "pipe", "ignore"]
1619
+ }).trim();
1620
+ function resolveRef(repo, ref) {
1621
+ return gitOut(repo, ["rev-parse", "--short", ref]);
1622
+ }
1623
+ function currentBranch(repo) {
1624
+ return gitOut(repo, ["rev-parse", "--abbrev-ref", "HEAD"]);
1625
+ }
1626
+
1627
+ // src/query.ts
1628
+ var GraphQuery = class {
1629
+ graph;
1630
+ byId;
1631
+ out;
1632
+ inc;
1633
+ constructor(graph) {
1634
+ this.graph = graph;
1635
+ this.byId = new Map(graph.nodes.map((n) => [n.id, n]));
1636
+ this.out = /* @__PURE__ */ new Map();
1637
+ this.inc = /* @__PURE__ */ new Map();
1638
+ for (const e of graph.edges) {
1639
+ push(this.out, e.src, e);
1640
+ push(this.inc, e.dst, e);
1641
+ }
1642
+ }
1643
+ /** Find nodes by partial name or FQCN, or filter by component and domain. */
1644
+ find(q) {
1645
+ const needle = q.query?.toLowerCase();
1646
+ const hits = this.graph.nodes.filter((n) => {
1647
+ if (q.domain && n.domain !== q.domain) return false;
1648
+ if (q.component && n.component !== q.component) return false;
1649
+ if (!needle) return true;
1650
+ return n.name.toLowerCase().includes(needle) || n.id.toLowerCase().includes(needle);
1651
+ });
1652
+ hits.sort((a, b) => {
1653
+ const ea = a.name.toLowerCase() === needle ? 0 : 1;
1654
+ const eb = b.name.toLowerCase() === needle ? 0 : 1;
1655
+ return ea - eb || a.name.length - b.name.length;
1656
+ });
1657
+ return hits.slice(0, q.limit ?? 40);
1658
+ }
1659
+ /** Resolve one node from an exact name or FQCN. Ambiguity returns candidates. */
1660
+ resolve(name) {
1661
+ const exact = this.byId.get(name);
1662
+ if (exact) return { node: exact, candidates: [] };
1663
+ const byName = this.graph.nodes.filter((n) => n.name === name);
1664
+ if (byName.length === 1) return { node: byName[0], candidates: [] };
1665
+ if (byName.length > 1) return { candidates: byName };
1666
+ return { candidates: this.find({ query: name, limit: 8 }) };
1667
+ }
1668
+ node(id) {
1669
+ return this.byId.get(id);
1670
+ }
1671
+ outgoing(id) {
1672
+ return this.out.get(id) ?? [];
1673
+ }
1674
+ incoming(id) {
1675
+ return this.inc.get(id) ?? [];
1676
+ }
1677
+ /** What a change to this node reaches — reverse dependencies, N hops out. */
1678
+ impact(id, hops = 2) {
1679
+ const seen = /* @__PURE__ */ new Set([id]);
1680
+ const out = [];
1681
+ let frontier = [id];
1682
+ for (let h = 1; h <= hops; h++) {
1683
+ const next = [];
1684
+ for (const cur of frontier) {
1685
+ for (const e of this.incoming(cur)) {
1686
+ if (seen.has(e.src)) continue;
1687
+ seen.add(e.src);
1688
+ next.push(e.src);
1689
+ const n = this.byId.get(e.src);
1690
+ if (n) out.push({ hop: h, node: n });
1691
+ }
1692
+ }
1693
+ frontier = next;
1694
+ if (!frontier.length) break;
1695
+ }
1696
+ return out;
1697
+ }
1698
+ /** Whether this graph can be compared against a base (built with --base). */
1699
+ get hasBase() {
1700
+ return this.graph.nodes.some((n) => n.status !== void 0);
1701
+ }
1702
+ violations(scope = "all") {
1703
+ return this.graph.edges.filter(
1704
+ (e) => e.violation && (scope === "all" || e.newViolation === true)
1705
+ );
1706
+ }
1707
+ /** Which declared methods each consumer actually calls — the ISP signal. */
1708
+ contractUsage(id) {
1709
+ const target = this.byId.get(id);
1710
+ const declared = target?.api ?? [];
1711
+ const consumers = this.incoming(id).filter((e) => e.contracts.length).map((e) => ({ name: this.byId.get(e.src)?.name ?? e.src, used: e.contracts }));
1712
+ const usedNames = new Set(consumers.flatMap((c) => c.used.map(methodName)));
1713
+ return {
1714
+ declared,
1715
+ consumers,
1716
+ unusedByAll: declared.filter((d) => !usedNames.has(methodName(d)))
1717
+ };
1718
+ }
1719
+ delta() {
1720
+ return {
1721
+ added: this.graph.nodes.filter((n) => n.status === "added"),
1722
+ modified: this.graph.nodes.filter((n) => n.status === "modified"),
1723
+ addedEdges: this.graph.edges.filter((e) => e.status === "added")
1724
+ };
1725
+ }
1726
+ };
1727
+ function push(m, k, v) {
1728
+ const arr = m.get(k);
1729
+ if (arr) arr.push(v);
1730
+ else m.set(k, [v]);
1731
+ }
1732
+ var methodName = (sig) => {
1733
+ const i = sig.indexOf("(");
1734
+ return i > 0 ? sig.slice(0, i) : sig;
1735
+ };
1736
+
1737
+ // src/source.ts
1738
+ function stamp(srcAbs, profilePath) {
1739
+ let newest = 0;
1740
+ let count = 0;
1741
+ const walk2 = (dir) => {
1742
+ for (const e of readdirSync3(dir, { withFileTypes: true })) {
1743
+ const p = join4(dir, e.name);
1744
+ if (e.isDirectory()) walk2(p);
1745
+ else if (e.name.endsWith(".kt")) {
1746
+ count++;
1747
+ const m = statSync3(p).mtimeMs;
1748
+ if (m > newest) newest = m;
1749
+ }
1750
+ }
1751
+ };
1752
+ try {
1753
+ walk2(srcAbs);
1754
+ } catch {
1755
+ return "unreadable";
1756
+ }
1757
+ let profileM = 0;
1758
+ try {
1759
+ profileM = statSync3(profilePath).mtimeMs;
1760
+ } catch {
1761
+ }
1762
+ return `${count}:${newest}:${profileM}`;
1763
+ }
1764
+ var GraphSource = class {
1765
+ cfg;
1766
+ /** The base is a git ref and therefore immutable — extracted once per process. */
1767
+ baseGraph;
1768
+ last;
1769
+ constructor(cfg) {
1770
+ this.cfg = cfg;
1771
+ }
1772
+ /** The current graph. Returns the cache when the source has not moved. */
1773
+ graph() {
1774
+ const s = stamp(this.cfg.srcAbs, this.cfg.profilePath);
1775
+ if (this.last && this.last.stamp === s) return this.last.graph;
1776
+ const { profile, project, srcAbs } = this.cfg;
1777
+ const ref = safe(() => `${currentBranch(this.cfg.repo)}@${resolveRef(this.cfg.repo, "HEAD")}`) ?? "working-tree";
1778
+ const head = extractKotlin(srcAbs, profile, project, ref);
1779
+ let graph = head;
1780
+ let delta;
1781
+ if (this.cfg.base) {
1782
+ delta = diff(this.base(), head);
1783
+ graph = applyStatus(head, delta);
1784
+ }
1785
+ this.last = { stamp: s, graph, query: new GraphQuery(graph), ...delta ? { delta } : {} };
1786
+ return graph;
1787
+ }
1788
+ /** A query object over the current graph. */
1789
+ query() {
1790
+ this.graph();
1791
+ return this.last.query;
1792
+ }
1793
+ /** The delta against the base, or undefined when no base was given. */
1794
+ delta() {
1795
+ this.graph();
1796
+ return this.last?.delta;
1797
+ }
1798
+ base() {
1799
+ if (this.baseGraph) return this.baseGraph;
1800
+ const { repo, srcRel, profile, project, base } = this.cfg;
1801
+ const { dir, cleanup } = exportRef(repo, base, srcRel);
1802
+ try {
1803
+ this.baseGraph = extractKotlin(join4(dir, srcRel), profile, project, base);
1804
+ } finally {
1805
+ cleanup();
1806
+ }
1807
+ return this.baseGraph;
1808
+ }
1809
+ };
1810
+ var safe = (f) => {
1811
+ try {
1812
+ return f();
1813
+ } catch {
1814
+ return void 0;
1815
+ }
1816
+ };
1817
+
1818
+ // src/view/server.ts
1819
+ import { createServer } from "http";
1820
+ import { dirname, join as join5 } from "path";
1821
+
1822
+ // src/view/html.ts
1823
+ var shell = () => `<!doctype html>
1824
+ <meta charset="utf-8"><title>hexwright</title>
1825
+ <style>
1826
+ *{box-sizing:border-box} body{margin:0;font:13px -apple-system,Helvetica,sans-serif;
1827
+ background:#0e1116;color:#e6edf3;display:flex;height:100vh;overflow:hidden}
1828
+ #side{width:340px;flex:none;background:#161b22;border-right:1px solid #30363d;
1829
+ padding:16px 18px;overflow-y:auto}
1830
+ #cy{flex:1;background:#0e1116}
1831
+ h1{font-size:15px;margin:0 0 4px} #ref{color:#6e7681;font-size:10.5px;word-break:break-all}
1832
+ .sub{color:#8b949e;font-size:11px;margin-bottom:14px;line-height:1.5}
1833
+ .sub .d{padding-left:11px;margin:1px 0 6px;color:#7d8590;line-height:1.55;
1834
+ border-left:1px solid #30363d} .sub b{color:#adbac7;font-weight:600}
1835
+ h2{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:#8b949e;
1836
+ margin:16px 0 7px;border-top:1px solid #30363d;padding-top:12px}
1837
+ label{display:flex;align-items:center;gap:7px;padding:3px 0;cursor:pointer;font-size:12px}
1838
+ label:hover{color:#fff} input[type=checkbox]{accent-color:#58a6ff;margin:0}
1839
+ label i{display:inline-block;width:14px;border-top:2px solid #8b949e;vertical-align:4px;
1840
+ margin:0 3px 0 1px;flex:none} label i.d{border-top-style:dashed}
1841
+ .sw{width:11px;height:11px;flex:none} .n{margin-left:auto;color:#6e7681;font-size:10px}
1842
+ button{background:#21262d;color:#c9d1d9;border:1px solid #30363d;border-radius:5px;
1843
+ padding:5px 9px;font-size:11px;cursor:pointer;margin:2px 3px 2px 0}
1844
+ button:hover{background:#30363d} button.on{background:#1f6feb;border-color:#1f6feb;color:#fff}
1845
+ #stat{margin:0 0 10px;padding:7px 9px;background:#1c2128;border-radius:5px;
1846
+ color:#8b949e;font-size:11px}
1847
+ #deltaBox{background:#1c2128;border:1px solid #30363d;border-radius:6px;padding:9px;
1848
+ margin:9px 0;font-size:11px;line-height:1.7;display:none}
1849
+ .k{display:inline-block;width:9px;height:9px;border:2px solid #f0883e;border-radius:2px;
1850
+ vertical-align:-1px;margin-right:4px}
1851
+ .k2{display:inline-block;width:11px;border-top:2px dashed #a371f7;vertical-align:4px;
1852
+ margin-right:4px}
1853
+ .lg{font-size:10.5px;color:#8b949e;line-height:1.9;margin-top:4px}
1854
+ #info{position:fixed;right:14px;top:14px;background:#161b22ee;border:1px solid #30363d;
1855
+ border-radius:7px;padding:11px 13px;max-width:360px;font-size:12px;display:none;line-height:1.6}
1856
+ #info table{border-collapse:collapse;width:100%;margin-top:7px}
1857
+ #info td{padding:3px 0;vertical-align:top;font-size:11.5px}
1858
+ #info td:first-child{color:#8b949e;width:88px;white-space:nowrap}
1859
+ #info .ih{font-size:13px;font-weight:600;color:#58a6ff;border-bottom:1px solid #30363d;
1860
+ padding-bottom:6px}
1861
+ #info .mut{color:#6e7681;font-size:10.5px}
1862
+ #info .x{float:right;cursor:pointer;color:#8b949e;font-size:16px;line-height:1;padding:0 2px}
1863
+ #info .x:hover{color:#fff}
1864
+ #info .hint{color:#6e7681;font-size:10px;margin-top:8px;border-top:1px solid #30363d;padding-top:6px}
1865
+ .dirbar{display:flex;gap:4px;margin:9px 0 6px;border-top:1px solid #30363d;padding-top:9px}
1866
+ .dirb{flex:1;margin:0;font-size:10.5px;padding:4px 2px}
1867
+ .lks{max-height:250px;overflow-y:auto;margin:0 -3px}
1868
+ .lk{display:flex;align-items:center;gap:4px;padding:3px 5px;border-radius:4px;
1869
+ cursor:pointer;font-size:11.5px;white-space:nowrap;overflow:hidden}
1870
+ .lk:hover{background:#21262d} .lk .ar{color:#58a6ff;flex:none;width:11px}
1871
+ .lk .rel{margin-left:auto;color:#6e7681;font-size:9.5px;flex:none;padding-left:6px}
1872
+ .lk .sig{white-space:normal;font-family:ui-monospace,Menlo,monospace;font-size:10.5px;
1873
+ line-height:1.5;color:#8b949e} .lk .sig b{color:#79c0ff;font-weight:600}
1874
+ </style>
1875
+ <div id="side">
1876
+ <h1 id="title">\u2026</h1>
1877
+ <div id="ref"></div>
1878
+ <div class="sub" style="margin-top:6px">Color = domain \xB7 Saturation = hexagonal position</div>
1879
+ <div id="deltaBox"><div id="delta"></div></div>
1880
+ <div id="stat"></div>
1881
+
1882
+ <h2>View</h2>
1883
+ <button id="pcore">Core only</button><button id="pall">All</button>
1884
+ <label style="margin-top:9px"><input type=checkbox id="deltaOnly">
1885
+ <b style="color:#f0883e">Branch delta only</b></label>
1886
+ <div class="sub" style="margin:4px 0 0"><div class="d">independent of the component filter \u2014
1887
+ combine it with <b>Core only</b></div></div>
1888
+
1889
+ <h2>Layout</h2>
1890
+ <button id="bhex">Hexagonal</button><button id="bgrid">Grid</button>
1891
+ <div class="sub" style="margin:8px 0 0">
1892
+ <b>Hexagonal</b><div class="d">concentric rings per domain \u2014
1893
+ Entity \u2192 Service \u2192 UseCase \u2192 Port \u2192 DTO\xB7VO</div>
1894
+ <b>Grid</b><div class="d">list-like. both are fixed coordinates, so branches stay comparable.</div></div>
1895
+ <div style="margin-top:10px;border-top:1px solid #30363d;padding-top:10px">
1896
+ <button id="borg" style="width:100%;margin:0">\u26A1 Organic re-layout</button></div>
1897
+ <div class="sub" style="margin:6px 0 0"><div class="d">re-packs on every filter change.
1898
+ On <b>Hexagonal</b> it moves the <b>domain boxes</b> \u2014 the concentric rings inside each
1899
+ domain stay intact. On <b>Grid</b> it is a free-form force layout over the nodes.
1900
+ Off = keep the fixed coordinates above.</div></div>
1901
+
1902
+ <h2>Edges</h2>
1903
+ <label><input type=checkbox id="showId">Show identifier edges</label>
1904
+ <label><input type=checkbox id="crossOnly">Cross-domain edges only</label>
1905
+ <div class="sub" style="margin:6px 0 0"><b>Identifier edges</b><div class="d">references like
1906
+ FamilyId\xB7UserId \u2014 a shared coordinate system, not coupling. Hidden by default.</div></div>
1907
+
1908
+ <h2>Edge types</h2>
1909
+ <label><input type=checkbox class=rf value="DEPENDS_ON" checked>
1910
+ <i></i>DEPENDS_ON <span class="mut">straight \xB7 open V</span><span class="n" id="cDEPENDS_ON"></span></label>
1911
+ <label><input type=checkbox class=rf value="IMPLEMENTS" checked>
1912
+ <i class="d"></i>IMPLEMENTS <span class="mut">curved \xB7 long dash</span><span class="n" id="cIMPLEMENTS"></span></label>
1913
+ <label><input type=checkbox class=rf value="EXTENDS" checked>
1914
+ <i class="d"></i>EXTENDS <span class="mut">curved \xB7 short dash</span><span class="n" id="cEXTENDS"></span></label>
1915
+ <div style="margin-top:10px;border-top:1px solid #30363d;padding-top:10px">
1916
+ <button id="bviol" style="width:100%;margin:0">\u26A0 Violations only</button></div>
1917
+ <div class="sub" style="margin:6px 0 0"><div class="d">combines with the filters above \u2014
1918
+ keeps only the nodes and edges a violation runs through. Turns on <b>Organic</b>
1919
+ while active; switching it off restores exactly what you had.</div></div>
1920
+ <div class="lg">
1921
+ <b style="color:#f85149">Design violation</b> \u2014 thick red<span class="n" id="cviol"></span><br>
1922
+ \xB7 cross-domain Entity access<br>\xB7 Entity leaked into a contract<br>
1923
+ \xB7 inbound adapter touching an Entity<br>\xB7 application \u2192 adapter back-reference<br>
1924
+ <span style="color:#f0883e">\u25A0</span> added &nbsp;<span style="color:#a371f7">\u25A0</span> modified
1925
+ </div>
1926
+
1927
+ <h2>Components</h2><div id="comps"></div>
1928
+ <h2>Domains</h2>
1929
+ <button id="allDomOn">All</button><button id="allDomOff">None</button>
1930
+ <div id="doms"></div>
1931
+ </div>
1932
+ <div id="cy"></div><div id="info"></div>
1933
+ <script src="/client.js"></script>
1934
+ `;
1935
+
1936
+ // src/view/server.ts
1937
+ var HERE = dirname(new URL(import.meta.url).pathname);
1938
+ async function clientBundle() {
1939
+ const prebuilt = join5(HERE, "client.js");
1940
+ const { existsSync: existsSync2, readFileSync: readFileSync4 } = await import("fs");
1941
+ if (existsSync2(prebuilt)) return readFileSync4(prebuilt, "utf8");
1942
+ const esbuild = await import("./main-CUYP5EVX.js");
1943
+ const r = await esbuild.build({
1944
+ entryPoints: [join5(HERE, "client.ts")],
1945
+ bundle: true,
1946
+ format: "iife",
1947
+ target: "es2022",
1948
+ minify: true,
1949
+ write: false,
1950
+ logLevel: "silent"
1951
+ });
1952
+ return r.outputFiles?.[0]?.text ?? "";
1953
+ }
1954
+ async function serveHttp(opt) {
1955
+ const client = await clientBundle();
1956
+ const server = createServer((req, res) => {
1957
+ const url = req.url ?? "/";
1958
+ if (url === "/" || url.startsWith("/?")) {
1959
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
1960
+ res.end(shell());
1961
+ return;
1962
+ }
1963
+ if (url === "/client.js") {
1964
+ res.writeHead(200, { "content-type": "text/javascript; charset=utf-8" });
1965
+ res.end(client);
1966
+ return;
1967
+ }
1968
+ if (url === "/api/graph") {
1969
+ const g = opt.graph();
1970
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1971
+ res.end(JSON.stringify(g));
1972
+ return;
1973
+ }
1974
+ res.writeHead(404).end("not found");
1975
+ });
1976
+ await new Promise((ok2) => server.listen(opt.port, ok2));
1977
+ return `http://localhost:${opt.port}`;
1978
+ }
1979
+
1980
+ // src/cli.ts
1981
+ var HERE2 = dirname2(new URL(import.meta.url).pathname);
1982
+ var USAGE = `hexwright \u2014 design graph for hexagonal codebases
1983
+
1984
+ hexwright extract --repo <path> [options] print graph summary
1985
+ hexwright check --repo <path> [options] exit 1 on violations
1986
+ hexwright mcp --repo <path> [options] serve MCP over stdio
1987
+ hexwright serve --repo <path> [options] web UI (and MCP with --mcp)
1988
+ hexwright render --repo <path> [options] write an SVG (and PNG) for a PR
1989
+
1990
+ Options
1991
+ --repo <path> target repository (required)
1992
+ --src <path> source root, relative to repo [auto: Gradle module]
1993
+ --base <ref> branch/commit to diff against [none]
1994
+ --project <name> name shown in outputs [repo dir name]
1995
+ --base-package <pkg> package prefix before the domain segment [auto]
1996
+ --profile <file> architecture profile [hexagonal-kotlin.yml]
1997
+ --out <dir> write graph.json / graph.tsv here
1998
+ --json print graph as JSON to stdout
1999
+ --port <n> web UI port [7800]
2000
+ --mcp also serve MCP over stdio (with serve)
2001
+
2002
+ check only
2003
+ --scope <s> all | delta [all]
2004
+ delta = fail only on violations this branch introduced;
2005
+ requires --base. Lets you gate a repository that already
2006
+ has violations without fixing them all first.
2007
+
2008
+ render only
2009
+ --view <v> delta | impact | core | all | domain:<name> [delta]
2010
+ --layout <l> organic | grid | hex
2011
+ [organic for delta\xB7domain, hex for core\xB7all]
2012
+ --image <file> output path; .svg, or .png to rasterize [graph.svg]
2013
+ --identifiers include identifier-only references (FamilyId\xB7UserId)
2014
+ `;
2015
+ function main() {
2016
+ const { values: v, positionals } = parseArgs({
2017
+ allowPositionals: true,
2018
+ options: {
2019
+ repo: { type: "string" },
2020
+ src: { type: "string" },
2021
+ base: { type: "string" },
2022
+ project: { type: "string" },
2023
+ "base-package": { type: "string" },
2024
+ profile: { type: "string" },
2025
+ out: { type: "string" },
2026
+ json: { type: "boolean", default: false },
2027
+ port: { type: "string", default: "7800" },
2028
+ mcp: { type: "boolean", default: false },
2029
+ scope: { type: "string", default: "all" },
2030
+ view: { type: "string", default: "delta" },
2031
+ layout: { type: "string" },
2032
+ // no default — chosen to suit the view
2033
+ image: { type: "string" },
2034
+ identifiers: { type: "boolean", default: false },
2035
+ help: { type: "boolean", short: "h", default: false }
2036
+ }
2037
+ });
2038
+ const cmd = positionals[0] ?? "extract";
2039
+ if (v.help || !v.repo || !["extract", "check", "mcp", "serve", "render"].includes(cmd)) {
2040
+ process.stdout.write(USAGE);
2041
+ return v.help ? 0 : 1;
2042
+ }
2043
+ const repo = resolve(v.repo);
2044
+ const project = v.project ?? repo.split("/").filter(Boolean).pop() ?? "project";
2045
+ const profilePath = v.profile ?? join6(HERE2, "..", "profiles", "hexagonal-kotlin.yml");
2046
+ const profile = loadProfile(profilePath, v["base-package"] ? { base: v["base-package"] } : {});
2047
+ let det;
2048
+ try {
2049
+ det = detectSource(repo, v.src);
2050
+ } catch (e) {
2051
+ process.stderr.write(`${e.message}
2052
+ `);
2053
+ return 1;
2054
+ }
2055
+ if (det.subprojects > 0) {
2056
+ process.stderr.write(
2057
+ `warning: settings.gradle declares ${det.subprojects} subproject(s). hexwright assumes a single module and analyzes only ${det.srcRel}
2058
+ `
2059
+ );
2060
+ }
2061
+ const source = new GraphSource({
2062
+ repo,
2063
+ srcAbs: det.srcAbs,
2064
+ srcRel: det.srcRel,
2065
+ profile,
2066
+ profilePath,
2067
+ project,
2068
+ ...v.base ? { base: v.base } : {}
2069
+ });
2070
+ let graph;
2071
+ try {
2072
+ graph = source.graph();
2073
+ } catch (e) {
2074
+ process.stderr.write(`${e.message}
2075
+ `);
2076
+ return 1;
2077
+ }
2078
+ const d = source.delta();
2079
+ const summary = d ? deltaSummary(d, graph) : "";
2080
+ const violations = graph.edges.filter((e) => e.violation);
2081
+ const deltaScope = v.scope === "delta";
2082
+ if (deltaScope && !v.base) {
2083
+ process.stderr.write("--scope delta needs --base <ref> to know what is new\n");
2084
+ return 1;
2085
+ }
2086
+ const gated = deltaScope ? violations.filter((e) => e.newViolation) : violations;
2087
+ if (cmd === "serve") {
2088
+ void serveHttp({ port: Number(v.port), graph: () => source.graph() }).then((url) => {
2089
+ process.stderr.write(
2090
+ `hexwright \u2014 ${project} @ ${graph.ref}
2091
+ ${graph.nodes.length} nodes \xB7 ${graph.edges.length} edges \xB7 ${violations.length} violations${v.base ? ` \xB7 delta vs ${v.base}` : ""}
2092
+ web ${url}
2093
+ `
2094
+ );
2095
+ if (v.mcp) void serveStdio(() => source.query(), project);
2096
+ });
2097
+ return -1;
2098
+ }
2099
+ if (cmd === "render") {
2100
+ const out = v.image ?? "graph.svg";
2101
+ let sel;
2102
+ try {
2103
+ sel = select(graph, v.view, v.identifiers);
2104
+ } catch (e) {
2105
+ process.stderr.write(`${e.message}
2106
+ `);
2107
+ return 1;
2108
+ }
2109
+ const layout = v.layout === "hex" || v.layout === "grid" || v.layout === "organic" ? v.layout : v.view === "core" || v.view === "all" ? "hex" : "organic";
2110
+ const svg = renderSvg(sel.graph, {
2111
+ layout,
2112
+ viewLabel: sel.label,
2113
+ showIdentifiers: v.identifiers
2114
+ });
2115
+ const dir = dirname2(resolve(out));
2116
+ mkdirSync(dir, { recursive: true });
2117
+ const svgPath = out.endsWith(".png") ? `${out.slice(0, -4)}.svg` : out;
2118
+ writeFileSync(svgPath, svg);
2119
+ process.stdout.write(
2120
+ `${project} @ ${graph.ref}
2121
+ view ${sel.label}
2122
+ drew ${sel.graph.nodes.length} types \xB7 ${sel.graph.edges.length} relations \xB7 ${sel.graph.edges.filter((e) => e.violation).length} violations
2123
+ wrote ${svgPath} (${Math.round(svg.length / 1024)} KB)
2124
+ `
2125
+ );
2126
+ if (out.endsWith(".png")) {
2127
+ const px = toPng(svg, out);
2128
+ process.stdout.write(
2129
+ px ? ` wrote ${out} (${px})
2130
+ ` : ` note PNG skipped \u2014 install @resvg/resvg-js for rasterizing
2131
+ `
2132
+ );
2133
+ }
2134
+ return 0;
2135
+ }
2136
+ if (cmd === "mcp") {
2137
+ process.stderr.write(
2138
+ `hexwright mcp \u2014 ${project} @ ${graph.ref}
2139
+ ${graph.nodes.length} nodes \xB7 ${graph.edges.length} edges \xB7 ${violations.length} violations${v.base ? ` \xB7 delta vs ${v.base}` : ""}
2140
+ `
2141
+ );
2142
+ void serveStdio(() => source.query(), project);
2143
+ return -1;
2144
+ }
2145
+ if (v.json) {
2146
+ process.stdout.write(`${JSON.stringify(graph, null, 1)}
2147
+ `);
2148
+ } else {
2149
+ const byComp = /* @__PURE__ */ new Map();
2150
+ for (const n of graph.nodes) byComp.set(n.component, (byComp.get(n.component) ?? 0) + 1);
2151
+ const byRel = /* @__PURE__ */ new Map();
2152
+ for (const e of graph.edges) byRel.set(e.rel, (byRel.get(e.rel) ?? 0) + 1);
2153
+ process.stdout.write(`${project} @ ${graph.ref}
2154
+ `);
2155
+ process.stdout.write(
2156
+ ` nodes ${graph.nodes.length} ${[...byComp].sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k} ${n}`).join(" \xB7 ")}
2157
+ `
2158
+ );
2159
+ process.stdout.write(
2160
+ ` edges ${graph.edges.length} ${[...byRel].map(([k, n]) => `${k} ${n}`).join(" \xB7 ")}
2161
+ `
2162
+ );
2163
+ process.stdout.write(` domains ${new Set(graph.nodes.map((n) => n.domain)).size}
2164
+ `);
2165
+ process.stdout.write(` source ${det.srcRel} (${det.how})
2166
+ `);
2167
+ if (summary) process.stdout.write(`
2168
+ delta vs ${v.base}
2169
+ ${indent(summary)}
2170
+ `);
2171
+ const shown = cmd === "check" ? gated : violations;
2172
+ if (shown.length) {
2173
+ const carried = violations.length - gated.length;
2174
+ process.stdout.write(
2175
+ `
2176
+ violations ${shown.length}` + (deltaScope ? ` new (${carried} pre-existing, not gated)
2177
+ ` : "\n")
2178
+ );
2179
+ const byId = new Map(graph.nodes.map((n) => [n.id, n]));
2180
+ for (const e of shown) {
2181
+ const s = byId.get(e.src);
2182
+ const d2 = byId.get(e.dst);
2183
+ process.stdout.write(
2184
+ ` ${s?.name} \u2192 ${d2?.domain}.${d2?.name} ${e.violation}
2185
+ ${s?.file}
2186
+ `
2187
+ );
2188
+ }
2189
+ } else if (deltaScope) {
2190
+ process.stdout.write(`
2191
+ violations 0 new (${violations.length} pre-existing, not gated)
2192
+ `);
2193
+ } else {
2194
+ process.stdout.write("\nviolations 0\n");
2195
+ }
2196
+ }
2197
+ if (v.out) {
2198
+ mkdirSync(v.out, { recursive: true });
2199
+ writeFileSync(join6(v.out, "graph.json"), `${JSON.stringify(graph, null, 1)}
2200
+ `);
2201
+ writeFileSync(join6(v.out, "graph.tsv"), toTsv(graph));
2202
+ process.stderr.write(`
2203
+ wrote ${join6(v.out, "graph.json")} \xB7 graph.tsv
2204
+ `);
2205
+ }
2206
+ return cmd === "check" && gated.length ? 1 : 0;
2207
+ }
2208
+ function toPng(svg, out) {
2209
+ try {
2210
+ const req = createRequire(import.meta.url);
2211
+ const { Resvg } = req("@resvg/resvg-js");
2212
+ const img = new Resvg(svg, {
2213
+ font: { loadSystemFonts: true, defaultFontFamily: "DejaVu Sans" }
2214
+ }).render();
2215
+ writeFileSync(out, img.asPng());
2216
+ return `${img.width}\xD7${img.height}`;
2217
+ } catch {
2218
+ return void 0;
2219
+ }
2220
+ }
2221
+ var indent = (s) => s.split("\n").map((l) => ` ${l}`).join("\n");
2222
+ var code = main();
2223
+ if (code >= 0) process.exitCode = code;