mellos-mapping 0.18.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
@@ -3,7 +3,7 @@ import { createRequire } from 'node:module'; const require = createRequire(impor
3
3
 
4
4
  // src/watch/watch.ts
5
5
  import { realpathSync, statSync } from "node:fs";
6
- import { join as join2 } from "node:path";
6
+ import { dirname as dirname2, join as join2 } from "node:path";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
8
 
9
9
  // src/domain/types.ts
@@ -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,
@@ -1535,19 +1613,8 @@ function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_RO
1535
1613
  }
1536
1614
  return lines.slice(0, rows);
1537
1615
  }
1538
- var SPLASH_FONT = {
1539
- M: ["# #", "## ##", "# # #", "# #", "# #"],
1540
- E: ["####", "#", "###", "#", "####"],
1541
- L: ["#", "#", "#", "#", "####"],
1542
- O: [" ###", "# #", "# #", "# #", " ###"],
1543
- S: [" ####", "#", " ###", " #", "####"],
1544
- A: [" ###", "# #", "#####", "# #", "# #"],
1545
- P: ["####", "# #", "####", "#", "#"],
1546
- I: ["###", " #", " #", " #", "###"],
1547
- N: ["# #", "## #", "# # #", "# ##", "# #"],
1548
- G: [" ####", "#", "# ##", "# #", " ###"]
1549
- };
1550
- var SPLASH_ROWS = 5;
1616
+ var WATER_ROWS = 7;
1617
+ var WATER_COLS_MAX = 60;
1551
1618
  var SPLASH_SHADES = {
1552
1619
  unicode: ["\u2591", "\u2591", "\u2592", "\u2592", "\u2593", "\u2593", "\u2588", "\u2588"],
1553
1620
  ascii: [".", ".", ":", ":", "=", "=", "#", "#"]
@@ -1557,23 +1624,24 @@ var SPINNER_FRAMES = {
1557
1624
  unicode: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
1558
1625
  ascii: ["|", "/", "-", "\\"]
1559
1626
  };
1560
- function wordArt(word) {
1561
- const glyphs = [...word.toUpperCase()].map((ch) => SPLASH_FONT[ch]).filter((g) => g !== void 0);
1562
- const widths = glyphs.map((g) => Math.max(...g.map((r) => r.length)));
1563
- const rows = [];
1564
- for (let r = 0; r < SPLASH_ROWS; r++) {
1565
- rows.push(glyphs.map((g, i) => (g[r] ?? "").padEnd(widths[i], " ")).join(" "));
1566
- }
1567
- return rows;
1568
- }
1569
- function splashArt() {
1570
- const words = [wordArt("MELLOS"), wordArt("MAPPING")];
1571
- const width = Math.max(...words.flat().map((r) => r.length));
1572
- const centered = words.map((rows) => {
1573
- const own = Math.max(...rows.map((r) => r.length));
1574
- return rows.map((r) => " ".repeat(Math.floor((width - own) / 2)) + r);
1575
- });
1576
- return [...centered[0], "", ...centered[1]];
1627
+ function elapsedLabel(ms) {
1628
+ const s = Math.max(0, Math.floor(ms / 1e3));
1629
+ const m = Math.floor(s / 60);
1630
+ const h = Math.floor(m / 60);
1631
+ const two = (n) => String(n).padStart(2, "0");
1632
+ return h > 0 ? `${h}:${two(m % 60)}:${two(s % 60)}` : `${m}:${two(s % 60)}`;
1633
+ }
1634
+ function waitingInfo(s, width) {
1635
+ const w = Math.max(1, width);
1636
+ const clock = s.elapsedMs !== void 0 ? ` \xB7 waiting ${elapsedLabel(s.elapsedMs)}` : "";
1637
+ const lines = [
1638
+ `watching ${s.defaultFile}`,
1639
+ ` and ${join2(s.pagesDir, "*.json")}`,
1640
+ `polling every ${s.intervalMs} ms${clock}`
1641
+ ];
1642
+ for (const b of s.broken) lines.push(`! ${b}`);
1643
+ lines.push("the map appears at the first mmap_declare");
1644
+ return lines.map((l) => fitWidth(l, w));
1577
1645
  }
1578
1646
  var WAVE_INTERVAL = 18;
1579
1647
  var WAVE_LIFETIME = 64;
@@ -1624,39 +1692,41 @@ function waveAt(ripples, x, y) {
1624
1692
  function waveLevel(value) {
1625
1693
  return Math.max(-WAVE_LEVELS, Math.min(WAVE_LEVELS, Math.round(value * WAVE_GAIN)));
1626
1694
  }
1627
- function splashFrame(notice, frame, width, height, unicode, color) {
1628
- const art = splashArt();
1629
- const artWidth = Math.max(...art.map((r) => r.length));
1630
- if (width < artWidth + 2 || height < art.length + 2) return void 0;
1695
+ function splashFrame(notice, info, frame, width, height, unicode, color) {
1696
+ const fieldW = Math.min(width - 4, WATER_COLS_MAX);
1697
+ if (fieldW < 24 || height < WATER_ROWS + info.length + 3) return void 0;
1631
1698
  const mode = unicode ? "unicode" : "ascii";
1632
1699
  const shades = SPLASH_SHADES[mode];
1633
- const solid = shades[shades.length - 1];
1634
- const indent = " ".repeat(Math.floor((width - artWidth) / 2));
1635
- const ripples = liveRipples(frame, artWidth, art.length);
1636
- const inkOf = (x, y) => {
1637
- const level = waveLevel(waveAt(ripples, x, y));
1638
- return color ? `38;5;${WAVE_RAMP[WAVE_LEVELS + level]}` : shades[Math.abs(level)];
1639
- };
1640
- const paintRow = (row, y) => {
1641
- const cells = [...row].map((ch, x) => ch === "#" ? inkOf(x, y) : void 0);
1700
+ const indent = " ".repeat(Math.max(0, Math.floor((width - fieldW) / 2)));
1701
+ const ripples = liveRipples(frame, fieldW, WATER_ROWS);
1702
+ const paintRow = (y) => {
1703
+ const levels = Array.from({ length: fieldW }, (_, x) => waveLevel(waveAt(ripples, x, y)));
1642
1704
  let out = "";
1643
- for (let i = 0; i < cells.length; ) {
1644
- const cell = cells[i];
1705
+ for (let i = 0; i < fieldW; ) {
1706
+ const level = levels[i];
1645
1707
  let j = i;
1646
- while (j < cells.length && cells[j] === cell) j++;
1647
- if (cell === void 0) out += " ".repeat(j - i);
1648
- else out += color ? `\x1B[${cell}m${solid.repeat(j - i)}${RESET}` : cell.repeat(j - i);
1708
+ while (j < fieldW && levels[j] === level) j++;
1709
+ if (level === 0) out += " ".repeat(j - i);
1710
+ else {
1711
+ const ink = shades[Math.abs(level)].repeat(j - i);
1712
+ out += color ? `\x1B[38;5;${WAVE_RAMP[WAVE_LEVELS + level]}m${ink}${RESET}` : ink;
1713
+ }
1649
1714
  i = j;
1650
1715
  }
1651
1716
  return out;
1652
1717
  };
1718
+ const dim = (s) => color ? `\x1B[90m${s}${RESET}` : s;
1653
1719
  const spinner = SPINNER_FRAMES[mode];
1654
1720
  const status = fitWidth(`${spinner[frame % spinner.length]} ${notice}`, Math.max(1, width - 2));
1655
1721
  const statusIndent = " ".repeat(Math.max(0, Math.floor((width - displayWidth(status)) / 2)));
1722
+ const infoWidth = Math.max(0, ...info.map((l) => displayWidth(l)));
1723
+ const infoIndent = " ".repeat(Math.max(0, Math.floor((width - infoWidth) / 2)));
1656
1724
  const block = [
1657
- ...art.map((row, y) => row.trim() === "" ? "" : indent + paintRow(row, y)),
1725
+ ...Array.from({ length: WATER_ROWS }, (_, y) => indent + paintRow(y)),
1658
1726
  "",
1659
- statusIndent + (color ? `\x1B[90m${status}${RESET}` : status)
1727
+ statusIndent + dim(status),
1728
+ "",
1729
+ ...info.map((l) => infoIndent + dim(l))
1660
1730
  ];
1661
1731
  return [...Array.from({ length: Math.max(0, Math.floor((height - block.length) / 2)) }, () => ""), ...block];
1662
1732
  }
@@ -1680,13 +1750,16 @@ function mapPanel(map, unicode, width, rows = PANEL_CONTENT_ROWS) {
1680
1750
  }
1681
1751
  function main() {
1682
1752
  const cfg = parseArgs(process.argv.slice(2), process.cwd());
1753
+ migrateLegacyStore(cfg.file);
1683
1754
  const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
1684
1755
  const mouseActive = interactive && cfg.mouse;
1685
1756
  let lastFrame = "";
1686
1757
  let spinnerFrame = 0;
1687
1758
  let splashTick = 0;
1759
+ const startedAt = Date.now();
1760
+ const standbyNotice = "waiting for the first mmap_declare ...";
1688
1761
  let map;
1689
- let notice = `waiting for ${cfg.file} ...`;
1762
+ let notice = standbyNotice;
1690
1763
  let lastCols = process.stdout.columns ?? 0;
1691
1764
  let lastRows = process.stdout.rows ?? 0;
1692
1765
  let pageFiles = [cfg.file];
@@ -1755,7 +1828,7 @@ function main() {
1755
1828
  const entry = pageData.get(file);
1756
1829
  if (entry !== void 0 && entry.fresh) pageData.set(file, { ...entry, fresh: false });
1757
1830
  map = entry?.map;
1758
- notice = map === void 0 ? `waiting for ${file} ...` : "";
1831
+ notice = map !== void 0 ? "" : entry?.error ?? (file === cfg.file ? standbyNotice : `waiting for ${file} ...`);
1759
1832
  const top = topFiles();
1760
1833
  const tabIndex = top.indexOf(file);
1761
1834
  if (tabIndex >= 0) tabScroll = tabScrollFor(pageTabsOf(top), viewWidth(), cfg.unicode, tabScroll, tabIndex);
@@ -1813,7 +1886,17 @@ function main() {
1813
1886
  lastContent = { w: windowed.contentWidth, h: windowed.contentHeight };
1814
1887
  if (offsetX !== 0 || offsetY !== 0) panned = ` (+${offsetX},+${offsetY})`;
1815
1888
  } else {
1816
- body = (interactive ? splashFrame(notice, splashTick, viewW, viewH, cfg.unicode, cfg.color) : void 0) ?? [fitWidth(notice, viewW)];
1889
+ const info = waitingInfo(
1890
+ {
1891
+ defaultFile: cfg.file,
1892
+ pagesDir: join2(dirname2(cfg.file), PAGES_DIR_NAME),
1893
+ intervalMs: cfg.intervalMs,
1894
+ elapsedMs: interactive ? Date.now() - startedAt : void 0,
1895
+ broken: [...pageData.values()].flatMap((e) => e.map === void 0 && e.error !== void 0 ? [e.error] : [])
1896
+ },
1897
+ Math.max(1, viewW - 2)
1898
+ );
1899
+ body = (interactive ? splashFrame(notice, info, splashTick, viewW, viewH, cfg.unicode, cfg.color) : void 0) ?? [fitWidth(notice, viewW), "", ...info.map((l) => fitWidth(` ${l}`, viewW))];
1817
1900
  }
1818
1901
  if (notice !== "" && map !== void 0) {
1819
1902
  body[body.length - 1] = fitWidth(` ${notice}`, viewW);
@@ -1917,8 +2000,15 @@ function main() {
1917
2000
  flash = { text: `${cfg.unicode ? "\u229E " : ""}${title} updated`, until: Date.now() + 4e3 };
1918
2001
  }
1919
2002
  } else if (loaded.error.kind === "malformed-json") {
2003
+ pageData.set(file, {
2004
+ map: entry?.map,
2005
+ mtimeMs: entry?.mtimeMs ?? -1,
2006
+ fresh: entry?.fresh ?? false,
2007
+ error: describeStoreError(loaded.error)
2008
+ });
2009
+ if (file === activeFile && entry?.map === void 0) notice = describeStoreError(loaded.error);
1920
2010
  } else {
1921
- pageData.set(file, { map: entry?.map, mtimeMs, fresh: entry?.fresh ?? false });
2011
+ pageData.set(file, { map: entry?.map, mtimeMs, fresh: entry?.fresh ?? false, error: describeStoreError(loaded.error) });
1922
2012
  if (file === activeFile) notice = describeStoreError(loaded.error);
1923
2013
  }
1924
2014
  }
@@ -2143,6 +2233,7 @@ export {
2143
2233
  clampPanelRows,
2144
2234
  diveOrigin,
2145
2235
  dividerRow,
2236
+ elapsedLabel,
2146
2237
  fitWidth,
2147
2238
  launchedAsEntry,
2148
2239
  liveRipples,
@@ -2153,14 +2244,13 @@ export {
2153
2244
  pageTabRow,
2154
2245
  panelRowsFromDividerY,
2155
2246
  parseArgs,
2156
- splashArt,
2157
2247
  splashFrame,
2158
2248
  tabScrollFor,
2159
2249
  topLevelFiles,
2160
2250
  usableColumns,
2251
+ waitingInfo,
2161
2252
  waveAt,
2162
2253
  waveHash,
2163
2254
  waveLevel,
2164
- wordArt,
2165
2255
  wrapWidth
2166
2256
  };