mellos-mapping 0.20.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 +360 -63
- package/README.zh-CN.md +314 -54
- package/dist/hook-session-start.mjs +239 -0
- package/dist/mmap.mjs +338 -0
- package/dist/server.mjs +1614 -809
- package/dist/store-paths.mjs +107 -0
- package/dist/watch.mjs +1391 -760
- package/lib/domain/ops.d.ts +71 -12
- package/lib/domain/ops.js +145 -14
- package/lib/domain/types.d.ts +47 -6
- package/lib/domain/types.js +34 -3
- 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 +32 -46
- package/lib/render/render.js +58 -789
- 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 +53 -4
- package/lib/semantics/semantics.js +130 -6
- package/lib/semantics/vocabulary.d.ts +79 -0
- package/lib/semantics/vocabulary.js +112 -0
- package/lib/store/format.d.ts +17 -0
- package/lib/store/format.js +185 -66
- package/lib/store/store.d.ts +220 -20
- package/lib/store/store.js +491 -38
- package/package.json +12 -4
- 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
package/lib/domain/ops.d.ts
CHANGED
|
@@ -5,14 +5,25 @@
|
|
|
5
5
|
* before any new value is built, and the commit expression can no longer
|
|
6
6
|
* fail. Inputs are never mutated; the result always carries a fresh map.
|
|
7
7
|
*
|
|
8
|
-
* These functions enforce the structural invariants I1-
|
|
8
|
+
* These functions enforce the structural invariants I1-I10 documented in
|
|
9
9
|
* types.ts and nothing else. In particular there are no workflow rules here:
|
|
10
10
|
* any status may be set at any time, in any order. Discipline lives with the
|
|
11
11
|
* caller; this layer only keeps the map structurally true.
|
|
12
|
+
*
|
|
13
|
+
* Every declared thing can also be REVISED, because a ghost design is a
|
|
14
|
+
* hypothesis and revising it is honest work: a node moves band (moveNode), a
|
|
15
|
+
* band is renamed or re-ordered (updateLayer), a group or lane is relabeled
|
|
16
|
+
* (updateGroup, updateLane), and every optional field a node carries can be
|
|
17
|
+
* cleared as explicitly as it was set (null, never an empty string).
|
|
18
|
+
*/
|
|
19
|
+
import { type GroupId, type LaneId, type LayerId, type MapError, type MapKind, type MellosMap, type NodeId, type NodeKind, type NodeStatus, type Rank, type Result, type SubmapRef } from './types.js';
|
|
20
|
+
/**
|
|
21
|
+
* Set, replace or clear the map title.
|
|
22
|
+
* @param title - the new title; null (or an explicit undefined) removes the
|
|
23
|
+
* field entirely, so a cleared title serializes as an absent key rather
|
|
24
|
+
* than as an empty string nobody can tell from a real one.
|
|
12
25
|
*/
|
|
13
|
-
|
|
14
|
-
/** Set or replace the map title. */
|
|
15
|
-
export declare function setTitle(map: MellosMap, title: string): MellosMap;
|
|
26
|
+
export declare function setTitle(map: MellosMap, title: string | null | undefined): MellosMap;
|
|
16
27
|
/** Set or replace the map kind (presentation intent — never structural). */
|
|
17
28
|
export declare function setKind(map: MellosMap, kind: MapKind): MellosMap;
|
|
18
29
|
export interface DeclareLaneInput {
|
|
@@ -21,6 +32,8 @@ export interface DeclareLaneInput {
|
|
|
21
32
|
}
|
|
22
33
|
/** Add a new lane (I8). Declaration order is left-to-right render order. */
|
|
23
34
|
export declare function declareLane(map: MellosMap, input: DeclareLaneInput): Result<MellosMap, MapError>;
|
|
35
|
+
/** Relabel a lane. The column keeps its id, its order and its members. */
|
|
36
|
+
export declare function updateLane(map: MellosMap, id: LaneId, label: string): Result<MellosMap, MapError>;
|
|
24
37
|
/**
|
|
25
38
|
* Remove a lane.
|
|
26
39
|
* Postcondition: former members stay on the map, merely off-lane — removing
|
|
@@ -30,16 +43,34 @@ export declare function removeLane(map: MellosMap, id: LaneId): Result<MellosMap
|
|
|
30
43
|
export interface DeclareLayerInput {
|
|
31
44
|
readonly id: LayerId;
|
|
32
45
|
readonly name: string;
|
|
33
|
-
readonly rank:
|
|
46
|
+
readonly rank: Rank;
|
|
34
47
|
}
|
|
35
48
|
/** Add a new band. Refuses duplicate ids and duplicate ranks (I1). */
|
|
36
49
|
export declare function declareLayer(map: MellosMap, input: DeclareLayerInput): Result<MellosMap, MapError>;
|
|
50
|
+
export interface UpdateLayerInput {
|
|
51
|
+
readonly name?: string;
|
|
52
|
+
readonly rank?: Rank;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Rename a band and/or move it in the vertical order. Absent fields are left
|
|
56
|
+
* untouched; the band keeps its id and its occupants either way.
|
|
57
|
+
*
|
|
58
|
+
* A rank change is the one revision that can invalidate the whole graph, so
|
|
59
|
+
* it is validated against the map as a whole before anything changes:
|
|
60
|
+
* - the new rank is free (I1) — bands stay totally ordered;
|
|
61
|
+
* - EVERY edge, not only the ones touching this band, still points
|
|
62
|
+
* strictly downward under the new order (I4). Re-ranking a band moves
|
|
63
|
+
* all of its nodes at once, so an edge two bands away can be the one
|
|
64
|
+
* that breaks; the refusal names it.
|
|
65
|
+
* @returns the reordered map, or the first refusal.
|
|
66
|
+
*/
|
|
67
|
+
export declare function updateLayer(map: MellosMap, id: LayerId, input: UpdateLayerInput): Result<MellosMap, MapError>;
|
|
37
68
|
export interface DeclareGroupInput {
|
|
38
69
|
readonly id: GroupId;
|
|
39
70
|
readonly label: string;
|
|
40
71
|
readonly layer: LayerId;
|
|
41
72
|
}
|
|
42
|
-
/** Add a new group to an existing band (I6). */
|
|
73
|
+
/** Add a new group to an existing band (I6), under an id no node holds (I10). */
|
|
43
74
|
export declare function declareGroup(map: MellosMap, input: DeclareGroupInput): Result<MellosMap, MapError>;
|
|
44
75
|
/** Rename a group. */
|
|
45
76
|
export declare function updateGroup(map: MellosMap, id: GroupId, label: string): Result<MellosMap, MapError>;
|
|
@@ -62,6 +93,12 @@ export interface DeclareNodeInput {
|
|
|
62
93
|
readonly label: string;
|
|
63
94
|
readonly layer: LayerId;
|
|
64
95
|
readonly status?: NodeStatus;
|
|
96
|
+
/**
|
|
97
|
+
* Verification evidence, for a node declared straight into `done` — work
|
|
98
|
+
* already finished when the map is drawn is as entitled to its proof as
|
|
99
|
+
* work finished under the map's eyes.
|
|
100
|
+
*/
|
|
101
|
+
readonly evidence?: string;
|
|
65
102
|
readonly detail?: string;
|
|
66
103
|
readonly group?: GroupId;
|
|
67
104
|
readonly kind?: NodeKind;
|
|
@@ -69,8 +106,9 @@ export interface DeclareNodeInput {
|
|
|
69
106
|
readonly submap?: SubmapRef;
|
|
70
107
|
}
|
|
71
108
|
/**
|
|
72
|
-
* Add a new node to an existing band (I2, I3)
|
|
73
|
-
* group (I7) and/or an existing lane
|
|
109
|
+
* Add a new node to an existing band (I2, I3) under an id no group holds
|
|
110
|
+
* (I10), optionally joining a same-band group (I7) and/or an existing lane
|
|
111
|
+
* (I9).
|
|
74
112
|
*/
|
|
75
113
|
export declare function declareNode(map: MellosMap, input: DeclareNodeInput): Result<MellosMap, MapError>;
|
|
76
114
|
/**
|
|
@@ -83,8 +121,10 @@ export interface UpdateNodeInput {
|
|
|
83
121
|
readonly id: NodeId;
|
|
84
122
|
readonly status?: NodeStatus;
|
|
85
123
|
readonly label?: string;
|
|
86
|
-
|
|
87
|
-
readonly
|
|
124
|
+
/** Text replaces the evidence; null clears it (a node demoted back to a plan). */
|
|
125
|
+
readonly evidence?: string | null;
|
|
126
|
+
/** Text replaces the design notes; null clears them. */
|
|
127
|
+
readonly detail?: string | null;
|
|
88
128
|
/** A GroupId joins that group (I7 validated); null leaves the current group. */
|
|
89
129
|
readonly group?: GroupId | null;
|
|
90
130
|
/** A NodeKind sets the presentation kind; null clears it. */
|
|
@@ -96,10 +136,29 @@ export interface UpdateNodeInput {
|
|
|
96
136
|
}
|
|
97
137
|
/**
|
|
98
138
|
* Update a node's status, label, evidence, design detail, group membership,
|
|
99
|
-
* kind and/or
|
|
100
|
-
* the
|
|
139
|
+
* kind, lane and/or submap link. Absent fields are left untouched, null
|
|
140
|
+
* clears the field. No transition rules: the ledger records whatever the
|
|
141
|
+
* caller reports, whenever they report it.
|
|
142
|
+
* Postcondition: the node keeps its band — moving between bands is moveNode,
|
|
143
|
+
* which is the operation that re-checks I4.
|
|
101
144
|
*/
|
|
102
145
|
export declare function updateNode(map: MellosMap, input: UpdateNodeInput): Result<MellosMap, MapError>;
|
|
146
|
+
/**
|
|
147
|
+
* Move a node to another band — the revision every ghost design eventually
|
|
148
|
+
* needs, and the only way to empty a band without deleting work.
|
|
149
|
+
*
|
|
150
|
+
* Validated before anything changes:
|
|
151
|
+
* - the target band exists (I2);
|
|
152
|
+
* - every edge touching the node still points strictly downward from its
|
|
153
|
+
* NEW rank (I4) — a move that would flatten or invert a dependency is
|
|
154
|
+
* refused, naming the edge that blocks it;
|
|
155
|
+
* - group membership (I7) still holds. A grouped node may only move to its
|
|
156
|
+
* group's band; the move never silently ungroups it, because dropping a
|
|
157
|
+
* subsystem membership is a decision the caller must make out loud with
|
|
158
|
+
* updateNode({ group: null }).
|
|
159
|
+
* @returns the map with the node rebanded, or the first refusal.
|
|
160
|
+
*/
|
|
161
|
+
export declare function moveNode(map: MellosMap, id: NodeId, layer: LayerId): Result<MellosMap, MapError>;
|
|
103
162
|
/**
|
|
104
163
|
* Remove a node.
|
|
105
164
|
* Postcondition (explicit part of this contract): every edge touching the
|
package/lib/domain/ops.js
CHANGED
|
@@ -5,10 +5,16 @@
|
|
|
5
5
|
* before any new value is built, and the commit expression can no longer
|
|
6
6
|
* fail. Inputs are never mutated; the result always carries a fresh map.
|
|
7
7
|
*
|
|
8
|
-
* These functions enforce the structural invariants I1-
|
|
8
|
+
* These functions enforce the structural invariants I1-I10 documented in
|
|
9
9
|
* types.ts and nothing else. In particular there are no workflow rules here:
|
|
10
10
|
* any status may be set at any time, in any order. Discipline lives with the
|
|
11
11
|
* caller; this layer only keeps the map structurally true.
|
|
12
|
+
*
|
|
13
|
+
* Every declared thing can also be REVISED, because a ghost design is a
|
|
14
|
+
* hypothesis and revising it is honest work: a node moves band (moveNode), a
|
|
15
|
+
* band is renamed or re-ordered (updateLayer), a group or lane is relabeled
|
|
16
|
+
* (updateGroup, updateLane), and every optional field a node carries can be
|
|
17
|
+
* cleared as explicitly as it was set (null, never an empty string).
|
|
12
18
|
*/
|
|
13
19
|
import { err, ok, } from './types.js';
|
|
14
20
|
function findLayer(map, id) {
|
|
@@ -32,8 +38,28 @@ function checkMembership(map, node, nodeLayer, group) {
|
|
|
32
38
|
function hasEdge(map, from, to) {
|
|
33
39
|
return map.edges.some((e) => e.from === from && e.to === to);
|
|
34
40
|
}
|
|
35
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Validate that a new id does not already name the OTHER kind of box (I10).
|
|
43
|
+
* Ids are compared as raw slugs on purpose: the shared namespace is exactly
|
|
44
|
+
* what the brands cannot express, which is why this check exists.
|
|
45
|
+
*/
|
|
46
|
+
function checkIdSpace(map, id, declaring) {
|
|
47
|
+
const taken = declaring === 'node'
|
|
48
|
+
? map.groups.some((g) => g.id === id)
|
|
49
|
+
: map.nodes.some((n) => n.id === id);
|
|
50
|
+
return taken ? { kind: 'id-collision', id, taken: declaring === 'node' ? 'group' : 'node' } : undefined;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Set, replace or clear the map title.
|
|
54
|
+
* @param title - the new title; null (or an explicit undefined) removes the
|
|
55
|
+
* field entirely, so a cleared title serializes as an absent key rather
|
|
56
|
+
* than as an empty string nobody can tell from a real one.
|
|
57
|
+
*/
|
|
36
58
|
export function setTitle(map, title) {
|
|
59
|
+
if (title === null || title === undefined) {
|
|
60
|
+
const { title: _dropped, ...rest } = map;
|
|
61
|
+
return rest;
|
|
62
|
+
}
|
|
37
63
|
return { ...map, title };
|
|
38
64
|
}
|
|
39
65
|
/** Set or replace the map kind (presentation intent — never structural). */
|
|
@@ -49,6 +75,12 @@ export function declareLane(map, input) {
|
|
|
49
75
|
return err({ kind: 'duplicate-lane', id: input.id });
|
|
50
76
|
return ok({ ...map, lanes: [...map.lanes, { id: input.id, label: input.label }] });
|
|
51
77
|
}
|
|
78
|
+
/** Relabel a lane. The column keeps its id, its order and its members. */
|
|
79
|
+
export function updateLane(map, id, label) {
|
|
80
|
+
if (!findLane(map, id))
|
|
81
|
+
return err({ kind: 'unknown-lane', id });
|
|
82
|
+
return ok({ ...map, lanes: map.lanes.map((l) => (l.id === id ? { ...l, label } : l)) });
|
|
83
|
+
}
|
|
52
84
|
/**
|
|
53
85
|
* Remove a lane.
|
|
54
86
|
* Postcondition: former members stay on the map, merely off-lane — removing
|
|
@@ -77,10 +109,53 @@ export function declareLayer(map, input) {
|
|
|
77
109
|
return err({ kind: 'duplicate-rank', rank: input.rank, existing: rankHolder.id });
|
|
78
110
|
return ok({ ...map, layers: [...map.layers, { id: input.id, name: input.name, rank: input.rank }] });
|
|
79
111
|
}
|
|
80
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Rename a band and/or move it in the vertical order. Absent fields are left
|
|
114
|
+
* untouched; the band keeps its id and its occupants either way.
|
|
115
|
+
*
|
|
116
|
+
* A rank change is the one revision that can invalidate the whole graph, so
|
|
117
|
+
* it is validated against the map as a whole before anything changes:
|
|
118
|
+
* - the new rank is free (I1) — bands stay totally ordered;
|
|
119
|
+
* - EVERY edge, not only the ones touching this band, still points
|
|
120
|
+
* strictly downward under the new order (I4). Re-ranking a band moves
|
|
121
|
+
* all of its nodes at once, so an edge two bands away can be the one
|
|
122
|
+
* that breaks; the refusal names it.
|
|
123
|
+
* @returns the reordered map, or the first refusal.
|
|
124
|
+
*/
|
|
125
|
+
export function updateLayer(map, id, input) {
|
|
126
|
+
const layer = findLayer(map, id);
|
|
127
|
+
if (!layer)
|
|
128
|
+
return err({ kind: 'unknown-layer', id });
|
|
129
|
+
if (input.rank !== undefined && input.rank !== layer.rank) {
|
|
130
|
+
const rankHolder = map.layers.find((l) => l.rank === input.rank && l.id !== id);
|
|
131
|
+
if (rankHolder)
|
|
132
|
+
return err({ kind: 'duplicate-rank', rank: input.rank, existing: rankHolder.id });
|
|
133
|
+
// Layers are guaranteed to exist for stored nodes (I2), so no lookup misses.
|
|
134
|
+
const rankAfter = (nodeId) => {
|
|
135
|
+
const nodeLayer = findNode(map, nodeId).layer;
|
|
136
|
+
return nodeLayer === id ? input.rank : findLayer(map, nodeLayer).rank;
|
|
137
|
+
};
|
|
138
|
+
for (const e of map.edges) {
|
|
139
|
+
const fromRank = rankAfter(e.from);
|
|
140
|
+
const toRank = rankAfter(e.to);
|
|
141
|
+
if (fromRank <= toRank)
|
|
142
|
+
return err({ kind: 'edge-not-downward', from: e.from, fromRank, to: e.to, toRank });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const updated = {
|
|
146
|
+
...layer,
|
|
147
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
148
|
+
...(input.rank !== undefined ? { rank: input.rank } : {}),
|
|
149
|
+
};
|
|
150
|
+
return ok({ ...map, layers: map.layers.map((l) => (l.id === id ? updated : l)) });
|
|
151
|
+
}
|
|
152
|
+
/** Add a new group to an existing band (I6), under an id no node holds (I10). */
|
|
81
153
|
export function declareGroup(map, input) {
|
|
82
154
|
if (findGroup(map, input.id))
|
|
83
155
|
return err({ kind: 'duplicate-group', id: input.id });
|
|
156
|
+
const collision = checkIdSpace(map, input.id, 'group');
|
|
157
|
+
if (collision)
|
|
158
|
+
return err(collision);
|
|
84
159
|
if (!findLayer(map, input.layer))
|
|
85
160
|
return err({ kind: 'unknown-layer', id: input.layer });
|
|
86
161
|
return ok({ ...map, groups: [...map.groups, { id: input.id, label: input.label, layer: input.layer }] });
|
|
@@ -133,12 +208,16 @@ export function mapStatus(map) {
|
|
|
133
208
|
return aggregateStatus(map.nodes);
|
|
134
209
|
}
|
|
135
210
|
/**
|
|
136
|
-
* Add a new node to an existing band (I2, I3)
|
|
137
|
-
* group (I7) and/or an existing lane
|
|
211
|
+
* Add a new node to an existing band (I2, I3) under an id no group holds
|
|
212
|
+
* (I10), optionally joining a same-band group (I7) and/or an existing lane
|
|
213
|
+
* (I9).
|
|
138
214
|
*/
|
|
139
215
|
export function declareNode(map, input) {
|
|
140
216
|
if (findNode(map, input.id))
|
|
141
217
|
return err({ kind: 'duplicate-node', id: input.id });
|
|
218
|
+
const collision = checkIdSpace(map, input.id, 'node');
|
|
219
|
+
if (collision)
|
|
220
|
+
return err(collision);
|
|
142
221
|
if (!findLayer(map, input.layer))
|
|
143
222
|
return err({ kind: 'unknown-layer', id: input.layer });
|
|
144
223
|
if (input.group !== undefined) {
|
|
@@ -153,6 +232,7 @@ export function declareNode(map, input) {
|
|
|
153
232
|
label: input.label,
|
|
154
233
|
layer: input.layer,
|
|
155
234
|
status: input.status ?? 'planned',
|
|
235
|
+
...(input.evidence !== undefined ? { evidence: input.evidence } : {}),
|
|
156
236
|
...(input.detail !== undefined ? { detail: input.detail } : {}),
|
|
157
237
|
...(input.group !== undefined ? { group: input.group } : {}),
|
|
158
238
|
...(input.kind !== undefined ? { kind: input.kind } : {}),
|
|
@@ -184,10 +264,20 @@ export function linkNodes(map, from, to, label) {
|
|
|
184
264
|
return err({ kind: 'edge-not-downward', from, fromRank, to, toRank });
|
|
185
265
|
return ok({ ...map, edges: [...map.edges, { from, to, ...(label !== undefined ? { label } : {}) }] });
|
|
186
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Resolve one optional-and-clearable field: absent input keeps the stored
|
|
269
|
+
* value, null erases it, anything else replaces it.
|
|
270
|
+
*/
|
|
271
|
+
function resolveOptional(input, current) {
|
|
272
|
+
return input === undefined ? current : input === null ? undefined : input;
|
|
273
|
+
}
|
|
187
274
|
/**
|
|
188
275
|
* Update a node's status, label, evidence, design detail, group membership,
|
|
189
|
-
* kind and/or
|
|
190
|
-
* the
|
|
276
|
+
* kind, lane and/or submap link. Absent fields are left untouched, null
|
|
277
|
+
* clears the field. No transition rules: the ledger records whatever the
|
|
278
|
+
* caller reports, whenever they report it.
|
|
279
|
+
* Postcondition: the node keeps its band — moving between bands is moveNode,
|
|
280
|
+
* which is the operation that re-checks I4.
|
|
191
281
|
*/
|
|
192
282
|
export function updateNode(map, input) {
|
|
193
283
|
const node = findNode(map, input.id);
|
|
@@ -201,24 +291,65 @@ export function updateNode(map, input) {
|
|
|
201
291
|
if (input.lane !== undefined && input.lane !== null && !findLane(map, input.lane)) {
|
|
202
292
|
return err({ kind: 'unknown-lane', id: input.lane });
|
|
203
293
|
}
|
|
204
|
-
const { group: currentGroup, kind: currentKind, lane: currentLane, submap: currentSubmap, ...bare } = node;
|
|
205
|
-
const nextGroup = input.group
|
|
206
|
-
const nextKind = input.kind
|
|
207
|
-
const nextLane = input.lane
|
|
208
|
-
const nextSubmap = input.submap
|
|
294
|
+
const { group: currentGroup, kind: currentKind, lane: currentLane, submap: currentSubmap, evidence: currentEvidence, detail: currentDetail, ...bare } = node;
|
|
295
|
+
const nextGroup = resolveOptional(input.group, currentGroup);
|
|
296
|
+
const nextKind = resolveOptional(input.kind, currentKind);
|
|
297
|
+
const nextLane = resolveOptional(input.lane, currentLane);
|
|
298
|
+
const nextSubmap = resolveOptional(input.submap, currentSubmap);
|
|
299
|
+
const nextEvidence = resolveOptional(input.evidence, currentEvidence);
|
|
300
|
+
const nextDetail = resolveOptional(input.detail, currentDetail);
|
|
209
301
|
const updated = {
|
|
210
302
|
...bare,
|
|
303
|
+
...(nextEvidence !== undefined ? { evidence: nextEvidence } : {}),
|
|
304
|
+
...(nextDetail !== undefined ? { detail: nextDetail } : {}),
|
|
211
305
|
...(nextGroup !== undefined ? { group: nextGroup } : {}),
|
|
212
306
|
...(nextKind !== undefined ? { kind: nextKind } : {}),
|
|
213
307
|
...(nextLane !== undefined ? { lane: nextLane } : {}),
|
|
214
308
|
...(nextSubmap !== undefined ? { submap: nextSubmap } : {}),
|
|
215
309
|
...(input.status !== undefined ? { status: input.status } : {}),
|
|
216
310
|
...(input.label !== undefined ? { label: input.label } : {}),
|
|
217
|
-
...(input.evidence !== undefined ? { evidence: input.evidence } : {}),
|
|
218
|
-
...(input.detail !== undefined ? { detail: input.detail } : {}),
|
|
219
311
|
};
|
|
220
312
|
return ok({ ...map, nodes: map.nodes.map((n) => (n.id === input.id ? updated : n)) });
|
|
221
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Move a node to another band — the revision every ghost design eventually
|
|
316
|
+
* needs, and the only way to empty a band without deleting work.
|
|
317
|
+
*
|
|
318
|
+
* Validated before anything changes:
|
|
319
|
+
* - the target band exists (I2);
|
|
320
|
+
* - every edge touching the node still points strictly downward from its
|
|
321
|
+
* NEW rank (I4) — a move that would flatten or invert a dependency is
|
|
322
|
+
* refused, naming the edge that blocks it;
|
|
323
|
+
* - group membership (I7) still holds. A grouped node may only move to its
|
|
324
|
+
* group's band; the move never silently ungroups it, because dropping a
|
|
325
|
+
* subsystem membership is a decision the caller must make out loud with
|
|
326
|
+
* updateNode({ group: null }).
|
|
327
|
+
* @returns the map with the node rebanded, or the first refusal.
|
|
328
|
+
*/
|
|
329
|
+
export function moveNode(map, id, layer) {
|
|
330
|
+
const node = findNode(map, id);
|
|
331
|
+
if (!node)
|
|
332
|
+
return err({ kind: 'unknown-node', id });
|
|
333
|
+
const target = findLayer(map, layer);
|
|
334
|
+
if (!target)
|
|
335
|
+
return err({ kind: 'unknown-layer', id: layer });
|
|
336
|
+
if (node.group !== undefined) {
|
|
337
|
+
const bad = checkMembership(map, id, layer, node.group);
|
|
338
|
+
if (bad)
|
|
339
|
+
return err(bad);
|
|
340
|
+
}
|
|
341
|
+
// Layers are guaranteed to exist for stored nodes (I2), so no lookup misses.
|
|
342
|
+
const rankAfter = (nodeId) => nodeId === id ? target.rank : findLayer(map, findNode(map, nodeId).layer).rank;
|
|
343
|
+
for (const e of map.edges) {
|
|
344
|
+
if (e.from !== id && e.to !== id)
|
|
345
|
+
continue;
|
|
346
|
+
const fromRank = rankAfter(e.from);
|
|
347
|
+
const toRank = rankAfter(e.to);
|
|
348
|
+
if (fromRank <= toRank)
|
|
349
|
+
return err({ kind: 'edge-not-downward', from: e.from, fromRank, to: e.to, toRank });
|
|
350
|
+
}
|
|
351
|
+
return ok({ ...map, nodes: map.nodes.map((n) => (n.id === id ? { ...n, layer } : n)) });
|
|
352
|
+
}
|
|
222
353
|
/**
|
|
223
354
|
* Remove a node.
|
|
224
355
|
* Postcondition (explicit part of this contract): every edge touching the
|
package/lib/domain/types.d.ts
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Structural invariants owned by this layer (and only these — the map is a
|
|
10
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
|
|
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.
|
|
12
14
|
* I2. Every node belongs to exactly one existing layer.
|
|
13
15
|
* I3. Node ids are unique.
|
|
14
16
|
* I4. An edge `from -> to` means "from USES to" and requires
|
|
@@ -26,6 +28,13 @@
|
|
|
26
28
|
* bands (a sequence participant, a swim lane); lane declaration order
|
|
27
29
|
* is left-to-right render order.
|
|
28
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.
|
|
29
38
|
*
|
|
30
39
|
* The map kind (dev | architecture | dataflow | behavior-tree | sequence) is
|
|
31
40
|
* presentation intent, not structure: every kind shares the same invariants,
|
|
@@ -90,6 +99,34 @@ export declare function makeGroupId(raw: string): Result<GroupId, InvalidId>;
|
|
|
90
99
|
export declare function makeLaneId(raw: string): Result<LaneId, InvalidId>;
|
|
91
100
|
export declare function makeNodeKind(raw: string): Result<NodeKind, InvalidId>;
|
|
92
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>;
|
|
93
130
|
/**
|
|
94
131
|
* Closed vocabulary of map kinds — the diagram's presentation intent.
|
|
95
132
|
* 'dev' (the default) is the progress ledger; the rest are documentation
|
|
@@ -112,7 +149,7 @@ export declare function makeNodeStatus(raw: string): Result<NodeStatus, {
|
|
|
112
149
|
export interface MapLayer {
|
|
113
150
|
readonly id: LayerId;
|
|
114
151
|
readonly name: string;
|
|
115
|
-
readonly rank:
|
|
152
|
+
readonly rank: Rank;
|
|
116
153
|
}
|
|
117
154
|
/**
|
|
118
155
|
* A labeled cluster of same-band nodes — a subsystem. The far zoom renders
|
|
@@ -172,7 +209,7 @@ export interface MellosMap {
|
|
|
172
209
|
}
|
|
173
210
|
export declare const EMPTY_MAP: MellosMap;
|
|
174
211
|
/** Every way an operation can be refused, as data. */
|
|
175
|
-
export type MapError = InvalidId | {
|
|
212
|
+
export type MapError = InvalidId | InvalidRank | {
|
|
176
213
|
readonly kind: 'invalid-status';
|
|
177
214
|
readonly raw: string;
|
|
178
215
|
} | {
|
|
@@ -180,7 +217,7 @@ export type MapError = InvalidId | {
|
|
|
180
217
|
readonly id: LayerId;
|
|
181
218
|
} | {
|
|
182
219
|
readonly kind: 'duplicate-rank';
|
|
183
|
-
readonly rank:
|
|
220
|
+
readonly rank: Rank;
|
|
184
221
|
readonly existing: LayerId;
|
|
185
222
|
} | {
|
|
186
223
|
readonly kind: 'duplicate-node';
|
|
@@ -205,6 +242,10 @@ export type MapError = InvalidId | {
|
|
|
205
242
|
} | {
|
|
206
243
|
readonly kind: 'duplicate-group';
|
|
207
244
|
readonly id: GroupId;
|
|
245
|
+
} | {
|
|
246
|
+
readonly kind: 'id-collision';
|
|
247
|
+
readonly id: NodeId | GroupId;
|
|
248
|
+
readonly taken: 'node' | 'group';
|
|
208
249
|
} | {
|
|
209
250
|
readonly kind: 'unknown-group';
|
|
210
251
|
readonly id: GroupId;
|
|
@@ -234,9 +275,9 @@ export type MapError = InvalidId | {
|
|
|
234
275
|
} | {
|
|
235
276
|
readonly kind: 'edge-not-downward';
|
|
236
277
|
readonly from: NodeId;
|
|
237
|
-
readonly fromRank:
|
|
278
|
+
readonly fromRank: Rank;
|
|
238
279
|
readonly to: NodeId;
|
|
239
|
-
readonly toRank:
|
|
280
|
+
readonly toRank: Rank;
|
|
240
281
|
};
|
|
241
282
|
/** Human-readable rendering of a MapError, for tool results and logs. */
|
|
242
283
|
export declare function describeMapError(e: MapError): string;
|
package/lib/domain/types.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Structural invariants owned by this layer (and only these — the map is a
|
|
10
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
|
|
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.
|
|
12
14
|
* I2. Every node belongs to exactly one existing layer.
|
|
13
15
|
* I3. Node ids are unique.
|
|
14
16
|
* I4. An edge `from -> to` means "from USES to" and requires
|
|
@@ -26,6 +28,13 @@
|
|
|
26
28
|
* bands (a sequence participant, a swim lane); lane declaration order
|
|
27
29
|
* is left-to-right render order.
|
|
28
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.
|
|
29
38
|
*
|
|
30
39
|
* The map kind (dev | architecture | dataflow | behavior-tree | sequence) is
|
|
31
40
|
* presentation intent, not structure: every kind shares the same invariants,
|
|
@@ -58,6 +67,22 @@ export function makeNodeKind(raw) {
|
|
|
58
67
|
export function makeSubmapRef(raw) {
|
|
59
68
|
return ID_RULE.test(raw) ? ok(raw) : err({ kind: 'invalid-id', raw, rule: ID_RULE_TEXT });
|
|
60
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
|
+
}
|
|
61
86
|
/**
|
|
62
87
|
* Closed vocabulary of map kinds — the diagram's presentation intent.
|
|
63
88
|
* 'dev' (the default) is the progress ledger; the rest are documentation
|
|
@@ -80,6 +105,8 @@ export function describeMapError(e) {
|
|
|
80
105
|
switch (e.kind) {
|
|
81
106
|
case 'invalid-id':
|
|
82
107
|
return `invalid id "${e.raw}" (rule: ${e.rule})`;
|
|
108
|
+
case 'invalid-rank':
|
|
109
|
+
return `invalid rank ${e.raw} (rule: ${e.rule})`;
|
|
83
110
|
case 'invalid-status':
|
|
84
111
|
return `invalid status "${e.raw}" (expected: ${NODE_STATUSES.join(' | ')})`;
|
|
85
112
|
case 'duplicate-layer':
|
|
@@ -100,6 +127,9 @@ export function describeMapError(e) {
|
|
|
100
127
|
return `node "${e.id}" cannot depend on itself`;
|
|
101
128
|
case 'duplicate-group':
|
|
102
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}"`);
|
|
103
133
|
case 'unknown-group':
|
|
104
134
|
return `group "${e.id}" does not exist`;
|
|
105
135
|
case 'invalid-map-kind':
|
|
@@ -112,9 +142,10 @@ export function describeMapError(e) {
|
|
|
112
142
|
return (`node "${e.node}" (layer ${e.nodeLayer}) cannot join group "${e.group}" (layer ${e.groupLayer}); ` +
|
|
113
143
|
`groups cluster nodes within one band`);
|
|
114
144
|
case 'layer-not-empty':
|
|
115
|
-
return `layer "${e.id}" still holds node "${e.occupant}"; move
|
|
145
|
+
return (`layer "${e.id}" still holds node "${e.occupant}"; move its nodes to another band (moveNode) ` +
|
|
146
|
+
`or remove them (removeNode) first`);
|
|
116
147
|
case 'layer-holds-group':
|
|
117
|
-
return `layer "${e.id}" still holds group "${e.occupant}"; remove its groups first`;
|
|
148
|
+
return `layer "${e.id}" still holds group "${e.occupant}"; remove its groups (removeGroup) first`;
|
|
118
149
|
case 'edge-not-downward':
|
|
119
150
|
return (`edge ${e.from} (rank ${e.fromRank}) -> ${e.to} (rank ${e.toRank}) is not strictly downward; ` +
|
|
120
151
|
`dependencies may only point to a lower layer`);
|
|
@@ -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;
|