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.
- package/README.md +366 -56
- package/README.zh-CN.md +319 -47
- package/dist/hook-session-start.mjs +239 -0
- package/dist/mmap.mjs +338 -0
- package/dist/server.mjs +1737 -897
- package/dist/store-paths.mjs +107 -0
- package/dist/watch.mjs +1612 -902
- package/lib/domain/ops.d.ts +171 -0
- package/lib/domain/ops.js +384 -0
- package/lib/domain/types.d.ts +283 -0
- package/lib/domain/types.js +153 -0
- package/lib/render/canvas.d.ts +50 -0
- package/lib/render/canvas.js +210 -0
- package/lib/render/draw.d.ts +37 -0
- package/lib/render/draw.js +111 -0
- package/lib/render/layout.d.ts +89 -0
- package/lib/render/layout.js +200 -0
- package/lib/render/options.d.ts +39 -0
- package/lib/render/options.js +10 -0
- package/lib/render/render.d.ts +88 -0
- package/lib/render/render.js +128 -0
- package/lib/render/routing.d.ts +56 -0
- package/lib/render/routing.js +244 -0
- package/lib/render/skins.d.ts +54 -0
- package/lib/render/skins.js +99 -0
- package/lib/render/width.d.ts +24 -0
- package/lib/render/width.js +139 -0
- package/lib/render/zoom-geometry.d.ts +52 -0
- package/lib/render/zoom-geometry.js +56 -0
- package/lib/semantics/semantics.d.ts +169 -0
- package/lib/semantics/semantics.js +380 -0
- package/lib/semantics/vocabulary.d.ts +79 -0
- package/lib/semantics/vocabulary.js +112 -0
- package/lib/store/format.d.ts +67 -0
- package/lib/store/format.js +334 -0
- package/lib/store/store.d.ts +296 -0
- package/lib/store/store.js +734 -0
- package/package.json +41 -5
- package/scripts/codex-register.mjs +89 -20
- package/scripts/install-mmap-command.mjs +293 -0
- package/scripts/mmap.mjs +213 -0
- package/scripts/open-pane.mjs +115 -254
- 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;
|