mellos-mapping 0.22.1 → 0.24.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
@@ -2,8 +2,8 @@
2
2
  import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
3
3
 
4
4
  // src/watch/watch.ts
5
- import { realpathSync, statSync as statSync2 } from "node:fs";
6
- import { dirname as dirname7, join as join6 } from "node:path";
5
+ import { realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
6
+ import { dirname as dirname9, join as join8 } from "node:path";
7
7
 
8
8
  // src/domain/types.ts
9
9
  var ok = (value) => ({ ok: true, value });
@@ -234,6 +234,392 @@ function updateNode(map, input) {
234
234
  return ok({ ...map, nodes: map.nodes.map((n) => n.id === input.id ? updated : n) });
235
235
  }
236
236
 
237
+ // src/store/project.ts
238
+ import { existsSync, realpathSync } from "node:fs";
239
+ import { dirname, join, resolve } from "node:path";
240
+ import { homedir, tmpdir } from "node:os";
241
+ function resolveProjectDirectory(cwd, stopAt = [homedir(), tmpdir()]) {
242
+ let start = resolve(cwd);
243
+ try {
244
+ start = realpathSync(start);
245
+ } catch {
246
+ }
247
+ let dir = start;
248
+ const boundaries = new Set(stopAt.map((path) => {
249
+ try {
250
+ return realpathSync(path);
251
+ } catch {
252
+ return resolve(path);
253
+ }
254
+ }));
255
+ while (true) {
256
+ if (dir !== start && boundaries.has(dir)) return start;
257
+ if (existsSync(join(dir, ".git")) || existsSync(join(dir, ".mellos", "map.json")) || existsSync(join(dir, ".mellos", "pages"))) return dir;
258
+ const parent = dirname(dir);
259
+ if (parent === dir) return start;
260
+ dir = parent;
261
+ }
262
+ }
263
+
264
+ // src/store/transaction.ts
265
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
266
+ import { dirname as dirname2, join as join2 } from "node:path";
267
+ import { randomUUID, createHash } from "node:crypto";
268
+
269
+ // src/domain/context.ts
270
+ function sourceError(raw) {
271
+ if (!Array.isArray(raw) || raw.length > 100) return "sources must be an array of at most 100 file references";
272
+ for (const item of raw) {
273
+ if (!item || typeof item !== "object" || Array.isArray(item)) return "source must be an object";
274
+ const s = item;
275
+ if (Object.keys(s).some((k) => k !== "path" && k !== "sha256")) return "unknown source field";
276
+ if (typeof s.path !== "string" || s.path.length > 1024 || !s.path || /[\u0000-\u001f\u007f-\u009f\\:]/.test(s.path) || s.path.startsWith("/") || s.path.split("/").some((p) => !p || p === "." || p === "..")) return "source path must be relative to the project, with forward slashes and no traversal";
277
+ if (s.sha256 !== void 0 && (typeof s.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(s.sha256))) return "source sha256 must be a lowercase SHA256 hash";
278
+ }
279
+ return void 0;
280
+ }
281
+ function contextError(raw) {
282
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return "context must be an object";
283
+ for (const [key, value] of Object.entries(raw)) {
284
+ if (key !== "summary" && key !== "next") return "unknown context field";
285
+ if (typeof value !== "string" || value.length > 2e3 || /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/.test(value)) return "context fields must be text of at most 2000 characters";
286
+ }
287
+ return void 0;
288
+ }
289
+
290
+ // src/domain/text.ts
291
+ var NO_CONTROLS = /^[^\u0000-\u001f\u007f-\u009f]*$/;
292
+ var NO_CONTROLS_TEXT = "one line of text; control characters (ESC, newline, tab) are not allowed";
293
+ var NO_CONTROLS_BUT_BREAKS = /^[^\u0000-\u0008\u000b-\u001f\u007f-\u009f]*$/;
294
+ var NO_CONTROLS_BUT_BREAKS_TEXT = "text with optional newlines (\\n) and tabs; other control characters (ESC, BEL, CR) are not allowed";
295
+ function mapTextError(map) {
296
+ if (map.context !== void 0) {
297
+ const error2 = contextError(map.context);
298
+ if (error2) return error2;
299
+ }
300
+ const check = (field, value, multiline = false) => value === void 0 || (multiline ? NO_CONTROLS_BUT_BREAKS : NO_CONTROLS).test(value) ? void 0 : `${field}: ${multiline ? NO_CONTROLS_BUT_BREAKS_TEXT : NO_CONTROLS_TEXT}`;
301
+ let error = check("title", map.title);
302
+ if (error) return error;
303
+ for (const [i, layer] of map.layers.entries()) {
304
+ error = check(`layers[${i}].name`, layer.name);
305
+ if (error) return error;
306
+ }
307
+ for (const name of ["lanes", "groups"]) {
308
+ for (const [i, item] of map[name].entries()) {
309
+ error = check(`${name}[${i}].label`, item.label);
310
+ if (error) return error;
311
+ }
312
+ }
313
+ for (const [i, node] of map.nodes.entries()) {
314
+ if (node.sources !== void 0) {
315
+ const error2 = sourceError(node.sources);
316
+ if (error2) return `nodes[${i}]: ${error2}`;
317
+ }
318
+ for (const name of ["label", "evidence", "detail"]) {
319
+ error = check(`nodes[${i}].${name}`, node[name], name !== "label");
320
+ if (error) return error;
321
+ }
322
+ }
323
+ for (const [i, edge] of map.edges.entries()) {
324
+ error = check(`edges[${i}].label`, edge.label);
325
+ if (error) return error;
326
+ }
327
+ return void 0;
328
+ }
329
+ function terminalText(text, multiline = false) {
330
+ return text.replace(multiline ? /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g : /[\u0000-\u001f\u007f-\u009f]/g, "?");
331
+ }
332
+
333
+ // src/store/format.ts
334
+ var STATE_FILE_VERSION = 1;
335
+ function makePageId(raw) {
336
+ return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
337
+ }
338
+ function describeStoreError(e) {
339
+ switch (e.kind) {
340
+ case "not-found":
341
+ return `no map file at ${e.path}`;
342
+ case "malformed-json":
343
+ return `map file ${e.path} is not valid JSON: ${e.detail}`;
344
+ case "bad-shape":
345
+ return `map file ${e.path} has an unexpected shape: ${e.detail}`;
346
+ case "invariant-violation":
347
+ return `map file ${e.path} violates a structural invariant: ${describeMapError(e.violation)}`;
348
+ case "save-failed":
349
+ return `could not write ${e.path}: ${e.detail}`;
350
+ case "delete-failed":
351
+ return `could not delete ${e.path}: ${e.detail}`;
352
+ }
353
+ }
354
+ function isRecord(v) {
355
+ return typeof v === "object" && v !== null && !Array.isArray(v);
356
+ }
357
+ function describeValue(v) {
358
+ if (v === void 0) return "missing";
359
+ if (v === null) return "null";
360
+ if (Array.isArray(v)) return "an array";
361
+ return `a ${typeof v}`;
362
+ }
363
+ function badShape(path, where, expected, got) {
364
+ return err({ kind: "bad-shape", path, detail: `${where} is ${describeValue(got)}, expected ${expected}` });
365
+ }
366
+ function arrayField(raw, key, path, presence) {
367
+ const v = raw[key];
368
+ if (Array.isArray(v)) return ok(v);
369
+ if (v === void 0 && presence === "optional") return ok([]);
370
+ return badShape(path, `"${key}"`, "an array", v);
371
+ }
372
+ function requiredString(rec, key, where, path) {
373
+ const v = rec[key];
374
+ return typeof v === "string" ? ok(v) : badShape(path, `${where}.${key}`, "a string", v);
375
+ }
376
+ function optionalString(rec, key, where, path) {
377
+ const v = rec[key];
378
+ if (v === void 0) return ok(void 0);
379
+ return typeof v === "string" ? ok(v) : badShape(path, `${where}.${key}`, "a string", v);
380
+ }
381
+ function parseMap(raw, path) {
382
+ if (!isRecord(raw)) return err({ kind: "bad-shape", path, detail: "root is not an object" });
383
+ if (raw["version"] !== STATE_FILE_VERSION && raw["version"] !== 2) {
384
+ return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${STATE_FILE_VERSION} or 2` });
385
+ }
386
+ const layers = arrayField(raw, "layers", path, "required");
387
+ if (!layers.ok) return layers;
388
+ const nodes = arrayField(raw, "nodes", path, "required");
389
+ if (!nodes.ok) return nodes;
390
+ const edges = arrayField(raw, "edges", path, "required");
391
+ if (!edges.ok) return edges;
392
+ const lanes = arrayField(raw, "lanes", path, "optional");
393
+ if (!lanes.ok) return lanes;
394
+ const groups = arrayField(raw, "groups", path, "optional");
395
+ if (!groups.ok) return groups;
396
+ let map = EMPTY_MAP;
397
+ if (raw["context"] !== void 0) {
398
+ const error = contextError(raw["context"]);
399
+ if (error) return err({ kind: "bad-shape", path, detail: error });
400
+ map = { ...map, context: raw["context"] };
401
+ }
402
+ const title = optionalString(raw, "title", "map", path);
403
+ if (!title.ok) return title;
404
+ if (title.value !== void 0) map = setTitle(map, title.value);
405
+ const rawKind = optionalString(raw, "kind", "map", path);
406
+ if (!rawKind.ok) return rawKind;
407
+ if (rawKind.value !== void 0) {
408
+ const kind = makeMapKind(rawKind.value);
409
+ if (!kind.ok) return err({ kind: "invariant-violation", path, violation: kind.error });
410
+ map = setKind(map, kind.value);
411
+ }
412
+ for (const [i, rawLayer] of layers.value.entries()) {
413
+ const where = `layers[${i}]`;
414
+ if (!isRecord(rawLayer)) return badShape(path, where, "an object", rawLayer);
415
+ const rawId = requiredString(rawLayer, "id", where, path);
416
+ if (!rawId.ok) return rawId;
417
+ const id = makeLayerId(rawId.value);
418
+ if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
419
+ const name = requiredString(rawLayer, "name", where, path);
420
+ if (!name.ok) return name;
421
+ const rawRank = rawLayer["rank"];
422
+ if (typeof rawRank !== "number") return badShape(path, `${where}.rank`, "a number", rawRank);
423
+ const rank = makeRank(rawRank);
424
+ if (!rank.ok) return err({ kind: "invariant-violation", path, violation: rank.error });
425
+ const next = declareLayer(map, { id: id.value, name: name.value, rank: rank.value });
426
+ if (!next.ok) return err({ kind: "invariant-violation", path, violation: next.error });
427
+ map = next.value;
428
+ }
429
+ for (const [i, rawLane] of lanes.value.entries()) {
430
+ const where = `lanes[${i}]`;
431
+ if (!isRecord(rawLane)) return badShape(path, where, "an object", rawLane);
432
+ const rawId = requiredString(rawLane, "id", where, path);
433
+ if (!rawId.ok) return rawId;
434
+ const id = makeLaneId(rawId.value);
435
+ if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
436
+ const label = requiredString(rawLane, "label", where, path);
437
+ if (!label.ok) return label;
438
+ const declared = declareLane(map, { id: id.value, label: label.value });
439
+ if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
440
+ map = declared.value;
441
+ }
442
+ for (const [i, rawGroup] of groups.value.entries()) {
443
+ const where = `groups[${i}]`;
444
+ if (!isRecord(rawGroup)) return badShape(path, where, "an object", rawGroup);
445
+ const rawId = requiredString(rawGroup, "id", where, path);
446
+ if (!rawId.ok) return rawId;
447
+ const id = makeGroupId(rawId.value);
448
+ if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
449
+ const rawLayer = requiredString(rawGroup, "layer", where, path);
450
+ if (!rawLayer.ok) return rawLayer;
451
+ const layer = makeLayerId(rawLayer.value);
452
+ if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
453
+ const label = requiredString(rawGroup, "label", where, path);
454
+ if (!label.ok) return label;
455
+ const declared = declareGroup(map, { id: id.value, label: label.value, layer: layer.value });
456
+ if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
457
+ map = declared.value;
458
+ }
459
+ for (const [i, rawNode] of nodes.value.entries()) {
460
+ const where = `nodes[${i}]`;
461
+ if (!isRecord(rawNode)) return badShape(path, where, "an object", rawNode);
462
+ const rawId = requiredString(rawNode, "id", where, path);
463
+ if (!rawId.ok) return rawId;
464
+ const id = makeNodeId(rawId.value);
465
+ if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
466
+ const rawLayer = requiredString(rawNode, "layer", where, path);
467
+ if (!rawLayer.ok) return rawLayer;
468
+ const layer = makeLayerId(rawLayer.value);
469
+ if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
470
+ const rawStatus = requiredString(rawNode, "status", where, path);
471
+ if (!rawStatus.ok) return rawStatus;
472
+ const status = makeNodeStatus(rawStatus.value);
473
+ if (!status.ok) return err({ kind: "invariant-violation", path, violation: status.error });
474
+ const label = requiredString(rawNode, "label", where, path);
475
+ if (!label.ok) return label;
476
+ const detail = optionalString(rawNode, "detail", where, path);
477
+ if (!detail.ok) return detail;
478
+ const rawGroup = optionalString(rawNode, "group", where, path);
479
+ if (!rawGroup.ok) return rawGroup;
480
+ let group;
481
+ if (rawGroup.value !== void 0) {
482
+ const made = makeGroupId(rawGroup.value);
483
+ if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
484
+ group = made.value;
485
+ }
486
+ const rawNodeKind = optionalString(rawNode, "kind", where, path);
487
+ if (!rawNodeKind.ok) return rawNodeKind;
488
+ let nodeKind;
489
+ if (rawNodeKind.value !== void 0) {
490
+ const made = makeNodeKind(rawNodeKind.value);
491
+ if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
492
+ nodeKind = made.value;
493
+ }
494
+ const rawLane = optionalString(rawNode, "lane", where, path);
495
+ if (!rawLane.ok) return rawLane;
496
+ let lane;
497
+ if (rawLane.value !== void 0) {
498
+ const made = makeLaneId(rawLane.value);
499
+ if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
500
+ lane = made.value;
501
+ }
502
+ const rawSubmap = optionalString(rawNode, "submap", where, path);
503
+ if (!rawSubmap.ok) return rawSubmap;
504
+ let submap;
505
+ if (rawSubmap.value !== void 0) {
506
+ const made = makeSubmapRef(rawSubmap.value);
507
+ if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
508
+ submap = made.value;
509
+ }
510
+ const declared = declareNode(map, {
511
+ id: id.value,
512
+ label: label.value,
513
+ layer: layer.value,
514
+ status: status.value,
515
+ ...detail.value !== void 0 ? { detail: detail.value } : {},
516
+ ...group !== void 0 ? { group } : {},
517
+ ...nodeKind !== void 0 ? { kind: nodeKind } : {},
518
+ ...lane !== void 0 ? { lane } : {},
519
+ ...submap !== void 0 ? { submap } : {}
520
+ });
521
+ if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
522
+ map = declared.value;
523
+ const evidence = optionalString(rawNode, "evidence", where, path);
524
+ if (!evidence.ok) return evidence;
525
+ if (evidence.value !== void 0) {
526
+ const updated = updateNode(map, { id: id.value, evidence: evidence.value });
527
+ if (!updated.ok) return err({ kind: "invariant-violation", path, violation: updated.error });
528
+ map = updated.value;
529
+ }
530
+ if (rawNode["sources"] !== void 0) {
531
+ const error = sourceError(rawNode["sources"]);
532
+ if (error) return err({ kind: "bad-shape", path, detail: `${where}: ${error}` });
533
+ map = { ...map, nodes: map.nodes.map((n) => n.id === id.value ? { ...n, sources: rawNode["sources"] } : n) };
534
+ }
535
+ }
536
+ for (const [i, rawEdge] of edges.value.entries()) {
537
+ const where = `edges[${i}]`;
538
+ if (!isRecord(rawEdge)) return badShape(path, where, "an object", rawEdge);
539
+ const rawFrom = requiredString(rawEdge, "from", where, path);
540
+ if (!rawFrom.ok) return rawFrom;
541
+ const from = makeNodeId(rawFrom.value);
542
+ if (!from.ok) return err({ kind: "invariant-violation", path, violation: from.error });
543
+ const rawTo = requiredString(rawEdge, "to", where, path);
544
+ if (!rawTo.ok) return rawTo;
545
+ const to = makeNodeId(rawTo.value);
546
+ if (!to.ok) return err({ kind: "invariant-violation", path, violation: to.error });
547
+ const label = optionalString(rawEdge, "label", where, path);
548
+ if (!label.ok) return label;
549
+ const linked = linkNodes(map, from.value, to.value, label.value);
550
+ if (!linked.ok) return err({ kind: "invariant-violation", path, violation: linked.error });
551
+ map = linked.value;
552
+ }
553
+ const textError = mapTextError(map);
554
+ return textError ? err({ kind: "bad-shape", path, detail: textError }) : ok(map);
555
+ }
556
+
557
+ // src/store/transaction.ts
558
+ var LedgerError = class extends Error {
559
+ constructor(code, message, details = {}) {
560
+ super(message);
561
+ this.code = code;
562
+ this.details = details;
563
+ }
564
+ };
565
+ function storeDirectory(file) {
566
+ const dir = dirname2(file);
567
+ return dir.endsWith("/pages") || dir.endsWith("\\pages") ? dirname2(dir) : dir;
568
+ }
569
+ function deadOwner(lock) {
570
+ try {
571
+ const owner = JSON.parse(readFileSync(join2(lock, "owner.json"), "utf8"));
572
+ if (!Number.isSafeInteger(owner.pid) || owner.pid <= 0) return false;
573
+ try {
574
+ process.kill(owner.pid, 0);
575
+ return false;
576
+ } catch (e) {
577
+ return e.code === "ESRCH";
578
+ }
579
+ } catch {
580
+ return false;
581
+ }
582
+ }
583
+ function withStoreLock(file, action) {
584
+ const dir = storeDirectory(file);
585
+ mkdirSync(dir, { recursive: true });
586
+ const lock = join2(dir, ".write-lock");
587
+ const owner = join2(lock, "owner.json");
588
+ const token = randomUUID();
589
+ try {
590
+ mkdirSync(lock);
591
+ } catch (error) {
592
+ if (error.code !== "EEXIST") throw error;
593
+ if (!deadOwner(lock)) throw new LedgerError("BUSY", `Another writer owns ${lock}; retry after it completes. An orphan without owner metadata needs manual inspection.`);
594
+ try {
595
+ mkdirSync(join2(lock, ".reap"));
596
+ } catch {
597
+ throw new LedgerError("BUSY", "Another process is recovering the writer lock.");
598
+ }
599
+ if (!deadOwner(lock)) {
600
+ rmSync(join2(lock, ".reap"), { recursive: true, force: true });
601
+ throw new LedgerError("BUSY", "Writer ownership changed.");
602
+ }
603
+ rmSync(lock, { recursive: true });
604
+ try {
605
+ mkdirSync(lock);
606
+ } catch {
607
+ throw new LedgerError("BUSY", "Another writer acquired the recovered lock.");
608
+ }
609
+ }
610
+ let initialized = false;
611
+ try {
612
+ writeFileSync(owner, JSON.stringify({ pid: process.pid, token }), { flag: "wx" });
613
+ initialized = true;
614
+ return action();
615
+ } finally {
616
+ try {
617
+ if (!initialized || JSON.parse(readFileSync(owner, "utf8")).token === token) rmSync(lock, { recursive: true });
618
+ } catch {
619
+ }
620
+ }
621
+ }
622
+
237
623
  // src/semantics/vocabulary.ts
238
624
  var STATUS_GLYPHS = {
239
625
  planned: ["\xB7", "."],
@@ -462,55 +848,20 @@ function mostRecentKey(keys, mtimeOf2) {
462
848
  }
463
849
  function flipForSequence(map) {
464
850
  if (map.kind !== "sequence") return map;
465
- return {
466
- ...map,
467
- // VIOLATION: state-explicit-in-types - `-l.rank as Rank` produces a value
468
- // the Rank brand promises cannot exist: mirroring 0..99 gives -99..0, and
469
- // makeRank would refuse every one of them. The alternative is a second
470
- // ordered-position type (an unbranded `order` field) threaded through the
471
- // renderer's whole layout stage purely so this one derived map can be
472
- // typed — a large change to express "these ranks are an order, not a
473
- // stored value". What makes it safe is the same thing that makes it
474
- // wrong: this map only ever reaches a renderer, which compares ranks and
475
- // never writes them (same contract as aggregateMap).
476
- layers: map.layers.map((l) => ({ ...l, rank: -l.rank })),
477
- edges: map.edges.map((e) => ({ from: e.to, to: e.from, ...e.label !== void 0 ? { label: e.label } : {} }))
478
- };
479
- }
480
-
481
- // src/domain/text.ts
482
- var NO_CONTROLS = /^[^\u0000-\u001f\u007f-\u009f]*$/;
483
- var NO_CONTROLS_TEXT = "one line of text; control characters (ESC, newline, tab) are not allowed";
484
- var NO_CONTROLS_BUT_BREAKS = /^[^\u0000-\u0008\u000b-\u001f\u007f-\u009f]*$/;
485
- var NO_CONTROLS_BUT_BREAKS_TEXT = "text with optional newlines (\\n) and tabs; other control characters (ESC, BEL, CR) are not allowed";
486
- function mapTextError(map) {
487
- const check = (field, value, multiline = false) => value === void 0 || (multiline ? NO_CONTROLS_BUT_BREAKS : NO_CONTROLS).test(value) ? void 0 : `${field}: ${multiline ? NO_CONTROLS_BUT_BREAKS_TEXT : NO_CONTROLS_TEXT}`;
488
- let error = check("title", map.title);
489
- if (error) return error;
490
- for (const [i, layer] of map.layers.entries()) {
491
- error = check(`layers[${i}].name`, layer.name);
492
- if (error) return error;
493
- }
494
- for (const name of ["lanes", "groups"]) {
495
- for (const [i, item] of map[name].entries()) {
496
- error = check(`${name}[${i}].label`, item.label);
497
- if (error) return error;
498
- }
499
- }
500
- for (const [i, node] of map.nodes.entries()) {
501
- for (const name of ["label", "evidence", "detail"]) {
502
- error = check(`nodes[${i}].${name}`, node[name], name !== "label");
503
- if (error) return error;
504
- }
505
- }
506
- for (const [i, edge] of map.edges.entries()) {
507
- error = check(`edges[${i}].label`, edge.label);
508
- if (error) return error;
509
- }
510
- return void 0;
511
- }
512
- function terminalText(text, multiline = false) {
513
- return text.replace(multiline ? /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g : /[\u0000-\u001f\u007f-\u009f]/g, "?");
851
+ return {
852
+ ...map,
853
+ // VIOLATION: state-explicit-in-types - `-l.rank as Rank` produces a value
854
+ // the Rank brand promises cannot exist: mirroring 0..99 gives -99..0, and
855
+ // makeRank would refuse every one of them. The alternative is a second
856
+ // ordered-position type (an unbranded `order` field) threaded through the
857
+ // renderer's whole layout stage purely so this one derived map can be
858
+ // typed — a large change to express "these ranks are an order, not a
859
+ // stored value". What makes it safe is the same thing that makes it
860
+ // wrong: this map only ever reaches a renderer, which compares ranks and
861
+ // never writes them (same contract as aggregateMap).
862
+ layers: map.layers.map((l) => ({ ...l, rank: -l.rank })),
863
+ edges: map.edges.map((e) => ({ from: e.to, to: e.from, ...e.label !== void 0 ? { label: e.label } : {} }))
864
+ };
514
865
  }
515
866
 
516
867
  // src/render/width.ts
@@ -1396,223 +1747,9 @@ function paint(scene, opts) {
1396
1747
  return { canvas, hits };
1397
1748
  }
1398
1749
 
1399
- // src/store/format.ts
1400
- var STATE_FILE_VERSION = 1;
1401
- function makePageId(raw) {
1402
- return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
1403
- }
1404
- function describeStoreError(e) {
1405
- switch (e.kind) {
1406
- case "not-found":
1407
- return `no map file at ${e.path}`;
1408
- case "malformed-json":
1409
- return `map file ${e.path} is not valid JSON: ${e.detail}`;
1410
- case "bad-shape":
1411
- return `map file ${e.path} has an unexpected shape: ${e.detail}`;
1412
- case "invariant-violation":
1413
- return `map file ${e.path} violates a structural invariant: ${describeMapError(e.violation)}`;
1414
- case "save-failed":
1415
- return `could not write ${e.path}: ${e.detail}`;
1416
- case "delete-failed":
1417
- return `could not delete ${e.path}: ${e.detail}`;
1418
- }
1419
- }
1420
- function isRecord(v) {
1421
- return typeof v === "object" && v !== null && !Array.isArray(v);
1422
- }
1423
- function describeValue(v) {
1424
- if (v === void 0) return "missing";
1425
- if (v === null) return "null";
1426
- if (Array.isArray(v)) return "an array";
1427
- return `a ${typeof v}`;
1428
- }
1429
- function badShape(path, where, expected, got) {
1430
- return err({ kind: "bad-shape", path, detail: `${where} is ${describeValue(got)}, expected ${expected}` });
1431
- }
1432
- function arrayField(raw, key, path, presence) {
1433
- const v = raw[key];
1434
- if (Array.isArray(v)) return ok(v);
1435
- if (v === void 0 && presence === "optional") return ok([]);
1436
- return badShape(path, `"${key}"`, "an array", v);
1437
- }
1438
- function requiredString(rec, key, where, path) {
1439
- const v = rec[key];
1440
- return typeof v === "string" ? ok(v) : badShape(path, `${where}.${key}`, "a string", v);
1441
- }
1442
- function optionalString(rec, key, where, path) {
1443
- const v = rec[key];
1444
- if (v === void 0) return ok(void 0);
1445
- return typeof v === "string" ? ok(v) : badShape(path, `${where}.${key}`, "a string", v);
1446
- }
1447
- function parseMap(raw, path) {
1448
- if (!isRecord(raw)) return err({ kind: "bad-shape", path, detail: "root is not an object" });
1449
- if (raw["version"] !== STATE_FILE_VERSION) {
1450
- return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${STATE_FILE_VERSION}` });
1451
- }
1452
- const layers = arrayField(raw, "layers", path, "required");
1453
- if (!layers.ok) return layers;
1454
- const nodes = arrayField(raw, "nodes", path, "required");
1455
- if (!nodes.ok) return nodes;
1456
- const edges = arrayField(raw, "edges", path, "required");
1457
- if (!edges.ok) return edges;
1458
- const lanes = arrayField(raw, "lanes", path, "optional");
1459
- if (!lanes.ok) return lanes;
1460
- const groups = arrayField(raw, "groups", path, "optional");
1461
- if (!groups.ok) return groups;
1462
- let map = EMPTY_MAP;
1463
- const title = optionalString(raw, "title", "map", path);
1464
- if (!title.ok) return title;
1465
- if (title.value !== void 0) map = setTitle(map, title.value);
1466
- const rawKind = optionalString(raw, "kind", "map", path);
1467
- if (!rawKind.ok) return rawKind;
1468
- if (rawKind.value !== void 0) {
1469
- const kind = makeMapKind(rawKind.value);
1470
- if (!kind.ok) return err({ kind: "invariant-violation", path, violation: kind.error });
1471
- map = setKind(map, kind.value);
1472
- }
1473
- for (const [i, rawLayer] of layers.value.entries()) {
1474
- const where = `layers[${i}]`;
1475
- if (!isRecord(rawLayer)) return badShape(path, where, "an object", rawLayer);
1476
- const rawId = requiredString(rawLayer, "id", where, path);
1477
- if (!rawId.ok) return rawId;
1478
- const id = makeLayerId(rawId.value);
1479
- if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
1480
- const name = requiredString(rawLayer, "name", where, path);
1481
- if (!name.ok) return name;
1482
- const rawRank = rawLayer["rank"];
1483
- if (typeof rawRank !== "number") return badShape(path, `${where}.rank`, "a number", rawRank);
1484
- const rank = makeRank(rawRank);
1485
- if (!rank.ok) return err({ kind: "invariant-violation", path, violation: rank.error });
1486
- const next = declareLayer(map, { id: id.value, name: name.value, rank: rank.value });
1487
- if (!next.ok) return err({ kind: "invariant-violation", path, violation: next.error });
1488
- map = next.value;
1489
- }
1490
- for (const [i, rawLane] of lanes.value.entries()) {
1491
- const where = `lanes[${i}]`;
1492
- if (!isRecord(rawLane)) return badShape(path, where, "an object", rawLane);
1493
- const rawId = requiredString(rawLane, "id", where, path);
1494
- if (!rawId.ok) return rawId;
1495
- const id = makeLaneId(rawId.value);
1496
- if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
1497
- const label = requiredString(rawLane, "label", where, path);
1498
- if (!label.ok) return label;
1499
- const declared = declareLane(map, { id: id.value, label: label.value });
1500
- if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
1501
- map = declared.value;
1502
- }
1503
- for (const [i, rawGroup] of groups.value.entries()) {
1504
- const where = `groups[${i}]`;
1505
- if (!isRecord(rawGroup)) return badShape(path, where, "an object", rawGroup);
1506
- const rawId = requiredString(rawGroup, "id", where, path);
1507
- if (!rawId.ok) return rawId;
1508
- const id = makeGroupId(rawId.value);
1509
- if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
1510
- const rawLayer = requiredString(rawGroup, "layer", where, path);
1511
- if (!rawLayer.ok) return rawLayer;
1512
- const layer = makeLayerId(rawLayer.value);
1513
- if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
1514
- const label = requiredString(rawGroup, "label", where, path);
1515
- if (!label.ok) return label;
1516
- const declared = declareGroup(map, { id: id.value, label: label.value, layer: layer.value });
1517
- if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
1518
- map = declared.value;
1519
- }
1520
- for (const [i, rawNode] of nodes.value.entries()) {
1521
- const where = `nodes[${i}]`;
1522
- if (!isRecord(rawNode)) return badShape(path, where, "an object", rawNode);
1523
- const rawId = requiredString(rawNode, "id", where, path);
1524
- if (!rawId.ok) return rawId;
1525
- const id = makeNodeId(rawId.value);
1526
- if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
1527
- const rawLayer = requiredString(rawNode, "layer", where, path);
1528
- if (!rawLayer.ok) return rawLayer;
1529
- const layer = makeLayerId(rawLayer.value);
1530
- if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
1531
- const rawStatus = requiredString(rawNode, "status", where, path);
1532
- if (!rawStatus.ok) return rawStatus;
1533
- const status = makeNodeStatus(rawStatus.value);
1534
- if (!status.ok) return err({ kind: "invariant-violation", path, violation: status.error });
1535
- const label = requiredString(rawNode, "label", where, path);
1536
- if (!label.ok) return label;
1537
- const detail = optionalString(rawNode, "detail", where, path);
1538
- if (!detail.ok) return detail;
1539
- const rawGroup = optionalString(rawNode, "group", where, path);
1540
- if (!rawGroup.ok) return rawGroup;
1541
- let group;
1542
- if (rawGroup.value !== void 0) {
1543
- const made = makeGroupId(rawGroup.value);
1544
- if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
1545
- group = made.value;
1546
- }
1547
- const rawNodeKind = optionalString(rawNode, "kind", where, path);
1548
- if (!rawNodeKind.ok) return rawNodeKind;
1549
- let nodeKind;
1550
- if (rawNodeKind.value !== void 0) {
1551
- const made = makeNodeKind(rawNodeKind.value);
1552
- if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
1553
- nodeKind = made.value;
1554
- }
1555
- const rawLane = optionalString(rawNode, "lane", where, path);
1556
- if (!rawLane.ok) return rawLane;
1557
- let lane;
1558
- if (rawLane.value !== void 0) {
1559
- const made = makeLaneId(rawLane.value);
1560
- if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
1561
- lane = made.value;
1562
- }
1563
- const rawSubmap = optionalString(rawNode, "submap", where, path);
1564
- if (!rawSubmap.ok) return rawSubmap;
1565
- let submap;
1566
- if (rawSubmap.value !== void 0) {
1567
- const made = makeSubmapRef(rawSubmap.value);
1568
- if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
1569
- submap = made.value;
1570
- }
1571
- const declared = declareNode(map, {
1572
- id: id.value,
1573
- label: label.value,
1574
- layer: layer.value,
1575
- status: status.value,
1576
- ...detail.value !== void 0 ? { detail: detail.value } : {},
1577
- ...group !== void 0 ? { group } : {},
1578
- ...nodeKind !== void 0 ? { kind: nodeKind } : {},
1579
- ...lane !== void 0 ? { lane } : {},
1580
- ...submap !== void 0 ? { submap } : {}
1581
- });
1582
- if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
1583
- map = declared.value;
1584
- const evidence = optionalString(rawNode, "evidence", where, path);
1585
- if (!evidence.ok) return evidence;
1586
- if (evidence.value !== void 0) {
1587
- const updated = updateNode(map, { id: id.value, evidence: evidence.value });
1588
- if (!updated.ok) return err({ kind: "invariant-violation", path, violation: updated.error });
1589
- map = updated.value;
1590
- }
1591
- }
1592
- for (const [i, rawEdge] of edges.value.entries()) {
1593
- const where = `edges[${i}]`;
1594
- if (!isRecord(rawEdge)) return badShape(path, where, "an object", rawEdge);
1595
- const rawFrom = requiredString(rawEdge, "from", where, path);
1596
- if (!rawFrom.ok) return rawFrom;
1597
- const from = makeNodeId(rawFrom.value);
1598
- if (!from.ok) return err({ kind: "invariant-violation", path, violation: from.error });
1599
- const rawTo = requiredString(rawEdge, "to", where, path);
1600
- if (!rawTo.ok) return rawTo;
1601
- const to = makeNodeId(rawTo.value);
1602
- if (!to.ok) return err({ kind: "invariant-violation", path, violation: to.error });
1603
- const label = optionalString(rawEdge, "label", where, path);
1604
- if (!label.ok) return label;
1605
- const linked = linkNodes(map, from.value, to.value, label.value);
1606
- if (!linked.ok) return err({ kind: "invariant-violation", path, violation: linked.error });
1607
- map = linked.value;
1608
- }
1609
- const textError = mapTextError(map);
1610
- return textError ? err({ kind: "bad-shape", path, detail: textError }) : ok(map);
1611
- }
1612
-
1613
1750
  // src/store/atomic.ts
1614
- import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
1615
- import { dirname } from "node:path";
1751
+ import { mkdirSync as mkdirSync2, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
1752
+ import { dirname as dirname3 } from "node:path";
1616
1753
  var RENAME_BACKOFF_STEP_MS = 10;
1617
1754
  var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOENT"]);
1618
1755
  function sleepSync(ms) {
@@ -1620,7 +1757,7 @@ function sleepSync(ms) {
1620
1757
  }
1621
1758
  function discardTemp(tmp) {
1622
1759
  try {
1623
- rmSync(tmp, { force: true });
1760
+ rmSync2(tmp, { force: true });
1624
1761
  } catch {
1625
1762
  }
1626
1763
  }
@@ -1630,8 +1767,8 @@ function errnoOf(e) {
1630
1767
  function writeAtomic(path, contents, maxAttempts) {
1631
1768
  const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2, 10)}.tmp`;
1632
1769
  try {
1633
- mkdirSync(dirname(path), { recursive: true });
1634
- writeFileSync(tmp, contents, "utf8");
1770
+ mkdirSync2(dirname3(path), { recursive: true });
1771
+ writeFileSync2(tmp, contents, "utf8");
1635
1772
  } catch (e) {
1636
1773
  discardTemp(tmp);
1637
1774
  return err({ kind: "save-failed", path, detail: `writing the temp file failed: ${errnoOf(e)}` });
@@ -1654,13 +1791,13 @@ function writeAtomic(path, contents, maxAttempts) {
1654
1791
  }
1655
1792
 
1656
1793
  // src/store/pages.ts
1657
- import { existsSync, readdirSync, rmSync as rmSync2 } from "node:fs";
1658
- import { basename, dirname as dirname2, join } from "node:path";
1794
+ import { existsSync as existsSync2, readdirSync, rmSync as rmSync3 } from "node:fs";
1795
+ import { basename, dirname as dirname4, join as join3 } from "node:path";
1659
1796
  var STORE_DIR_NAME = ".mellos";
1660
- var STATE_FILE_RELATIVE_PATH = join(STORE_DIR_NAME, "map.json");
1797
+ var STATE_FILE_RELATIVE_PATH = join3(STORE_DIR_NAME, "map.json");
1661
1798
  var PAGES_DIR_NAME = "pages";
1662
1799
  function pageFilePath(defaultFile, page) {
1663
- return page === void 0 ? defaultFile : join(dirname2(defaultFile), PAGES_DIR_NAME, `${page}.json`);
1800
+ return page === void 0 ? defaultFile : join3(dirname4(defaultFile), PAGES_DIR_NAME, `${page}.json`);
1664
1801
  }
1665
1802
  function pageIdOfFile(defaultFile, path) {
1666
1803
  if (path === defaultFile) return void 0;
@@ -1669,20 +1806,20 @@ function pageIdOfFile(defaultFile, path) {
1669
1806
  }
1670
1807
  function listPageFiles(defaultFile) {
1671
1808
  const out = [];
1672
- if (existsSync(defaultFile)) out.push(defaultFile);
1809
+ if (existsSync2(defaultFile)) out.push(defaultFile);
1673
1810
  let entries = [];
1674
1811
  try {
1675
- entries = readdirSync(join(dirname2(defaultFile), PAGES_DIR_NAME));
1812
+ entries = readdirSync(join3(dirname4(defaultFile), PAGES_DIR_NAME));
1676
1813
  } catch {
1677
1814
  }
1678
1815
  for (const e of entries.sort()) {
1679
- if (e.endsWith(".json")) out.push(join(dirname2(defaultFile), PAGES_DIR_NAME, e));
1816
+ if (e.endsWith(".json")) out.push(join3(dirname4(defaultFile), PAGES_DIR_NAME, e));
1680
1817
  }
1681
1818
  return out;
1682
1819
  }
1683
1820
  function deletePageFile(path) {
1684
1821
  try {
1685
- rmSync2(path, { force: true });
1822
+ rmSync3(path, { force: true });
1686
1823
  return ok(void 0);
1687
1824
  } catch (e) {
1688
1825
  return err({ kind: "delete-failed", path, detail: errnoOf(e) });
@@ -1690,8 +1827,8 @@ function deletePageFile(path) {
1690
1827
  }
1691
1828
 
1692
1829
  // src/store/channels.ts
1693
- import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync as rmSync4 } from "node:fs";
1694
- import { dirname as dirname4, join as join3 } from "node:path";
1830
+ import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync as rmSync5 } from "node:fs";
1831
+ import { dirname as dirname6, join as join5 } from "node:path";
1695
1832
 
1696
1833
  // src/store/json-text.ts
1697
1834
  function isRecord2(v) {
@@ -1702,16 +1839,16 @@ function stripBom(text) {
1702
1839
  }
1703
1840
 
1704
1841
  // src/store/viewers.ts
1705
- import { readdirSync as readdirSync2, readFileSync, statSync, rmSync as rmSync3 } from "node:fs";
1706
- import { dirname as dirname3, join as join2 } from "node:path";
1842
+ import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync, rmSync as rmSync4 } from "node:fs";
1843
+ import { dirname as dirname5, join as join4 } from "node:path";
1707
1844
  var VIEWERS_DIR_NAME = "viewers";
1708
1845
  var VIEWER_FILE_VERSION = 1;
1709
1846
  var VIEWER_HEARTBEAT_MS = 1e3;
1710
1847
  function viewersDirPath(defaultFile) {
1711
- return join2(dirname3(defaultFile), VIEWERS_DIR_NAME);
1848
+ return join4(dirname5(defaultFile), VIEWERS_DIR_NAME);
1712
1849
  }
1713
1850
  function viewerFilePath(defaultFile, pid) {
1714
- return join2(viewersDirPath(defaultFile), `${pid}.json`);
1851
+ return join4(viewersDirPath(defaultFile), `${pid}.json`);
1715
1852
  }
1716
1853
  function publishViewer(defaultFile, pid, report) {
1717
1854
  const body = { version: VIEWER_FILE_VERSION, page: report.page ?? null, follow: report.follow, owner: report.owner };
@@ -1720,7 +1857,7 @@ function publishViewer(defaultFile, pid, report) {
1720
1857
  }
1721
1858
  function retireViewer(defaultFile, pid) {
1722
1859
  try {
1723
- rmSync3(viewerFilePath(defaultFile, pid), { force: true });
1860
+ rmSync4(viewerFilePath(defaultFile, pid), { force: true });
1724
1861
  } catch {
1725
1862
  }
1726
1863
  }
@@ -1731,21 +1868,21 @@ function focusFilePath(defaultFile, pid) {
1731
1868
  return paneChannelPath(defaultFile, FOCUS_FILE_NAME, pid);
1732
1869
  }
1733
1870
  function paneChannelPath(defaultFile, channel, pid) {
1734
- if (pid === void 0) return join3(dirname4(defaultFile), channel);
1871
+ if (pid === void 0) return join5(dirname6(defaultFile), channel);
1735
1872
  if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error("Invalid pane process id");
1736
- return join3(viewersDirPath(defaultFile), `${pid}.${channel}`);
1873
+ return join5(viewersDirPath(defaultFile), `${pid}.${channel}`);
1737
1874
  }
1738
1875
  function takeFocusRequest(defaultFile, pid) {
1739
1876
  const targeted = pid === void 0 ? void 0 : focusFilePath(defaultFile, pid);
1740
- const path = targeted !== void 0 && existsSync2(targeted) ? targeted : focusFilePath(defaultFile);
1877
+ const path = targeted !== void 0 && existsSync3(targeted) ? targeted : focusFilePath(defaultFile);
1741
1878
  let raw;
1742
1879
  try {
1743
- raw = readFileSync2(path, "utf8");
1880
+ raw = readFileSync3(path, "utf8");
1744
1881
  } catch {
1745
1882
  return void 0;
1746
1883
  }
1747
1884
  try {
1748
- rmSync4(path, { force: true });
1885
+ rmSync5(path, { force: true });
1749
1886
  } catch {
1750
1887
  }
1751
1888
  let parsed2;
@@ -1767,10 +1904,10 @@ function quitFilePath(defaultFile, pid) {
1767
1904
  }
1768
1905
  function takeQuitRequest(defaultFile, pid) {
1769
1906
  const targeted = pid === void 0 ? void 0 : quitFilePath(defaultFile, pid);
1770
- const path = targeted !== void 0 && existsSync2(targeted) ? targeted : quitFilePath(defaultFile);
1907
+ const path = targeted !== void 0 && existsSync3(targeted) ? targeted : quitFilePath(defaultFile);
1771
1908
  let raw;
1772
1909
  try {
1773
- raw = readFileSync2(path, "utf8");
1910
+ raw = readFileSync3(path, "utf8");
1774
1911
  } catch {
1775
1912
  return false;
1776
1913
  }
@@ -1785,45 +1922,45 @@ function takeQuitRequest(defaultFile, pid) {
1785
1922
  }
1786
1923
  function sweepQuitRequest(defaultFile, pid) {
1787
1924
  try {
1788
- rmSync4(quitFilePath(defaultFile, pid), { force: true });
1925
+ rmSync5(quitFilePath(defaultFile, pid), { force: true });
1789
1926
  } catch {
1790
1927
  }
1791
1928
  }
1792
1929
 
1793
1930
  // src/store/policy.ts
1794
- import { dirname as dirname5, join as join4 } from "node:path";
1931
+ import { dirname as dirname7, join as join6 } from "node:path";
1795
1932
  var CONFIG_FILE_NAME = "config.json";
1796
1933
  function configFilePath(defaultFile) {
1797
- return join4(dirname5(defaultFile), CONFIG_FILE_NAME);
1934
+ return join6(dirname7(defaultFile), CONFIG_FILE_NAME);
1798
1935
  }
1799
1936
 
1800
1937
  // src/store/migration.ts
1801
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, renameSync as renameSync2 } from "node:fs";
1802
- import { dirname as dirname6, join as join5 } from "node:path";
1803
- var LEGACY_STATE_FILE_RELATIVE_PATH = join5(".claude", "mellos-mapping.json");
1938
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, renameSync as renameSync2 } from "node:fs";
1939
+ import { dirname as dirname8, join as join7 } from "node:path";
1940
+ var LEGACY_STATE_FILE_RELATIVE_PATH = join7(".claude", "mellos-mapping.json");
1804
1941
  var LEGACY_PAGES_DIR_NAME = "mellos-mapping.pages";
1805
1942
  var LEGACY_CONFIG_FILE_NAME = "mellos-mapping.config.json";
1806
1943
  function migrateLegacyStore(defaultFile) {
1807
- const projectRoot = dirname6(dirname6(defaultFile));
1808
- const legacyDefault = join5(projectRoot, LEGACY_STATE_FILE_RELATIVE_PATH);
1809
- const legacyPages = join5(dirname6(legacyDefault), LEGACY_PAGES_DIR_NAME);
1810
- const legacyConfig = join5(dirname6(legacyDefault), LEGACY_CONFIG_FILE_NAME);
1811
- const hasLegacy = existsSync3(legacyDefault) || existsSync3(legacyPages) || existsSync3(legacyConfig);
1812
- const hasCurrent = existsSync3(defaultFile) || existsSync3(join5(dirname6(defaultFile), PAGES_DIR_NAME)) || existsSync3(configFilePath(defaultFile));
1944
+ const projectRoot = dirname8(dirname8(defaultFile));
1945
+ const legacyDefault = join7(projectRoot, LEGACY_STATE_FILE_RELATIVE_PATH);
1946
+ const legacyPages = join7(dirname8(legacyDefault), LEGACY_PAGES_DIR_NAME);
1947
+ const legacyConfig = join7(dirname8(legacyDefault), LEGACY_CONFIG_FILE_NAME);
1948
+ const hasLegacy = existsSync4(legacyDefault) || existsSync4(legacyPages) || existsSync4(legacyConfig);
1949
+ const hasCurrent = existsSync4(defaultFile) || existsSync4(join7(dirname8(defaultFile), PAGES_DIR_NAME)) || existsSync4(configFilePath(defaultFile));
1813
1950
  if (!hasLegacy || hasCurrent) return false;
1814
- mkdirSync2(dirname6(defaultFile), { recursive: true });
1815
- if (existsSync3(legacyDefault)) renameSync2(legacyDefault, defaultFile);
1816
- if (existsSync3(legacyPages)) renameSync2(legacyPages, join5(dirname6(defaultFile), PAGES_DIR_NAME));
1817
- if (existsSync3(legacyConfig)) renameSync2(legacyConfig, configFilePath(defaultFile));
1951
+ mkdirSync3(dirname8(defaultFile), { recursive: true });
1952
+ if (existsSync4(legacyDefault)) renameSync2(legacyDefault, defaultFile);
1953
+ if (existsSync4(legacyPages)) renameSync2(legacyPages, join7(dirname8(defaultFile), PAGES_DIR_NAME));
1954
+ if (existsSync4(legacyConfig)) renameSync2(legacyConfig, configFilePath(defaultFile));
1818
1955
  return true;
1819
1956
  }
1820
1957
 
1821
1958
  // src/store/maps.ts
1822
- import { readFileSync as readFileSync3 } from "node:fs";
1959
+ import { readFileSync as readFileSync4 } from "node:fs";
1823
1960
  function loadMapFile(path) {
1824
1961
  let text;
1825
1962
  try {
1826
- text = readFileSync3(path, "utf8");
1963
+ text = readFileSync4(path, "utf8");
1827
1964
  } catch (e) {
1828
1965
  const code = e.code;
1829
1966
  if (code === "ENOENT") return err({ kind: "not-found", path });
@@ -2276,7 +2413,7 @@ function describeArgsError(e) {
2276
2413
  }
2277
2414
  var USAGE = "usage: mellos-mapping-watch [--file <map.json>] [--page <slug>] [--interval <ms>] [--ascii] [--no-color] [--no-mouse] [--no-follow]";
2278
2415
  function parseArgs(argv, cwd) {
2279
- let file = join6(cwd, STATE_FILE_RELATIVE_PATH);
2416
+ let file = join8(resolveProjectDirectory(cwd), STATE_FILE_RELATIVE_PATH);
2280
2417
  let intervalMs = POLL_INTERVAL_DEFAULT_MS;
2281
2418
  let unicode = true;
2282
2419
  let color = true;
@@ -2558,7 +2695,7 @@ function waitingInfo(s, width) {
2558
2695
  const clock = s.elapsedMs !== void 0 ? ` \xB7 waiting ${elapsedLabel(s.elapsedMs)}` : "";
2559
2696
  const lines = [
2560
2697
  `watching ${s.defaultFile}`,
2561
- ` and ${join6(s.pagesDir, "*.json")}`,
2698
+ ` and ${join8(s.pagesDir, "*.json")}`,
2562
2699
  `polling every ${s.intervalMs} ms${clock}`
2563
2700
  ];
2564
2701
  for (const b of s.broken) lines.push(`! ${b}`);
@@ -2598,12 +2735,12 @@ function viewerReportOf(pane, defaultFile) {
2598
2735
  const shown = pane.activeFile ?? pane.pendingFocusFile ?? defaultFile;
2599
2736
  return { page: pageIdOfFile(defaultFile, shown), follow: pane.follow };
2600
2737
  }
2601
- function runWatcher(cfg, io = nativeWatcherIO()) {
2738
+ function runWatcher(cfg, io2 = nativeWatcherIO()) {
2602
2739
  const renderScene = createWindowRenderer();
2603
2740
  if (migrateLegacyStore(cfg.file)) console.error("mellos-mapping: moved the legacy .claude map store to .mellos/ \u2014 commit the move.");
2604
2741
  sweepQuitRequest(cfg.file);
2605
2742
  sweepQuitRequest(cfg.file, process.pid);
2606
- const interactive = io.interactive;
2743
+ const interactive = io2.interactive;
2607
2744
  const mouseActive = interactive && cfg.mouse;
2608
2745
  let lastFrame = "";
2609
2746
  let spinnerFrame = 0;
@@ -2612,8 +2749,8 @@ function runWatcher(cfg, io = nativeWatcherIO()) {
2612
2749
  const standbyNotice = "waiting for the first mmap_declare ...";
2613
2750
  let map;
2614
2751
  let notice = standbyNotice;
2615
- let lastCols = io.output.columns ?? 0;
2616
- let lastRows = io.output.rows ?? 0;
2752
+ let lastCols = io2.output.columns ?? 0;
2753
+ let lastRows = io2.output.rows ?? 0;
2617
2754
  let pane = initialPaneState(
2618
2755
  cfg.follow,
2619
2756
  cfg.page === void 0 ? void 0 : pageFilePath(cfg.file, cfg.page)
@@ -2640,8 +2777,8 @@ function runWatcher(cfg, io = nativeWatcherIO()) {
2640
2777
  }
2641
2778
  return false;
2642
2779
  };
2643
- const viewWidth = () => usableColumns(io.output.columns ?? FALLBACK_COLUMNS);
2644
- const viewHeight = () => Math.max(1, (io.output.rows ?? FALLBACK_ROWS) - (1 + panelContentRows) - 1 - tabRows());
2780
+ const viewWidth = () => usableColumns(io2.output.columns ?? FALLBACK_COLUMNS);
2781
+ const viewHeight = () => Math.max(1, (io2.output.rows ?? FALLBACK_ROWS) - (1 + panelContentRows) - 1 - tabRows());
2645
2782
  const dividerY = () => tabRows() + viewHeight() + 1;
2646
2783
  const pageTabsOf = (files) => files.map((f) => {
2647
2784
  const entry = entryOf(pane, f);
@@ -2696,20 +2833,20 @@ function runWatcher(cfg, io = nativeWatcherIO()) {
2696
2833
  return lastHits.find((h) => cx >= h.x && cx < h.x + h.w && cy >= h.y && cy < h.y + h.h)?.id;
2697
2834
  };
2698
2835
  const publishPresence = () => {
2699
- io.report?.(viewerReportOf(pane, cfg.file));
2836
+ io2.report?.(viewerReportOf(pane, cfg.file));
2700
2837
  publishViewer(cfg.file, process.pid, {
2701
2838
  ...viewerReportOf(pane, cfg.file),
2702
2839
  ...cfg.owner === void 0 ? {} : { owner: cfg.owner }
2703
2840
  });
2704
2841
  };
2705
- if (interactive) io.input.setRawMode(true);
2706
- const terminal = openTerminalSession({ interactive, mouse: cfg.mouse, write: (text) => io.output.write(text) });
2842
+ if (interactive) io2.input.setRawMode(true);
2843
+ const terminal = openTerminalSession({ interactive, mouse: cfg.mouse, write: (text) => io2.output.write(text) });
2707
2844
  const frameOutput = interactive ? createFrameOutput({
2708
- write: (text) => io.output.write(text),
2845
+ write: (text) => io2.output.write(text),
2709
2846
  onDrain: (ready) => {
2710
- io.output.once("drain", ready);
2847
+ io2.output.once("drain", ready);
2711
2848
  return () => {
2712
- io.output.off("drain", ready);
2849
+ io2.output.off("drain", ready);
2713
2850
  };
2714
2851
  }
2715
2852
  }) : void 0;
@@ -2740,9 +2877,9 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2740
2877
  const paint2 = () => {
2741
2878
  clearTimeout(paintTimer);
2742
2879
  paintTimer = void 0;
2743
- const cols = io.output.columns ?? FALLBACK_COLUMNS;
2880
+ const cols = io2.output.columns ?? FALLBACK_COLUMNS;
2744
2881
  const viewW = viewWidth();
2745
- panelContentRows = clampPanelRows(panelContentRows, io.output.rows ?? FALLBACK_ROWS, tabRows());
2882
+ panelContentRows = clampPanelRows(panelContentRows, io2.output.rows ?? FALLBACK_ROWS, tabRows());
2746
2883
  const viewH = viewHeight();
2747
2884
  const focus = view.hoverId ?? view.selectedId;
2748
2885
  let body;
@@ -2760,6 +2897,7 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2760
2897
  lastHits = [];
2761
2898
  } else {
2762
2899
  const windowed = rendered.value;
2900
+ if (map.nodes.length > 0) io2.onMapRendered?.();
2763
2901
  const maxX = Math.max(0, windowed.contentWidth - viewW);
2764
2902
  const maxY = Math.max(0, windowed.contentHeight - viewH);
2765
2903
  if (view.offsetX > maxX || view.offsetY > maxY || view.offsetX < 0 || view.offsetY < 0) {
@@ -2781,7 +2919,7 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2781
2919
  const info = waitingInfo(
2782
2920
  {
2783
2921
  defaultFile: cfg.file,
2784
- pagesDir: join6(dirname7(cfg.file), PAGES_DIR_NAME),
2922
+ pagesDir: join8(dirname9(cfg.file), PAGES_DIR_NAME),
2785
2923
  intervalMs: cfg.intervalMs,
2786
2924
  elapsedMs: interactive ? Date.now() - startedAt : void 0,
2787
2925
  broken: pane.pages.flatMap(
@@ -2853,7 +2991,7 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2853
2991
  } else {
2854
2992
  const frame = HOME + rows.map((row) => row + ERASE_LINE_END).join("\n");
2855
2993
  if (frame !== lastFrame) {
2856
- io.output.write(frame);
2994
+ io2.output.write(frame);
2857
2995
  lastFrame = frame;
2858
2996
  }
2859
2997
  }
@@ -2862,16 +3000,16 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2862
3000
  paintTimer ??= setTimeout(paint2, 16);
2863
3001
  };
2864
3002
  const handleResize = () => {
2865
- lastCols = io.output.columns ?? lastCols;
2866
- lastRows = io.output.rows ?? lastRows;
3003
+ lastCols = io2.output.columns ?? lastCols;
3004
+ lastRows = io2.output.rows ?? lastRows;
2867
3005
  lastFrame = "";
2868
3006
  frameOutput?.invalidate();
2869
- io.output.write(CLEAR_ALL);
3007
+ io2.output.write(CLEAR_ALL);
2870
3008
  paint2();
2871
3009
  };
2872
3010
  const tick = () => {
2873
3011
  if (takeQuitRequest(cfg.file, process.pid)) quit();
2874
- if ((io.output.columns ?? lastCols) !== lastCols || (io.output.rows ?? lastRows) !== lastRows) {
3012
+ if ((io2.output.columns ?? lastCols) !== lastCols || (io2.output.rows ?? lastRows) !== lastRows) {
2875
3013
  handleResize();
2876
3014
  }
2877
3015
  const discovered = listPageFiles(cfg.file);
@@ -2929,14 +3067,20 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2929
3067
  return;
2930
3068
  }
2931
3069
  const file = asked.request.file;
2932
- const removed = deletePageFile(file);
3070
+ let removed;
3071
+ try {
3072
+ removed = withStoreLock(file, () => deletePageFile(file));
3073
+ } catch (error) {
3074
+ flash = { text: String(error), until: now + FLASH_NOTICE_MS };
3075
+ return;
3076
+ }
2933
3077
  flash = removed.ok ? { text: `deleted ${pageName(file)}`, until: now + FLASH_ACK_MS } : { text: describeStoreError(removed.error), until: now + FLASH_NOTICE_MS };
2934
3078
  tick();
2935
3079
  };
2936
3080
  if (interactive) {
2937
- io.input.resume();
2938
- io.input.setEncoding("utf8");
2939
- io.input.on("data", (chunk) => {
3081
+ io2.input.resume();
3082
+ io2.input.setEncoding("utf8");
3083
+ io2.input.on("data", (chunk) => {
2940
3084
  const parsed2 = parseInput(pendingInput + chunk);
2941
3085
  pendingInput = parsed2.rest;
2942
3086
  let dirty = false;
@@ -3009,7 +3153,7 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
3009
3153
  break;
3010
3154
  case "mouse-drag":
3011
3155
  if (view.dividerDrag) {
3012
- const next = panelRowsFromDividerY(event.y, io.output.rows ?? FALLBACK_ROWS, tabRows());
3156
+ const next = panelRowsFromDividerY(event.y, io2.output.rows ?? FALLBACK_ROWS, tabRows());
3013
3157
  if (next !== panelContentRows) {
3014
3158
  panelContentRows = next;
3015
3159
  dirty = true;
@@ -3105,7 +3249,7 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
3105
3249
  else paint2();
3106
3250
  }
3107
3251
  });
3108
- io.output.on("resize", handleResize);
3252
+ io2.output.on("resize", handleResize);
3109
3253
  }
3110
3254
  tick();
3111
3255
  setInterval(tick, cfg.intervalMs);
@@ -3120,6 +3264,73 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
3120
3264
  }
3121
3265
  }
3122
3266
 
3267
+ // src/watch/cli.ts
3268
+ import { homedir as homedir2 } from "node:os";
3269
+ import { fileURLToPath } from "node:url";
3270
+
3271
+ // src/support/star-reminder.ts
3272
+ import { existsSync as existsSync5, readFileSync as readFileSync5, realpathSync as realpathSync3 } from "node:fs";
3273
+ import { basename as basename2, dirname as dirname10, join as join9 } from "node:path";
3274
+ var STAR_URL = "https://github.com/GuangminJu/mellos-mapping";
3275
+ var DAY_MS = 864e5;
3276
+ function isNpmInstallation(entry) {
3277
+ try {
3278
+ const root = dirname10(dirname10(realpathSync3(entry)));
3279
+ if (basename2(dirname10(root)).toLowerCase() !== "node_modules" || existsSync5(join9(root, ".git")) || existsSync5(join9(root, ".codex-plugin")) || existsSync5(join9(root, ".claude-plugin"))) return false;
3280
+ const pkg = JSON.parse(readFileSync5(join9(root, "package.json"), "utf8"));
3281
+ return pkg.name === "mellos-mapping" && pkg.bin?.mmap === "dist/mmap.mjs";
3282
+ } catch {
3283
+ return false;
3284
+ }
3285
+ }
3286
+ function starReminderFile(userBase) {
3287
+ return join9(userBase, ".mellos", "support", "star-reminder.json");
3288
+ }
3289
+ function enabledFlag(value) {
3290
+ return !!value && value !== "0" && value.toLowerCase() !== "false";
3291
+ }
3292
+ function validState(value) {
3293
+ if (!value || typeof value !== "object") return false;
3294
+ const s = value;
3295
+ return s.version === 1 && Number.isFinite(s.firstSeen) && s.firstSeen >= 0 && Number.isSafeInteger(s.lastDay) && s.lastDay >= Math.floor(s.firstSeen / DAY_MS) && Number.isSafeInteger(s.days) && s.days >= 1 && s.days <= 3 && typeof s.notified === "boolean";
3296
+ }
3297
+ function createStarReminder(options) {
3298
+ const eligible = isNpmInstallation(options.entry) && !enabledFlag(options.env["CI"]) && !enabledFlag(options.env["MELLOS_MAPPING_NO_STAR"]);
3299
+ const file = starReminderFile(options.userBase);
3300
+ return {
3301
+ visit() {
3302
+ if (!eligible) return false;
3303
+ try {
3304
+ return withStoreLock(file, () => {
3305
+ const now = (options.now ?? Date.now)();
3306
+ if (!Number.isFinite(now) || now < 0) return false;
3307
+ const day = Math.floor(now / DAY_MS);
3308
+ let state;
3309
+ try {
3310
+ const saved2 = JSON.parse(readFileSync5(file, "utf8"));
3311
+ if (!validState(saved2)) return false;
3312
+ state = saved2;
3313
+ } catch (error) {
3314
+ if (error.code !== "ENOENT") return false;
3315
+ state = { version: 1, firstSeen: now, lastDay: day, days: 1, notified: false };
3316
+ }
3317
+ if (state.notified || now < state.firstSeen || day < state.lastDay) return false;
3318
+ if (day > state.lastDay) {
3319
+ state.days = Math.min(3, state.days + 1);
3320
+ state.lastDay = day;
3321
+ }
3322
+ const show = state.days >= 3 && now - state.firstSeen >= 7 * DAY_MS;
3323
+ if (show) state.notified = true;
3324
+ const saved = writeAtomic(file, JSON.stringify(state, null, 2) + "\n", 1);
3325
+ return saved.ok && show;
3326
+ });
3327
+ } catch {
3328
+ return false;
3329
+ }
3330
+ }
3331
+ };
3332
+ }
3333
+
3123
3334
  // src/watch/cli.ts
3124
3335
  var parsed = parseArgs(process.argv.slice(2), process.cwd());
3125
3336
  if (!parsed.ok) {
@@ -3127,4 +3338,17 @@ if (!parsed.ok) {
3127
3338
  ${USAGE}`);
3128
3339
  process.exit(1);
3129
3340
  }
3130
- runWatcher(parsed.value);
3341
+ var viewed = false;
3342
+ var io = nativeWatcherIO();
3343
+ var reminder = createStarReminder({ entry: fileURLToPath(import.meta.url), userBase: homedir2(), env: process.env });
3344
+ runWatcher(parsed.value, { ...io, onMapRendered: () => {
3345
+ viewed = true;
3346
+ } });
3347
+ process.on("exit", (code) => {
3348
+ if (code === 0 && viewed && io.interactive && process.stderr.isTTY && process.env["TERM"] !== "dumb" && reminder.visit()) {
3349
+ process.stderr.write(`
3350
+ If Mellos Mapping has been useful, a GitHub Star is welcome: ${STAR_URL}
3351
+ This optional reminder will not appear again.
3352
+ `);
3353
+ }
3354
+ });