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,283 @@
|
|
|
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
|
|
12
|
+
* ordered). A rank is a Rank — a branded integer in a closed range, so
|
|
13
|
+
* "unique" and "strictly lower" are decidable; see makeRank.
|
|
14
|
+
* I2. Every node belongs to exactly one existing layer.
|
|
15
|
+
* I3. Node ids are unique.
|
|
16
|
+
* I4. An edge `from -> to` means "from USES to" and requires
|
|
17
|
+
* rank(layer(from)) > rank(layer(to)).
|
|
18
|
+
* Corollary: the graph is acyclic by construction — every edge strictly
|
|
19
|
+
* decreases rank, so no cycle detection is ever needed.
|
|
20
|
+
* Same-layer edges are rejected on purpose: if A needs a sibling B,
|
|
21
|
+
* either B is really a lower-layer concept or A and B are one node.
|
|
22
|
+
* I5. Node status is one of the closed vocabulary in NODE_STATUSES.
|
|
23
|
+
* I6. Group ids are unique; every group lives in an existing layer.
|
|
24
|
+
* I7. A node's group, when set, exists and lives in the node's own layer —
|
|
25
|
+
* a group is band-local cohesion (a labeled subsystem the far zoom can
|
|
26
|
+
* render); structure ACROSS bands is what layers and edges express.
|
|
27
|
+
* I8. Lane ids are unique. A lane is a named vertical column CROSSING all
|
|
28
|
+
* bands (a sequence participant, a swim lane); lane declaration order
|
|
29
|
+
* is left-to-right render order.
|
|
30
|
+
* I9. A node's lane, when set, exists.
|
|
31
|
+
* I10. Node ids and group ids share ONE namespace: an id names a node or a
|
|
32
|
+
* group, never both. I3 and I6 are per-set and a group is a BOX in
|
|
33
|
+
* every view that shows one — the far zoom replaces its members with
|
|
34
|
+
* it, and a detail panel resolves a hovered id against groups first.
|
|
35
|
+
* Two boxes under one id therefore make the second unreachable and
|
|
36
|
+
* the aggregated view ambiguous, which is a structural fault and not
|
|
37
|
+
* a rendering accident, so it is refused where ids are declared.
|
|
38
|
+
*
|
|
39
|
+
* The map kind (dev | architecture | dataflow | behavior-tree | sequence) is
|
|
40
|
+
* presentation intent, not structure: every kind shares the same invariants,
|
|
41
|
+
* and the renderer alone decides what the kind changes (legend, neutral
|
|
42
|
+
* status skins, lane emphasis).
|
|
43
|
+
*
|
|
44
|
+
* Everything here is immutable data plus pure types. No I/O, no clock, no
|
|
45
|
+
* process state.
|
|
46
|
+
*/
|
|
47
|
+
/** Result type — expected failures are values, never exceptions. */
|
|
48
|
+
export type Result<T, E> = {
|
|
49
|
+
readonly ok: true;
|
|
50
|
+
readonly value: T;
|
|
51
|
+
} | {
|
|
52
|
+
readonly ok: false;
|
|
53
|
+
readonly error: E;
|
|
54
|
+
};
|
|
55
|
+
export declare const ok: <T>(value: T) => {
|
|
56
|
+
ok: true;
|
|
57
|
+
value: T;
|
|
58
|
+
};
|
|
59
|
+
export declare const err: <E>(error: E) => {
|
|
60
|
+
ok: false;
|
|
61
|
+
error: E;
|
|
62
|
+
};
|
|
63
|
+
/** Ids are branded slugs, never raw strings, so a NodeId cannot leak into a LayerId slot. */
|
|
64
|
+
export type NodeId = string & {
|
|
65
|
+
readonly __brand: 'NodeId';
|
|
66
|
+
};
|
|
67
|
+
export type LayerId = string & {
|
|
68
|
+
readonly __brand: 'LayerId';
|
|
69
|
+
};
|
|
70
|
+
export type GroupId = string & {
|
|
71
|
+
readonly __brand: 'GroupId';
|
|
72
|
+
};
|
|
73
|
+
export type LaneId = string & {
|
|
74
|
+
readonly __brand: 'LaneId';
|
|
75
|
+
};
|
|
76
|
+
/** Open per-node vocabulary (selector, action, db …); known kinds get a glyph in the renderer. */
|
|
77
|
+
export type NodeKind = string & {
|
|
78
|
+
readonly __brand: 'NodeKind';
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Wiki-style link from a node to a child map's page. No existence invariant
|
|
82
|
+
* on purpose: declaring the reference before the page is legal — the pane
|
|
83
|
+
* simply has nowhere to dive until the page appears.
|
|
84
|
+
*/
|
|
85
|
+
export type SubmapRef = string & {
|
|
86
|
+
readonly __brand: 'SubmapRef';
|
|
87
|
+
};
|
|
88
|
+
/** The shared slug grammar for every id in the system (nodes, layers, groups, lanes, kinds, store pages). */
|
|
89
|
+
export declare const ID_RULE: RegExp;
|
|
90
|
+
export declare const ID_RULE_TEXT = "lowercase letters, digits and dashes, starting with a letter or digit, 1-64 chars";
|
|
91
|
+
export type InvalidId = {
|
|
92
|
+
readonly kind: 'invalid-id';
|
|
93
|
+
readonly raw: string;
|
|
94
|
+
readonly rule: string;
|
|
95
|
+
};
|
|
96
|
+
export declare function makeNodeId(raw: string): Result<NodeId, InvalidId>;
|
|
97
|
+
export declare function makeLayerId(raw: string): Result<LayerId, InvalidId>;
|
|
98
|
+
export declare function makeGroupId(raw: string): Result<GroupId, InvalidId>;
|
|
99
|
+
export declare function makeLaneId(raw: string): Result<LaneId, InvalidId>;
|
|
100
|
+
export declare function makeNodeKind(raw: string): Result<NodeKind, InvalidId>;
|
|
101
|
+
export declare function makeSubmapRef(raw: string): Result<SubmapRef, InvalidId>;
|
|
102
|
+
/**
|
|
103
|
+
* A band's position on the vertical order — a branded integer, never a raw
|
|
104
|
+
* number. The brand is what makes I1 and I4 hold: `===` dedupe (I1) and
|
|
105
|
+
* `fromRank > toRank` (I4) are both silently false for NaN, so a NaN rank
|
|
106
|
+
* would admit same-band and reciprocal edges and lose acyclicity. The closed
|
|
107
|
+
* range is the same one every surface (tool schema, file format) states, so
|
|
108
|
+
* a rank the domain accepts always survives a save/reload round trip.
|
|
109
|
+
*/
|
|
110
|
+
export type Rank = number & {
|
|
111
|
+
readonly __brand: 'Rank';
|
|
112
|
+
};
|
|
113
|
+
/** Bottom band — "primitives are the ground". */
|
|
114
|
+
export declare const RANK_MIN = 0;
|
|
115
|
+
/** Highest band. A map deeper than a hundred bands is a different problem. */
|
|
116
|
+
export declare const RANK_MAX = 99;
|
|
117
|
+
export declare const RANK_RULE_TEXT = "an integer in 0..99, 0 = bottom / most primitive";
|
|
118
|
+
export type InvalidRank = {
|
|
119
|
+
readonly kind: 'invalid-rank';
|
|
120
|
+
readonly raw: number;
|
|
121
|
+
readonly rule: string;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* The only way to obtain a Rank.
|
|
125
|
+
* @param raw - a candidate rank; NaN, Infinity, fractions and out-of-range
|
|
126
|
+
* integers are refused.
|
|
127
|
+
* @returns the branded rank, or the refusal as a value.
|
|
128
|
+
*/
|
|
129
|
+
export declare function makeRank(raw: number): Result<Rank, InvalidRank>;
|
|
130
|
+
/**
|
|
131
|
+
* Closed vocabulary of map kinds — the diagram's presentation intent.
|
|
132
|
+
* 'dev' (the default) is the progress ledger; the rest are documentation
|
|
133
|
+
* diagrams rendered with neutral skins. Structure is identical for all.
|
|
134
|
+
*/
|
|
135
|
+
export declare const MAP_KINDS: readonly ["dev", "architecture", "dataflow", "behavior-tree", "sequence"];
|
|
136
|
+
export type MapKind = (typeof MAP_KINDS)[number];
|
|
137
|
+
export declare function makeMapKind(raw: string): Result<MapKind, {
|
|
138
|
+
kind: 'invalid-map-kind';
|
|
139
|
+
raw: string;
|
|
140
|
+
}>;
|
|
141
|
+
/** Closed status vocabulary. Transitions are NOT policed — see module header. */
|
|
142
|
+
export declare const NODE_STATUSES: readonly ["planned", "in-progress", "done", "regressed"];
|
|
143
|
+
export type NodeStatus = (typeof NODE_STATUSES)[number];
|
|
144
|
+
export declare function makeNodeStatus(raw: string): Result<NodeStatus, {
|
|
145
|
+
kind: 'invalid-status';
|
|
146
|
+
raw: string;
|
|
147
|
+
}>;
|
|
148
|
+
/** A horizontal band. rank 0 is the bottom (most primitive) band. */
|
|
149
|
+
export interface MapLayer {
|
|
150
|
+
readonly id: LayerId;
|
|
151
|
+
readonly name: string;
|
|
152
|
+
readonly rank: Rank;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* A labeled cluster of same-band nodes — a subsystem. The far zoom renders
|
|
156
|
+
* groups instead of members, so the overview keeps meaningful names. A
|
|
157
|
+
* group's status is always DERIVED from its members (see groupStatus),
|
|
158
|
+
* never stored.
|
|
159
|
+
*/
|
|
160
|
+
export interface MapGroup {
|
|
161
|
+
readonly id: GroupId;
|
|
162
|
+
readonly label: string;
|
|
163
|
+
readonly layer: LayerId;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* A named vertical column crossing all bands (I8) — a sequence participant
|
|
167
|
+
* or an architecture swim lane. Declaration order is left-to-right.
|
|
168
|
+
*/
|
|
169
|
+
export interface MapLane {
|
|
170
|
+
readonly id: LaneId;
|
|
171
|
+
readonly label: string;
|
|
172
|
+
}
|
|
173
|
+
/** A unit of work living in exactly one band. */
|
|
174
|
+
export interface MapNode {
|
|
175
|
+
readonly id: NodeId;
|
|
176
|
+
readonly label: string;
|
|
177
|
+
readonly layer: LayerId;
|
|
178
|
+
readonly status: NodeStatus;
|
|
179
|
+
/** Verification evidence for `done`, or the observed breakage for `regressed`. */
|
|
180
|
+
readonly evidence?: string;
|
|
181
|
+
/** Design notes: responsibility, contract, key decisions. Free text. */
|
|
182
|
+
readonly detail?: string;
|
|
183
|
+
/** Membership in a same-band group (I7), for the aggregated far zoom. */
|
|
184
|
+
readonly group?: GroupId;
|
|
185
|
+
/** Per-node kind (selector, action, db …); known kinds render as a glyph prefix. */
|
|
186
|
+
readonly kind?: NodeKind;
|
|
187
|
+
/** Column membership (I9), for laned kinds such as sequence. */
|
|
188
|
+
readonly lane?: LaneId;
|
|
189
|
+
/** Child map page: the pane badges the node ⊞ and double-click dives in. */
|
|
190
|
+
readonly submap?: SubmapRef;
|
|
191
|
+
}
|
|
192
|
+
/** `from` USES `to`. Must point strictly downward (invariant I4). */
|
|
193
|
+
export interface DepEdge {
|
|
194
|
+
readonly from: NodeId;
|
|
195
|
+
readonly to: NodeId;
|
|
196
|
+
/** What flows along the edge: a protocol, a message, a data name. */
|
|
197
|
+
readonly label?: string;
|
|
198
|
+
}
|
|
199
|
+
/** The whole map. A plain immutable value — operations return new maps. */
|
|
200
|
+
export interface MellosMap {
|
|
201
|
+
readonly title?: string;
|
|
202
|
+
/** Presentation intent; absent means 'dev' (the progress ledger). */
|
|
203
|
+
readonly kind?: MapKind;
|
|
204
|
+
readonly layers: readonly MapLayer[];
|
|
205
|
+
readonly groups: readonly MapGroup[];
|
|
206
|
+
readonly lanes: readonly MapLane[];
|
|
207
|
+
readonly nodes: readonly MapNode[];
|
|
208
|
+
readonly edges: readonly DepEdge[];
|
|
209
|
+
}
|
|
210
|
+
export declare const EMPTY_MAP: MellosMap;
|
|
211
|
+
/** Every way an operation can be refused, as data. */
|
|
212
|
+
export type MapError = InvalidId | InvalidRank | {
|
|
213
|
+
readonly kind: 'invalid-status';
|
|
214
|
+
readonly raw: string;
|
|
215
|
+
} | {
|
|
216
|
+
readonly kind: 'duplicate-layer';
|
|
217
|
+
readonly id: LayerId;
|
|
218
|
+
} | {
|
|
219
|
+
readonly kind: 'duplicate-rank';
|
|
220
|
+
readonly rank: Rank;
|
|
221
|
+
readonly existing: LayerId;
|
|
222
|
+
} | {
|
|
223
|
+
readonly kind: 'duplicate-node';
|
|
224
|
+
readonly id: NodeId;
|
|
225
|
+
} | {
|
|
226
|
+
readonly kind: 'unknown-layer';
|
|
227
|
+
readonly id: LayerId;
|
|
228
|
+
} | {
|
|
229
|
+
readonly kind: 'unknown-node';
|
|
230
|
+
readonly id: NodeId;
|
|
231
|
+
} | {
|
|
232
|
+
readonly kind: 'duplicate-edge';
|
|
233
|
+
readonly from: NodeId;
|
|
234
|
+
readonly to: NodeId;
|
|
235
|
+
} | {
|
|
236
|
+
readonly kind: 'unknown-edge';
|
|
237
|
+
readonly from: NodeId;
|
|
238
|
+
readonly to: NodeId;
|
|
239
|
+
} | {
|
|
240
|
+
readonly kind: 'self-edge';
|
|
241
|
+
readonly id: NodeId;
|
|
242
|
+
} | {
|
|
243
|
+
readonly kind: 'duplicate-group';
|
|
244
|
+
readonly id: GroupId;
|
|
245
|
+
} | {
|
|
246
|
+
readonly kind: 'id-collision';
|
|
247
|
+
readonly id: NodeId | GroupId;
|
|
248
|
+
readonly taken: 'node' | 'group';
|
|
249
|
+
} | {
|
|
250
|
+
readonly kind: 'unknown-group';
|
|
251
|
+
readonly id: GroupId;
|
|
252
|
+
} | {
|
|
253
|
+
readonly kind: 'invalid-map-kind';
|
|
254
|
+
readonly raw: string;
|
|
255
|
+
} | {
|
|
256
|
+
readonly kind: 'duplicate-lane';
|
|
257
|
+
readonly id: LaneId;
|
|
258
|
+
} | {
|
|
259
|
+
readonly kind: 'unknown-lane';
|
|
260
|
+
readonly id: LaneId;
|
|
261
|
+
} | {
|
|
262
|
+
readonly kind: 'group-layer-mismatch';
|
|
263
|
+
readonly node: NodeId;
|
|
264
|
+
readonly nodeLayer: LayerId;
|
|
265
|
+
readonly group: GroupId;
|
|
266
|
+
readonly groupLayer: LayerId;
|
|
267
|
+
} | {
|
|
268
|
+
readonly kind: 'layer-not-empty';
|
|
269
|
+
readonly id: LayerId;
|
|
270
|
+
readonly occupant: NodeId;
|
|
271
|
+
} | {
|
|
272
|
+
readonly kind: 'layer-holds-group';
|
|
273
|
+
readonly id: LayerId;
|
|
274
|
+
readonly occupant: GroupId;
|
|
275
|
+
} | {
|
|
276
|
+
readonly kind: 'edge-not-downward';
|
|
277
|
+
readonly from: NodeId;
|
|
278
|
+
readonly fromRank: Rank;
|
|
279
|
+
readonly to: NodeId;
|
|
280
|
+
readonly toRank: Rank;
|
|
281
|
+
};
|
|
282
|
+
/** Human-readable rendering of a MapError, for tool results and logs. */
|
|
283
|
+
export declare function describeMapError(e: MapError): string;
|
|
@@ -0,0 +1,153 @@
|
|
|
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
|
|
12
|
+
* ordered). A rank is a Rank — a branded integer in a closed range, so
|
|
13
|
+
* "unique" and "strictly lower" are decidable; see makeRank.
|
|
14
|
+
* I2. Every node belongs to exactly one existing layer.
|
|
15
|
+
* I3. Node ids are unique.
|
|
16
|
+
* I4. An edge `from -> to` means "from USES to" and requires
|
|
17
|
+
* rank(layer(from)) > rank(layer(to)).
|
|
18
|
+
* Corollary: the graph is acyclic by construction — every edge strictly
|
|
19
|
+
* decreases rank, so no cycle detection is ever needed.
|
|
20
|
+
* Same-layer edges are rejected on purpose: if A needs a sibling B,
|
|
21
|
+
* either B is really a lower-layer concept or A and B are one node.
|
|
22
|
+
* I5. Node status is one of the closed vocabulary in NODE_STATUSES.
|
|
23
|
+
* I6. Group ids are unique; every group lives in an existing layer.
|
|
24
|
+
* I7. A node's group, when set, exists and lives in the node's own layer —
|
|
25
|
+
* a group is band-local cohesion (a labeled subsystem the far zoom can
|
|
26
|
+
* render); structure ACROSS bands is what layers and edges express.
|
|
27
|
+
* I8. Lane ids are unique. A lane is a named vertical column CROSSING all
|
|
28
|
+
* bands (a sequence participant, a swim lane); lane declaration order
|
|
29
|
+
* is left-to-right render order.
|
|
30
|
+
* I9. A node's lane, when set, exists.
|
|
31
|
+
* I10. Node ids and group ids share ONE namespace: an id names a node or a
|
|
32
|
+
* group, never both. I3 and I6 are per-set and a group is a BOX in
|
|
33
|
+
* every view that shows one — the far zoom replaces its members with
|
|
34
|
+
* it, and a detail panel resolves a hovered id against groups first.
|
|
35
|
+
* Two boxes under one id therefore make the second unreachable and
|
|
36
|
+
* the aggregated view ambiguous, which is a structural fault and not
|
|
37
|
+
* a rendering accident, so it is refused where ids are declared.
|
|
38
|
+
*
|
|
39
|
+
* The map kind (dev | architecture | dataflow | behavior-tree | sequence) is
|
|
40
|
+
* presentation intent, not structure: every kind shares the same invariants,
|
|
41
|
+
* and the renderer alone decides what the kind changes (legend, neutral
|
|
42
|
+
* status skins, lane emphasis).
|
|
43
|
+
*
|
|
44
|
+
* Everything here is immutable data plus pure types. No I/O, no clock, no
|
|
45
|
+
* process state.
|
|
46
|
+
*/
|
|
47
|
+
export const ok = (value) => ({ ok: true, value });
|
|
48
|
+
export const err = (error) => ({ ok: false, error });
|
|
49
|
+
/** The shared slug grammar for every id in the system (nodes, layers, groups, lanes, kinds, store pages). */
|
|
50
|
+
export const ID_RULE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
51
|
+
export const ID_RULE_TEXT = 'lowercase letters, digits and dashes, starting with a letter or digit, 1-64 chars';
|
|
52
|
+
export function makeNodeId(raw) {
|
|
53
|
+
return ID_RULE.test(raw) ? ok(raw) : err({ kind: 'invalid-id', raw, rule: ID_RULE_TEXT });
|
|
54
|
+
}
|
|
55
|
+
export function makeLayerId(raw) {
|
|
56
|
+
return ID_RULE.test(raw) ? ok(raw) : err({ kind: 'invalid-id', raw, rule: ID_RULE_TEXT });
|
|
57
|
+
}
|
|
58
|
+
export function makeGroupId(raw) {
|
|
59
|
+
return ID_RULE.test(raw) ? ok(raw) : err({ kind: 'invalid-id', raw, rule: ID_RULE_TEXT });
|
|
60
|
+
}
|
|
61
|
+
export function makeLaneId(raw) {
|
|
62
|
+
return ID_RULE.test(raw) ? ok(raw) : err({ kind: 'invalid-id', raw, rule: ID_RULE_TEXT });
|
|
63
|
+
}
|
|
64
|
+
export function makeNodeKind(raw) {
|
|
65
|
+
return ID_RULE.test(raw) ? ok(raw) : err({ kind: 'invalid-id', raw, rule: ID_RULE_TEXT });
|
|
66
|
+
}
|
|
67
|
+
export function makeSubmapRef(raw) {
|
|
68
|
+
return ID_RULE.test(raw) ? ok(raw) : err({ kind: 'invalid-id', raw, rule: ID_RULE_TEXT });
|
|
69
|
+
}
|
|
70
|
+
/** Bottom band — "primitives are the ground". */
|
|
71
|
+
export const RANK_MIN = 0;
|
|
72
|
+
/** Highest band. A map deeper than a hundred bands is a different problem. */
|
|
73
|
+
export const RANK_MAX = 99;
|
|
74
|
+
export const RANK_RULE_TEXT = `an integer in ${RANK_MIN}..${RANK_MAX}, 0 = bottom / most primitive`;
|
|
75
|
+
/**
|
|
76
|
+
* The only way to obtain a Rank.
|
|
77
|
+
* @param raw - a candidate rank; NaN, Infinity, fractions and out-of-range
|
|
78
|
+
* integers are refused.
|
|
79
|
+
* @returns the branded rank, or the refusal as a value.
|
|
80
|
+
*/
|
|
81
|
+
export function makeRank(raw) {
|
|
82
|
+
return Number.isInteger(raw) && raw >= RANK_MIN && raw <= RANK_MAX
|
|
83
|
+
? ok(raw)
|
|
84
|
+
: err({ kind: 'invalid-rank', raw, rule: RANK_RULE_TEXT });
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Closed vocabulary of map kinds — the diagram's presentation intent.
|
|
88
|
+
* 'dev' (the default) is the progress ledger; the rest are documentation
|
|
89
|
+
* diagrams rendered with neutral skins. Structure is identical for all.
|
|
90
|
+
*/
|
|
91
|
+
export const MAP_KINDS = ['dev', 'architecture', 'dataflow', 'behavior-tree', 'sequence'];
|
|
92
|
+
export function makeMapKind(raw) {
|
|
93
|
+
return MAP_KINDS.includes(raw) ? ok(raw) : err({ kind: 'invalid-map-kind', raw });
|
|
94
|
+
}
|
|
95
|
+
/** Closed status vocabulary. Transitions are NOT policed — see module header. */
|
|
96
|
+
export const NODE_STATUSES = ['planned', 'in-progress', 'done', 'regressed'];
|
|
97
|
+
export function makeNodeStatus(raw) {
|
|
98
|
+
return NODE_STATUSES.includes(raw)
|
|
99
|
+
? ok(raw)
|
|
100
|
+
: err({ kind: 'invalid-status', raw });
|
|
101
|
+
}
|
|
102
|
+
export const EMPTY_MAP = { layers: [], groups: [], lanes: [], nodes: [], edges: [] };
|
|
103
|
+
/** Human-readable rendering of a MapError, for tool results and logs. */
|
|
104
|
+
export function describeMapError(e) {
|
|
105
|
+
switch (e.kind) {
|
|
106
|
+
case 'invalid-id':
|
|
107
|
+
return `invalid id "${e.raw}" (rule: ${e.rule})`;
|
|
108
|
+
case 'invalid-rank':
|
|
109
|
+
return `invalid rank ${e.raw} (rule: ${e.rule})`;
|
|
110
|
+
case 'invalid-status':
|
|
111
|
+
return `invalid status "${e.raw}" (expected: ${NODE_STATUSES.join(' | ')})`;
|
|
112
|
+
case 'duplicate-layer':
|
|
113
|
+
return `layer "${e.id}" already exists`;
|
|
114
|
+
case 'duplicate-rank':
|
|
115
|
+
return `rank ${e.rank} is already taken by layer "${e.existing}"`;
|
|
116
|
+
case 'duplicate-node':
|
|
117
|
+
return `node "${e.id}" already exists`;
|
|
118
|
+
case 'unknown-layer':
|
|
119
|
+
return `layer "${e.id}" does not exist`;
|
|
120
|
+
case 'unknown-node':
|
|
121
|
+
return `node "${e.id}" does not exist`;
|
|
122
|
+
case 'duplicate-edge':
|
|
123
|
+
return `edge ${e.from} -> ${e.to} already exists`;
|
|
124
|
+
case 'unknown-edge':
|
|
125
|
+
return `edge ${e.from} -> ${e.to} does not exist`;
|
|
126
|
+
case 'self-edge':
|
|
127
|
+
return `node "${e.id}" cannot depend on itself`;
|
|
128
|
+
case 'duplicate-group':
|
|
129
|
+
return `group "${e.id}" already exists`;
|
|
130
|
+
case 'id-collision':
|
|
131
|
+
return (`id "${e.id}" already names a ${e.taken} on this map; nodes and groups share one id namespace ` +
|
|
132
|
+
`(both render as boxes, so one id must mean one box) — rename "${e.id}"`);
|
|
133
|
+
case 'unknown-group':
|
|
134
|
+
return `group "${e.id}" does not exist`;
|
|
135
|
+
case 'invalid-map-kind':
|
|
136
|
+
return `invalid map kind "${e.raw}" (expected: ${MAP_KINDS.join(' | ')})`;
|
|
137
|
+
case 'duplicate-lane':
|
|
138
|
+
return `lane "${e.id}" already exists`;
|
|
139
|
+
case 'unknown-lane':
|
|
140
|
+
return `lane "${e.id}" does not exist`;
|
|
141
|
+
case 'group-layer-mismatch':
|
|
142
|
+
return (`node "${e.node}" (layer ${e.nodeLayer}) cannot join group "${e.group}" (layer ${e.groupLayer}); ` +
|
|
143
|
+
`groups cluster nodes within one band`);
|
|
144
|
+
case 'layer-not-empty':
|
|
145
|
+
return (`layer "${e.id}" still holds node "${e.occupant}"; move its nodes to another band (moveNode) ` +
|
|
146
|
+
`or remove them (removeNode) first`);
|
|
147
|
+
case 'layer-holds-group':
|
|
148
|
+
return `layer "${e.id}" still holds group "${e.occupant}"; remove its groups (removeGroup) first`;
|
|
149
|
+
case 'edge-not-downward':
|
|
150
|
+
return (`edge ${e.from} (rank ${e.fromRank}) -> ${e.to} (rank ${e.toRank}) is not strictly downward; ` +
|
|
151
|
+
`dependencies may only point to a lower layer`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 4a — the drawing surface: a grid of cells that knows how to merge
|
|
3
|
+
* crossing lines, and how to emit itself as terminal rows.
|
|
4
|
+
*
|
|
5
|
+
* Two kinds of ink live in a cell. A LITERAL is a character somebody chose (a
|
|
6
|
+
* label, a box border); a MASK is a set of directions a routed line passes
|
|
7
|
+
* through, and the character comes out of the mask at emit time. That is what
|
|
8
|
+
* makes junctions free: two wires crossing simply union their masks, and ┼
|
|
9
|
+
* appears without anyone routing around anything.
|
|
10
|
+
*
|
|
11
|
+
* The surface knows nothing about maps, statuses or zoom — it takes a Style
|
|
12
|
+
* (the ink palette below) and coordinates, and it is the only place ANSI is
|
|
13
|
+
* produced.
|
|
14
|
+
*/
|
|
15
|
+
import type { RenderOptions, Viewport } from './options.js';
|
|
16
|
+
export type Style = 'none' | 'dim' | 'amber' | 'green' | 'greenDim' | 'red' | 'faint';
|
|
17
|
+
/** SGR parameter per style; combined with bold ("1") at emit time. */
|
|
18
|
+
export declare const SGR: Readonly<Record<Style, string>>;
|
|
19
|
+
export declare const ANSI_RESET = "\u001B[0m";
|
|
20
|
+
export declare const UP = 1;
|
|
21
|
+
export declare const DOWN = 2;
|
|
22
|
+
export declare const LEFT = 4;
|
|
23
|
+
export declare const RIGHT = 8;
|
|
24
|
+
export declare class Canvas {
|
|
25
|
+
private readonly rows;
|
|
26
|
+
private cell;
|
|
27
|
+
get height(): number;
|
|
28
|
+
get width(): number;
|
|
29
|
+
/** Write literal text starting at (x, y). Returns the column just past it. */
|
|
30
|
+
text(x: number, y: number, s: string, style: Style, bold?: boolean): number;
|
|
31
|
+
/** Merge a routed-line direction mask into (x, y). */
|
|
32
|
+
line(x: number, y: number, mask: number, heavyHorizontal?: boolean, bright?: boolean): void;
|
|
33
|
+
/**
|
|
34
|
+
* Emit terminal lines, optionally windowed to a viewport. Slicing happens
|
|
35
|
+
* at the cell level so ANSI codes reopen correctly inside the window and a
|
|
36
|
+
* CJK character cut in half at either edge degrades to a space instead of
|
|
37
|
+
* shifting the whole row. Routed wiring (mask cells) emits FAINT — the
|
|
38
|
+
* circuit board recedes, the boxes glow.
|
|
39
|
+
*/
|
|
40
|
+
emit(opts: RenderOptions, viewport?: Viewport): string[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Draw an orthogonal polyline through `points` (consecutive points must share
|
|
44
|
+
* an x or a y). Interior cells of a segment carry the segment's axis mask;
|
|
45
|
+
* every point cell carries only the directions of the segments that actually
|
|
46
|
+
* touch it — so path endpoints become clean junction stubs (e.g. ┬ when
|
|
47
|
+
* entering a box border) and turning points become corner characters, all via
|
|
48
|
+
* the same mask union. Zero-length segments vanish naturally.
|
|
49
|
+
*/
|
|
50
|
+
export declare function drawPath(canvas: Canvas, points: ReadonlyArray<readonly [number, number]>, bright?: boolean): void;
|