mellos-mapping 0.19.0 → 0.20.2

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.
Files changed (43) hide show
  1. package/README.md +366 -56
  2. package/README.zh-CN.md +319 -47
  3. package/dist/hook-session-start.mjs +239 -0
  4. package/dist/mmap.mjs +338 -0
  5. package/dist/server.mjs +1737 -897
  6. package/dist/store-paths.mjs +107 -0
  7. package/dist/watch.mjs +1612 -902
  8. package/lib/domain/ops.d.ts +171 -0
  9. package/lib/domain/ops.js +384 -0
  10. package/lib/domain/types.d.ts +283 -0
  11. package/lib/domain/types.js +153 -0
  12. package/lib/render/canvas.d.ts +50 -0
  13. package/lib/render/canvas.js +210 -0
  14. package/lib/render/draw.d.ts +37 -0
  15. package/lib/render/draw.js +111 -0
  16. package/lib/render/layout.d.ts +89 -0
  17. package/lib/render/layout.js +200 -0
  18. package/lib/render/options.d.ts +39 -0
  19. package/lib/render/options.js +10 -0
  20. package/lib/render/render.d.ts +88 -0
  21. package/lib/render/render.js +128 -0
  22. package/lib/render/routing.d.ts +56 -0
  23. package/lib/render/routing.js +244 -0
  24. package/lib/render/skins.d.ts +54 -0
  25. package/lib/render/skins.js +99 -0
  26. package/lib/render/width.d.ts +24 -0
  27. package/lib/render/width.js +139 -0
  28. package/lib/render/zoom-geometry.d.ts +52 -0
  29. package/lib/render/zoom-geometry.js +56 -0
  30. package/lib/semantics/semantics.d.ts +169 -0
  31. package/lib/semantics/semantics.js +380 -0
  32. package/lib/semantics/vocabulary.d.ts +79 -0
  33. package/lib/semantics/vocabulary.js +112 -0
  34. package/lib/store/format.d.ts +67 -0
  35. package/lib/store/format.js +334 -0
  36. package/lib/store/store.d.ts +296 -0
  37. package/lib/store/store.js +734 -0
  38. package/package.json +41 -5
  39. package/scripts/codex-register.mjs +89 -20
  40. package/scripts/install-mmap-command.mjs +293 -0
  41. package/scripts/mmap.mjs +213 -0
  42. package/scripts/open-pane.mjs +115 -254
  43. package/scripts/pane-core.mjs +418 -0
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Layer 4a — the status vocabulary as this medium wears it.
3
+ *
4
+ * The GLYPHS are the map's shared alphabet and live in ../semantics; what a
5
+ * TERMINAL owns is decided here and only here: which border repertoire a
6
+ * status draws with, and which SGR color it burns. One table, so the pane's
7
+ * chrome (tab strip, panel header) and the picture's boxes cannot drift
8
+ * apart — the pane reads statusSgr from this very file.
9
+ */
10
+ import { kindGlyph, spinnerGlyph, statusGlyph, unverifiedDoneGlyph } from '../semantics/semantics.js';
11
+ import { SGR } from './canvas.js';
12
+ /** The color role a status face wears in a terminal. */
13
+ export function styleFor(face) {
14
+ switch (face) {
15
+ case 'planned':
16
+ return 'dim';
17
+ case 'in-progress':
18
+ return 'amber';
19
+ case 'done':
20
+ return 'green';
21
+ case 'done-unverified':
22
+ return 'greenDim';
23
+ case 'regressed':
24
+ return 'red';
25
+ }
26
+ }
27
+ /**
28
+ * SGR parameters of a status' skin, for terminal chrome painted outside the
29
+ * canvas (a tab strip, a panel header). The pane's chrome and the picture's
30
+ * boxes therefore wear one palette by construction, not by a second table.
31
+ */
32
+ export function statusSgr(status) {
33
+ return SGR[styleFor(status)];
34
+ }
35
+ export function skinFor(face, unicode) {
36
+ const style = styleFor(face);
37
+ if (!unicode) {
38
+ return face === 'planned'
39
+ ? { h: '.', v: ':', corners: ['+', '+', '+', '+'], style }
40
+ : { h: '-', v: '|', corners: ['+', '+', '+', '+'], style };
41
+ }
42
+ switch (face) {
43
+ case 'planned':
44
+ return { h: '╌', v: '╎', corners: ['╭', '╮', '╰', '╯'], style };
45
+ case 'in-progress':
46
+ return { h: '─', v: '│', corners: ['╭', '╮', '╰', '╯'], style };
47
+ // an unverified done keeps the heavy border of done — it is the same
48
+ // claim, told with a hollow glyph and a dimmer green
49
+ case 'done':
50
+ case 'done-unverified':
51
+ case 'regressed':
52
+ return { h: '━', v: '┃', corners: ['┏', '┓', '┗', '┛'], style };
53
+ }
54
+ }
55
+ /**
56
+ * The glyph a node shows for its status face. A terminal box CAN animate, so
57
+ * in-progress spins through the shared frames; every other face is a shared
58
+ * static glyph.
59
+ */
60
+ export function glyphFor(face, opts) {
61
+ if (face === 'in-progress')
62
+ return spinnerGlyph(opts.spinnerFrame, opts.unicode);
63
+ return face === 'done-unverified' ? unverifiedDoneGlyph(opts.unicode) : statusGlyph(face, opts.unicode);
64
+ }
65
+ /** Plain solid box for documentation diagrams — presence, not progress. */
66
+ export function neutralSkin(unicode) {
67
+ return unicode
68
+ ? { h: '─', v: '│', corners: ['╭', '╮', '╰', '╯'], style: 'none' }
69
+ : { h: '-', v: '|', corners: ['+', '+', '+', '+'], style: 'none' };
70
+ }
71
+ /** The glyph slot of a box on a documentation page: the KIND, or a bullet. */
72
+ export function neutralGlyph(node, unicode) {
73
+ return ((node.kind !== undefined ? kindGlyph(node.kind, unicode) : undefined) ?? (unicode ? '·' : '.'));
74
+ }
75
+ /**
76
+ * Ids of the boxes whose `done` has nothing behind it.
77
+ *
78
+ * Computed against the map as DECLARED, not as drawn: an aggregated group box
79
+ * carries no evidence field of its own, and reading that absence literally
80
+ * would paint every grouped far-zoom view as unverified. A group is exactly
81
+ * as backed as the members it stands for.
82
+ */
83
+ export function unverifiedDoneIds(declared, drawn) {
84
+ const out = new Set();
85
+ const declaredById = new Map(declared.nodes.map((n) => [n.id, n]));
86
+ for (const node of drawn.nodes) {
87
+ if (node.status !== 'done')
88
+ continue;
89
+ const own = declaredById.get(node.id);
90
+ if (own !== undefined) {
91
+ if (own.evidence === undefined)
92
+ out.add(node.id);
93
+ }
94
+ else if (declared.nodes.some((m) => m.group === node.id && m.status === 'done' && m.evidence === undefined)) {
95
+ out.add(node.id); // a group box stands for a member nobody verified
96
+ }
97
+ }
98
+ return out;
99
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Layer 4a — how many terminal columns a string occupies.
3
+ *
4
+ * Everything above this file lays out a grid, and a grid is only as honest as
5
+ * its widths: a label measured one column short shears every row below its
6
+ * box, and one column long leaves a gap no border closes. Labels are DATA —
7
+ * people write Chinese, emoji and accented letters — so this is not a detail
8
+ * of the drawing code but its foundation.
9
+ *
10
+ * The two tables are the Unicode East Asian Width W/F set and the
11
+ * zero-advance set, kept deliberately narrow: everything the standard calls
12
+ * AMBIGUOUS (■ ● ✗ ○ and the rest of this map's own glyph alphabet) stays ONE
13
+ * column, which is what a Western terminal draws.
14
+ *
15
+ * Pure functions of a string; no canvas, no options, no I/O.
16
+ */
17
+ /** Columns one code point occupies: 0 (a mark riding on its base), 1, or 2. */
18
+ export declare function charWidth(cp: number): number;
19
+ /** Terminal column width of a string (CJK chars occupy two columns). */
20
+ export declare function displayWidth(text: string): number;
21
+ /** Truncate to a display width, ANSI-free input, appending … when cut. */
22
+ export declare function fitWidth(s: string, width: number): string;
23
+ /** Hard word-wrap by display width (CJK-aware, splits anywhere). */
24
+ export declare function wrapWidth(s: string, width: number): string[];
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Layer 4a — how many terminal columns a string occupies.
3
+ *
4
+ * Everything above this file lays out a grid, and a grid is only as honest as
5
+ * its widths: a label measured one column short shears every row below its
6
+ * box, and one column long leaves a gap no border closes. Labels are DATA —
7
+ * people write Chinese, emoji and accented letters — so this is not a detail
8
+ * of the drawing code but its foundation.
9
+ *
10
+ * The two tables are the Unicode East Asian Width W/F set and the
11
+ * zero-advance set, kept deliberately narrow: everything the standard calls
12
+ * AMBIGUOUS (■ ● ✗ ○ and the rest of this map's own glyph alphabet) stays ONE
13
+ * column, which is what a Western terminal draws.
14
+ *
15
+ * Pure functions of a string; no canvas, no options, no I/O.
16
+ */
17
+ const WIDE_RANGES = [
18
+ [0x1100, 0x115f], // Hangul Jamo
19
+ // Wide symbols scattered through the BMP — mostly emoji that predate the
20
+ // emoji planes (⌚ ⏰ ⚡ ✅ ✨ ❌ ❓ ⭐ ⬛ …).
21
+ [0x231a, 0x231b],
22
+ [0x2329, 0x232a],
23
+ [0x23e9, 0x23ec],
24
+ [0x23f0, 0x23f0],
25
+ [0x23f3, 0x23f3],
26
+ [0x25fd, 0x25fe],
27
+ [0x2614, 0x2615],
28
+ [0x2648, 0x2653],
29
+ [0x267f, 0x267f],
30
+ [0x2693, 0x2693],
31
+ [0x26a1, 0x26a1],
32
+ [0x26aa, 0x26ab],
33
+ [0x26bd, 0x26be],
34
+ [0x26c4, 0x26c5],
35
+ [0x26ce, 0x26ce],
36
+ [0x26d4, 0x26d4],
37
+ [0x26ea, 0x26ea],
38
+ [0x26f2, 0x26f3],
39
+ [0x26f5, 0x26f5],
40
+ [0x26fa, 0x26fa],
41
+ [0x26fd, 0x26fd],
42
+ [0x2705, 0x2705],
43
+ [0x270a, 0x270b],
44
+ [0x2728, 0x2728],
45
+ [0x274c, 0x274c],
46
+ [0x274e, 0x274e],
47
+ [0x2753, 0x2755],
48
+ [0x2757, 0x2757],
49
+ [0x2795, 0x2797],
50
+ [0x27b0, 0x27b0],
51
+ [0x27bf, 0x27bf],
52
+ [0x2b1b, 0x2b1c],
53
+ [0x2b50, 0x2b50],
54
+ [0x2b55, 0x2b55],
55
+ [0x2e80, 0xa4cf], // CJK radicals .. Yi (covers CJK Unified Ideographs)
56
+ [0xa960, 0xa97f],
57
+ [0xac00, 0xd7a3], // Hangul syllables
58
+ [0xf900, 0xfaff], // CJK compatibility ideographs
59
+ [0xfe10, 0xfe19],
60
+ [0xfe30, 0xfe6f],
61
+ [0xff00, 0xff60], // fullwidth forms
62
+ [0xffe0, 0xffe6],
63
+ [0x1f300, 0x1f64f], // pictographs, transport, emoticons (🚀 🎯 😀 …)
64
+ [0x1f680, 0x1f6ff],
65
+ [0x1f900, 0x1f9ff], // supplemental symbols (🤖 🧱 …)
66
+ [0x1fa70, 0x1faff], // symbols extended-A
67
+ [0x20000, 0x3fffd], // CJK extension planes
68
+ ];
69
+ /** Code points that advance the cursor by nothing at all. */
70
+ const ZERO_WIDTH_RANGES = [
71
+ [0x0300, 0x036f], // combining diacritical marks (decomposed 'e' + ´)
72
+ [0x1ab0, 0x1aff],
73
+ [0x1dc0, 0x1dff],
74
+ [0x200b, 0x200f], // zero-width space .. RLM, zero-width joiner among them
75
+ [0x20d0, 0x20f0], // combining marks for symbols
76
+ [0xfe00, 0xfe0f], // variation selectors, VS16 (emoji presentation) included
77
+ [0xfe20, 0xfe2f], // combining half marks
78
+ [0x1f3fb, 0x1f3ff], // emoji skin tone modifiers — always applied to a base
79
+ ];
80
+ function inRanges(cp, ranges) {
81
+ for (const [lo, hi] of ranges) {
82
+ if (cp >= lo && cp <= hi)
83
+ return true;
84
+ }
85
+ return false;
86
+ }
87
+ /** Columns one code point occupies: 0 (a mark riding on its base), 1, or 2. */
88
+ export function charWidth(cp) {
89
+ if (inRanges(cp, ZERO_WIDTH_RANGES))
90
+ return 0;
91
+ return inRanges(cp, WIDE_RANGES) ? 2 : 1;
92
+ }
93
+ /** Terminal column width of a string (CJK chars occupy two columns). */
94
+ export function displayWidth(text) {
95
+ let w = 0;
96
+ for (const ch of text)
97
+ w += charWidth(ch.codePointAt(0));
98
+ return w;
99
+ }
100
+ /** Truncate to a display width, ANSI-free input, appending … when cut. */
101
+ export function fitWidth(s, width) {
102
+ if (displayWidth(s) <= width)
103
+ return s;
104
+ let out = '';
105
+ let w = 0;
106
+ for (const ch of s) {
107
+ const cw = displayWidth(ch);
108
+ if (w + cw > width - 1)
109
+ break;
110
+ out += ch;
111
+ w += cw;
112
+ }
113
+ return out + '…';
114
+ }
115
+ /** Hard word-wrap by display width (CJK-aware, splits anywhere). */
116
+ export function wrapWidth(s, width) {
117
+ const lines = [];
118
+ let line = '';
119
+ let w = 0;
120
+ for (const ch of s.replace(/\r/g, '')) {
121
+ if (ch === '\n') {
122
+ lines.push(line);
123
+ line = '';
124
+ w = 0;
125
+ continue;
126
+ }
127
+ const cw = displayWidth(ch);
128
+ if (w + cw > width) {
129
+ lines.push(line);
130
+ line = '';
131
+ w = 0;
132
+ }
133
+ line += ch;
134
+ w += cw;
135
+ }
136
+ if (line !== '')
137
+ lines.push(line);
138
+ return lines;
139
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Layer 4a — what one rung of the zoom ladder buys, in terminal cells.
3
+ *
4
+ * Terminals cannot scale glyphs, so zooming out COMPRESSES geometry instead:
5
+ * gaps, breathing rows and padding shrink, labels truncate toward a
6
+ * scale-proportional budget, and only when a further step would leave labels
7
+ * too short to mean anything does the picture switch mode. WHICH steps switch
8
+ * mode is semantics (zoomMode over in ../semantics); this module owns only
9
+ * the whitespace and label budgets each step spends.
10
+ */
11
+ import { type ZoomStep } from '../semantics/semantics.js';
12
+ /** Height of a box with nothing unfolded inside it: border, content, border. */
13
+ export declare const BOX_H = 3;
14
+ /** Columns between two boxes of one band at the roomy default. */
15
+ export declare const BOX_GAP = 2;
16
+ /** Columns before the leftmost box; the picture never starts at the edge. */
17
+ export declare const LEFT_MARGIN = 2;
18
+ /** Bar cells kept left of the narrowest band label, so a band still reads as a band. */
19
+ export declare const BAR_MIN_RUN = 7;
20
+ /** Box content budget for a detail step; present exactly when mode is 'detail'. */
21
+ export interface DetailBudget {
22
+ /** Clamp range for the box's inner width. */
23
+ readonly innerMin: number;
24
+ readonly innerMax: number;
25
+ /** Design-note rows shown before the … cut. */
26
+ readonly noteRows: number;
27
+ }
28
+ /** How one zoom step translates into whitespace geometry; see the module header. */
29
+ export interface ZoomGeometry {
30
+ readonly mode: 'constellation' | 'boxes' | 'detail';
31
+ /** Label width multiplier while scaling down (boxes mode). */
32
+ readonly scale: number;
33
+ /** Inner padding around "glyph label" (1 = the roomy standard look). */
34
+ readonly pad: 0 | 1;
35
+ readonly boxGap: number;
36
+ /** Breathing rows around wire track rows in a band gap. */
37
+ readonly breathe: 0 | 1;
38
+ /** Blank row after the title. */
39
+ readonly titleGap: 0 | 1;
40
+ /** Blank row between a band bar and its boxes. */
41
+ readonly barGap: 0 | 1;
42
+ /** Band bars carry done/total counts once boxes are too small to speak. */
43
+ readonly bandCounts: boolean;
44
+ /** Content budget of the detail steps; set exactly when mode is 'detail'. */
45
+ readonly detail?: DetailBudget;
46
+ }
47
+ export declare function zoomGeometry(zoom: ZoomStep): ZoomGeometry;
48
+ /**
49
+ * Geometry for the aggregated far zoom: tight chrome, but FULL labels — the
50
+ * point of collapsing a map into its groups is reading their names.
51
+ */
52
+ export declare const AGGREGATE_GEO: ZoomGeometry;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Layer 4a — what one rung of the zoom ladder buys, in terminal cells.
3
+ *
4
+ * Terminals cannot scale glyphs, so zooming out COMPRESSES geometry instead:
5
+ * gaps, breathing rows and padding shrink, labels truncate toward a
6
+ * scale-proportional budget, and only when a further step would leave labels
7
+ * too short to mean anything does the picture switch mode. WHICH steps switch
8
+ * mode is semantics (zoomMode over in ../semantics); this module owns only
9
+ * the whitespace and label budgets each step spends.
10
+ */
11
+ import { zoomMode } from '../semantics/semantics.js';
12
+ /** Height of a box with nothing unfolded inside it: border, content, border. */
13
+ export const BOX_H = 3;
14
+ /** Columns between two boxes of one band at the roomy default. */
15
+ export const BOX_GAP = 2;
16
+ /** Columns before the leftmost box; the picture never starts at the edge. */
17
+ export const LEFT_MARGIN = 2;
18
+ /** Bar cells kept left of the narrowest band label, so a band still reads as a band. */
19
+ export const BAR_MIN_RUN = 7;
20
+ /** +1: a readable unfold. +2: the box grows into a reading card. */
21
+ const DETAIL_BUDGET = { innerMin: 22, innerMax: 32, noteRows: 3 };
22
+ const DETAIL_PLUS_BUDGET = { innerMin: 30, innerMax: 48, noteRows: 12 };
23
+ export function zoomGeometry(zoom) {
24
+ const m = zoomMode(zoom);
25
+ const mode = m === 'overview' ? 'constellation' : m;
26
+ switch (zoom) {
27
+ case 2:
28
+ return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_PLUS_BUDGET };
29
+ case 1:
30
+ return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_BUDGET };
31
+ case 0:
32
+ return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
33
+ case -1:
34
+ return { mode, scale: 0.85, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
35
+ case -2:
36
+ return { mode, scale: 0.7, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: false };
37
+ case -3:
38
+ return { mode, scale: 0.55, pad: 0, boxGap: 1, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
39
+ case -4:
40
+ return { mode, scale: 0, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
41
+ }
42
+ }
43
+ /**
44
+ * Geometry for the aggregated far zoom: tight chrome, but FULL labels — the
45
+ * point of collapsing a map into its groups is reading their names.
46
+ */
47
+ export const AGGREGATE_GEO = {
48
+ mode: 'boxes',
49
+ scale: 1,
50
+ pad: 0,
51
+ boxGap: 1,
52
+ breathe: 0,
53
+ titleGap: 0,
54
+ barGap: 1,
55
+ bandCounts: false,
56
+ };
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Layer 1c — medium-neutral VIEW SEMANTICS of a MellosMap.
3
+ *
4
+ * Every renderer (the terminal pane, a web panel, a future editor view) must
5
+ * agree on what a zoom step MEANS, when a map aggregates into its groups,
6
+ * how sequence time is oriented, and which map kinds render neutrally.
7
+ * Those rules live here, pure of any medium: no cells, no colors, no DOM,
8
+ * no I/O. Geometry — how a mode maps onto character cells or pixels — stays
9
+ * private to each renderer.
10
+ *
11
+ * The shared ALPHABET (status glyphs, spinner frames, node-kind glyphs) is
12
+ * medium-neutral for the same reason and lives beside this file in
13
+ * ./vocabulary.js, re-exported here so consumers keep one import site.
14
+ */
15
+ import type { MapGroup, MapNode, MellosMap, NodeStatus } from '../domain/types.js';
16
+ export { NODE_KIND_GLYPHS, SPINNER_FRAMES, STATUS_GLYPHS, UNVERIFIED_DONE_GLYPHS, kindGlyph, spinnerGlyph, statusGlyph, unverifiedDoneGlyph, } from './vocabulary.js';
17
+ /** One wheel tick on the zoom ladder. */
18
+ export type ZoomStep = -4 | -3 | -2 | -1 | 0 | 1 | 2;
19
+ export declare const ZOOM_MIN: ZoomStep;
20
+ export declare const ZOOM_MAX: ZoomStep;
21
+ export declare const ZOOM_DEFAULT: ZoomStep;
22
+ export declare function clampZoom(n: number): ZoomStep;
23
+ /**
24
+ * What a zoom step MEANS, before any renderer decides what it looks like:
25
+ * scaling only compresses, the ends of the ladder switch mode. 'detail'
26
+ * unfolds evidence and design notes; 'overview' switches to the aggregated
27
+ * far view (groups become the nodes — see aggregateMap); 'boxes' is every
28
+ * step in between, where boxes stay boxes and only whitespace and label
29
+ * budgets change.
30
+ */
31
+ export declare function zoomMode(zoom: ZoomStep): 'detail' | 'boxes' | 'overview';
32
+ /** What a footer shows: a percentage while scaling, a mode name at the ends. */
33
+ export declare function zoomLabel(zoom: ZoomStep): string;
34
+ /** Documentation kinds render neutrally: no status skins, no progress counts. */
35
+ export declare function isNeutralKind(map: MellosMap): boolean;
36
+ /**
37
+ * The derived coarse picture the far zoom renders when the map declares
38
+ * groups: each group becomes ONE labeled node (status derived from members,
39
+ * label carrying done/total), ungrouped nodes stay themselves, and edges
40
+ * collapse onto representatives (intra-group wiring disappears into the
41
+ * box). Derived for rendering only — never persisted. A map without groups
42
+ * returns undefined and falls back to whatever anonymous overview the
43
+ * renderer draws.
44
+ */
45
+ export declare function aggregateMap(map: MellosMap): MellosMap | undefined;
46
+ /** One neighbour across a dependency edge, with what flows along it. */
47
+ export interface NeighborRef {
48
+ readonly id: string;
49
+ readonly label: string;
50
+ readonly status: NodeStatus;
51
+ readonly edgeLabel?: string;
52
+ }
53
+ /** Everything a detail panel derives for a focused NODE. */
54
+ export interface NodeFocus {
55
+ readonly kind: 'node';
56
+ readonly node: MapNode;
57
+ readonly layerName: string;
58
+ readonly laneLabel?: string;
59
+ /** Lower neighbours this node stands on (on sequence pages: the earlier events). */
60
+ readonly uses: readonly NeighborRef[];
61
+ /** Upper neighbours standing on this node (on sequence pages: the later events). */
62
+ readonly usedBy: readonly NeighborRef[];
63
+ }
64
+ /** Everything a detail panel derives for a focused GROUP (the far zoom's boxes). */
65
+ export interface GroupFocus {
66
+ readonly kind: 'group';
67
+ readonly group: MapGroup;
68
+ readonly status: NodeStatus;
69
+ readonly layerName: string;
70
+ readonly members: readonly MapNode[];
71
+ /** Outside-the-group lower neighbours, each shown as its own group when it has one. */
72
+ readonly uses: readonly NeighborRef[];
73
+ /** Outside-the-group upper neighbours, each shown as its own group when it has one. */
74
+ readonly usedBy: readonly NeighborRef[];
75
+ }
76
+ /**
77
+ * Derive what a detail panel says about one focused id — a node, or a group
78
+ * when the far zoom's aggregated boxes are what the pointer is over. Pure
79
+ * data: every renderer picks its own glyphs, colors, and words (a sequence
80
+ * page reads uses/usedBy as after/before; that is the caller's vocabulary).
81
+ * Groups are resolved first, which is unambiguous: one id names one box on
82
+ * the map (I10), so no node can be shadowed by a group of the same name.
83
+ * @param map - the map the focus lives in.
84
+ * @param focusId - node or group id.
85
+ * @returns the focus view, or undefined when the id names neither.
86
+ *
87
+ * VIOLATION: no-primitive-obsession - `focusId` is a raw string, not a brand.
88
+ * It has to be: the caller is a hit test over the picture, and at the far
89
+ * zoom that picture is the AGGREGATED map, whose boxes are groups. So the id
90
+ * arriving here is a NodeId or a GroupId and the caller cannot know which —
91
+ * deciding that is this function's whole job. `NodeId | GroupId` would type
92
+ * it, but every hit-test path (BoxHit.id, RenderOptions.focus, the pane's
93
+ * hover/selection state) would then have to carry that union and cast into it
94
+ * at the same boundary, moving the cast rather than removing it. The value is
95
+ * validated the only way it can be: an id that names neither answers
96
+ * undefined.
97
+ */
98
+ export declare function focusInfo(map: MellosMap, focusId: string): NodeFocus | GroupFocus | undefined;
99
+ /**
100
+ * Page slugs some node of any map dives into — the raw fact only, WITHOUT
101
+ * the rule a sibling strip needs on top of it (that is interiorPages).
102
+ * @param maps - every known page's map (undefined entries are skipped).
103
+ * @returns the referenced submap slugs.
104
+ */
105
+ export declare function submapRefs(maps: Iterable<MellosMap | undefined>): Set<string>;
106
+ /**
107
+ * Which pages are INTERIOR: reached by diving through a node, never by
108
+ * sitting beside their parent as a sibling tab.
109
+ *
110
+ * "Referenced by anyone" is NOT the rule, because a strip computed that way
111
+ * can erase itself. Two refinements make it total:
112
+ * - a page never hides itself. A node whose submap names its own page is a
113
+ * loop with no bottom, not a parent link — read as one, it deleted the
114
+ * tab of the very page it was drawn on.
115
+ * - a page referenced only from pages it can itself reach keeps its tab. A
116
+ * link cycle has no outside, so hiding every page in it leaves a strip
117
+ * with nothing in it and a client with nowhere left to go.
118
+ * Everything else is interior: some page OUTSIDE its own loop dives into it,
119
+ * and that page's node is the way in.
120
+ *
121
+ * @param pages - every known page as (slug, map) pairs; the default page has
122
+ * no slug a node could name, so it is passed as undefined and is never
123
+ * interior. Entries with no map (unreadable, not yet loaded) contribute no
124
+ * links.
125
+ * @returns the slugs to keep out of a sibling strip.
126
+ */
127
+ export declare function interiorPages(pages: Iterable<readonly [string | undefined, MellosMap | undefined]>): Set<string>;
128
+ /**
129
+ * Where a sub-map page was dived into from: the entry whose map links the
130
+ * page, plus the linking node's label. Derived by scan, so a breadcrumb
131
+ * survives any client restart with an empty dive stack.
132
+ * @param entries - known pages as (key, map) pairs; keys are caller-owned.
133
+ * @param pageId - the sub-map page's slug.
134
+ * @returns the linking entry's key and node label, or undefined for a top-level page.
135
+ */
136
+ export declare function diveParent<K>(entries: Iterable<readonly [K, MellosMap | undefined]>, pageId: string): {
137
+ parent: K;
138
+ label: string;
139
+ } | undefined;
140
+ /**
141
+ * The page a client shows when nobody asked for one: the most recently
142
+ * WRITTEN page — the ledger last touched is almost always the effort under
143
+ * way. Keys without a readable timestamp lose; an empty set answers the
144
+ * first key (caller-ordered: default page first, then slug order).
145
+ * @param keys - candidate page keys in the caller's fallback order.
146
+ * @param mtimeOf - last-written timestamp of a key, undefined when unknown.
147
+ * @returns the winning key, or undefined for an empty candidate set.
148
+ *
149
+ * VIOLATION: prefer-objects - `mtimeOf` is a function parameter, which this
150
+ * layer otherwise avoids. It is what keeps the rule medium-neutral: the two
151
+ * callers ask different worlds for the same fact — the pane stats a file, the
152
+ * browser panel reads an mtime the host already sent over the wire — and
153
+ * neither timestamp source can be named here without dragging a filesystem or
154
+ * a transport into a module that must stay free of both. The alternative,
155
+ * taking `readonly [K, number | undefined][]`, only moves the same lookup to
156
+ * the caller and makes it allocate a pair per page per tick. The parameter is
157
+ * a pure query, called once per key, never stored.
158
+ */
159
+ export declare function mostRecentKey<K>(keys: readonly K[], mtimeOf: (key: K) => number | undefined): K | undefined;
160
+ /**
161
+ * Sequence pages read like the classic diagram: time flows DOWNWARD, the
162
+ * earliest step right under the participant headers. The stored map keeps
163
+ * rank 0 = earliest with edges pointing later -> earlier ("later stands on
164
+ * earlier"); this derived value inverts the ranks and reverses the edges so
165
+ * unchanged top-down machinery draws top-down time — each wire now runs
166
+ * from the sender's moment down into the receiver's. Derived for rendering
167
+ * only, never persisted (same contract as aggregateMap).
168
+ */
169
+ export declare function flipForSequence(map: MellosMap): MellosMap;