mellos-mapping 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -4
- package/README.zh-CN.md +29 -4
- package/dist/server.mjs +185 -62
- package/dist/watch.mjs +306 -216
- package/lib/domain/ops.d.ts +112 -0
- package/lib/domain/ops.js +253 -0
- package/lib/domain/types.d.ts +242 -0
- package/lib/domain/types.js +122 -0
- package/lib/render/render.d.ts +102 -0
- package/lib/render/render.js +859 -0
- package/lib/semantics/semantics.d.ts +120 -0
- package/lib/semantics/semantics.js +256 -0
- package/lib/store/format.d.ts +50 -0
- package/lib/store/format.js +215 -0
- package/lib/store/store.d.ts +96 -0
- package/lib/store/store.js +281 -0
- package/package.json +30 -2
- package/scripts/codex-register.mjs +1 -1
- package/scripts/open-pane.mjs +1 -1
|
@@ -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;
|