mellos-mapping 0.19.0 → 0.20.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/README.md +17 -4
- package/README.zh-CN.md +16 -4
- package/dist/server.mjs +147 -112
- package/dist/watch.mjs +238 -159
- package/lib/domain/ops.d.ts +112 -0
- package/lib/domain/ops.js +253 -0
- package/lib/domain/types.d.ts +242 -0
- package/lib/domain/types.js +122 -0
- package/lib/render/render.d.ts +102 -0
- package/lib/render/render.js +859 -0
- package/lib/semantics/semantics.d.ts +120 -0
- package/lib/semantics/semantics.js +256 -0
- package/lib/store/format.d.ts +50 -0
- package/lib/store/format.js +215 -0
- package/lib/store/store.d.ts +96 -0
- package/lib/store/store.js +281 -0
- package/package.json +30 -2
- package/scripts/codex-register.mjs +1 -1
- package/scripts/open-pane.mjs +1 -1
package/dist/watch.mjs
CHANGED
|
@@ -199,13 +199,18 @@ function updateNode(map, input) {
|
|
|
199
199
|
return ok({ ...map, nodes: map.nodes.map((n) => n.id === input.id ? updated : n) });
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
// src/
|
|
202
|
+
// src/semantics/semantics.ts
|
|
203
203
|
var ZOOM_MIN = -4;
|
|
204
204
|
var ZOOM_MAX = 2;
|
|
205
205
|
var ZOOM_DEFAULT = 0;
|
|
206
206
|
function clampZoom(n) {
|
|
207
207
|
return Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, Math.round(n)));
|
|
208
208
|
}
|
|
209
|
+
function zoomMode(zoom) {
|
|
210
|
+
if (zoom >= 1) return "detail";
|
|
211
|
+
if (zoom <= -4) return "overview";
|
|
212
|
+
return "boxes";
|
|
213
|
+
}
|
|
209
214
|
function zoomLabel(zoom) {
|
|
210
215
|
switch (zoom) {
|
|
211
216
|
case 2:
|
|
@@ -224,6 +229,136 @@ function zoomLabel(zoom) {
|
|
|
224
229
|
return "overview";
|
|
225
230
|
}
|
|
226
231
|
}
|
|
232
|
+
function isNeutralKind(map) {
|
|
233
|
+
return map.kind !== void 0 && map.kind !== "dev";
|
|
234
|
+
}
|
|
235
|
+
function aggregateMap(map) {
|
|
236
|
+
if (map.groups.length === 0) return void 0;
|
|
237
|
+
const representative = /* @__PURE__ */ new Map();
|
|
238
|
+
for (const n of map.nodes) representative.set(n.id, n.group ?? n.id);
|
|
239
|
+
const nodes = map.groups.map((g) => {
|
|
240
|
+
const members = map.nodes.filter((n) => n.group === g.id);
|
|
241
|
+
const done = members.filter((n) => n.status === "done").length;
|
|
242
|
+
return {
|
|
243
|
+
id: g.id,
|
|
244
|
+
// neutral kinds document structure, not progress — no member counts
|
|
245
|
+
label: isNeutralKind(map) ? g.label : `${g.label} ${done}/${members.length}`,
|
|
246
|
+
layer: g.layer,
|
|
247
|
+
status: groupStatus(map, g.id)
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
for (const n of map.nodes) if (n.group === void 0) nodes.push(n);
|
|
251
|
+
const seen = /* @__PURE__ */ new Set();
|
|
252
|
+
const edges = [];
|
|
253
|
+
for (const e of map.edges) {
|
|
254
|
+
const from = representative.get(e.from);
|
|
255
|
+
const to = representative.get(e.to);
|
|
256
|
+
if (from === to || seen.has(`${from}->${to}`)) continue;
|
|
257
|
+
seen.add(`${from}->${to}`);
|
|
258
|
+
edges.push({ from, to });
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
...map.title !== void 0 ? { title: map.title } : {},
|
|
262
|
+
...map.kind !== void 0 ? { kind: map.kind } : {},
|
|
263
|
+
layers: map.layers,
|
|
264
|
+
groups: [],
|
|
265
|
+
lanes: map.lanes,
|
|
266
|
+
nodes,
|
|
267
|
+
edges
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function focusInfo(map, focusId) {
|
|
271
|
+
const layerNameOf = (layerId) => map.layers.find((l) => l.id === layerId)?.name ?? layerId;
|
|
272
|
+
const group = map.groups.find((g) => g.id === focusId);
|
|
273
|
+
if (group) {
|
|
274
|
+
const members = map.nodes.filter((n) => n.group === group.id);
|
|
275
|
+
const memberIds = new Set(members.map((n) => n.id));
|
|
276
|
+
const rep = (id) => {
|
|
277
|
+
const n = map.nodes.find((x) => x.id === id);
|
|
278
|
+
const owner = n.group !== void 0 ? map.groups.find((g) => g.id === n.group) : void 0;
|
|
279
|
+
return owner !== void 0 ? { id: owner.id, label: owner.label, status: groupStatus(map, owner.id) } : { id: n.id, label: n.label, status: n.status };
|
|
280
|
+
};
|
|
281
|
+
const dedupe = (refs) => {
|
|
282
|
+
const seen = /* @__PURE__ */ new Set();
|
|
283
|
+
const out = [];
|
|
284
|
+
for (const r of refs) {
|
|
285
|
+
if (seen.has(r.id)) continue;
|
|
286
|
+
seen.add(r.id);
|
|
287
|
+
out.push(r);
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
};
|
|
291
|
+
return {
|
|
292
|
+
kind: "group",
|
|
293
|
+
group,
|
|
294
|
+
status: groupStatus(map, group.id),
|
|
295
|
+
layerName: layerNameOf(group.layer),
|
|
296
|
+
members,
|
|
297
|
+
uses: dedupe(
|
|
298
|
+
map.edges.filter((e) => memberIds.has(e.from) && !memberIds.has(e.to)).map((e) => rep(e.to))
|
|
299
|
+
),
|
|
300
|
+
usedBy: dedupe(
|
|
301
|
+
map.edges.filter((e) => memberIds.has(e.to) && !memberIds.has(e.from)).map((e) => rep(e.from))
|
|
302
|
+
)
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const node = map.nodes.find((n) => n.id === focusId);
|
|
306
|
+
if (!node) return void 0;
|
|
307
|
+
const ref = (id, edgeLabel) => {
|
|
308
|
+
const n = map.nodes.find((x) => x.id === id);
|
|
309
|
+
return {
|
|
310
|
+
id,
|
|
311
|
+
label: n?.label ?? id,
|
|
312
|
+
status: n?.status ?? "planned",
|
|
313
|
+
...edgeLabel !== void 0 ? { edgeLabel } : {}
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
const laneLabel = node.lane !== void 0 ? map.lanes.find((l) => l.id === node.lane)?.label : void 0;
|
|
317
|
+
return {
|
|
318
|
+
kind: "node",
|
|
319
|
+
node,
|
|
320
|
+
layerName: layerNameOf(node.layer),
|
|
321
|
+
...laneLabel !== void 0 ? { laneLabel } : {},
|
|
322
|
+
uses: map.edges.filter((e) => e.from === node.id).map((e) => ref(e.to, e.label)),
|
|
323
|
+
usedBy: map.edges.filter((e) => e.to === node.id).map((e) => ref(e.from, e.label))
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function submapRefs(maps) {
|
|
327
|
+
const refs = /* @__PURE__ */ new Set();
|
|
328
|
+
for (const m of maps) {
|
|
329
|
+
for (const n of m?.nodes ?? []) if (n.submap !== void 0) refs.add(n.submap);
|
|
330
|
+
}
|
|
331
|
+
return refs;
|
|
332
|
+
}
|
|
333
|
+
function diveParent(entries, pageId) {
|
|
334
|
+
for (const [key, m] of entries) {
|
|
335
|
+
const node = m?.nodes.find((n) => n.submap === pageId);
|
|
336
|
+
if (node !== void 0) return { parent: key, label: node.label };
|
|
337
|
+
}
|
|
338
|
+
return void 0;
|
|
339
|
+
}
|
|
340
|
+
function mostRecentKey(keys, mtimeOf) {
|
|
341
|
+
let best;
|
|
342
|
+
let bestMtime = -Infinity;
|
|
343
|
+
for (const key of keys) {
|
|
344
|
+
const mtime = mtimeOf(key);
|
|
345
|
+
if (mtime !== void 0 && mtime > bestMtime) {
|
|
346
|
+
best = key;
|
|
347
|
+
bestMtime = mtime;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return best ?? keys[0];
|
|
351
|
+
}
|
|
352
|
+
function flipForSequence(map) {
|
|
353
|
+
if (map.kind !== "sequence") return map;
|
|
354
|
+
return {
|
|
355
|
+
...map,
|
|
356
|
+
layers: map.layers.map((l) => ({ ...l, rank: -l.rank })),
|
|
357
|
+
edges: map.edges.map((e) => ({ from: e.to, to: e.from, ...e.label !== void 0 ? { label: e.label } : {} }))
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/render/render.ts
|
|
227
362
|
var WIDE_RANGES = [
|
|
228
363
|
[4352, 4447],
|
|
229
364
|
// Hangul Jamo
|
|
@@ -488,9 +623,6 @@ function kindGlyph(kind, unicode) {
|
|
|
488
623
|
const pair = NODE_KIND_GLYPHS[kind];
|
|
489
624
|
return pair === void 0 ? void 0 : unicode ? pair[0] : pair[1];
|
|
490
625
|
}
|
|
491
|
-
function isNeutralKind(map) {
|
|
492
|
-
return map.kind !== void 0 && map.kind !== "dev";
|
|
493
|
-
}
|
|
494
626
|
function neutralSkin(unicode) {
|
|
495
627
|
return unicode ? { h: "\u2500", v: "\u2502", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style: "none" } : { h: "-", v: "|", corners: ["+", "+", "+", "+"], style: "none" };
|
|
496
628
|
}
|
|
@@ -500,21 +632,23 @@ var LEFT_MARGIN = 2;
|
|
|
500
632
|
var DETAIL_BUDGET = { innerMin: 22, innerMax: 32, noteRows: 3 };
|
|
501
633
|
var DETAIL_PLUS_BUDGET = { innerMin: 30, innerMax: 48, noteRows: 12 };
|
|
502
634
|
function zoomGeometry(zoom) {
|
|
635
|
+
const m = zoomMode(zoom);
|
|
636
|
+
const mode = m === "overview" ? "constellation" : m;
|
|
503
637
|
switch (zoom) {
|
|
504
638
|
case 2:
|
|
505
|
-
return { mode
|
|
639
|
+
return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_PLUS_BUDGET };
|
|
506
640
|
case 1:
|
|
507
|
-
return { mode
|
|
641
|
+
return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_BUDGET };
|
|
508
642
|
case 0:
|
|
509
|
-
return { mode
|
|
643
|
+
return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
|
|
510
644
|
case -1:
|
|
511
|
-
return { mode
|
|
645
|
+
return { mode, scale: 0.85, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
|
|
512
646
|
case -2:
|
|
513
|
-
return { mode
|
|
647
|
+
return { mode, scale: 0.7, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: false };
|
|
514
648
|
case -3:
|
|
515
|
-
return { mode
|
|
649
|
+
return { mode, scale: 0.55, pad: 0, boxGap: 1, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
|
|
516
650
|
case -4:
|
|
517
|
-
return { mode
|
|
651
|
+
return { mode, scale: 0, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
|
|
518
652
|
}
|
|
519
653
|
}
|
|
520
654
|
var LABEL_BUDGET_MIN = 4;
|
|
@@ -567,41 +701,6 @@ function renderMapWindow(map, opts, viewport) {
|
|
|
567
701
|
hits: built.hits
|
|
568
702
|
};
|
|
569
703
|
}
|
|
570
|
-
function aggregateMap(map) {
|
|
571
|
-
if (map.groups.length === 0) return void 0;
|
|
572
|
-
const representative = /* @__PURE__ */ new Map();
|
|
573
|
-
for (const n of map.nodes) representative.set(n.id, n.group ?? n.id);
|
|
574
|
-
const nodes = map.groups.map((g) => {
|
|
575
|
-
const members = map.nodes.filter((n) => n.group === g.id);
|
|
576
|
-
const done = members.filter((n) => n.status === "done").length;
|
|
577
|
-
return {
|
|
578
|
-
id: g.id,
|
|
579
|
-
// neutral kinds document structure, not progress — no member counts
|
|
580
|
-
label: isNeutralKind(map) ? g.label : `${g.label} ${done}/${members.length}`,
|
|
581
|
-
layer: g.layer,
|
|
582
|
-
status: groupStatus(map, g.id)
|
|
583
|
-
};
|
|
584
|
-
});
|
|
585
|
-
for (const n of map.nodes) if (n.group === void 0) nodes.push(n);
|
|
586
|
-
const seen = /* @__PURE__ */ new Set();
|
|
587
|
-
const edges = [];
|
|
588
|
-
for (const e of map.edges) {
|
|
589
|
-
const from = representative.get(e.from);
|
|
590
|
-
const to = representative.get(e.to);
|
|
591
|
-
if (from === to || seen.has(`${from}->${to}`)) continue;
|
|
592
|
-
seen.add(`${from}->${to}`);
|
|
593
|
-
edges.push({ from, to });
|
|
594
|
-
}
|
|
595
|
-
return {
|
|
596
|
-
...map.title !== void 0 ? { title: map.title } : {},
|
|
597
|
-
...map.kind !== void 0 ? { kind: map.kind } : {},
|
|
598
|
-
layers: map.layers,
|
|
599
|
-
groups: [],
|
|
600
|
-
lanes: map.lanes,
|
|
601
|
-
nodes,
|
|
602
|
-
edges
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
704
|
var AGGREGATE_GEO = {
|
|
606
705
|
mode: "boxes",
|
|
607
706
|
scale: 1,
|
|
@@ -612,14 +711,6 @@ var AGGREGATE_GEO = {
|
|
|
612
711
|
barGap: 1,
|
|
613
712
|
bandCounts: false
|
|
614
713
|
};
|
|
615
|
-
function flipForSequence(map) {
|
|
616
|
-
if (map.kind !== "sequence") return map;
|
|
617
|
-
return {
|
|
618
|
-
...map,
|
|
619
|
-
layers: map.layers.map((l) => ({ ...l, rank: -l.rank })),
|
|
620
|
-
edges: map.edges.map((e) => ({ from: e.to, to: e.from, ...e.label !== void 0 ? { label: e.label } : {} }))
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
714
|
function buildCanvas(map, opts) {
|
|
624
715
|
const oriented = flipForSequence(map);
|
|
625
716
|
const plainGeo = zoomGeometry(opts.zoom ?? ZOOM_DEFAULT);
|
|
@@ -969,62 +1060,12 @@ function drawBox(canvas, box, opts, neutral, focused = false) {
|
|
|
969
1060
|
// src/store/store.ts
|
|
970
1061
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
971
1062
|
import { basename, dirname, join } from "node:path";
|
|
1063
|
+
|
|
1064
|
+
// src/store/format.ts
|
|
972
1065
|
var STATE_FILE_VERSION = 1;
|
|
973
|
-
var STATE_FILE_RELATIVE_PATH = join(".claude", "mellos-mapping.json");
|
|
974
|
-
var PAGES_DIR_NAME = "mellos-mapping.pages";
|
|
975
1066
|
function makePageId(raw) {
|
|
976
1067
|
return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
|
|
977
1068
|
}
|
|
978
|
-
function pageFilePath(defaultFile, page) {
|
|
979
|
-
return page === void 0 ? defaultFile : join(dirname(defaultFile), PAGES_DIR_NAME, `${page}.json`);
|
|
980
|
-
}
|
|
981
|
-
function pageIdOfFile(defaultFile, path) {
|
|
982
|
-
if (path === defaultFile) return void 0;
|
|
983
|
-
const name = basename(path);
|
|
984
|
-
return name.endsWith(".json") ? name.slice(0, -".json".length) : name;
|
|
985
|
-
}
|
|
986
|
-
function listPageFiles(defaultFile) {
|
|
987
|
-
const out = [];
|
|
988
|
-
if (existsSync(defaultFile)) out.push(defaultFile);
|
|
989
|
-
let entries = [];
|
|
990
|
-
try {
|
|
991
|
-
entries = readdirSync(join(dirname(defaultFile), PAGES_DIR_NAME));
|
|
992
|
-
} catch {
|
|
993
|
-
}
|
|
994
|
-
for (const e of entries.sort()) {
|
|
995
|
-
if (e.endsWith(".json")) out.push(join(dirname(defaultFile), PAGES_DIR_NAME, e));
|
|
996
|
-
}
|
|
997
|
-
return out;
|
|
998
|
-
}
|
|
999
|
-
var FOCUS_FILE_NAME = "mellos-mapping.focus";
|
|
1000
|
-
function focusFilePath(defaultFile) {
|
|
1001
|
-
return join(dirname(defaultFile), FOCUS_FILE_NAME);
|
|
1002
|
-
}
|
|
1003
|
-
function takeFocusRequest(defaultFile) {
|
|
1004
|
-
const path = focusFilePath(defaultFile);
|
|
1005
|
-
let raw;
|
|
1006
|
-
try {
|
|
1007
|
-
raw = readFileSync(path, "utf8");
|
|
1008
|
-
} catch {
|
|
1009
|
-
return void 0;
|
|
1010
|
-
}
|
|
1011
|
-
try {
|
|
1012
|
-
rmSync(path, { force: true });
|
|
1013
|
-
} catch {
|
|
1014
|
-
}
|
|
1015
|
-
let parsed;
|
|
1016
|
-
try {
|
|
1017
|
-
parsed = JSON.parse(raw);
|
|
1018
|
-
} catch {
|
|
1019
|
-
return void 0;
|
|
1020
|
-
}
|
|
1021
|
-
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
1022
|
-
const page = parsed.page;
|
|
1023
|
-
if (page === void 0 || page === null) return { page: void 0 };
|
|
1024
|
-
if (typeof page !== "string") return void 0;
|
|
1025
|
-
const id = makePageId(page);
|
|
1026
|
-
return id.ok ? { page: id.value } : void 0;
|
|
1027
|
-
}
|
|
1028
1069
|
function describeStoreError(e) {
|
|
1029
1070
|
switch (e.kind) {
|
|
1030
1071
|
case "not-found":
|
|
@@ -1166,6 +1207,81 @@ function parseMap(raw, path) {
|
|
|
1166
1207
|
}
|
|
1167
1208
|
return ok(map);
|
|
1168
1209
|
}
|
|
1210
|
+
|
|
1211
|
+
// src/store/store.ts
|
|
1212
|
+
var STATE_FILE_RELATIVE_PATH = join(".mellos", "map.json");
|
|
1213
|
+
var PAGES_DIR_NAME = "pages";
|
|
1214
|
+
function pageFilePath(defaultFile, page) {
|
|
1215
|
+
return page === void 0 ? defaultFile : join(dirname(defaultFile), PAGES_DIR_NAME, `${page}.json`);
|
|
1216
|
+
}
|
|
1217
|
+
function pageIdOfFile(defaultFile, path) {
|
|
1218
|
+
if (path === defaultFile) return void 0;
|
|
1219
|
+
const name = basename(path);
|
|
1220
|
+
return name.endsWith(".json") ? name.slice(0, -".json".length) : name;
|
|
1221
|
+
}
|
|
1222
|
+
function listPageFiles(defaultFile) {
|
|
1223
|
+
const out = [];
|
|
1224
|
+
if (existsSync(defaultFile)) out.push(defaultFile);
|
|
1225
|
+
let entries = [];
|
|
1226
|
+
try {
|
|
1227
|
+
entries = readdirSync(join(dirname(defaultFile), PAGES_DIR_NAME));
|
|
1228
|
+
} catch {
|
|
1229
|
+
}
|
|
1230
|
+
for (const e of entries.sort()) {
|
|
1231
|
+
if (e.endsWith(".json")) out.push(join(dirname(defaultFile), PAGES_DIR_NAME, e));
|
|
1232
|
+
}
|
|
1233
|
+
return out;
|
|
1234
|
+
}
|
|
1235
|
+
var FOCUS_FILE_NAME = "focus";
|
|
1236
|
+
function focusFilePath(defaultFile) {
|
|
1237
|
+
return join(dirname(defaultFile), FOCUS_FILE_NAME);
|
|
1238
|
+
}
|
|
1239
|
+
function takeFocusRequest(defaultFile) {
|
|
1240
|
+
const path = focusFilePath(defaultFile);
|
|
1241
|
+
let raw;
|
|
1242
|
+
try {
|
|
1243
|
+
raw = readFileSync(path, "utf8");
|
|
1244
|
+
} catch {
|
|
1245
|
+
return void 0;
|
|
1246
|
+
}
|
|
1247
|
+
try {
|
|
1248
|
+
rmSync(path, { force: true });
|
|
1249
|
+
} catch {
|
|
1250
|
+
}
|
|
1251
|
+
let parsed;
|
|
1252
|
+
try {
|
|
1253
|
+
parsed = JSON.parse(raw);
|
|
1254
|
+
} catch {
|
|
1255
|
+
return void 0;
|
|
1256
|
+
}
|
|
1257
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
1258
|
+
const page = parsed.page;
|
|
1259
|
+
if (page === void 0 || page === null) return { page: void 0 };
|
|
1260
|
+
if (typeof page !== "string") return void 0;
|
|
1261
|
+
const id = makePageId(page);
|
|
1262
|
+
return id.ok ? { page: id.value } : void 0;
|
|
1263
|
+
}
|
|
1264
|
+
var CONFIG_FILE_NAME = "config.json";
|
|
1265
|
+
function configFilePath(defaultFile) {
|
|
1266
|
+
return join(dirname(defaultFile), CONFIG_FILE_NAME);
|
|
1267
|
+
}
|
|
1268
|
+
var LEGACY_STATE_FILE_RELATIVE_PATH = join(".claude", "mellos-mapping.json");
|
|
1269
|
+
var LEGACY_PAGES_DIR_NAME = "mellos-mapping.pages";
|
|
1270
|
+
var LEGACY_CONFIG_FILE_NAME = "mellos-mapping.config.json";
|
|
1271
|
+
function migrateLegacyStore(defaultFile) {
|
|
1272
|
+
const projectRoot = dirname(dirname(defaultFile));
|
|
1273
|
+
const legacyDefault = join(projectRoot, LEGACY_STATE_FILE_RELATIVE_PATH);
|
|
1274
|
+
const legacyPages = join(dirname(legacyDefault), LEGACY_PAGES_DIR_NAME);
|
|
1275
|
+
const legacyConfig = join(dirname(legacyDefault), LEGACY_CONFIG_FILE_NAME);
|
|
1276
|
+
const hasLegacy = existsSync(legacyDefault) || existsSync(legacyPages) || existsSync(legacyConfig);
|
|
1277
|
+
const hasCurrent = existsSync(defaultFile) || existsSync(join(dirname(defaultFile), PAGES_DIR_NAME)) || existsSync(configFilePath(defaultFile));
|
|
1278
|
+
if (!hasLegacy || hasCurrent) return false;
|
|
1279
|
+
mkdirSync(dirname(defaultFile), { recursive: true });
|
|
1280
|
+
if (existsSync(legacyDefault)) renameSync(legacyDefault, defaultFile);
|
|
1281
|
+
if (existsSync(legacyPages)) renameSync(legacyPages, join(dirname(defaultFile), PAGES_DIR_NAME));
|
|
1282
|
+
if (existsSync(legacyConfig)) renameSync(legacyConfig, configFilePath(defaultFile));
|
|
1283
|
+
return true;
|
|
1284
|
+
}
|
|
1169
1285
|
function loadMapFile(path) {
|
|
1170
1286
|
let text;
|
|
1171
1287
|
try {
|
|
@@ -1320,16 +1436,7 @@ function dividerRow(width, unicode, follow) {
|
|
|
1320
1436
|
return bar;
|
|
1321
1437
|
}
|
|
1322
1438
|
function mostRecentPageFile(files, mtimeOf) {
|
|
1323
|
-
|
|
1324
|
-
let bestMtime = -Infinity;
|
|
1325
|
-
for (const file of files) {
|
|
1326
|
-
const mtime = mtimeOf(file);
|
|
1327
|
-
if (mtime !== void 0 && mtime > bestMtime) {
|
|
1328
|
-
best = file;
|
|
1329
|
-
bestMtime = mtime;
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
return best ?? files[0];
|
|
1439
|
+
return mostRecentKey(files, mtimeOf);
|
|
1333
1440
|
}
|
|
1334
1441
|
var HIDE_CURSOR = "\x1B[?25l";
|
|
1335
1442
|
var SHOW_CURSOR = "\x1B[?25h";
|
|
@@ -1417,10 +1524,7 @@ function tabScrollFor(tabs, width, unicode, scroll, index) {
|
|
|
1417
1524
|
return s;
|
|
1418
1525
|
}
|
|
1419
1526
|
function topLevelFiles(defaultFile, files, mapOf) {
|
|
1420
|
-
const refs =
|
|
1421
|
-
for (const m of mapOf.values()) {
|
|
1422
|
-
for (const n of m?.nodes ?? []) if (n.submap !== void 0) refs.add(n.submap);
|
|
1423
|
-
}
|
|
1527
|
+
const refs = submapRefs(mapOf.values());
|
|
1424
1528
|
return files.filter((f) => {
|
|
1425
1529
|
const id = pageIdOfFile(defaultFile, f);
|
|
1426
1530
|
return id === void 0 || !refs.has(id);
|
|
@@ -1429,12 +1533,8 @@ function topLevelFiles(defaultFile, files, mapOf) {
|
|
|
1429
1533
|
function diveOrigin(defaultFile, file, files, mapOf) {
|
|
1430
1534
|
const id = pageIdOfFile(defaultFile, file);
|
|
1431
1535
|
if (id === void 0) return void 0;
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
const node = mapOf.get(f)?.nodes.find((n) => n.submap === id);
|
|
1435
|
-
if (node !== void 0) return { parent: f, label: node.label };
|
|
1436
|
-
}
|
|
1437
|
-
return void 0;
|
|
1536
|
+
const entries = files.filter((f) => f !== file).map((f) => [f, mapOf.get(f)]);
|
|
1537
|
+
return diveParent(entries, id);
|
|
1438
1538
|
}
|
|
1439
1539
|
function nearestHit(hits, cx, cy) {
|
|
1440
1540
|
let best;
|
|
@@ -1451,28 +1551,14 @@ function nearestHit(hits, cx, cy) {
|
|
|
1451
1551
|
function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_ROWS) {
|
|
1452
1552
|
const g = (s) => STATUS_GLYPH[s][unicode ? 0 : 1];
|
|
1453
1553
|
const pinMark = pinned ? unicode ? " \u2299 pinned" : " * pinned" : "";
|
|
1454
|
-
const
|
|
1455
|
-
if (
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
const status
|
|
1459
|
-
const layerName2 = map.layers.find((l) => l.id === group.layer)?.name ?? group.layer;
|
|
1554
|
+
const focus = focusInfo(map, focusId);
|
|
1555
|
+
if (focus === void 0) return void 0;
|
|
1556
|
+
const refText = (r) => `${g(r.status)} ${r.label}${r.edgeLabel !== void 0 ? ` (${r.edgeLabel})` : ""}`;
|
|
1557
|
+
if (focus.kind === "group") {
|
|
1558
|
+
const { group, status, layerName: layerName2, members } = focus;
|
|
1460
1559
|
const [right2, left2] = unicode ? ["\u2192", "\u2190"] : ["->", "<-"];
|
|
1461
|
-
const
|
|
1462
|
-
|
|
1463
|
-
const owner = n.group !== void 0 ? map.groups.find((gr) => gr.id === n.group) : void 0;
|
|
1464
|
-
return owner !== void 0 ? `${g(groupStatus(map, owner.id))} ${owner.label}` : `${g(n.status)} ${n.label}`;
|
|
1465
|
-
};
|
|
1466
|
-
const uses2 = [
|
|
1467
|
-
...new Set(
|
|
1468
|
-
map.edges.filter((e) => memberIds.has(e.from) && !memberIds.has(e.to)).map((e) => repLabel(e.to))
|
|
1469
|
-
)
|
|
1470
|
-
];
|
|
1471
|
-
const usedBy2 = [
|
|
1472
|
-
...new Set(
|
|
1473
|
-
map.edges.filter((e) => memberIds.has(e.to) && !memberIds.has(e.from)).map((e) => repLabel(e.from))
|
|
1474
|
-
)
|
|
1475
|
-
];
|
|
1560
|
+
const uses2 = focus.uses.map(refText);
|
|
1561
|
+
const usedBy2 = focus.usedBy.map(refText);
|
|
1476
1562
|
const lines2 = [
|
|
1477
1563
|
{
|
|
1478
1564
|
text: fitWidth(
|
|
@@ -1491,21 +1577,13 @@ function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_RO
|
|
|
1491
1577
|
while (lines2.length < rows) lines2.push({ text: "", sgr: "" });
|
|
1492
1578
|
return lines2.slice(0, rows);
|
|
1493
1579
|
}
|
|
1494
|
-
const node
|
|
1495
|
-
if (!node) return void 0;
|
|
1580
|
+
const { node, layerName, laneLabel } = focus;
|
|
1496
1581
|
const neutral = isNeutralKind(map);
|
|
1497
|
-
const layerName = map.layers.find((l) => l.id === node.layer)?.name ?? node.layer;
|
|
1498
1582
|
const [right, left] = unicode ? ["\u2192", "\u2190"] : ["->", "<-"];
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
};
|
|
1503
|
-
const withEdgeLabel = (base, label) => label !== void 0 ? `${base} (${label})` : base;
|
|
1504
|
-
const uses = map.edges.filter((e) => e.from === node.id).map((e) => withEdgeLabel(withGlyph(e.to), e.label));
|
|
1505
|
-
const usedBy = map.edges.filter((e) => e.to === node.id).map((e) => withEdgeLabel(withGlyph(e.from), e.label));
|
|
1506
|
-
const pin = pinned ? unicode ? " \u2299 pinned" : " * pinned" : "";
|
|
1583
|
+
const uses = focus.uses.map(refText);
|
|
1584
|
+
const usedBy = focus.usedBy.map(refText);
|
|
1585
|
+
const pin = pinMark;
|
|
1507
1586
|
const headGlyph = neutral ? (node.kind !== void 0 ? kindGlyph(node.kind, unicode) : void 0) ?? (unicode ? "\xB7" : ".") : g(node.status);
|
|
1508
|
-
const laneLabel = node.lane !== void 0 ? map.lanes.find((l) => l.id === node.lane)?.label : void 0;
|
|
1509
1587
|
const headParts = [
|
|
1510
1588
|
`${headGlyph} ${node.label} [${node.id}]`,
|
|
1511
1589
|
layerName,
|
|
@@ -1672,6 +1750,7 @@ function mapPanel(map, unicode, width, rows = PANEL_CONTENT_ROWS) {
|
|
|
1672
1750
|
}
|
|
1673
1751
|
function main() {
|
|
1674
1752
|
const cfg = parseArgs(process.argv.slice(2), process.cwd());
|
|
1753
|
+
migrateLegacyStore(cfg.file);
|
|
1675
1754
|
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
1676
1755
|
const mouseActive = interactive && cfg.mouse;
|
|
1677
1756
|
let lastFrame = "";
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 0 — pure operations on a MellosMap.
|
|
3
|
+
*
|
|
4
|
+
* Every operation follows validate -> prepare -> commit: all refusals happen
|
|
5
|
+
* before any new value is built, and the commit expression can no longer
|
|
6
|
+
* fail. Inputs are never mutated; the result always carries a fresh map.
|
|
7
|
+
*
|
|
8
|
+
* These functions enforce the structural invariants I1-I9 documented in
|
|
9
|
+
* types.ts and nothing else. In particular there are no workflow rules here:
|
|
10
|
+
* any status may be set at any time, in any order. Discipline lives with the
|
|
11
|
+
* caller; this layer only keeps the map structurally true.
|
|
12
|
+
*/
|
|
13
|
+
import { type GroupId, type LaneId, type LayerId, type MapError, type MapKind, type MellosMap, type NodeId, type NodeKind, type NodeStatus, type Result, type SubmapRef } from './types.js';
|
|
14
|
+
/** Set or replace the map title. */
|
|
15
|
+
export declare function setTitle(map: MellosMap, title: string): MellosMap;
|
|
16
|
+
/** Set or replace the map kind (presentation intent — never structural). */
|
|
17
|
+
export declare function setKind(map: MellosMap, kind: MapKind): MellosMap;
|
|
18
|
+
export interface DeclareLaneInput {
|
|
19
|
+
readonly id: LaneId;
|
|
20
|
+
readonly label: string;
|
|
21
|
+
}
|
|
22
|
+
/** Add a new lane (I8). Declaration order is left-to-right render order. */
|
|
23
|
+
export declare function declareLane(map: MellosMap, input: DeclareLaneInput): Result<MellosMap, MapError>;
|
|
24
|
+
/**
|
|
25
|
+
* Remove a lane.
|
|
26
|
+
* Postcondition: former members stay on the map, merely off-lane — removing
|
|
27
|
+
* a column label never destroys work records (same contract as removeGroup).
|
|
28
|
+
*/
|
|
29
|
+
export declare function removeLane(map: MellosMap, id: LaneId): Result<MellosMap, MapError>;
|
|
30
|
+
export interface DeclareLayerInput {
|
|
31
|
+
readonly id: LayerId;
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly rank: number;
|
|
34
|
+
}
|
|
35
|
+
/** Add a new band. Refuses duplicate ids and duplicate ranks (I1). */
|
|
36
|
+
export declare function declareLayer(map: MellosMap, input: DeclareLayerInput): Result<MellosMap, MapError>;
|
|
37
|
+
export interface DeclareGroupInput {
|
|
38
|
+
readonly id: GroupId;
|
|
39
|
+
readonly label: string;
|
|
40
|
+
readonly layer: LayerId;
|
|
41
|
+
}
|
|
42
|
+
/** Add a new group to an existing band (I6). */
|
|
43
|
+
export declare function declareGroup(map: MellosMap, input: DeclareGroupInput): Result<MellosMap, MapError>;
|
|
44
|
+
/** Rename a group. */
|
|
45
|
+
export declare function updateGroup(map: MellosMap, id: GroupId, label: string): Result<MellosMap, MapError>;
|
|
46
|
+
/**
|
|
47
|
+
* Remove a group.
|
|
48
|
+
* Postcondition: former members stay on the map, merely ungrouped — removing
|
|
49
|
+
* a cluster label never destroys work records.
|
|
50
|
+
*/
|
|
51
|
+
export declare function removeGroup(map: MellosMap, id: GroupId): Result<MellosMap, MapError>;
|
|
52
|
+
/**
|
|
53
|
+
* Derived, never stored: a group's aggregate status. Any regressed member
|
|
54
|
+
* cracks the group; else any spinner spins it; else all-done (non-empty)
|
|
55
|
+
* completes it; anything else is planned.
|
|
56
|
+
*/
|
|
57
|
+
export declare function groupStatus(map: MellosMap, id: GroupId): NodeStatus;
|
|
58
|
+
/** Derived, never stored: the whole map's aggregate status (same rules as groupStatus). */
|
|
59
|
+
export declare function mapStatus(map: MellosMap): NodeStatus;
|
|
60
|
+
export interface DeclareNodeInput {
|
|
61
|
+
readonly id: NodeId;
|
|
62
|
+
readonly label: string;
|
|
63
|
+
readonly layer: LayerId;
|
|
64
|
+
readonly status?: NodeStatus;
|
|
65
|
+
readonly detail?: string;
|
|
66
|
+
readonly group?: GroupId;
|
|
67
|
+
readonly kind?: NodeKind;
|
|
68
|
+
readonly lane?: LaneId;
|
|
69
|
+
readonly submap?: SubmapRef;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Add a new node to an existing band (I2, I3), optionally joining a same-band
|
|
73
|
+
* group (I7) and/or an existing lane (I9).
|
|
74
|
+
*/
|
|
75
|
+
export declare function declareNode(map: MellosMap, input: DeclareNodeInput): Result<MellosMap, MapError>;
|
|
76
|
+
/**
|
|
77
|
+
* Add the dependency edge `from USES to`, optionally labeled with what flows
|
|
78
|
+
* along it. Refuses self-edges, duplicates and any edge that does not point
|
|
79
|
+
* strictly downward (I4).
|
|
80
|
+
*/
|
|
81
|
+
export declare function linkNodes(map: MellosMap, from: NodeId, to: NodeId, label?: string): Result<MellosMap, MapError>;
|
|
82
|
+
export interface UpdateNodeInput {
|
|
83
|
+
readonly id: NodeId;
|
|
84
|
+
readonly status?: NodeStatus;
|
|
85
|
+
readonly label?: string;
|
|
86
|
+
readonly evidence?: string;
|
|
87
|
+
readonly detail?: string;
|
|
88
|
+
/** A GroupId joins that group (I7 validated); null leaves the current group. */
|
|
89
|
+
readonly group?: GroupId | null;
|
|
90
|
+
/** A NodeKind sets the presentation kind; null clears it. */
|
|
91
|
+
readonly kind?: NodeKind | null;
|
|
92
|
+
/** A LaneId joins that lane (I9 validated); null leaves the current lane. */
|
|
93
|
+
readonly lane?: LaneId | null;
|
|
94
|
+
/** A SubmapRef links a child map page; null unlinks it. */
|
|
95
|
+
readonly submap?: SubmapRef | null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Update a node's status, label, evidence, design detail, group membership,
|
|
99
|
+
* kind and/or lane. Absent fields are left untouched. No transition rules:
|
|
100
|
+
* the ledger records whatever the caller reports, whenever they report it.
|
|
101
|
+
*/
|
|
102
|
+
export declare function updateNode(map: MellosMap, input: UpdateNodeInput): Result<MellosMap, MapError>;
|
|
103
|
+
/**
|
|
104
|
+
* Remove a node.
|
|
105
|
+
* Postcondition (explicit part of this contract): every edge touching the
|
|
106
|
+
* node is removed with it — a map never holds edges to missing nodes.
|
|
107
|
+
*/
|
|
108
|
+
export declare function removeNode(map: MellosMap, id: NodeId): Result<MellosMap, MapError>;
|
|
109
|
+
/** Remove one dependency edge. */
|
|
110
|
+
export declare function removeEdge(map: MellosMap, from: NodeId, to: NodeId): Result<MellosMap, MapError>;
|
|
111
|
+
/** Remove a band. Only empty bands may go — neither a node (I2) nor a group (I6) may be orphaned. */
|
|
112
|
+
export declare function removeLayer(map: MellosMap, id: LayerId): Result<MellosMap, MapError>;
|