mellos-mapping 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Layer 0 — domain model of a Mellos Map.
3
+ *
4
+ * A Mellos Map is a layered dependency map of a system under construction:
5
+ * horizontal layer bands ordered by rank (rank 0 = bottom = most primitive),
6
+ * nodes living inside exactly one band, and dependency edges that may only
7
+ * point STRICTLY DOWNWARD across bands.
8
+ *
9
+ * Structural invariants owned by this layer (and only these — the map is a
10
+ * ledger, not a judge; it records work honestly and never polices workflow):
11
+ * I1. Layer ids are unique; layer ranks are unique (bands are totally ordered).
12
+ * I2. Every node belongs to exactly one existing layer.
13
+ * I3. Node ids are unique.
14
+ * I4. An edge `from -> to` means "from USES to" and requires
15
+ * rank(layer(from)) > rank(layer(to)).
16
+ * Corollary: the graph is acyclic by construction — every edge strictly
17
+ * decreases rank, so no cycle detection is ever needed.
18
+ * Same-layer edges are rejected on purpose: if A needs a sibling B,
19
+ * either B is really a lower-layer concept or A and B are one node.
20
+ * I5. Node status is one of the closed vocabulary in NODE_STATUSES.
21
+ * I6. Group ids are unique; every group lives in an existing layer.
22
+ * I7. A node's group, when set, exists and lives in the node's own layer —
23
+ * a group is band-local cohesion (a labeled subsystem the far zoom can
24
+ * render); structure ACROSS bands is what layers and edges express.
25
+ * I8. Lane ids are unique. A lane is a named vertical column CROSSING all
26
+ * bands (a sequence participant, a swim lane); lane declaration order
27
+ * is left-to-right render order.
28
+ * I9. A node's lane, when set, exists.
29
+ *
30
+ * The map kind (dev | architecture | dataflow | behavior-tree | sequence) is
31
+ * presentation intent, not structure: every kind shares the same invariants,
32
+ * and the renderer alone decides what the kind changes (legend, neutral
33
+ * status skins, lane emphasis).
34
+ *
35
+ * Everything here is immutable data plus pure types. No I/O, no clock, no
36
+ * process state.
37
+ */
38
+ export const ok = (value) => ({ ok: true, value });
39
+ export const err = (error) => ({ ok: false, error });
40
+ /** The shared slug grammar for every id in the system (nodes, layers, groups, lanes, kinds, store pages). */
41
+ export const ID_RULE = /^[a-z0-9][a-z0-9-]{0,63}$/;
42
+ export const ID_RULE_TEXT = 'lowercase letters, digits and dashes, starting with a letter or digit, 1-64 chars';
43
+ export function makeNodeId(raw) {
44
+ return ID_RULE.test(raw) ? ok(raw) : err({ kind: 'invalid-id', raw, rule: ID_RULE_TEXT });
45
+ }
46
+ export function makeLayerId(raw) {
47
+ return ID_RULE.test(raw) ? ok(raw) : err({ kind: 'invalid-id', raw, rule: ID_RULE_TEXT });
48
+ }
49
+ export function makeGroupId(raw) {
50
+ return ID_RULE.test(raw) ? ok(raw) : err({ kind: 'invalid-id', raw, rule: ID_RULE_TEXT });
51
+ }
52
+ export function makeLaneId(raw) {
53
+ return ID_RULE.test(raw) ? ok(raw) : err({ kind: 'invalid-id', raw, rule: ID_RULE_TEXT });
54
+ }
55
+ export function makeNodeKind(raw) {
56
+ return ID_RULE.test(raw) ? ok(raw) : err({ kind: 'invalid-id', raw, rule: ID_RULE_TEXT });
57
+ }
58
+ export function makeSubmapRef(raw) {
59
+ return ID_RULE.test(raw) ? ok(raw) : err({ kind: 'invalid-id', raw, rule: ID_RULE_TEXT });
60
+ }
61
+ /**
62
+ * Closed vocabulary of map kinds — the diagram's presentation intent.
63
+ * 'dev' (the default) is the progress ledger; the rest are documentation
64
+ * diagrams rendered with neutral skins. Structure is identical for all.
65
+ */
66
+ export const MAP_KINDS = ['dev', 'architecture', 'dataflow', 'behavior-tree', 'sequence'];
67
+ export function makeMapKind(raw) {
68
+ return MAP_KINDS.includes(raw) ? ok(raw) : err({ kind: 'invalid-map-kind', raw });
69
+ }
70
+ /** Closed status vocabulary. Transitions are NOT policed — see module header. */
71
+ export const NODE_STATUSES = ['planned', 'in-progress', 'done', 'regressed'];
72
+ export function makeNodeStatus(raw) {
73
+ return NODE_STATUSES.includes(raw)
74
+ ? ok(raw)
75
+ : err({ kind: 'invalid-status', raw });
76
+ }
77
+ export const EMPTY_MAP = { layers: [], groups: [], lanes: [], nodes: [], edges: [] };
78
+ /** Human-readable rendering of a MapError, for tool results and logs. */
79
+ export function describeMapError(e) {
80
+ switch (e.kind) {
81
+ case 'invalid-id':
82
+ return `invalid id "${e.raw}" (rule: ${e.rule})`;
83
+ case 'invalid-status':
84
+ return `invalid status "${e.raw}" (expected: ${NODE_STATUSES.join(' | ')})`;
85
+ case 'duplicate-layer':
86
+ return `layer "${e.id}" already exists`;
87
+ case 'duplicate-rank':
88
+ return `rank ${e.rank} is already taken by layer "${e.existing}"`;
89
+ case 'duplicate-node':
90
+ return `node "${e.id}" already exists`;
91
+ case 'unknown-layer':
92
+ return `layer "${e.id}" does not exist`;
93
+ case 'unknown-node':
94
+ return `node "${e.id}" does not exist`;
95
+ case 'duplicate-edge':
96
+ return `edge ${e.from} -> ${e.to} already exists`;
97
+ case 'unknown-edge':
98
+ return `edge ${e.from} -> ${e.to} does not exist`;
99
+ case 'self-edge':
100
+ return `node "${e.id}" cannot depend on itself`;
101
+ case 'duplicate-group':
102
+ return `group "${e.id}" already exists`;
103
+ case 'unknown-group':
104
+ return `group "${e.id}" does not exist`;
105
+ case 'invalid-map-kind':
106
+ return `invalid map kind "${e.raw}" (expected: ${MAP_KINDS.join(' | ')})`;
107
+ case 'duplicate-lane':
108
+ return `lane "${e.id}" already exists`;
109
+ case 'unknown-lane':
110
+ return `lane "${e.id}" does not exist`;
111
+ case 'group-layer-mismatch':
112
+ return (`node "${e.node}" (layer ${e.nodeLayer}) cannot join group "${e.group}" (layer ${e.groupLayer}); ` +
113
+ `groups cluster nodes within one band`);
114
+ case 'layer-not-empty':
115
+ return `layer "${e.id}" still holds node "${e.occupant}"; move or remove its nodes first`;
116
+ case 'layer-holds-group':
117
+ return `layer "${e.id}" still holds group "${e.occupant}"; remove its groups first`;
118
+ case 'edge-not-downward':
119
+ return (`edge ${e.from} (rank ${e.fromRank}) -> ${e.to} (rank ${e.toRank}) is not strictly downward; ` +
120
+ `dependencies may only point to a lower layer`);
121
+ }
122
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Layer 4 (presentation) — pure rendering of a MellosMap to terminal lines.
3
+ *
4
+ * Shared by the MCP `mmap_view` tool (monochrome) and the split-pane watcher
5
+ * (colored, animated). Pure function of (map, options): no I/O, no clock —
6
+ * animation is driven by the caller passing a spinner frame index.
7
+ *
8
+ * Visual language — a dark circuit board:
9
+ * rank 0 renders at the BOTTOM of the picture ("primitives are the
10
+ * ground"). Wiring and band bars are FAINT; the glowing things are the
11
+ * nodes. Junctions where a wire enters a box inherit the box's color,
12
+ * like lit pins. Dependency lines only ever travel downward.
13
+ *
14
+ * planned dashed dim rounded box, '·' — a ghost: designed, not built
15
+ * in-progress amber rounded box, spinner — where attention currently is
16
+ * done heavy green box, '■' — built and verified
17
+ * regressed heavy red box, '✗' — was done, foundation cracked
18
+ *
19
+ * Routing preference, in order:
20
+ * 1. STRAIGHT — an adjacent-band edge whose box borders share a free
21
+ * column is one vertical line, no corners.
22
+ * 2. DOGLEG — descend, run horizontally on a track row in the gap above
23
+ * the target band, descend. Tracks are PACKED: segments that do not
24
+ * overlap share a row, keeping bands close together.
25
+ * 3. THREAD — a skip-level edge descends through the nearest column that
26
+ * is free of boxes in every intermediate band (threading the needle
27
+ * between boxes); only if no such column exists does it fall back to a
28
+ * private column on the right margin.
29
+ * Crossings merge into proper junction characters via a direction-bitmask
30
+ * union instead of any routing cleverness.
31
+ *
32
+ * Zoom — terminals cannot scale glyphs, so zooming out first COMPRESSES the
33
+ * geometry (gaps, breathing rows, padding shrink; labels truncate toward a
34
+ * scale-proportional budget) while boxes stay boxes. Only when a further
35
+ * step would leave labels too short to mean anything does the picture switch
36
+ * mode — to a borderless glyph constellation whose band bars carry
37
+ * done/total counts. Zooming in past 100% unfolds evidence and design notes
38
+ * inside the boxes; a second step widens the boxes and unfolds the notes
39
+ * further. The ladder, one wheel tick per step:
40
+ * +2 detail+ wider boxes, design notes unfold almost fully
41
+ * +1 detail evidence + design notes unfold inside boxes
42
+ * 0 100% the standard working view (default, unchanged)
43
+ * -1 85% labels truncated to 85%, geometry still roomy
44
+ * -2 70% padding and breathing rows collapse
45
+ * -3 55% tightest meaningful boxes; band bars gain done/total
46
+ * -4 overview MODE SWITCH: borderless status glyphs, pure topology
47
+ * The same layout/routing machinery runs at every step; only the per-node
48
+ * box spec (size, border, content) and the whitespace geometry change.
49
+ */
50
+ import type { MellosMap } from '../domain/types.js';
51
+ import { type ZoomStep } from '../semantics/semantics.js';
52
+ export { type ZoomStep, ZOOM_DEFAULT, ZOOM_MAX, ZOOM_MIN, clampZoom, isNeutralKind, zoomLabel } from '../semantics/semantics.js';
53
+ export interface RenderOptions {
54
+ /** Emit ANSI color codes. */
55
+ readonly color: boolean;
56
+ /** Use box-drawing characters; false falls back to pure ASCII. */
57
+ readonly unicode: boolean;
58
+ /** Spinner frame index for in-progress nodes; caller advances it over time. */
59
+ readonly spinnerFrame: number;
60
+ /**
61
+ * Node id to spotlight: its box border and every wire touching it render
62
+ * bright instead of faint. Color mode only — monochrome output ignores it.
63
+ */
64
+ readonly focus?: string | undefined;
65
+ /** Position on the zoom ladder; omitted means ZOOM_DEFAULT (100%). */
66
+ readonly zoom?: ZoomStep | undefined;
67
+ }
68
+ /** A window over the rendered picture, in cell coordinates (0-based). */
69
+ export interface Viewport {
70
+ readonly x: number;
71
+ readonly y: number;
72
+ readonly width: number;
73
+ readonly height: number;
74
+ }
75
+ /** Terminal column width of a string (CJK chars occupy two columns). */
76
+ export declare function displayWidth(text: string): number;
77
+ /** Truncate to a display width, ANSI-free input, appending … when cut. */
78
+ export declare function fitWidth(s: string, width: number): string;
79
+ /** Hard word-wrap by display width (CJK-aware, splits anywhere). */
80
+ export declare function wrapWidth(s: string, width: number): string[];
81
+ /** Glyph for a node kind, or undefined for unknown kinds. Shared with the watcher's panel. */
82
+ export declare function kindGlyph(kind: string, unicode: boolean): string | undefined;
83
+ /** Render the whole map as terminal lines. */
84
+ export declare function renderMap(map: MellosMap, opts: RenderOptions): string[];
85
+ /** Where a node's box sits on the full (unwindowed) picture, for hit testing. */
86
+ export interface BoxHit {
87
+ readonly id: string;
88
+ readonly x: number;
89
+ readonly y: number;
90
+ readonly w: number;
91
+ readonly h: number;
92
+ }
93
+ export interface WindowedRender {
94
+ readonly lines: string[];
95
+ /** Full extent of the picture, for viewport clamping. */
96
+ readonly contentWidth: number;
97
+ readonly contentHeight: number;
98
+ /** Node hit regions in full-picture coordinates, for mouse interaction. */
99
+ readonly hits: readonly BoxHit[];
100
+ }
101
+ /** Render only the given viewport of the map, plus the full content extent. */
102
+ export declare function renderMapWindow(map: MellosMap, opts: RenderOptions, viewport: Viewport): WindowedRender;