mellos-mapping 0.12.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/watch.mjs ADDED
@@ -0,0 +1,1884 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
3
+
4
+ // src/watch/watch.ts
5
+ import { realpathSync, statSync } from "node:fs";
6
+ import { join as join2 } from "node:path";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+
9
+ // src/domain/types.ts
10
+ var ok = (value) => ({ ok: true, value });
11
+ var err = (error) => ({ ok: false, error });
12
+ var ID_RULE = /^[a-z0-9][a-z0-9-]{0,63}$/;
13
+ var ID_RULE_TEXT = "lowercase letters, digits and dashes, starting with a letter or digit, 1-64 chars";
14
+ function makeNodeId(raw) {
15
+ return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
16
+ }
17
+ function makeLayerId(raw) {
18
+ return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
19
+ }
20
+ function makeGroupId(raw) {
21
+ return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
22
+ }
23
+ function makeLaneId(raw) {
24
+ return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
25
+ }
26
+ function makeNodeKind(raw) {
27
+ return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
28
+ }
29
+ function makeSubmapRef(raw) {
30
+ return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
31
+ }
32
+ var MAP_KINDS = ["dev", "architecture", "dataflow", "behavior-tree", "sequence"];
33
+ function makeMapKind(raw) {
34
+ return MAP_KINDS.includes(raw) ? ok(raw) : err({ kind: "invalid-map-kind", raw });
35
+ }
36
+ var NODE_STATUSES = ["planned", "in-progress", "done", "regressed"];
37
+ function makeNodeStatus(raw) {
38
+ return NODE_STATUSES.includes(raw) ? ok(raw) : err({ kind: "invalid-status", raw });
39
+ }
40
+ var EMPTY_MAP = { layers: [], groups: [], lanes: [], nodes: [], edges: [] };
41
+ function describeMapError(e) {
42
+ switch (e.kind) {
43
+ case "invalid-id":
44
+ return `invalid id "${e.raw}" (rule: ${e.rule})`;
45
+ case "invalid-status":
46
+ return `invalid status "${e.raw}" (expected: ${NODE_STATUSES.join(" | ")})`;
47
+ case "duplicate-layer":
48
+ return `layer "${e.id}" already exists`;
49
+ case "duplicate-rank":
50
+ return `rank ${e.rank} is already taken by layer "${e.existing}"`;
51
+ case "duplicate-node":
52
+ return `node "${e.id}" already exists`;
53
+ case "unknown-layer":
54
+ return `layer "${e.id}" does not exist`;
55
+ case "unknown-node":
56
+ return `node "${e.id}" does not exist`;
57
+ case "duplicate-edge":
58
+ return `edge ${e.from} -> ${e.to} already exists`;
59
+ case "unknown-edge":
60
+ return `edge ${e.from} -> ${e.to} does not exist`;
61
+ case "self-edge":
62
+ return `node "${e.id}" cannot depend on itself`;
63
+ case "duplicate-group":
64
+ return `group "${e.id}" already exists`;
65
+ case "unknown-group":
66
+ return `group "${e.id}" does not exist`;
67
+ case "invalid-map-kind":
68
+ return `invalid map kind "${e.raw}" (expected: ${MAP_KINDS.join(" | ")})`;
69
+ case "duplicate-lane":
70
+ return `lane "${e.id}" already exists`;
71
+ case "unknown-lane":
72
+ return `lane "${e.id}" does not exist`;
73
+ case "group-layer-mismatch":
74
+ return `node "${e.node}" (layer ${e.nodeLayer}) cannot join group "${e.group}" (layer ${e.groupLayer}); groups cluster nodes within one band`;
75
+ case "layer-not-empty":
76
+ return `layer "${e.id}" still holds node "${e.occupant}"; move or remove its nodes first`;
77
+ case "layer-holds-group":
78
+ return `layer "${e.id}" still holds group "${e.occupant}"; remove its groups first`;
79
+ case "edge-not-downward":
80
+ return `edge ${e.from} (rank ${e.fromRank}) -> ${e.to} (rank ${e.toRank}) is not strictly downward; dependencies may only point to a lower layer`;
81
+ }
82
+ }
83
+
84
+ // src/domain/ops.ts
85
+ function findLayer(map, id) {
86
+ return map.layers.find((l) => l.id === id);
87
+ }
88
+ function findNode(map, id) {
89
+ return map.nodes.find((n) => n.id === id);
90
+ }
91
+ function findGroup(map, id) {
92
+ return map.groups.find((g) => g.id === id);
93
+ }
94
+ function checkMembership(map, node, nodeLayer, group) {
95
+ const g = findGroup(map, group);
96
+ if (!g) return { kind: "unknown-group", id: group };
97
+ if (g.layer !== nodeLayer)
98
+ return { kind: "group-layer-mismatch", node, nodeLayer, group, groupLayer: g.layer };
99
+ return void 0;
100
+ }
101
+ function hasEdge(map, from, to) {
102
+ return map.edges.some((e) => e.from === from && e.to === to);
103
+ }
104
+ function setTitle(map, title) {
105
+ return { ...map, title };
106
+ }
107
+ function setKind(map, kind) {
108
+ return { ...map, kind };
109
+ }
110
+ function findLane(map, id) {
111
+ return map.lanes.find((l) => l.id === id);
112
+ }
113
+ function declareLane(map, input) {
114
+ if (findLane(map, input.id)) return err({ kind: "duplicate-lane", id: input.id });
115
+ return ok({ ...map, lanes: [...map.lanes, { id: input.id, label: input.label }] });
116
+ }
117
+ function declareLayer(map, input) {
118
+ if (findLayer(map, input.id)) return err({ kind: "duplicate-layer", id: input.id });
119
+ const rankHolder = map.layers.find((l) => l.rank === input.rank);
120
+ if (rankHolder) return err({ kind: "duplicate-rank", rank: input.rank, existing: rankHolder.id });
121
+ return ok({ ...map, layers: [...map.layers, { id: input.id, name: input.name, rank: input.rank }] });
122
+ }
123
+ function declareGroup(map, input) {
124
+ if (findGroup(map, input.id)) return err({ kind: "duplicate-group", id: input.id });
125
+ if (!findLayer(map, input.layer)) return err({ kind: "unknown-layer", id: input.layer });
126
+ return ok({ ...map, groups: [...map.groups, { id: input.id, label: input.label, layer: input.layer }] });
127
+ }
128
+ function aggregateStatus(nodes) {
129
+ if (nodes.some((n) => n.status === "regressed")) return "regressed";
130
+ if (nodes.some((n) => n.status === "in-progress")) return "in-progress";
131
+ if (nodes.length > 0 && nodes.every((n) => n.status === "done")) return "done";
132
+ return "planned";
133
+ }
134
+ function groupStatus(map, id) {
135
+ return aggregateStatus(map.nodes.filter((n) => n.group === id));
136
+ }
137
+ function mapStatus(map) {
138
+ return aggregateStatus(map.nodes);
139
+ }
140
+ function declareNode(map, input) {
141
+ if (findNode(map, input.id)) return err({ kind: "duplicate-node", id: input.id });
142
+ if (!findLayer(map, input.layer)) return err({ kind: "unknown-layer", id: input.layer });
143
+ if (input.group !== void 0) {
144
+ const bad = checkMembership(map, input.id, input.layer, input.group);
145
+ if (bad) return err(bad);
146
+ }
147
+ if (input.lane !== void 0 && !findLane(map, input.lane)) return err({ kind: "unknown-lane", id: input.lane });
148
+ const node = {
149
+ id: input.id,
150
+ label: input.label,
151
+ layer: input.layer,
152
+ status: input.status ?? "planned",
153
+ ...input.detail !== void 0 ? { detail: input.detail } : {},
154
+ ...input.group !== void 0 ? { group: input.group } : {},
155
+ ...input.kind !== void 0 ? { kind: input.kind } : {},
156
+ ...input.lane !== void 0 ? { lane: input.lane } : {},
157
+ ...input.submap !== void 0 ? { submap: input.submap } : {}
158
+ };
159
+ return ok({ ...map, nodes: [...map.nodes, node] });
160
+ }
161
+ function linkNodes(map, from, to, label) {
162
+ if (from === to) return err({ kind: "self-edge", id: from });
163
+ const fromNode = findNode(map, from);
164
+ if (!fromNode) return err({ kind: "unknown-node", id: from });
165
+ const toNode = findNode(map, to);
166
+ if (!toNode) return err({ kind: "unknown-node", id: to });
167
+ if (hasEdge(map, from, to)) return err({ kind: "duplicate-edge", from, to });
168
+ const fromRank = findLayer(map, fromNode.layer).rank;
169
+ const toRank = findLayer(map, toNode.layer).rank;
170
+ if (fromRank <= toRank) return err({ kind: "edge-not-downward", from, fromRank, to, toRank });
171
+ return ok({ ...map, edges: [...map.edges, { from, to, ...label !== void 0 ? { label } : {} }] });
172
+ }
173
+ function updateNode(map, input) {
174
+ const node = findNode(map, input.id);
175
+ if (!node) return err({ kind: "unknown-node", id: input.id });
176
+ if (input.group !== void 0 && input.group !== null) {
177
+ const bad = checkMembership(map, node.id, node.layer, input.group);
178
+ if (bad) return err(bad);
179
+ }
180
+ if (input.lane !== void 0 && input.lane !== null && !findLane(map, input.lane)) {
181
+ return err({ kind: "unknown-lane", id: input.lane });
182
+ }
183
+ const { group: currentGroup, kind: currentKind, lane: currentLane, submap: currentSubmap, ...bare } = node;
184
+ const nextGroup = input.group === void 0 ? currentGroup : input.group === null ? void 0 : input.group;
185
+ const nextKind = input.kind === void 0 ? currentKind : input.kind === null ? void 0 : input.kind;
186
+ const nextLane = input.lane === void 0 ? currentLane : input.lane === null ? void 0 : input.lane;
187
+ const nextSubmap = input.submap === void 0 ? currentSubmap : input.submap === null ? void 0 : input.submap;
188
+ const updated = {
189
+ ...bare,
190
+ ...nextGroup !== void 0 ? { group: nextGroup } : {},
191
+ ...nextKind !== void 0 ? { kind: nextKind } : {},
192
+ ...nextLane !== void 0 ? { lane: nextLane } : {},
193
+ ...nextSubmap !== void 0 ? { submap: nextSubmap } : {},
194
+ ...input.status !== void 0 ? { status: input.status } : {},
195
+ ...input.label !== void 0 ? { label: input.label } : {},
196
+ ...input.evidence !== void 0 ? { evidence: input.evidence } : {},
197
+ ...input.detail !== void 0 ? { detail: input.detail } : {}
198
+ };
199
+ return ok({ ...map, nodes: map.nodes.map((n) => n.id === input.id ? updated : n) });
200
+ }
201
+
202
+ // src/render/render.ts
203
+ var ZOOM_MIN = -4;
204
+ var ZOOM_MAX = 1;
205
+ var ZOOM_DEFAULT = 0;
206
+ function clampZoom(n) {
207
+ return Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, Math.round(n)));
208
+ }
209
+ function zoomLabel(zoom) {
210
+ switch (zoom) {
211
+ case 1:
212
+ return "detail";
213
+ case 0:
214
+ return "100%";
215
+ case -1:
216
+ return "85%";
217
+ case -2:
218
+ return "70%";
219
+ case -3:
220
+ return "55%";
221
+ case -4:
222
+ return "overview";
223
+ }
224
+ }
225
+ var WIDE_RANGES = [
226
+ [4352, 4447],
227
+ // Hangul Jamo
228
+ [11904, 42191],
229
+ // CJK radicals .. Yi (covers CJK Unified Ideographs)
230
+ [43360, 43391],
231
+ [44032, 55203],
232
+ // Hangul syllables
233
+ [63744, 64255],
234
+ // CJK compatibility ideographs
235
+ [65040, 65049],
236
+ [65072, 65135],
237
+ [65280, 65376],
238
+ // fullwidth forms
239
+ [65504, 65510],
240
+ [131072, 262141]
241
+ // CJK extension planes
242
+ ];
243
+ function charWidth(cp) {
244
+ for (const [lo, hi] of WIDE_RANGES) {
245
+ if (cp >= lo && cp <= hi) return 2;
246
+ }
247
+ return 1;
248
+ }
249
+ function displayWidth(text) {
250
+ let w = 0;
251
+ for (const ch of text) w += charWidth(ch.codePointAt(0));
252
+ return w;
253
+ }
254
+ function fitWidth(s, width) {
255
+ if (displayWidth(s) <= width) return s;
256
+ let out = "";
257
+ let w = 0;
258
+ for (const ch of s) {
259
+ const cw = displayWidth(ch);
260
+ if (w + cw > width - 1) break;
261
+ out += ch;
262
+ w += cw;
263
+ }
264
+ return out + "\u2026";
265
+ }
266
+ function wrapWidth(s, width) {
267
+ const lines = [];
268
+ let line = "";
269
+ let w = 0;
270
+ for (const ch of s.replace(/\r/g, "")) {
271
+ if (ch === "\n") {
272
+ lines.push(line);
273
+ line = "";
274
+ w = 0;
275
+ continue;
276
+ }
277
+ const cw = displayWidth(ch);
278
+ if (w + cw > width) {
279
+ lines.push(line);
280
+ line = "";
281
+ w = 0;
282
+ }
283
+ line += ch;
284
+ w += cw;
285
+ }
286
+ if (line !== "") lines.push(line);
287
+ return lines;
288
+ }
289
+ var UP = 1;
290
+ var DOWN = 2;
291
+ var LEFT = 4;
292
+ var RIGHT = 8;
293
+ var LIGHT_BY_MASK = {
294
+ [UP]: "\u2502",
295
+ [DOWN]: "\u2502",
296
+ [LEFT]: "\u2500",
297
+ [RIGHT]: "\u2500",
298
+ [UP | DOWN]: "\u2502",
299
+ [LEFT | RIGHT]: "\u2500",
300
+ [DOWN | RIGHT]: "\u250C",
301
+ [DOWN | LEFT]: "\u2510",
302
+ [UP | RIGHT]: "\u2514",
303
+ [UP | LEFT]: "\u2518",
304
+ [UP | DOWN | RIGHT]: "\u251C",
305
+ [UP | DOWN | LEFT]: "\u2524",
306
+ [DOWN | LEFT | RIGHT]: "\u252C",
307
+ [UP | LEFT | RIGHT]: "\u2534",
308
+ [UP | DOWN | LEFT | RIGHT]: "\u253C"
309
+ };
310
+ function maskChar(mask, heavyHorizontal, unicode) {
311
+ if (!unicode) {
312
+ const hasV = (mask & (UP | DOWN)) !== 0;
313
+ const hasH = (mask & (LEFT | RIGHT)) !== 0;
314
+ if (hasV && hasH) return "+";
315
+ return hasV ? "|" : "-";
316
+ }
317
+ if (heavyHorizontal) {
318
+ if (mask === (LEFT | RIGHT)) return "\u2501";
319
+ if (mask === (UP | DOWN | LEFT | RIGHT)) return "\u253F";
320
+ }
321
+ return LIGHT_BY_MASK[mask] ?? "\u253C";
322
+ }
323
+ var SGR = {
324
+ none: "",
325
+ dim: "2",
326
+ amber: "33",
327
+ green: "32",
328
+ red: "31",
329
+ faint: "90"
330
+ };
331
+ var ANSI_RESET = "\x1B[0m";
332
+ var BORDER_JUNCTION = {
333
+ "\u2500": { down: "\u252C", up: "\u2534" },
334
+ "\u254C": { down: "\u252C", up: "\u2534" },
335
+ "\u2501": { down: "\u252F", up: "\u2537" },
336
+ "-": { down: "+", up: "+" },
337
+ ".": { down: "+", up: "+" }
338
+ };
339
+ var Canvas = class {
340
+ rows = [];
341
+ cell(x, y) {
342
+ while (this.rows.length <= y) this.rows.push([]);
343
+ const row = this.rows[y];
344
+ while (row.length <= x) row.push({ mask: 0, heavyHorizontal: false, bright: false, style: "none", bold: false });
345
+ return row[x];
346
+ }
347
+ get height() {
348
+ return this.rows.length;
349
+ }
350
+ get width() {
351
+ return this.rows.reduce((max, row) => Math.max(max, row.length), 0);
352
+ }
353
+ /** Write literal text starting at (x, y). Returns the column just past it. */
354
+ text(x, y, s, style, bold = false) {
355
+ let cx = x;
356
+ for (const ch of s) {
357
+ const c = this.cell(cx, y);
358
+ c.literal = ch;
359
+ c.style = style;
360
+ c.bold = bold;
361
+ const w = charWidth(ch.codePointAt(0));
362
+ if (w === 2) {
363
+ const phantom = this.cell(cx + 1, y);
364
+ phantom.literal = "";
365
+ phantom.style = style;
366
+ }
367
+ cx += w;
368
+ }
369
+ return cx;
370
+ }
371
+ /** Merge a routed-line direction mask into (x, y). */
372
+ line(x, y, mask, heavyHorizontal = false, bright = false) {
373
+ const c = this.cell(x, y);
374
+ if (c.literal !== void 0) {
375
+ const junction = BORDER_JUNCTION[c.literal];
376
+ const replacement = mask & DOWN ? junction?.down : mask & UP ? junction?.up : void 0;
377
+ if (replacement !== void 0) c.literal = replacement;
378
+ return;
379
+ }
380
+ c.mask |= mask;
381
+ c.heavyHorizontal = c.heavyHorizontal || heavyHorizontal;
382
+ c.bright = c.bright || bright;
383
+ }
384
+ /**
385
+ * Emit terminal lines, optionally windowed to a viewport. Slicing happens
386
+ * at the cell level so ANSI codes reopen correctly inside the window and a
387
+ * CJK character cut in half at either edge degrades to a space instead of
388
+ * shifting the whole row. Routed wiring (mask cells) emits FAINT — the
389
+ * circuit board recedes, the boxes glow.
390
+ */
391
+ emit(opts, viewport) {
392
+ const vp = viewport ?? { x: 0, y: 0, width: this.width, height: this.height };
393
+ const out = [];
394
+ for (let y = vp.y; y < vp.y + vp.height; y++) {
395
+ const row = this.rows[y] ?? [];
396
+ let line = "";
397
+ let open = "";
398
+ const end = Math.min(vp.x + vp.width, row.length);
399
+ for (let x = Math.max(0, vp.x); x < end; x++) {
400
+ const c = row[x];
401
+ const isWire = c.literal === void 0 && c.mask !== 0;
402
+ let ch = c.literal !== void 0 ? c.literal : isWire ? maskChar(c.mask, c.heavyHorizontal, opts.unicode) : " ";
403
+ if (ch === "") {
404
+ if (x !== Math.max(0, vp.x)) continue;
405
+ ch = " ";
406
+ } else if (charWidth(ch.codePointAt(0)) === 2 && x + 1 >= vp.x + vp.width) {
407
+ ch = " ";
408
+ }
409
+ const params = ch === " " ? "" : isWire ? c.bright ? "1" : SGR.faint : [SGR[c.style], c.bold ? "1" : ""].filter(Boolean).join(";");
410
+ if (opts.color && params !== open) {
411
+ line += (open !== "" ? ANSI_RESET : "") + (params !== "" ? `\x1B[${params}m` : "");
412
+ open = params;
413
+ }
414
+ line += ch;
415
+ }
416
+ if (opts.color && open !== "") line += ANSI_RESET;
417
+ out.push(line.replace(/ +$/, ""));
418
+ }
419
+ return out;
420
+ }
421
+ };
422
+ function drawPath(canvas, points, bright = false) {
423
+ for (let i = 0; i + 1 < points.length; i++) {
424
+ const [x1, y1] = points[i];
425
+ const [x2, y2] = points[i + 1];
426
+ if (x1 === x2 && y1 === y2) continue;
427
+ if (x1 === x2) {
428
+ const [lo, hi] = y1 < y2 ? [y1, y2] : [y2, y1];
429
+ for (let yy = lo + 1; yy < hi; yy++) canvas.line(x1, yy, UP | DOWN, false, bright);
430
+ canvas.line(x1, y1, y2 > y1 ? DOWN : UP, false, bright);
431
+ canvas.line(x1, y2, y2 > y1 ? UP : DOWN, false, bright);
432
+ } else {
433
+ const [lo, hi] = x1 < x2 ? [x1, x2] : [x2, x1];
434
+ for (let xx = lo + 1; xx < hi; xx++) canvas.line(xx, y1, LEFT | RIGHT, false, bright);
435
+ canvas.line(x1, y1, x2 > x1 ? RIGHT : LEFT, false, bright);
436
+ canvas.line(x2, y1, x2 > x1 ? LEFT : RIGHT, false, bright);
437
+ }
438
+ }
439
+ }
440
+ var SPINNER_UNICODE = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
441
+ var SPINNER_ASCII = ["|", "/", "-", "\\"];
442
+ function skinFor(status, unicode) {
443
+ const style = status === "planned" ? "dim" : status === "in-progress" ? "amber" : status === "done" ? "green" : "red";
444
+ if (!unicode) {
445
+ return status === "planned" ? { h: ".", v: ":", corners: ["+", "+", "+", "+"], style } : { h: "-", v: "|", corners: ["+", "+", "+", "+"], style };
446
+ }
447
+ switch (status) {
448
+ case "planned":
449
+ return { h: "\u254C", v: "\u254E", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style };
450
+ case "in-progress":
451
+ return { h: "\u2500", v: "\u2502", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style };
452
+ case "done":
453
+ case "regressed":
454
+ return { h: "\u2501", v: "\u2503", corners: ["\u250F", "\u2513", "\u2517", "\u251B"], style };
455
+ }
456
+ }
457
+ function glyphFor(status, opts) {
458
+ const spinner = opts.unicode ? SPINNER_UNICODE : SPINNER_ASCII;
459
+ switch (status) {
460
+ case "planned":
461
+ return opts.unicode ? "\xB7" : ".";
462
+ case "in-progress":
463
+ return spinner[opts.spinnerFrame % spinner.length];
464
+ case "done":
465
+ return opts.unicode ? "\u25A0" : "#";
466
+ case "regressed":
467
+ return opts.unicode ? "\u2717" : "X";
468
+ }
469
+ }
470
+ var NODE_KIND_GLYPHS = {
471
+ selector: ["?", "?"],
472
+ sequence: ["\xBB", ">"],
473
+ parallel: ["\u2016", "="],
474
+ decorator: ["\u25CC", "o"],
475
+ condition: ["\u25C7", "c"],
476
+ action: ["\xB7", "."],
477
+ source: ["\u25CB", "o"],
478
+ transform: ["\u25D0", "%"],
479
+ sink: ["\u25CF", "*"],
480
+ service: ["\u25C6", "S"],
481
+ db: ["\u25A4", "D"],
482
+ queue: ["\u2263", "Q"],
483
+ ui: ["\u25A3", "U"]
484
+ };
485
+ function kindGlyph(kind, unicode) {
486
+ const pair = NODE_KIND_GLYPHS[kind];
487
+ return pair === void 0 ? void 0 : unicode ? pair[0] : pair[1];
488
+ }
489
+ function isNeutralKind(map) {
490
+ return map.kind !== void 0 && map.kind !== "dev";
491
+ }
492
+ function neutralSkin(unicode) {
493
+ return unicode ? { h: "\u2500", v: "\u2502", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style: "none" } : { h: "-", v: "|", corners: ["+", "+", "+", "+"], style: "none" };
494
+ }
495
+ var BOX_H = 3;
496
+ var BOX_GAP = 2;
497
+ var LEFT_MARGIN = 2;
498
+ function zoomGeometry(zoom) {
499
+ switch (zoom) {
500
+ case 1:
501
+ return { mode: "detail", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
502
+ case 0:
503
+ return { mode: "boxes", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
504
+ case -1:
505
+ return { mode: "boxes", scale: 0.85, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
506
+ case -2:
507
+ return { mode: "boxes", scale: 0.7, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: false };
508
+ case -3:
509
+ return { mode: "boxes", scale: 0.55, pad: 0, boxGap: 1, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
510
+ case -4:
511
+ return { mode: "constellation", scale: 0, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
512
+ }
513
+ }
514
+ var DETAIL_INNER_MIN = 22;
515
+ var DETAIL_INNER_MAX = 32;
516
+ var DETAIL_NOTE_ROWS = 3;
517
+ var LABEL_BUDGET_MIN = 4;
518
+ function boxSpec(node, geo, unicode, neutral) {
519
+ const glyph = node.kind !== void 0 ? kindGlyph(node.kind, unicode) : void 0;
520
+ const badge = node.submap !== void 0 ? unicode ? " \u229E" : " +" : "";
521
+ const badgeW = displayWidth(badge);
522
+ const text = !neutral && glyph !== void 0 ? `${glyph} ${node.label}` : node.label;
523
+ if (geo.mode === "constellation") {
524
+ return { w: 3, h: 1, label: "", pad: 0, borderless: true, extra: [] };
525
+ }
526
+ if (geo.mode === "detail") {
527
+ const innerW = Math.min(Math.max(displayWidth(text) + badgeW + 4, DETAIL_INNER_MIN), DETAIL_INNER_MAX);
528
+ const extra = [];
529
+ if (node.evidence !== void 0) extra.push({ text: fitWidth(` ${node.evidence}`, innerW), style: "faint" });
530
+ if (node.detail !== void 0) {
531
+ const wrapped = wrapWidth(node.detail, innerW - 2);
532
+ for (let i = 0; i < Math.min(wrapped.length, DETAIL_NOTE_ROWS); i++) {
533
+ const cut = i === DETAIL_NOTE_ROWS - 1 && wrapped.length > DETAIL_NOTE_ROWS;
534
+ extra.push({ text: ` ${cut ? fitWidth(wrapped[i] + "\u2026", innerW - 2) : wrapped[i]}`, style: "none" });
535
+ }
536
+ }
537
+ return {
538
+ w: innerW + 2,
539
+ h: BOX_H + extra.length,
540
+ label: fitWidth(text, innerW - 4 - badgeW) + badge,
541
+ pad: 1,
542
+ borderless: false,
543
+ extra
544
+ };
545
+ }
546
+ const budget = Math.max(LABEL_BUDGET_MIN, Math.ceil(displayWidth(text) * geo.scale));
547
+ const label = fitWidth(text, budget) + badge;
548
+ return {
549
+ w: displayWidth(label) + 4 + 2 * geo.pad,
550
+ h: BOX_H,
551
+ label,
552
+ pad: geo.pad,
553
+ borderless: false,
554
+ extra: []
555
+ };
556
+ }
557
+ function renderMapWindow(map, opts, viewport) {
558
+ const built = buildCanvas(map, opts);
559
+ return {
560
+ lines: built.canvas.emit(opts, viewport),
561
+ contentWidth: built.canvas.width,
562
+ contentHeight: built.canvas.height,
563
+ hits: built.hits
564
+ };
565
+ }
566
+ function aggregateMap(map) {
567
+ if (map.groups.length === 0) return void 0;
568
+ const representative = /* @__PURE__ */ new Map();
569
+ for (const n of map.nodes) representative.set(n.id, n.group ?? n.id);
570
+ const nodes = map.groups.map((g) => {
571
+ const members = map.nodes.filter((n) => n.group === g.id);
572
+ const done = members.filter((n) => n.status === "done").length;
573
+ return {
574
+ id: g.id,
575
+ // neutral kinds document structure, not progress — no member counts
576
+ label: isNeutralKind(map) ? g.label : `${g.label} ${done}/${members.length}`,
577
+ layer: g.layer,
578
+ status: groupStatus(map, g.id)
579
+ };
580
+ });
581
+ for (const n of map.nodes) if (n.group === void 0) nodes.push(n);
582
+ const seen = /* @__PURE__ */ new Set();
583
+ const edges = [];
584
+ for (const e of map.edges) {
585
+ const from = representative.get(e.from);
586
+ const to = representative.get(e.to);
587
+ if (from === to || seen.has(`${from}->${to}`)) continue;
588
+ seen.add(`${from}->${to}`);
589
+ edges.push({ from, to });
590
+ }
591
+ return {
592
+ ...map.title !== void 0 ? { title: map.title } : {},
593
+ ...map.kind !== void 0 ? { kind: map.kind } : {},
594
+ layers: map.layers,
595
+ groups: [],
596
+ lanes: map.lanes,
597
+ nodes,
598
+ edges
599
+ };
600
+ }
601
+ var AGGREGATE_GEO = {
602
+ mode: "boxes",
603
+ scale: 1,
604
+ pad: 0,
605
+ boxGap: 1,
606
+ breathe: 0,
607
+ titleGap: 0,
608
+ barGap: 1,
609
+ bandCounts: false
610
+ };
611
+ function flipForSequence(map) {
612
+ if (map.kind !== "sequence") return map;
613
+ return {
614
+ ...map,
615
+ layers: map.layers.map((l) => ({ ...l, rank: -l.rank })),
616
+ edges: map.edges.map((e) => ({ from: e.to, to: e.from, ...e.label !== void 0 ? { label: e.label } : {} }))
617
+ };
618
+ }
619
+ function buildCanvas(map, opts) {
620
+ const oriented = flipForSequence(map);
621
+ const plainGeo = zoomGeometry(opts.zoom ?? ZOOM_DEFAULT);
622
+ const aggregated = plainGeo.mode === "constellation" ? aggregateMap(oriented) : void 0;
623
+ return buildCanvasWith(aggregated ?? oriented, opts, aggregated !== void 0 ? AGGREGATE_GEO : plainGeo);
624
+ }
625
+ function buildCanvasWith(map, opts, geo) {
626
+ const canvas = new Canvas();
627
+ const neutral = isNeutralKind(map);
628
+ const bands = [...map.layers].sort((a, b) => b.rank - a.rank);
629
+ if (bands.length === 0) {
630
+ canvas.text(0, 0, map.title ?? "mellos mapping", "none", true);
631
+ canvas.text(0, 2, "(empty map \u2014 declare layers and nodes to begin)", "dim");
632
+ return { canvas, hits: [] };
633
+ }
634
+ const bandIndexOf = new Map(bands.map((l, i) => [l.id, i]));
635
+ const boxes = /* @__PURE__ */ new Map();
636
+ const bandBoxes = bands.map(() => []);
637
+ for (const node of map.nodes) {
638
+ const band = bandIndexOf.get(node.layer);
639
+ const box = { node, ...boxSpec(node, geo, opts.unicode, neutral), x: LEFT_MARGIN, y: 0 };
640
+ bandBoxes[band].push(box);
641
+ boxes.set(node.id, box);
642
+ }
643
+ const laneCount = map.lanes.length;
644
+ const laneX = [];
645
+ const laneW = [];
646
+ if (laneCount === 0) {
647
+ for (const row of bandBoxes) {
648
+ let x = LEFT_MARGIN;
649
+ for (const box of row) {
650
+ box.x = x;
651
+ x += box.w + geo.boxGap;
652
+ }
653
+ }
654
+ } else {
655
+ const laneGap = geo.boxGap + 2;
656
+ const laneIndexOf = new Map(map.lanes.map((l, i) => [l.id, i]));
657
+ const regions = laneCount + 1;
658
+ const grouped = bandBoxes.map((row) => {
659
+ const cells = Array.from({ length: regions }, () => []);
660
+ for (const box of row) {
661
+ const lane = box.node.lane;
662
+ cells[lane !== void 0 ? laneIndexOf.get(lane) : regions - 1].push(box);
663
+ }
664
+ return cells;
665
+ });
666
+ const regionW = Array.from({ length: regions }, () => 0);
667
+ for (const cells of grouped) {
668
+ for (let i = 0; i < regions; i++) {
669
+ const rowW = cells[i].reduce((sum, b, k) => sum + b.w + (k > 0 ? geo.boxGap : 0), 0);
670
+ regionW[i] = Math.max(regionW[i], rowW);
671
+ }
672
+ }
673
+ for (let i = 0; i < laneCount; i++) regionW[i] = Math.max(regionW[i], displayWidth(map.lanes[i].label) + 2);
674
+ let x0 = LEFT_MARGIN;
675
+ for (let i = 0; i < regions; i++) {
676
+ laneX.push(x0);
677
+ laneW.push(regionW[i]);
678
+ x0 += regionW[i] + laneGap;
679
+ }
680
+ for (const cells of grouped) {
681
+ for (let i = 0; i < regions; i++) {
682
+ let x = laneX[i];
683
+ for (const box of cells[i]) {
684
+ box.x = x;
685
+ x += box.w + geo.boxGap;
686
+ }
687
+ }
688
+ }
689
+ }
690
+ const bandLabel = bands.map((l, i) => {
691
+ const row = bandBoxes[i];
692
+ const done = row.filter((b) => b.node.status === "done").length;
693
+ return geo.bandCounts && row.length > 0 && !neutral ? ` ${l.name} ${done}/${row.length}` : ` ${l.name}`;
694
+ });
695
+ let contentWidth = LEFT_MARGIN;
696
+ for (const row of bandBoxes) {
697
+ const last = row[row.length - 1];
698
+ if (last) contentWidth = Math.max(contentWidth, last.x + last.w);
699
+ }
700
+ for (let i = 0; i < laneCount; i++) contentWidth = Math.max(contentWidth, laneX[i] + laneW[i]);
701
+ for (const label of bandLabel) contentWidth = Math.max(contentWidth, LEFT_MARGIN + displayWidth(label) + 7);
702
+ const routes = map.edges.map((e) => {
703
+ const fromBox = boxes.get(e.from);
704
+ const toBox = boxes.get(e.to);
705
+ return {
706
+ fromBox,
707
+ toBox,
708
+ fromBand: bandIndexOf.get(fromBox.node.layer),
709
+ toBand: bandIndexOf.get(toBox.node.layer)
710
+ };
711
+ });
712
+ const claimedColumns = /* @__PURE__ */ new Map();
713
+ const isFree = (box, x) => !(claimedColumns.get(box)?.has(x) ?? false);
714
+ const claim = (box, x) => {
715
+ let set = claimedColumns.get(box);
716
+ if (!set) claimedColumns.set(box, set = /* @__PURE__ */ new Set());
717
+ set.add(x);
718
+ return x;
719
+ };
720
+ const straightX = /* @__PURE__ */ new Map();
721
+ for (const r of routes) {
722
+ if (r.toBand - r.fromBand !== 1) continue;
723
+ const lo = Math.max(r.fromBox.x + 1, r.toBox.x + 1);
724
+ const hi = Math.min(r.fromBox.x + r.fromBox.w - 2, r.toBox.x + r.toBox.w - 2);
725
+ if (lo > hi) continue;
726
+ const mid = Math.floor((lo + hi) / 2);
727
+ for (let d = 0; d <= hi - lo && !straightX.has(r); d++) {
728
+ for (const x of d === 0 ? [mid] : [mid - d, mid + d]) {
729
+ if (x >= lo && x <= hi && isFree(r.fromBox, x) && isFree(r.toBox, x)) {
730
+ straightX.set(r, claim(r.toBox, claim(r.fromBox, x)));
731
+ break;
732
+ }
733
+ }
734
+ }
735
+ }
736
+ const bent = routes.filter((r) => !straightX.has(r));
737
+ const outgoing = /* @__PURE__ */ new Map();
738
+ const incoming = /* @__PURE__ */ new Map();
739
+ for (const r of bent) {
740
+ outgoing.set(r.fromBox, [...outgoing.get(r.fromBox) ?? [], r]);
741
+ incoming.set(r.toBox, [...incoming.get(r.toBox) ?? [], r]);
742
+ }
743
+ const freeSlot = (box, k, n) => {
744
+ const lo = box.x + 1;
745
+ const hi = box.x + box.w - 2;
746
+ const ideal = box.x + Math.min(box.w - 2, Math.max(1, Math.round((k + 1) * (box.w - 1) / (n + 1))));
747
+ for (let d = 0; d <= hi - lo; d++) {
748
+ for (const x of d === 0 ? [ideal] : [ideal - d, ideal + d]) {
749
+ if (x >= lo && x <= hi && isFree(box, x)) return claim(box, x);
750
+ }
751
+ }
752
+ return ideal;
753
+ };
754
+ const attach = /* @__PURE__ */ new Map();
755
+ for (const r of bent) {
756
+ const outs = outgoing.get(r.fromBox);
757
+ const ins = incoming.get(r.toBox);
758
+ attach.set(r, {
759
+ sx: freeSlot(r.fromBox, outs.indexOf(r), outs.length),
760
+ ex: freeSlot(r.toBox, ins.indexOf(r), ins.length)
761
+ });
762
+ }
763
+ const skipRoutes = bent.filter((r) => r.toBand - r.fromBand > 1);
764
+ const usedDescent = /* @__PURE__ */ new Set();
765
+ const descentX = /* @__PURE__ */ new Map();
766
+ let fallbackCount = 0;
767
+ const blockedByBox = (band, x) => bandBoxes[band].some((b) => x >= b.x && x <= b.x + b.w - 1);
768
+ for (const r of skipRoutes) {
769
+ const { ex } = attach.get(r);
770
+ let chosen;
771
+ for (let d = 0; d <= contentWidth && chosen === void 0; d++) {
772
+ for (const c of d === 0 ? [ex] : [ex - d, ex + d]) {
773
+ if (c < LEFT_MARGIN || c > contentWidth + 1 || usedDescent.has(c)) continue;
774
+ let blocked = false;
775
+ for (let b = r.fromBand + 1; b < r.toBand && !blocked; b++) blocked = blockedByBox(b, c);
776
+ if (!blocked) {
777
+ chosen = c;
778
+ break;
779
+ }
780
+ }
781
+ }
782
+ if (chosen === void 0) chosen = contentWidth + 2 + fallbackCount++ * 2;
783
+ usedDescent.add(chosen);
784
+ descentX.set(r, chosen);
785
+ }
786
+ const totalWidth = fallbackCount > 0 ? contentWidth + 2 + fallbackCount * 2 : contentWidth;
787
+ const gapCount = bands.length - 1;
788
+ const gapSegments = Array.from({ length: gapCount }, () => []);
789
+ const segmentOf = /* @__PURE__ */ new Map();
790
+ for (const r of bent) {
791
+ const { sx, ex } = attach.get(r);
792
+ if (r.toBand - r.fromBand === 1) {
793
+ const landing = { route: r, kind: "landing", lo: Math.min(sx, ex), hi: Math.max(sx, ex) };
794
+ gapSegments[r.toBand - 1].push(landing);
795
+ segmentOf.set(r, { landing });
796
+ } else {
797
+ const c = descentX.get(r);
798
+ const exit = { route: r, kind: "exit", lo: Math.min(sx, c), hi: Math.max(sx, c) };
799
+ const landing = { route: r, kind: "landing", lo: Math.min(c, ex), hi: Math.max(c, ex) };
800
+ gapSegments[r.fromBand].push(exit);
801
+ gapSegments[r.toBand - 1].push(landing);
802
+ segmentOf.set(r, { exit, landing });
803
+ }
804
+ }
805
+ const segmentRow = /* @__PURE__ */ new Map();
806
+ const gapRowCount = gapSegments.map((segments) => {
807
+ const rowEnds = [];
808
+ for (const s of [...segments].sort((a, b) => a.lo - b.lo)) {
809
+ let row = rowEnds.findIndex((end) => s.lo > end + 1);
810
+ if (row === -1) {
811
+ rowEnds.push(s.hi);
812
+ row = rowEnds.length - 1;
813
+ } else {
814
+ rowEnds[row] = Math.max(rowEnds[row], s.hi);
815
+ }
816
+ segmentRow.set(s, row);
817
+ }
818
+ return rowEnds.length;
819
+ });
820
+ let y = 0;
821
+ if (map.title !== void 0) y += 1 + geo.titleGap;
822
+ let laneHeaderY;
823
+ if (laneCount > 0) {
824
+ laneHeaderY = y;
825
+ y += 1 + geo.barGap;
826
+ }
827
+ const barY = [];
828
+ const gapTrackStartY = [];
829
+ for (let b = 0; b < bands.length; b++) {
830
+ barY.push(y);
831
+ y += 1 + geo.barGap;
832
+ const row = bandBoxes[b];
833
+ for (const box of row) box.y = y;
834
+ y += row.reduce((max, box) => Math.max(max, box.h), geo.mode === "constellation" ? 1 : BOX_H);
835
+ if (b < gapCount) {
836
+ y += geo.breathe;
837
+ gapTrackStartY.push(y);
838
+ y += gapRowCount[b];
839
+ y += geo.breathe;
840
+ }
841
+ }
842
+ const legendY = y + 1;
843
+ const rowYOf = (gap, s) => gapTrackStartY[gap] + segmentRow.get(s);
844
+ if (map.title !== void 0) canvas.text(LEFT_MARGIN, 0, map.title, "none", true);
845
+ if (laneHeaderY !== void 0) {
846
+ for (let i = 0; i < laneCount; i++) {
847
+ const label = fitWidth(map.lanes[i].label, laneW[i]);
848
+ const cx = laneX[i] + Math.max(0, Math.floor((laneW[i] - displayWidth(label)) / 2));
849
+ canvas.text(cx, laneHeaderY, label, "faint", true);
850
+ }
851
+ }
852
+ for (let b = 0; b < bands.length; b++) {
853
+ const label = bandLabel[b];
854
+ for (let x = 0; x < totalWidth; x++) canvas.line(x, barY[b], LEFT | RIGHT, true);
855
+ const labelStart = (fallbackCount > 0 ? contentWidth : totalWidth) - displayWidth(label);
856
+ canvas.text(labelStart, barY[b], label, "none", true);
857
+ }
858
+ for (const box of boxes.values()) {
859
+ drawBox(canvas, box, opts, neutral, opts.focus !== void 0 && box.node.id === opts.focus);
860
+ }
861
+ for (const r of routes) {
862
+ const sy = r.fromBox.y + r.fromBox.h - 1;
863
+ const ey = r.toBox.y;
864
+ const bright = opts.focus !== void 0 && (r.fromBox.node.id === opts.focus || r.toBox.node.id === opts.focus);
865
+ const direct = straightX.get(r);
866
+ if (direct !== void 0) {
867
+ drawPath(
868
+ canvas,
869
+ [
870
+ [direct, sy],
871
+ [direct, ey]
872
+ ],
873
+ bright
874
+ );
875
+ continue;
876
+ }
877
+ const { sx, ex } = attach.get(r);
878
+ const segments = segmentOf.get(r);
879
+ const landingY = rowYOf(r.toBand - 1, segments.landing);
880
+ if (r.toBand - r.fromBand === 1) {
881
+ drawPath(
882
+ canvas,
883
+ [
884
+ [sx, sy],
885
+ [sx, landingY],
886
+ [ex, landingY],
887
+ [ex, ey]
888
+ ],
889
+ bright
890
+ );
891
+ } else {
892
+ const c = descentX.get(r);
893
+ const exitY = rowYOf(r.fromBand, segments.exit);
894
+ drawPath(
895
+ canvas,
896
+ [
897
+ [sx, sy],
898
+ [sx, exitY],
899
+ [c, exitY],
900
+ [c, landingY],
901
+ [ex, landingY],
902
+ [ex, ey]
903
+ ],
904
+ bright
905
+ );
906
+ }
907
+ }
908
+ let lx = LEFT_MARGIN;
909
+ if (neutral) {
910
+ lx = canvas.text(lx, legendY, map.kind, "faint");
911
+ const seen = /* @__PURE__ */ new Set();
912
+ for (const n of map.nodes) {
913
+ const k = n.kind;
914
+ if (k === void 0 || seen.has(k) || kindGlyph(k, opts.unicode) === void 0) continue;
915
+ seen.add(k);
916
+ lx = canvas.text(lx, legendY, " ", "none");
917
+ lx = canvas.text(lx, legendY, `${kindGlyph(k, opts.unicode)} ${k}`, "none");
918
+ }
919
+ } else {
920
+ const legendOpts = { ...opts, spinnerFrame: 0 };
921
+ const legendEntries = [
922
+ ["planned", "dim"],
923
+ ["in-progress", "amber"],
924
+ ["done", "green"],
925
+ ["regressed", "red"]
926
+ ];
927
+ for (const [status, style] of legendEntries) {
928
+ if (lx > LEFT_MARGIN) lx = canvas.text(lx, legendY, " ", "none");
929
+ lx = canvas.text(lx, legendY, `${glyphFor(status, legendOpts)} ${status}`, style);
930
+ }
931
+ }
932
+ const hits = [...boxes.values()].map((b) => ({
933
+ id: b.node.id,
934
+ x: b.x,
935
+ y: b.y,
936
+ w: b.w,
937
+ h: b.h
938
+ }));
939
+ return { canvas, hits };
940
+ }
941
+ function drawBox(canvas, box, opts, neutral, focused = false) {
942
+ const { node, x, y, w } = box;
943
+ const skin = neutral ? neutralSkin(opts.unicode) : skinFor(node.status, opts.unicode);
944
+ const slotGlyph = neutral ? (node.kind !== void 0 ? kindGlyph(node.kind, opts.unicode) : void 0) ?? (opts.unicode ? "\xB7" : ".") : glyphFor(node.status, opts);
945
+ if (box.borderless) {
946
+ canvas.text(x + 1, y, slotGlyph, skin.style, true);
947
+ return;
948
+ }
949
+ const inner = w - 2;
950
+ const pad = box.pad === 1 ? " " : "";
951
+ canvas.text(x, y, skin.corners[0] + skin.h.repeat(inner) + skin.corners[1], skin.style, focused);
952
+ canvas.text(x, y + 1, skin.v, skin.style, focused);
953
+ canvas.text(x + 1, y + 1, `${pad}${slotGlyph} ${box.label}${pad}`, skin.style, true);
954
+ canvas.text(x + w - 1, y + 1, skin.v, skin.style, focused);
955
+ for (let i = 0; i < box.extra.length; i++) {
956
+ const row = box.extra[i];
957
+ const yy = y + 2 + i;
958
+ canvas.text(x, yy, skin.v, skin.style, focused);
959
+ canvas.text(x + 1, yy, row.text, row.style);
960
+ canvas.text(x + w - 1, yy, skin.v, skin.style, focused);
961
+ }
962
+ canvas.text(x, y + box.h - 1, skin.corners[2] + skin.h.repeat(inner) + skin.corners[3], skin.style, focused);
963
+ }
964
+
965
+ // src/store/store.ts
966
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
967
+ import { basename, dirname, join } from "node:path";
968
+ var STATE_FILE_VERSION = 1;
969
+ var STATE_FILE_RELATIVE_PATH = join(".claude", "mellos-mapping.json");
970
+ var PAGES_DIR_NAME = "mellos-mapping.pages";
971
+ function pageFilePath(defaultFile, page) {
972
+ return page === void 0 ? defaultFile : join(dirname(defaultFile), PAGES_DIR_NAME, `${page}.json`);
973
+ }
974
+ function pageIdOfFile(defaultFile, path) {
975
+ if (path === defaultFile) return void 0;
976
+ const name = basename(path);
977
+ return name.endsWith(".json") ? name.slice(0, -".json".length) : name;
978
+ }
979
+ function listPageFiles(defaultFile) {
980
+ const out = [];
981
+ if (existsSync(defaultFile)) out.push(defaultFile);
982
+ let entries = [];
983
+ try {
984
+ entries = readdirSync(join(dirname(defaultFile), PAGES_DIR_NAME));
985
+ } catch {
986
+ }
987
+ for (const e of entries.sort()) {
988
+ if (e.endsWith(".json")) out.push(join(dirname(defaultFile), PAGES_DIR_NAME, e));
989
+ }
990
+ return out;
991
+ }
992
+ function describeStoreError(e) {
993
+ switch (e.kind) {
994
+ case "not-found":
995
+ return `no map file at ${e.path}`;
996
+ case "malformed-json":
997
+ return `map file ${e.path} is not valid JSON: ${e.detail}`;
998
+ case "bad-shape":
999
+ return `map file ${e.path} has an unexpected shape: ${e.detail}`;
1000
+ case "invariant-violation":
1001
+ return `map file ${e.path} violates a structural invariant: ${describeMapError(e.violation)}`;
1002
+ }
1003
+ }
1004
+ function isRecord(v) {
1005
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1006
+ }
1007
+ function asArray(v) {
1008
+ return Array.isArray(v) ? v : [];
1009
+ }
1010
+ function optionalString(v) {
1011
+ return typeof v === "string" ? v : void 0;
1012
+ }
1013
+ function parseMap(raw, path) {
1014
+ if (!isRecord(raw)) return err({ kind: "bad-shape", path, detail: "root is not an object" });
1015
+ if (raw["version"] !== STATE_FILE_VERSION) {
1016
+ return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${STATE_FILE_VERSION}` });
1017
+ }
1018
+ let map = EMPTY_MAP;
1019
+ const title = optionalString(raw["title"]);
1020
+ if (title !== void 0) map = setTitle(map, title);
1021
+ const rawKind = optionalString(raw["kind"]);
1022
+ if (rawKind !== void 0) {
1023
+ const kind = makeMapKind(rawKind);
1024
+ if (!kind.ok) return err({ kind: "invariant-violation", path, violation: kind.error });
1025
+ map = setKind(map, kind.value);
1026
+ }
1027
+ for (const [i, rawLayer] of asArray(raw["layers"]).entries()) {
1028
+ if (!isRecord(rawLayer)) return err({ kind: "bad-shape", path, detail: `layers[${i}] is not an object` });
1029
+ const id = makeLayerId(String(rawLayer["id"] ?? ""));
1030
+ if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
1031
+ const name = optionalString(rawLayer["name"]);
1032
+ const rank = rawLayer["rank"];
1033
+ if (name === void 0 || typeof rank !== "number" || !Number.isInteger(rank)) {
1034
+ return err({ kind: "bad-shape", path, detail: `layers[${i}] needs a string name and an integer rank` });
1035
+ }
1036
+ const next = declareLayer(map, { id: id.value, name, rank });
1037
+ if (!next.ok) return err({ kind: "invariant-violation", path, violation: next.error });
1038
+ map = next.value;
1039
+ }
1040
+ for (const [i, rawLane] of asArray(raw["lanes"]).entries()) {
1041
+ if (!isRecord(rawLane)) return err({ kind: "bad-shape", path, detail: `lanes[${i}] is not an object` });
1042
+ const id = makeLaneId(String(rawLane["id"] ?? ""));
1043
+ if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
1044
+ const label = optionalString(rawLane["label"]);
1045
+ if (label === void 0) return err({ kind: "bad-shape", path, detail: `lanes[${i}] needs a string label` });
1046
+ const declared = declareLane(map, { id: id.value, label });
1047
+ if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
1048
+ map = declared.value;
1049
+ }
1050
+ for (const [i, rawGroup] of asArray(raw["groups"]).entries()) {
1051
+ if (!isRecord(rawGroup)) return err({ kind: "bad-shape", path, detail: `groups[${i}] is not an object` });
1052
+ const id = makeGroupId(String(rawGroup["id"] ?? ""));
1053
+ if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
1054
+ const layer = makeLayerId(String(rawGroup["layer"] ?? ""));
1055
+ if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
1056
+ const label = optionalString(rawGroup["label"]);
1057
+ if (label === void 0) return err({ kind: "bad-shape", path, detail: `groups[${i}] needs a string label` });
1058
+ const declared = declareGroup(map, { id: id.value, label, layer: layer.value });
1059
+ if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
1060
+ map = declared.value;
1061
+ }
1062
+ for (const [i, rawNode] of asArray(raw["nodes"]).entries()) {
1063
+ if (!isRecord(rawNode)) return err({ kind: "bad-shape", path, detail: `nodes[${i}] is not an object` });
1064
+ const id = makeNodeId(String(rawNode["id"] ?? ""));
1065
+ if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
1066
+ const layer = makeLayerId(String(rawNode["layer"] ?? ""));
1067
+ if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
1068
+ const status = makeNodeStatus(String(rawNode["status"] ?? ""));
1069
+ if (!status.ok) return err({ kind: "invariant-violation", path, violation: status.error });
1070
+ const label = optionalString(rawNode["label"]);
1071
+ if (label === void 0) return err({ kind: "bad-shape", path, detail: `nodes[${i}] needs a string label` });
1072
+ const detail = optionalString(rawNode["detail"]);
1073
+ const rawGroup = optionalString(rawNode["group"]);
1074
+ let group;
1075
+ if (rawGroup !== void 0) {
1076
+ const made = makeGroupId(rawGroup);
1077
+ if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
1078
+ group = made.value;
1079
+ }
1080
+ const rawNodeKind = optionalString(rawNode["kind"]);
1081
+ let nodeKind;
1082
+ if (rawNodeKind !== void 0) {
1083
+ const made = makeNodeKind(rawNodeKind);
1084
+ if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
1085
+ nodeKind = made.value;
1086
+ }
1087
+ const rawLane = optionalString(rawNode["lane"]);
1088
+ let lane;
1089
+ if (rawLane !== void 0) {
1090
+ const made = makeLaneId(rawLane);
1091
+ if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
1092
+ lane = made.value;
1093
+ }
1094
+ const rawSubmap = optionalString(rawNode["submap"]);
1095
+ let submap;
1096
+ if (rawSubmap !== void 0) {
1097
+ const made = makeSubmapRef(rawSubmap);
1098
+ if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
1099
+ submap = made.value;
1100
+ }
1101
+ const declared = declareNode(map, {
1102
+ id: id.value,
1103
+ label,
1104
+ layer: layer.value,
1105
+ status: status.value,
1106
+ ...detail !== void 0 ? { detail } : {},
1107
+ ...group !== void 0 ? { group } : {},
1108
+ ...nodeKind !== void 0 ? { kind: nodeKind } : {},
1109
+ ...lane !== void 0 ? { lane } : {},
1110
+ ...submap !== void 0 ? { submap } : {}
1111
+ });
1112
+ if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
1113
+ map = declared.value;
1114
+ const evidence = optionalString(rawNode["evidence"]);
1115
+ if (evidence !== void 0) {
1116
+ const updated = updateNode(map, { id: id.value, evidence });
1117
+ if (!updated.ok) return err({ kind: "invariant-violation", path, violation: updated.error });
1118
+ map = updated.value;
1119
+ }
1120
+ }
1121
+ for (const [i, rawEdge] of asArray(raw["edges"]).entries()) {
1122
+ if (!isRecord(rawEdge)) return err({ kind: "bad-shape", path, detail: `edges[${i}] is not an object` });
1123
+ const from = makeNodeId(String(rawEdge["from"] ?? ""));
1124
+ if (!from.ok) return err({ kind: "invariant-violation", path, violation: from.error });
1125
+ const to = makeNodeId(String(rawEdge["to"] ?? ""));
1126
+ if (!to.ok) return err({ kind: "invariant-violation", path, violation: to.error });
1127
+ const linked = linkNodes(map, from.value, to.value, optionalString(rawEdge["label"]));
1128
+ if (!linked.ok) return err({ kind: "invariant-violation", path, violation: linked.error });
1129
+ map = linked.value;
1130
+ }
1131
+ return ok(map);
1132
+ }
1133
+ function loadMapFile(path) {
1134
+ let text;
1135
+ try {
1136
+ text = readFileSync(path, "utf8");
1137
+ } catch (e) {
1138
+ const code = e.code;
1139
+ if (code === "ENOENT") return err({ kind: "not-found", path });
1140
+ throw e;
1141
+ }
1142
+ let raw;
1143
+ try {
1144
+ raw = JSON.parse(text);
1145
+ } catch (e) {
1146
+ return err({ kind: "malformed-json", path, detail: e.message });
1147
+ }
1148
+ return parseMap(raw, path);
1149
+ }
1150
+
1151
+ // src/watch/input.ts
1152
+ var KEY_H_STEP = 4;
1153
+ var KEY_V_STEP = 2;
1154
+ var WHEEL_V_STEP = 3;
1155
+ var MOTION = 32;
1156
+ var WHEEL = 64;
1157
+ var SHIFT = 4;
1158
+ var BUTTON_BITS = 3;
1159
+ var SGR_MOUSE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])/;
1160
+ var ARROW = /^\x1b\[([ABCD])/;
1161
+ var SHIFT_TAB = /^\x1b\[Z/;
1162
+ var PARTIAL_ESCAPE = /(?:\x1b|\x1b\[|\x1b\[<[\d;]*)$/;
1163
+ var ARROW_PAN = {
1164
+ A: { dx: 0, dy: -KEY_V_STEP },
1165
+ B: { dx: 0, dy: KEY_V_STEP },
1166
+ C: { dx: KEY_H_STEP, dy: 0 },
1167
+ D: { dx: -KEY_H_STEP, dy: 0 }
1168
+ };
1169
+ var KEY_PAN = {
1170
+ k: { dx: 0, dy: -KEY_V_STEP },
1171
+ j: { dx: 0, dy: KEY_V_STEP },
1172
+ l: { dx: KEY_H_STEP, dy: 0 },
1173
+ h: { dx: -KEY_H_STEP, dy: 0 }
1174
+ };
1175
+ function mouseEvent(code, x, y, final) {
1176
+ if (code & WHEEL) {
1177
+ const down = (code & 1) !== 0;
1178
+ return code & SHIFT ? { kind: "pan", dx: 0, dy: (down ? 1 : -1) * WHEEL_V_STEP } : { kind: "zoom", delta: down ? -1 : 1 };
1179
+ }
1180
+ const buttons = code & BUTTON_BITS;
1181
+ if (final === "m") return buttons === 0 ? { kind: "mouse-up", x, y } : void 0;
1182
+ if (code & MOTION) {
1183
+ if (buttons === 3) return { kind: "mouse-move", x, y };
1184
+ if (buttons === 0) return { kind: "mouse-drag", x, y };
1185
+ return void 0;
1186
+ }
1187
+ return buttons === 0 ? { kind: "mouse-down", x, y } : void 0;
1188
+ }
1189
+ function parseInput(chunk) {
1190
+ if (chunk === "\x1B") return { events: [{ kind: "clear" }], rest: "" };
1191
+ const events = [];
1192
+ let i = 0;
1193
+ while (i < chunk.length) {
1194
+ const slice = chunk.slice(i);
1195
+ const mouse = SGR_MOUSE.exec(slice);
1196
+ if (mouse) {
1197
+ const event = mouseEvent(Number(mouse[1]), Number(mouse[2]), Number(mouse[3]), mouse[4]);
1198
+ if (event) events.push(event);
1199
+ i += mouse[0].length;
1200
+ continue;
1201
+ }
1202
+ const arrow = ARROW.exec(slice);
1203
+ if (arrow) {
1204
+ const pan = ARROW_PAN[arrow[1]];
1205
+ events.push({ kind: "pan", ...pan });
1206
+ i += arrow[0].length;
1207
+ continue;
1208
+ }
1209
+ const shiftTab = SHIFT_TAB.exec(slice);
1210
+ if (shiftTab) {
1211
+ events.push({ kind: "prev-page" });
1212
+ i += shiftTab[0].length;
1213
+ continue;
1214
+ }
1215
+ const partial = PARTIAL_ESCAPE.exec(slice);
1216
+ if (partial && partial.index === 0) {
1217
+ return { events, rest: slice };
1218
+ }
1219
+ const ch = chunk[i];
1220
+ if (ch === "q" || ch === "Q" || ch === "" || ch === "") events.push({ kind: "quit" });
1221
+ else if (ch === "0") events.push({ kind: "reset" });
1222
+ else if (ch === "+" || ch === "=") events.push({ kind: "zoom", delta: 1 });
1223
+ else if (ch === "-") events.push({ kind: "zoom", delta: -1 });
1224
+ else if (ch === " ") events.push({ kind: "next-page" });
1225
+ else if (ch === "\x7F" || ch === "\b") events.push({ kind: "back" });
1226
+ else if (ch >= "1" && ch <= "9") events.push({ kind: "page", index: ch.charCodeAt(0) - "1".charCodeAt(0) });
1227
+ else if (KEY_PAN[ch]) events.push({ kind: "pan", ...KEY_PAN[ch] });
1228
+ i += 1;
1229
+ }
1230
+ return { events, rest: "" };
1231
+ }
1232
+
1233
+ // src/watch/watch.ts
1234
+ function parseArgs(argv, cwd) {
1235
+ let file = join2(cwd, STATE_FILE_RELATIVE_PATH);
1236
+ let intervalMs = 250;
1237
+ let unicode = true;
1238
+ let color = true;
1239
+ let mouse = true;
1240
+ for (let i = 0; i < argv.length; i++) {
1241
+ switch (argv[i]) {
1242
+ case "--file":
1243
+ file = argv[++i] ?? file;
1244
+ break;
1245
+ case "--interval":
1246
+ intervalMs = Math.max(50, Number(argv[++i]) || intervalMs);
1247
+ break;
1248
+ case "--ascii":
1249
+ unicode = false;
1250
+ break;
1251
+ case "--no-color":
1252
+ color = false;
1253
+ break;
1254
+ case "--no-mouse":
1255
+ mouse = false;
1256
+ break;
1257
+ default:
1258
+ break;
1259
+ }
1260
+ }
1261
+ return { file, intervalMs, unicode, color, mouse };
1262
+ }
1263
+ var HIDE_CURSOR = "\x1B[?25l";
1264
+ var SHOW_CURSOR = "\x1B[?25h";
1265
+ var CLEAR_ALL = "\x1B[H\x1B[2J";
1266
+ var HOME = "\x1B[H";
1267
+ var ERASE_LINE_END = "\x1B[K";
1268
+ var MOUSE_ON = "\x1B[?1003h\x1B[?1006h";
1269
+ var MOUSE_OFF = "\x1B[?1003l\x1B[?1006l";
1270
+ var RESET = "\x1B[0m";
1271
+ var PANEL_CONTENT_ROWS = 6;
1272
+ var PANEL_ROWS_MIN = 2;
1273
+ var MAP_ROWS_MIN = 4;
1274
+ function clampPanelRows(wanted, totalRows, tabRows) {
1275
+ const largest = totalRows - tabRows - MAP_ROWS_MIN - 2;
1276
+ return Math.max(PANEL_ROWS_MIN, Math.min(wanted, largest));
1277
+ }
1278
+ function panelRowsFromDividerY(termY, totalRows, tabRows) {
1279
+ return clampPanelRows(totalRows - termY - 1, totalRows, tabRows);
1280
+ }
1281
+ var STATUS_GLYPH = {
1282
+ planned: ["\xB7", "."],
1283
+ "in-progress": ["\u283F", "*"],
1284
+ done: ["\u25A0", "#"],
1285
+ regressed: ["\u2717", "X"]
1286
+ };
1287
+ var STATUS_SGR = {
1288
+ planned: "2",
1289
+ "in-progress": "33",
1290
+ done: "32",
1291
+ regressed: "31"
1292
+ };
1293
+ function anchorOffsets(anchor, offset, before, after) {
1294
+ if (anchor) {
1295
+ return {
1296
+ x: Math.round(offset.x + anchor.after.x + anchor.after.w / 2 - (anchor.before.x + anchor.before.w / 2)),
1297
+ y: Math.round(offset.y + anchor.after.y + anchor.after.h / 2 - (anchor.before.y + anchor.before.h / 2))
1298
+ };
1299
+ }
1300
+ return {
1301
+ x: before.w > 0 ? Math.round(offset.x * after.w / before.w) : 0,
1302
+ y: before.h > 0 ? Math.round(offset.y * after.h / before.h) : 0
1303
+ };
1304
+ }
1305
+ function pageTabRow(tabs, width, unicode) {
1306
+ const segments = [];
1307
+ let col = 1;
1308
+ for (const [index, tab] of tabs.entries()) {
1309
+ const room = width - (col - 1);
1310
+ if (room <= 3) break;
1311
+ const marker = tab.active ? unicode ? "\u25CF" : "*" : unicode ? "\u25CB" : "o";
1312
+ const glyph = STATUS_GLYPH[tab.status][unicode ? 0 : 1];
1313
+ const text = fitWidth(tab.neutral === true ? ` ${marker} ${tab.title} ` : ` ${marker} ${glyph} ${tab.title} `, room);
1314
+ const w = displayWidth(text);
1315
+ segments.push({
1316
+ text,
1317
+ sgr: tab.neutral === true ? tab.active ? "1" : tab.fresh ? "36" : "90" : tab.active ? `${STATUS_SGR[tab.status]};1` : tab.fresh ? STATUS_SGR[tab.status] : "90",
1318
+ lo: col,
1319
+ hi: col + w - 1,
1320
+ index
1321
+ });
1322
+ col += w;
1323
+ }
1324
+ return segments;
1325
+ }
1326
+ function topLevelFiles(defaultFile, files, mapOf) {
1327
+ const refs = /* @__PURE__ */ new Set();
1328
+ for (const m of mapOf.values()) {
1329
+ for (const n of m?.nodes ?? []) if (n.submap !== void 0) refs.add(n.submap);
1330
+ }
1331
+ return files.filter((f) => {
1332
+ const id = pageIdOfFile(defaultFile, f);
1333
+ return id === void 0 || !refs.has(id);
1334
+ });
1335
+ }
1336
+ function diveOrigin(defaultFile, file, files, mapOf) {
1337
+ const id = pageIdOfFile(defaultFile, file);
1338
+ if (id === void 0) return void 0;
1339
+ for (const f of files) {
1340
+ if (f === file) continue;
1341
+ const node = mapOf.get(f)?.nodes.find((n) => n.submap === id);
1342
+ if (node !== void 0) return { parent: f, label: node.label };
1343
+ }
1344
+ return void 0;
1345
+ }
1346
+ function nearestHit(hits, cx, cy) {
1347
+ let best;
1348
+ let bestDistance = Infinity;
1349
+ for (const h of hits) {
1350
+ const d = Math.abs(h.x + h.w / 2 - cx) + Math.abs(h.y + h.h / 2 - cy);
1351
+ if (d < bestDistance) {
1352
+ bestDistance = d;
1353
+ best = h;
1354
+ }
1355
+ }
1356
+ return best;
1357
+ }
1358
+ function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_ROWS) {
1359
+ const g = (s) => STATUS_GLYPH[s][unicode ? 0 : 1];
1360
+ const pinMark = pinned ? unicode ? " \u2299 pinned" : " * pinned" : "";
1361
+ const group = map.groups.find((gr) => gr.id === focusId);
1362
+ if (group) {
1363
+ const members = map.nodes.filter((n) => n.group === group.id);
1364
+ const memberIds = new Set(members.map((n) => n.id));
1365
+ const status = groupStatus(map, group.id);
1366
+ const layerName2 = map.layers.find((l) => l.id === group.layer)?.name ?? group.layer;
1367
+ const [right2, left2] = unicode ? ["\u2192", "\u2190"] : ["->", "<-"];
1368
+ const repLabel = (id) => {
1369
+ const n = map.nodes.find((x) => x.id === id);
1370
+ const owner = n.group !== void 0 ? map.groups.find((gr) => gr.id === n.group) : void 0;
1371
+ return owner !== void 0 ? `${g(groupStatus(map, owner.id))} ${owner.label}` : `${g(n.status)} ${n.label}`;
1372
+ };
1373
+ const uses2 = [
1374
+ ...new Set(
1375
+ map.edges.filter((e) => memberIds.has(e.from) && !memberIds.has(e.to)).map((e) => repLabel(e.to))
1376
+ )
1377
+ ];
1378
+ const usedBy2 = [
1379
+ ...new Set(
1380
+ map.edges.filter((e) => memberIds.has(e.to) && !memberIds.has(e.from)).map((e) => repLabel(e.from))
1381
+ )
1382
+ ];
1383
+ const lines2 = [
1384
+ {
1385
+ text: fitWidth(
1386
+ `${g(status)} ${group.label} [${group.id}] \xB7 ${layerName2} \xB7 ${status} \xB7 ${members.length} member(s)${pinMark}`,
1387
+ width
1388
+ ),
1389
+ sgr: `${STATUS_SGR[status]};1`
1390
+ },
1391
+ {
1392
+ text: fitWidth(`members: ${members.map((n) => `${g(n.status)} ${n.label}`).join(" ") || "\u2014"}`, width),
1393
+ sgr: ""
1394
+ },
1395
+ { text: fitWidth(`uses ${right2} ${uses2.join(" ") || "\u2014"}`, width), sgr: "" },
1396
+ { text: fitWidth(`used by ${left2} ${usedBy2.join(" ") || "\u2014"}`, width), sgr: "" }
1397
+ ];
1398
+ while (lines2.length < rows) lines2.push({ text: "", sgr: "" });
1399
+ return lines2.slice(0, rows);
1400
+ }
1401
+ const node = map.nodes.find((n) => n.id === focusId);
1402
+ if (!node) return void 0;
1403
+ const neutral = isNeutralKind(map);
1404
+ const layerName = map.layers.find((l) => l.id === node.layer)?.name ?? node.layer;
1405
+ const [right, left] = unicode ? ["\u2192", "\u2190"] : ["->", "<-"];
1406
+ const withGlyph = (id) => {
1407
+ const n = map.nodes.find((x) => x.id === id);
1408
+ return n ? `${g(n.status)} ${n.label}` : id;
1409
+ };
1410
+ const withEdgeLabel = (base, label) => label !== void 0 ? `${base} (${label})` : base;
1411
+ const uses = map.edges.filter((e) => e.from === node.id).map((e) => withEdgeLabel(withGlyph(e.to), e.label));
1412
+ const usedBy = map.edges.filter((e) => e.to === node.id).map((e) => withEdgeLabel(withGlyph(e.from), e.label));
1413
+ const pin = pinned ? unicode ? " \u2299 pinned" : " * pinned" : "";
1414
+ const headGlyph = neutral ? (node.kind !== void 0 ? kindGlyph(node.kind, unicode) : void 0) ?? (unicode ? "\xB7" : ".") : g(node.status);
1415
+ const laneLabel = node.lane !== void 0 ? map.lanes.find((l) => l.id === node.lane)?.label : void 0;
1416
+ const headParts = [
1417
+ `${headGlyph} ${node.label} [${node.id}]`,
1418
+ layerName,
1419
+ ...laneLabel !== void 0 ? [laneLabel] : [],
1420
+ ...node.kind !== void 0 ? [node.kind] : [],
1421
+ ...neutral ? [] : [node.status],
1422
+ ...node.submap !== void 0 ? [`${unicode ? "\u229E" : "+"} ${node.submap}`] : []
1423
+ ];
1424
+ const [usesWord, usedByWord] = map.kind === "sequence" ? ["after", "before"] : ["uses", "used by"];
1425
+ const lines = [
1426
+ {
1427
+ text: fitWidth(`${headParts.join(" \xB7 ")}${pin}`, width),
1428
+ sgr: neutral ? "1" : `${STATUS_SGR[node.status]};1`
1429
+ },
1430
+ { text: fitWidth(`evidence: ${node.evidence ?? "\u2014"}`, width), sgr: "90" },
1431
+ { text: fitWidth(`${usesWord} ${right} ${uses.join(" ") || "\u2014"}`, width), sgr: "" },
1432
+ { text: fitWidth(`${usedByWord} ${left} ${usedBy.join(" ") || "\u2014"}`, width), sgr: "" }
1433
+ ];
1434
+ const notes = node.detail !== void 0 ? wrapWidth(node.detail, width) : ["(no design notes yet)"];
1435
+ const room = Math.max(0, rows - lines.length);
1436
+ for (let i = 0; i < room; i++) {
1437
+ const last = i === room - 1 && notes.length > room;
1438
+ lines.push({
1439
+ text: last ? fitWidth(notes[i] + "\u2026", width) : notes[i] ?? "",
1440
+ sgr: node.detail !== void 0 ? "" : "90"
1441
+ });
1442
+ }
1443
+ return lines.slice(0, rows);
1444
+ }
1445
+ function mapPanel(map, unicode, width, rows = PANEL_CONTENT_ROWS) {
1446
+ const g = (s) => STATUS_GLYPH[s][unicode ? 0 : 1];
1447
+ const count = (s) => map.nodes.filter((n) => n.status === s).length;
1448
+ const statuses = ["done", "in-progress", "planned", "regressed"];
1449
+ const counts = statuses.filter((s) => count(s) > 0).map((s) => `${g(s)} ${count(s)} ${s}`).join(" ");
1450
+ const parts = [`${map.layers.length} layers`, `${map.nodes.length} nodes`, `${map.edges.length} edges`];
1451
+ if (map.lanes.length > 0) parts.push(`${map.lanes.length} lanes`);
1452
+ const lines = [
1453
+ { text: fitWidth(map.title ?? "mellos map", width), sgr: "1" },
1454
+ { text: fitWidth(parts.join(" \xB7 "), width), sgr: "90" },
1455
+ // documentation kinds document structure, not progress
1456
+ { text: fitWidth(isNeutralKind(map) ? `${map.kind} diagram` : counts, width), sgr: isNeutralKind(map) ? "90" : "" },
1457
+ { text: "", sgr: "" },
1458
+ { text: "hover a node to inspect \xB7 click to pin", sgr: "90" }
1459
+ ];
1460
+ while (lines.length < rows) lines.push({ text: "", sgr: "" });
1461
+ return lines.slice(0, rows);
1462
+ }
1463
+ function main() {
1464
+ const cfg = parseArgs(process.argv.slice(2), process.cwd());
1465
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
1466
+ const mouseActive = interactive && cfg.mouse;
1467
+ let lastFrame = "";
1468
+ let spinnerFrame = 0;
1469
+ let map;
1470
+ let notice = `waiting for ${cfg.file} ...`;
1471
+ let lastCols = process.stdout.columns ?? 0;
1472
+ let lastRows = process.stdout.rows ?? 0;
1473
+ let pageFiles = [cfg.file];
1474
+ const pageData = /* @__PURE__ */ new Map();
1475
+ const pageViews = /* @__PURE__ */ new Map();
1476
+ let activeFile;
1477
+ let firstScan = true;
1478
+ let lastTabSegments = [];
1479
+ let offsetX = 0;
1480
+ let offsetY = 0;
1481
+ let zoom = ZOOM_DEFAULT;
1482
+ let dragAnchor;
1483
+ let press;
1484
+ let hoverId;
1485
+ let selectedId;
1486
+ let lastHits = [];
1487
+ let lastContent = { w: 0, h: 0 };
1488
+ let pendingInput = "";
1489
+ let panelContentRows = PANEL_CONTENT_ROWS;
1490
+ let dividerDrag = false;
1491
+ let lastClick;
1492
+ const diveStack = [];
1493
+ let flash;
1494
+ let lastTabFiles = [];
1495
+ const maps = () => new Map([...pageData].map(([f, e]) => [f, e.map]));
1496
+ const topFiles = () => topLevelFiles(cfg.file, pageFiles, maps());
1497
+ const inSubmap = () => activeFile !== void 0 && !topFiles().includes(activeFile);
1498
+ const tabRows = () => topFiles().length > 1 || inSubmap() ? 1 : 0;
1499
+ const climbBack = () => {
1500
+ let parent = diveStack.pop();
1501
+ while (parent !== void 0 && !pageFiles.includes(parent)) parent = diveStack.pop();
1502
+ if (parent === void 0 && activeFile !== void 0) {
1503
+ parent = diveOrigin(cfg.file, activeFile, pageFiles, maps())?.parent;
1504
+ }
1505
+ if (parent !== void 0 && parent !== activeFile) {
1506
+ switchPage(parent);
1507
+ return true;
1508
+ }
1509
+ return false;
1510
+ };
1511
+ const viewHeight = () => Math.max(1, (process.stdout.rows ?? 30) - (1 + panelContentRows) - 1 - tabRows());
1512
+ const dividerY = () => tabRows() + viewHeight() + 1;
1513
+ const switchPage = (file) => {
1514
+ if (activeFile !== void 0) pageViews.set(activeFile, { offsetX, offsetY, zoom, selectedId });
1515
+ activeFile = file;
1516
+ const view = pageViews.get(file);
1517
+ offsetX = view?.offsetX ?? 0;
1518
+ offsetY = view?.offsetY ?? 0;
1519
+ zoom = view?.zoom ?? ZOOM_DEFAULT;
1520
+ selectedId = view?.selectedId;
1521
+ hoverId = void 0;
1522
+ const entry = pageData.get(file);
1523
+ if (entry !== void 0 && entry.fresh) pageData.set(file, { ...entry, fresh: false });
1524
+ map = entry?.map;
1525
+ notice = map === void 0 ? `waiting for ${file} ...` : "";
1526
+ };
1527
+ const hitTest = (termX, termY) => {
1528
+ const sx = termX - 1;
1529
+ const sy = termY - 1 - tabRows();
1530
+ if (sy < 0 || sy >= viewHeight()) return void 0;
1531
+ const cx = sx + offsetX;
1532
+ const cy = sy + offsetY;
1533
+ return lastHits.find((h) => cx >= h.x && cx < h.x + h.w && cy >= h.y && cy < h.y + h.h)?.id;
1534
+ };
1535
+ process.stdout.write(HIDE_CURSOR + CLEAR_ALL + (mouseActive ? MOUSE_ON : ""));
1536
+ const restore = () => {
1537
+ process.stdout.write((mouseActive ? MOUSE_OFF : "") + SHOW_CURSOR + "\n");
1538
+ process.exit(0);
1539
+ };
1540
+ process.on("SIGINT", restore);
1541
+ process.on("SIGTERM", restore);
1542
+ const paint = () => {
1543
+ const cols = process.stdout.columns ?? 100;
1544
+ panelContentRows = clampPanelRows(panelContentRows, process.stdout.rows ?? 30, tabRows());
1545
+ const viewH = viewHeight();
1546
+ const focus = hoverId ?? selectedId;
1547
+ let body;
1548
+ let panned = "";
1549
+ let pannable = false;
1550
+ if (map !== void 0) {
1551
+ const windowed = renderMapWindow(
1552
+ map,
1553
+ { color: cfg.color, unicode: cfg.unicode, spinnerFrame, focus, zoom },
1554
+ { x: offsetX, y: offsetY, width: cols, height: viewH }
1555
+ );
1556
+ const maxX = Math.max(0, windowed.contentWidth - cols);
1557
+ const maxY = Math.max(0, windowed.contentHeight - viewH);
1558
+ if (offsetX > maxX || offsetY > maxY || offsetX < 0 || offsetY < 0) {
1559
+ offsetX = Math.min(Math.max(0, offsetX), maxX);
1560
+ offsetY = Math.min(Math.max(0, offsetY), maxY);
1561
+ paint();
1562
+ return;
1563
+ }
1564
+ pannable = maxX > 0 || maxY > 0;
1565
+ body = windowed.lines;
1566
+ lastHits = windowed.hits;
1567
+ lastContent = { w: windowed.contentWidth, h: windowed.contentHeight };
1568
+ if (offsetX !== 0 || offsetY !== 0) panned = ` (+${offsetX},+${offsetY})`;
1569
+ } else {
1570
+ body = [fitWidth(notice, Math.max(1, cols - 1))];
1571
+ }
1572
+ if (notice !== "" && map !== void 0) {
1573
+ body[body.length - 1] = fitWidth(` ${notice}`, Math.max(1, cols - 1));
1574
+ }
1575
+ const panelWidth = Math.max(10, cols - 2);
1576
+ let panel;
1577
+ if (map === void 0) {
1578
+ panel = Array.from({ length: panelContentRows }, () => ({ text: "", sgr: "" }));
1579
+ } else if (focus !== void 0) {
1580
+ panel = nodePanel(map, focus, cfg.unicode, panelWidth, selectedId === focus, panelContentRows) ?? mapPanel(map, cfg.unicode, panelWidth, panelContentRows);
1581
+ } else {
1582
+ panel = mapPanel(map, cfg.unicode, panelWidth, panelContentRows);
1583
+ }
1584
+ const grip = cfg.unicode ? " \u22EF " : " ~ ";
1585
+ const bar = (cfg.unicode ? "\u2500" : "-").repeat(cols);
1586
+ const gripAt = Math.max(0, Math.floor((cols - grip.length) / 2));
1587
+ const separator = cols > grip.length + 2 ? bar.slice(0, gripAt) + grip + bar.slice(gripAt + grip.length) : bar;
1588
+ const panelRows = [
1589
+ cfg.color ? `\x1B[90m${separator}${RESET}` : separator,
1590
+ ...panel.map(
1591
+ (l) => cfg.color && l.sgr !== "" && l.text !== "" ? ` \x1B[${l.sgr}m${l.text}${RESET}` : ` ${l.text}`
1592
+ )
1593
+ ];
1594
+ let tabLine;
1595
+ lastTabFiles = topFiles();
1596
+ if (inSubmap() && activeFile !== void 0) {
1597
+ const stackParent = [...diveStack].reverse().find((f) => pageFiles.includes(f));
1598
+ const scanned = diveOrigin(cfg.file, activeFile, pageFiles, maps());
1599
+ const parentFile = stackParent ?? scanned?.parent;
1600
+ const parentTitle = parentFile !== void 0 ? pageData.get(parentFile)?.map?.title ?? (pageIdOfFile(cfg.file, parentFile) ?? "main") : "main";
1601
+ const nodeLabel = scanned?.label ?? map?.title ?? "";
1602
+ const crumbHead = ` ${cfg.unicode ? "\u232B" : "<"} ${parentTitle} ${cfg.unicode ? "\u25B8" : ">"} `;
1603
+ const head = { text: crumbHead, sgr: "90", lo: 1, hi: displayWidth(crumbHead), index: -1 };
1604
+ const tailText = fitWidth(`${nodeLabel} `, Math.max(1, cols - displayWidth(crumbHead)));
1605
+ const tail = {
1606
+ text: tailText,
1607
+ sgr: "1",
1608
+ lo: head.hi + 1,
1609
+ hi: head.hi + displayWidth(tailText),
1610
+ index: -1
1611
+ };
1612
+ lastTabSegments = [head, tail];
1613
+ tabLine = lastTabSegments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET}` : s.text).join("");
1614
+ } else if (tabRows() > 0) {
1615
+ const tabs = lastTabFiles.map((f) => {
1616
+ const m = pageData.get(f)?.map;
1617
+ return {
1618
+ title: m?.title ?? (pageIdOfFile(cfg.file, f) ?? "main"),
1619
+ status: m !== void 0 ? mapStatus(m) : "planned",
1620
+ active: f === activeFile,
1621
+ fresh: pageData.get(f)?.fresh ?? false,
1622
+ neutral: m !== void 0 && isNeutralKind(m)
1623
+ };
1624
+ });
1625
+ const segments = pageTabRow(tabs, cols, cfg.unicode);
1626
+ lastTabSegments = segments;
1627
+ tabLine = segments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET}` : s.text).join("");
1628
+ } else {
1629
+ lastTabSegments = [];
1630
+ }
1631
+ const zoomTag = `${cfg.unicode ? "\u2295" : "zoom"} ${zoomLabel(zoom)}`;
1632
+ const hint = !interactive ? cfg.file : (flash !== void 0 ? `${flash.text} \xB7 ` : "") + `${zoomTag} \xB7 wheel zoom \xB7 ` + (pannable ? "drag pan \xB7 " : "") + "hover/click \xB7 0 reset \xB7 q quit";
1633
+ const footerText = fitWidth(` ${hint}${panned}`, Math.max(1, cols - 1));
1634
+ const footer = cfg.color ? `\x1B[90m${footerText}${RESET}` : footerText;
1635
+ let frame = HOME;
1636
+ if (tabLine !== void 0) frame += tabLine + ERASE_LINE_END + "\n";
1637
+ for (let i = 0; i < viewH; i++) frame += (body[i] ?? "") + ERASE_LINE_END + "\n";
1638
+ for (const row of panelRows) frame += row + ERASE_LINE_END + "\n";
1639
+ frame += footer + ERASE_LINE_END;
1640
+ if (frame !== lastFrame) {
1641
+ process.stdout.write(frame);
1642
+ lastFrame = frame;
1643
+ }
1644
+ };
1645
+ const handleResize = () => {
1646
+ lastCols = process.stdout.columns ?? lastCols;
1647
+ lastRows = process.stdout.rows ?? lastRows;
1648
+ lastFrame = "";
1649
+ process.stdout.write(CLEAR_ALL);
1650
+ paint();
1651
+ };
1652
+ const tick = () => {
1653
+ if ((process.stdout.columns ?? lastCols) !== lastCols || (process.stdout.rows ?? lastRows) !== lastRows) {
1654
+ handleResize();
1655
+ }
1656
+ const discovered = listPageFiles(cfg.file);
1657
+ pageFiles = discovered.length > 0 ? discovered : [cfg.file];
1658
+ for (const known of [...pageData.keys()]) {
1659
+ if (!pageFiles.includes(known)) {
1660
+ pageData.delete(known);
1661
+ pageViews.delete(known);
1662
+ }
1663
+ }
1664
+ for (const file of pageFiles) {
1665
+ let mtimeMs;
1666
+ try {
1667
+ mtimeMs = statSync(file).mtimeMs;
1668
+ } catch {
1669
+ continue;
1670
+ }
1671
+ const entry = pageData.get(file);
1672
+ if (mtimeMs === entry?.mtimeMs) continue;
1673
+ const loaded = loadMapFile(file);
1674
+ if (loaded.ok) {
1675
+ const becameFresh = !firstScan && file !== activeFile;
1676
+ pageData.set(file, { map: loaded.value, mtimeMs, fresh: becameFresh });
1677
+ if (file === activeFile) {
1678
+ map = loaded.value;
1679
+ notice = "";
1680
+ } else if (becameFresh && !topFiles().includes(file)) {
1681
+ const title = loaded.value.title ?? (pageIdOfFile(cfg.file, file) ?? "?");
1682
+ flash = { text: `${cfg.unicode ? "\u229E " : ""}${title} updated`, until: Date.now() + 4e3 };
1683
+ }
1684
+ } else if (loaded.error.kind === "malformed-json") {
1685
+ } else {
1686
+ pageData.set(file, { map: entry?.map, mtimeMs, fresh: entry?.fresh ?? false });
1687
+ if (file === activeFile) notice = describeStoreError(loaded.error);
1688
+ }
1689
+ }
1690
+ firstScan = false;
1691
+ if (activeFile === void 0 || !pageFiles.includes(activeFile)) switchPage(pageFiles[0]);
1692
+ if ([...pageData.values()].some((p) => p.map?.nodes.some((n) => n.status === "in-progress"))) spinnerFrame++;
1693
+ if (flash !== void 0 && Date.now() > flash.until) flash = void 0;
1694
+ paint();
1695
+ };
1696
+ if (interactive) {
1697
+ process.stdin.setRawMode(true);
1698
+ process.stdin.resume();
1699
+ process.stdin.setEncoding("utf8");
1700
+ process.stdin.on("data", (chunk) => {
1701
+ const parsed = parseInput(pendingInput + chunk);
1702
+ pendingInput = parsed.rest;
1703
+ let dirty = false;
1704
+ for (const event of parsed.events) {
1705
+ switch (event.kind) {
1706
+ case "quit":
1707
+ restore();
1708
+ return;
1709
+ case "reset":
1710
+ offsetX = 0;
1711
+ offsetY = 0;
1712
+ zoom = ZOOM_DEFAULT;
1713
+ dirty = true;
1714
+ break;
1715
+ case "clear":
1716
+ if (selectedId !== void 0) selectedId = void 0;
1717
+ else climbBack();
1718
+ dirty = true;
1719
+ break;
1720
+ case "pan":
1721
+ offsetX += event.dx;
1722
+ offsetY += event.dy;
1723
+ dirty = true;
1724
+ break;
1725
+ case "zoom": {
1726
+ const next = clampZoom(zoom + event.delta);
1727
+ if (next === zoom || map === void 0) break;
1728
+ const cols = process.stdout.columns ?? 100;
1729
+ const anchorId = hoverId ?? selectedId ?? nearestHit(lastHits, offsetX + cols / 2, offsetY + viewHeight() / 2)?.id;
1730
+ const before = lastHits.find((h) => h.id === anchorId);
1731
+ zoom = next;
1732
+ const sized = renderMapWindow(
1733
+ map,
1734
+ { color: false, unicode: cfg.unicode, spinnerFrame: 0, zoom },
1735
+ { x: 0, y: 0, width: 0, height: 0 }
1736
+ );
1737
+ const after = before === void 0 ? void 0 : sized.hits.find((h) => h.id === before.id);
1738
+ const moved = anchorOffsets(
1739
+ before !== void 0 && after !== void 0 ? { before, after } : void 0,
1740
+ { x: offsetX, y: offsetY },
1741
+ lastContent,
1742
+ { w: sized.contentWidth, h: sized.contentHeight }
1743
+ );
1744
+ offsetX = moved.x;
1745
+ offsetY = moved.y;
1746
+ dirty = true;
1747
+ break;
1748
+ }
1749
+ case "mouse-move": {
1750
+ const over = hitTest(event.x, event.y);
1751
+ if (over !== hoverId) {
1752
+ hoverId = over;
1753
+ dirty = true;
1754
+ }
1755
+ break;
1756
+ }
1757
+ case "mouse-down":
1758
+ if (event.y === dividerY()) {
1759
+ dividerDrag = true;
1760
+ break;
1761
+ }
1762
+ dragAnchor = { x: event.x, y: event.y, ox: offsetX, oy: offsetY };
1763
+ press = { moved: false };
1764
+ break;
1765
+ case "mouse-drag":
1766
+ if (dividerDrag) {
1767
+ const next = panelRowsFromDividerY(event.y, process.stdout.rows ?? 30, tabRows());
1768
+ if (next !== panelContentRows) {
1769
+ panelContentRows = next;
1770
+ dirty = true;
1771
+ }
1772
+ break;
1773
+ }
1774
+ if (dragAnchor) {
1775
+ const nx = dragAnchor.ox - (event.x - dragAnchor.x);
1776
+ const ny = dragAnchor.oy - (event.y - dragAnchor.y);
1777
+ if (nx !== offsetX || ny !== offsetY) {
1778
+ offsetX = nx;
1779
+ offsetY = ny;
1780
+ if (press) press.moved = true;
1781
+ dirty = true;
1782
+ }
1783
+ }
1784
+ break;
1785
+ case "mouse-up":
1786
+ if (dividerDrag) {
1787
+ dividerDrag = false;
1788
+ break;
1789
+ }
1790
+ if (press && !press.moved) {
1791
+ const tabHit = tabRows() > 0 && event.y === 1 ? lastTabSegments.find((s) => event.x >= s.lo && event.x <= s.hi) : void 0;
1792
+ if (tabHit !== void 0) {
1793
+ if (tabHit.index === -1) {
1794
+ climbBack();
1795
+ } else {
1796
+ const target = lastTabFiles[tabHit.index];
1797
+ if (target !== void 0 && target !== activeFile) switchPage(target);
1798
+ }
1799
+ } else {
1800
+ const id = hitTest(event.x, event.y);
1801
+ const now = Date.now();
1802
+ if (id !== void 0 && lastClick?.id === id && now - lastClick.at <= 450) {
1803
+ const submap = map?.nodes.find((n) => n.id === id)?.submap;
1804
+ if (submap !== void 0 && activeFile !== void 0) {
1805
+ const target = pageFilePath(cfg.file, submap);
1806
+ if (pageFiles.includes(target) && target !== activeFile) {
1807
+ diveStack.push(activeFile);
1808
+ switchPage(target);
1809
+ } else if (!pageFiles.includes(target)) {
1810
+ flash = { text: `submap "${submap}" has no page yet`, until: now + 2500 };
1811
+ }
1812
+ }
1813
+ lastClick = void 0;
1814
+ } else {
1815
+ lastClick = id !== void 0 ? { id, at: now } : void 0;
1816
+ }
1817
+ selectedId = id;
1818
+ }
1819
+ dirty = true;
1820
+ }
1821
+ dragAnchor = void 0;
1822
+ press = void 0;
1823
+ break;
1824
+ case "next-page":
1825
+ case "prev-page": {
1826
+ const top = topFiles();
1827
+ if (top.length > 0 && activeFile !== void 0) {
1828
+ const current = top.indexOf(activeFile);
1829
+ const step = event.kind === "next-page" ? 1 : -1;
1830
+ const target = top[(current + step + top.length) % top.length];
1831
+ if (target !== activeFile) {
1832
+ switchPage(target);
1833
+ dirty = true;
1834
+ }
1835
+ }
1836
+ break;
1837
+ }
1838
+ case "page": {
1839
+ const target = topFiles()[event.index];
1840
+ if (target !== void 0 && target !== activeFile) {
1841
+ switchPage(target);
1842
+ dirty = true;
1843
+ }
1844
+ break;
1845
+ }
1846
+ case "back":
1847
+ if (climbBack()) dirty = true;
1848
+ break;
1849
+ }
1850
+ }
1851
+ if (dirty) paint();
1852
+ });
1853
+ process.stdout.on("resize", handleResize);
1854
+ }
1855
+ tick();
1856
+ setInterval(tick, cfg.intervalMs);
1857
+ }
1858
+ function launchedAsEntry(argv1, moduleUrl) {
1859
+ if (argv1 === void 0) return false;
1860
+ try {
1861
+ return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
1862
+ } catch {
1863
+ return pathToFileURL(argv1).href === moduleUrl;
1864
+ }
1865
+ }
1866
+ if (launchedAsEntry(process.argv[1], import.meta.url)) {
1867
+ main();
1868
+ }
1869
+ export {
1870
+ PANEL_ROWS_MIN,
1871
+ anchorOffsets,
1872
+ clampPanelRows,
1873
+ diveOrigin,
1874
+ fitWidth,
1875
+ launchedAsEntry,
1876
+ mapPanel,
1877
+ nearestHit,
1878
+ nodePanel,
1879
+ pageTabRow,
1880
+ panelRowsFromDividerY,
1881
+ parseArgs,
1882
+ topLevelFiles,
1883
+ wrapWidth
1884
+ };