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/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/render/render.ts
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: "detail", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_PLUS_BUDGET };
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: "detail", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_BUDGET };
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: "boxes", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
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: "boxes", scale: 0.85, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
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: "boxes", scale: 0.7, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: false };
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: "boxes", scale: 0.55, pad: 0, boxGap: 1, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
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: "constellation", scale: 0, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
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
- let best;
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 = /* @__PURE__ */ new Set();
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
- for (const f of files) {
1433
- if (f === file) continue;
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 group = map.groups.find((gr) => gr.id === focusId);
1455
- if (group) {
1456
- const members = map.nodes.filter((n) => n.group === group.id);
1457
- const memberIds = new Set(members.map((n) => n.id));
1458
- const status = groupStatus(map, group.id);
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 repLabel = (id) => {
1462
- const n = map.nodes.find((x) => x.id === id);
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 = map.nodes.find((n) => n.id === focusId);
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 withGlyph = (id) => {
1500
- const n = map.nodes.find((x) => x.id === id);
1501
- return n ? `${g(n.status)} ${n.label}` : id;
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>;