@penpair/core 0.3.7 → 0.4.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/dist/adapters.d.ts +1 -1
- package/dist/adapters.js +3 -0
- package/dist/approvals.d.ts +4 -1
- package/dist/approvals.js +7 -4
- package/dist/baseline-coverage.js +3 -3
- package/dist/baselines-config.d.ts +3 -0
- package/dist/baselines-config.js +3 -0
- package/dist/baselines.d.ts +7 -0
- package/dist/baselines.js +90 -21
- package/dist/bounded-file.d.ts +2 -0
- package/dist/bounded-file.js +38 -0
- package/dist/cli-main.js +64 -3
- package/dist/design-commands/context.d.ts +9 -0
- package/dist/design-commands/context.js +1 -0
- package/dist/design-commands/index.d.ts +3 -0
- package/dist/design-commands/index.js +7 -0
- package/dist/design-commands/map-command.d.ts +4 -0
- package/dist/design-commands/map-command.js +685 -0
- package/dist/design-commands/release-command.d.ts +3 -0
- package/dist/design-commands/release-command.js +239 -0
- package/dist/design-releases/artifact-paths.d.ts +6 -0
- package/dist/design-releases/artifact-paths.js +82 -0
- package/dist/design-releases/assemble.d.ts +3 -0
- package/dist/design-releases/assemble.js +176 -0
- package/dist/design-releases/check.d.ts +16 -0
- package/dist/design-releases/check.js +323 -0
- package/dist/design-releases/context.d.ts +2 -0
- package/dist/design-releases/context.js +119 -0
- package/dist/design-releases/contract-graph.d.ts +13 -0
- package/dist/design-releases/contract-graph.js +75 -0
- package/dist/design-releases/fidelity.d.ts +23 -0
- package/dist/design-releases/fidelity.js +328 -0
- package/dist/design-releases/finalize.d.ts +4 -0
- package/dist/design-releases/finalize.js +201 -0
- package/dist/design-releases/index.d.ts +8 -0
- package/dist/design-releases/index.js +7 -0
- package/dist/design-releases/live.d.ts +22 -0
- package/dist/design-releases/live.js +97 -0
- package/dist/design-releases/snippets.d.ts +5 -0
- package/dist/design-releases/snippets.js +89 -0
- package/dist/design-releases/store.d.ts +10 -0
- package/dist/design-releases/store.js +205 -0
- package/dist/design-releases/types.d.ts +161 -0
- package/dist/design-releases/types.js +1 -0
- package/dist/design-releases/validation.d.ts +6 -0
- package/dist/design-releases/validation.js +384 -0
- package/dist/design-releases/verify.d.ts +3 -0
- package/dist/design-releases/verify.js +148 -0
- package/dist/image-codec.d.ts +3 -0
- package/dist/image-codec.js +78 -0
- package/dist/image-evidence.d.ts +15 -0
- package/dist/image-evidence.js +121 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/inspection.js +13 -5
- package/dist/instance/config-schema.d.ts +4 -0
- package/dist/instance/config-schema.js +7 -0
- package/dist/map-penpot/content-validation.d.ts +3 -0
- package/dist/map-penpot/content-validation.js +57 -0
- package/dist/map-penpot/content.d.ts +22 -0
- package/dist/map-penpot/content.js +173 -0
- package/dist/map-penpot/details.d.ts +4 -0
- package/dist/map-penpot/details.js +84 -0
- package/dist/map-penpot/evidence-error.d.ts +5 -0
- package/dist/map-penpot/evidence-error.js +9 -0
- package/dist/map-penpot/icons.d.ts +5 -0
- package/dist/map-penpot/icons.js +79 -0
- package/dist/map-penpot/import-content.d.ts +2 -0
- package/dist/map-penpot/import-content.js +150 -0
- package/dist/map-penpot/import.d.ts +5 -0
- package/dist/map-penpot/import.js +255 -0
- package/dist/map-penpot/index.d.ts +11 -0
- package/dist/map-penpot/index.js +10 -0
- package/dist/map-penpot/path-geometry.d.ts +5 -0
- package/dist/map-penpot/path-geometry.js +51 -0
- package/dist/map-penpot/presentation.d.ts +20 -0
- package/dist/map-penpot/presentation.js +205 -0
- package/dist/map-penpot/projection.d.ts +8 -0
- package/dist/map-penpot/projection.js +176 -0
- package/dist/map-penpot/publish.d.ts +4 -0
- package/dist/map-penpot/publish.js +301 -0
- package/dist/map-penpot/release-maps.d.ts +21 -0
- package/dist/map-penpot/release-maps.js +104 -0
- package/dist/map-penpot/snippets.d.ts +4 -0
- package/dist/map-penpot/snippets.js +164 -0
- package/dist/map-penpot/status-labels.d.ts +7 -0
- package/dist/map-penpot/status-labels.js +7 -0
- package/dist/map-penpot/types.d.ts +161 -0
- package/dist/map-penpot/types.js +1 -0
- package/dist/maps/annotation-layout.d.ts +24 -0
- package/dist/maps/annotation-layout.js +85 -0
- package/dist/maps/context.d.ts +13 -0
- package/dist/maps/context.js +207 -0
- package/dist/maps/index.d.ts +9 -0
- package/dist/maps/index.js +9 -0
- package/dist/maps/layout.d.ts +35 -0
- package/dist/maps/layout.js +364 -0
- package/dist/maps/proposal.d.ts +5 -0
- package/dist/maps/proposal.js +67 -0
- package/dist/maps/render.d.ts +4 -0
- package/dist/maps/render.js +331 -0
- package/dist/maps/review.d.ts +16 -0
- package/dist/maps/review.js +90 -0
- package/dist/maps/store.d.ts +21 -0
- package/dist/maps/store.js +222 -0
- package/dist/maps/types.d.ts +206 -0
- package/dist/maps/types.js +1 -0
- package/dist/maps/validation.d.ts +10 -0
- package/dist/maps/validation.js +375 -0
- package/dist/skill-routing.js +4 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/skills/penpair-design-release/SKILL.md +55 -0
- package/skills/penpair-design-release/agents/openai.yaml +6 -0
- package/skills/penpair-design-release/references/workflow.md +55 -0
- package/skills/penpair-diagrams/SKILL.md +52 -0
- package/skills/penpair-diagrams/agents/openai.yaml +6 -0
- package/skills/penpair-diagrams/references/workflow.md +84 -0
- package/skills/penpair-handoff/SKILL.md +7 -0
- package/skills/penpair-handoff/references/handoff-flow.md +7 -1
- package/skills/penpair-integration-map/SKILL.md +44 -0
- package/skills/penpair-integration-map/agents/openai.yaml +6 -0
- package/skills/penpair-integration-map/references/workflow.md +33 -0
- package/skills/penpair-journey-map/SKILL.md +42 -0
- package/skills/penpair-journey-map/agents/openai.yaml +6 -0
- package/skills/penpair-journey-map/references/workflow.md +35 -0
- package/skills/penpair-verify/SKILL.md +7 -0
- package/skills/penpair-verify/references/browser-evidence.md +10 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
function boxesOverlap(left, right) {
|
|
2
|
+
return (left.x < right.x + right.width &&
|
|
3
|
+
right.x < left.x + left.width &&
|
|
4
|
+
left.y < right.y + right.height &&
|
|
5
|
+
right.y < left.y + left.height);
|
|
6
|
+
}
|
|
7
|
+
function segmentCrossesBox(from, to, box) {
|
|
8
|
+
if (from.x === to.x) {
|
|
9
|
+
return (from.x > box.x &&
|
|
10
|
+
from.x < box.x + box.width &&
|
|
11
|
+
Math.max(from.y, to.y) > box.y &&
|
|
12
|
+
Math.min(from.y, to.y) < box.y + box.height);
|
|
13
|
+
}
|
|
14
|
+
if (from.y === to.y) {
|
|
15
|
+
return (from.y > box.y &&
|
|
16
|
+
from.y < box.y + box.height &&
|
|
17
|
+
Math.max(from.x, to.x) > box.x &&
|
|
18
|
+
Math.min(from.x, to.x) < box.x + box.width);
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
export function mapDrawingGeometryProblems(nodes, edges) {
|
|
23
|
+
const problems = [];
|
|
24
|
+
const verticalTracks = new Map();
|
|
25
|
+
for (const node of nodes) {
|
|
26
|
+
if (node.x < 0 || node.y < 160) {
|
|
27
|
+
problems.push(`Card ${node.id} intrudes into the title/legend or outside the board`);
|
|
28
|
+
}
|
|
29
|
+
if (nodes.some((other) => other.id !== node.id && boxesOverlap(node, other))) {
|
|
30
|
+
problems.push(`Card ${node.id} overlaps another card`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const edge of edges) {
|
|
34
|
+
if (edge.labelBox.x < 0 ||
|
|
35
|
+
edge.labelBox.y < 160 ||
|
|
36
|
+
nodes.some((node) => boxesOverlap(edge.labelBox, node))) {
|
|
37
|
+
problems.push(`Edge label ${edge.id} overlaps a card/header or leaves the board`);
|
|
38
|
+
}
|
|
39
|
+
if (edges.some((other) => other.id !== edge.id && boxesOverlap(edge.labelBox, other.labelBox))) {
|
|
40
|
+
problems.push(`Edge label ${edge.id} overlaps another edge label`);
|
|
41
|
+
}
|
|
42
|
+
for (let index = 1; index < edge.points.length; index++) {
|
|
43
|
+
const from = edge.points[index - 1];
|
|
44
|
+
const to = edge.points[index];
|
|
45
|
+
if (from.x === to.x && from.y !== to.y) {
|
|
46
|
+
const low = Math.min(from.y, to.y);
|
|
47
|
+
const high = Math.max(from.y, to.y);
|
|
48
|
+
const track = verticalTracks.get(from.x) ?? [];
|
|
49
|
+
if (track.some((other) => other.id !== edge.id && other.low < high && low < other.high)) {
|
|
50
|
+
problems.push(`Connectors share an ambiguous vertical track: ${edge.id}`);
|
|
51
|
+
}
|
|
52
|
+
track.push({
|
|
53
|
+
id: edge.id,
|
|
54
|
+
low,
|
|
55
|
+
high,
|
|
56
|
+
});
|
|
57
|
+
verticalTracks.set(from.x, track);
|
|
58
|
+
}
|
|
59
|
+
if (nodes.some((node) => segmentCrossesBox(from, to, node)) ||
|
|
60
|
+
edges.some((other) => segmentCrossesBox(from, to, other.labelBox))) {
|
|
61
|
+
problems.push(`Connector ${edge.id} crosses a card or label`);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (problems.length >= 20) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return [
|
|
70
|
+
...new Set(problems),
|
|
71
|
+
].slice(0, 20);
|
|
72
|
+
}
|
|
73
|
+
export function mapLabelHeight(text, width) {
|
|
74
|
+
const charactersPerLine = Math.max(1, Math.floor(width / 16));
|
|
75
|
+
const lines = text
|
|
76
|
+
.split(/\r?\n/)
|
|
77
|
+
.reduce((total, line) => total + Math.max(1, Math.ceil(line.length / charactersPerLine)), 0);
|
|
78
|
+
return lines * 24 + 16;
|
|
79
|
+
}
|
|
80
|
+
export function mapEdgeLabelWidth(label) {
|
|
81
|
+
return Math.max(80, Math.min(240, Math.max(...label.split(/\r?\n/).map((line) => line.length)) * 7 + 16));
|
|
82
|
+
}
|
|
83
|
+
/** Labels occupy dedicated gaps; routes stay outside those boxes and all card interiors. */
|
|
84
|
+
export function routeMapEdges(nodes, edges) {
|
|
85
|
+
const byId = new Map(nodes.map((node) => [
|
|
86
|
+
node.id,
|
|
87
|
+
node,
|
|
88
|
+
]));
|
|
89
|
+
const columns = [
|
|
90
|
+
...new Set(nodes.map((node) => node.x)),
|
|
91
|
+
].sort((left, right) => left - right);
|
|
92
|
+
const corridorEnds = new Map();
|
|
93
|
+
const portTracks = new Map();
|
|
94
|
+
const takeTrack = (key) => {
|
|
95
|
+
const track = portTracks.get(key) ?? 0;
|
|
96
|
+
portTracks.set(key, track + 1);
|
|
97
|
+
return track;
|
|
98
|
+
};
|
|
99
|
+
const pending = [];
|
|
100
|
+
const routed = [];
|
|
101
|
+
for (const edge of [
|
|
102
|
+
...edges,
|
|
103
|
+
].sort((left, right) => {
|
|
104
|
+
const aligned = (entry) => {
|
|
105
|
+
const from = byId.get(entry.from);
|
|
106
|
+
const to = byId.get(entry.to);
|
|
107
|
+
return (to.x > from.x &&
|
|
108
|
+
columns.indexOf(to.x) === columns.indexOf(from.x) + 1 &&
|
|
109
|
+
from.y + from.height / 2 === to.y + to.height / 2);
|
|
110
|
+
};
|
|
111
|
+
return (Number(!aligned(left)) - Number(!aligned(right)) ||
|
|
112
|
+
(left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
|
|
113
|
+
})) {
|
|
114
|
+
const from = byId.get(edge.from);
|
|
115
|
+
const to = byId.get(edge.to);
|
|
116
|
+
if (!from || !to) {
|
|
117
|
+
throw new Error("Cannot route an edge with a missing node");
|
|
118
|
+
}
|
|
119
|
+
const sourceColumn = columns.indexOf(from.x);
|
|
120
|
+
const targetColumn = columns.indexOf(to.x);
|
|
121
|
+
if (targetColumn > sourceColumn + 1 || targetColumn < sourceColumn) {
|
|
122
|
+
pending.push({
|
|
123
|
+
edge,
|
|
124
|
+
from,
|
|
125
|
+
to,
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const cyclic = targetColumn === sourceColumn;
|
|
130
|
+
const labelWidth = mapEdgeLabelWidth(edge.label);
|
|
131
|
+
const labelX = cyclic
|
|
132
|
+
? from.x - labelWidth - 96
|
|
133
|
+
: (from.x + from.width + to.x - labelWidth) / 2;
|
|
134
|
+
const corridorKey = cyclic ? -from.x : from.x;
|
|
135
|
+
const sourceTrack = takeTrack(`${cyclic ? "left" : "right"}:${cyclic ? from.x : from.x + from.width}`);
|
|
136
|
+
const targetTrack = takeTrack(`left:${to.x}`);
|
|
137
|
+
const labelHeight = mapLabelHeight(edge.label, labelWidth);
|
|
138
|
+
const naturalY = to.y + to.height / 2 - labelHeight - 12;
|
|
139
|
+
const direct = !cyclic &&
|
|
140
|
+
from.y + from.height / 2 === to.y + to.height / 2 &&
|
|
141
|
+
naturalY >= 160 &&
|
|
142
|
+
!routed.some((entry) => boxesOverlap(entry.labelBox, {
|
|
143
|
+
x: labelX,
|
|
144
|
+
y: naturalY,
|
|
145
|
+
width: labelWidth,
|
|
146
|
+
height: labelHeight,
|
|
147
|
+
}));
|
|
148
|
+
const labelBox = {
|
|
149
|
+
x: labelX,
|
|
150
|
+
y: direct
|
|
151
|
+
? naturalY
|
|
152
|
+
: Math.max(naturalY, corridorEnds.get(corridorKey) ??
|
|
153
|
+
Math.max(180, Math.min(...nodes.map((node) => node.y)))),
|
|
154
|
+
width: labelWidth,
|
|
155
|
+
height: labelHeight,
|
|
156
|
+
};
|
|
157
|
+
corridorEnds.set(corridorKey, labelBox.y + labelBox.height + 80);
|
|
158
|
+
const routeY = labelBox.y + labelBox.height + 16;
|
|
159
|
+
const start = {
|
|
160
|
+
x: cyclic ? from.x : from.x + from.width,
|
|
161
|
+
y: from.y + from.height / 2,
|
|
162
|
+
};
|
|
163
|
+
const finish = {
|
|
164
|
+
x: to.x,
|
|
165
|
+
y: to.y + to.height / 2,
|
|
166
|
+
};
|
|
167
|
+
const points = direct
|
|
168
|
+
? [
|
|
169
|
+
start,
|
|
170
|
+
finish,
|
|
171
|
+
]
|
|
172
|
+
: cyclic
|
|
173
|
+
? [
|
|
174
|
+
start,
|
|
175
|
+
{
|
|
176
|
+
x: from.x - 24 - sourceTrack * 8,
|
|
177
|
+
y: start.y,
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
x: from.x - 24 - sourceTrack * 8,
|
|
181
|
+
y: routeY,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
x: labelBox.x - 16,
|
|
185
|
+
y: routeY,
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
x: labelBox.x - 16,
|
|
189
|
+
y: routeY + 20,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
x: to.x - 24 - targetTrack * 8,
|
|
193
|
+
y: routeY + 20,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
x: to.x - 24 - targetTrack * 8,
|
|
197
|
+
y: finish.y,
|
|
198
|
+
},
|
|
199
|
+
finish,
|
|
200
|
+
]
|
|
201
|
+
: [
|
|
202
|
+
start,
|
|
203
|
+
{
|
|
204
|
+
x: start.x + 24 + sourceTrack * 8,
|
|
205
|
+
y: start.y,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
x: start.x + 24 + sourceTrack * 8,
|
|
209
|
+
y: routeY,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
x: finish.x - 24 - targetTrack * 8,
|
|
213
|
+
y: routeY,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
x: finish.x - 24 - targetTrack * 8,
|
|
217
|
+
y: finish.y,
|
|
218
|
+
},
|
|
219
|
+
finish,
|
|
220
|
+
];
|
|
221
|
+
routed.push({
|
|
222
|
+
...edge,
|
|
223
|
+
points,
|
|
224
|
+
labelBox,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
let bottom = Math.max(180, ...nodes.map((node) => node.y + node.height), ...corridorEnds.values()) + 100;
|
|
228
|
+
for (const { edge, from, to } of pending) {
|
|
229
|
+
const sourceTrack = takeTrack(`right:${from.x + from.width}`);
|
|
230
|
+
const targetTrack = takeTrack(`left:${to.x}`);
|
|
231
|
+
const labelWidth = mapEdgeLabelWidth(edge.label);
|
|
232
|
+
const labelBox = {
|
|
233
|
+
x: Math.min(from.x, to.x) + Math.max(from.width, to.width) + 32,
|
|
234
|
+
y: bottom,
|
|
235
|
+
width: labelWidth,
|
|
236
|
+
height: mapLabelHeight(edge.label, labelWidth),
|
|
237
|
+
};
|
|
238
|
+
const routeY = labelBox.y + labelBox.height + 16;
|
|
239
|
+
const start = {
|
|
240
|
+
x: from.x + from.width,
|
|
241
|
+
y: from.y + from.height / 2,
|
|
242
|
+
};
|
|
243
|
+
const finish = {
|
|
244
|
+
x: to.x,
|
|
245
|
+
y: to.y + to.height / 2,
|
|
246
|
+
};
|
|
247
|
+
routed.push({
|
|
248
|
+
...edge,
|
|
249
|
+
labelBox,
|
|
250
|
+
points: [
|
|
251
|
+
start,
|
|
252
|
+
{
|
|
253
|
+
x: start.x + 24 + sourceTrack * 8,
|
|
254
|
+
y: start.y,
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
x: start.x + 24 + sourceTrack * 8,
|
|
258
|
+
y: routeY,
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
x: finish.x - 24 - targetTrack * 8,
|
|
262
|
+
y: routeY,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
x: finish.x - 24 - targetTrack * 8,
|
|
266
|
+
y: finish.y,
|
|
267
|
+
},
|
|
268
|
+
finish,
|
|
269
|
+
],
|
|
270
|
+
});
|
|
271
|
+
bottom = routeY + 80;
|
|
272
|
+
}
|
|
273
|
+
return routed.sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
|
|
274
|
+
}
|
|
275
|
+
/** Ignore DFS return edges for placement only; every connector remains in the drawing. */
|
|
276
|
+
export function mapNodeLayers(contract) {
|
|
277
|
+
const outgoing = new Map(contract.nodes.map((node) => [
|
|
278
|
+
node.id,
|
|
279
|
+
[],
|
|
280
|
+
]));
|
|
281
|
+
const incomingCount = new Map(contract.nodes.map((node) => [
|
|
282
|
+
node.id,
|
|
283
|
+
0,
|
|
284
|
+
]));
|
|
285
|
+
for (const edge of contract.edges) {
|
|
286
|
+
outgoing.get(edge.from).push(edge.to);
|
|
287
|
+
incomingCount.set(edge.to, incomingCount.get(edge.to) + 1);
|
|
288
|
+
}
|
|
289
|
+
const byId = new Map(contract.nodes.map((node) => [
|
|
290
|
+
node.id,
|
|
291
|
+
node,
|
|
292
|
+
]));
|
|
293
|
+
for (const neighbors of outgoing.values()) {
|
|
294
|
+
neighbors.sort((left, right) => Number(byId.get(left).kind !== "screen") - Number(byId.get(right).kind !== "screen") ||
|
|
295
|
+
(left < right ? -1 : left > right ? 1 : 0));
|
|
296
|
+
}
|
|
297
|
+
const state = new Map();
|
|
298
|
+
const successors = new Map(contract.nodes.map((node) => [
|
|
299
|
+
node.id,
|
|
300
|
+
new Set(),
|
|
301
|
+
]));
|
|
302
|
+
const roots = [
|
|
303
|
+
...contract.nodes,
|
|
304
|
+
].sort((left, right) => Number(incomingCount.get(left.id) > 0) - Number(incomingCount.get(right.id) > 0) ||
|
|
305
|
+
(left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
|
|
306
|
+
for (const root of roots) {
|
|
307
|
+
if (state.has(root.id)) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
state.set(root.id, "visiting");
|
|
311
|
+
const stack = [
|
|
312
|
+
{
|
|
313
|
+
id: root.id,
|
|
314
|
+
index: 0,
|
|
315
|
+
},
|
|
316
|
+
];
|
|
317
|
+
while (stack.length) {
|
|
318
|
+
const current = stack[stack.length - 1];
|
|
319
|
+
const next = outgoing.get(current.id)[current.index++];
|
|
320
|
+
if (next === undefined) {
|
|
321
|
+
state.set(current.id, "done");
|
|
322
|
+
stack.pop();
|
|
323
|
+
}
|
|
324
|
+
else if (state.get(next) !== "visiting") {
|
|
325
|
+
successors.get(current.id).add(next);
|
|
326
|
+
if (!state.has(next)) {
|
|
327
|
+
state.set(next, "visiting");
|
|
328
|
+
stack.push({
|
|
329
|
+
id: next,
|
|
330
|
+
index: 0,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const indegrees = new Map(contract.nodes.map((node) => [
|
|
337
|
+
node.id,
|
|
338
|
+
0,
|
|
339
|
+
]));
|
|
340
|
+
const depths = new Map(contract.nodes.map((node) => [
|
|
341
|
+
node.id,
|
|
342
|
+
0,
|
|
343
|
+
]));
|
|
344
|
+
for (const targets of successors.values()) {
|
|
345
|
+
for (const target of targets) {
|
|
346
|
+
indegrees.set(target, indegrees.get(target) + 1);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const queue = contract.nodes
|
|
350
|
+
.filter((node) => indegrees.get(node.id) === 0)
|
|
351
|
+
.map((node) => node.id);
|
|
352
|
+
while (queue.length) {
|
|
353
|
+
queue.sort();
|
|
354
|
+
const current = queue.shift();
|
|
355
|
+
for (const next of successors.get(current)) {
|
|
356
|
+
depths.set(next, Math.max(depths.get(next), depths.get(current) + 1));
|
|
357
|
+
indegrees.set(next, indegrees.get(next) - 1);
|
|
358
|
+
if (indegrees.get(next) === 0) {
|
|
359
|
+
queue.push(next);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return depths;
|
|
364
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { MapContract, MapProposal } from "./types.js";
|
|
2
|
+
export declare function validateMapProposal(raw: unknown): MapProposal;
|
|
3
|
+
export declare function createMapProposal(current: MapContract, proposed: MapContract, reason: string): MapProposal;
|
|
4
|
+
/** Accepting changes the canonical revision; it neither creates nor carries forward owner approval. */
|
|
5
|
+
export declare function acceptMapProposal(current: MapContract, raw: MapProposal): MapContract;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { observationHash } from "../journal.js";
|
|
2
|
+
import { hashMapContract, mapHash, mapObject, mapText, validateMapContract } from "./validation.js";
|
|
3
|
+
export function validateMapProposal(raw) {
|
|
4
|
+
const proposal = mapObject(raw, "Map proposal", [
|
|
5
|
+
"schemaVersion",
|
|
6
|
+
"kind",
|
|
7
|
+
"baseHash",
|
|
8
|
+
"proposedHash",
|
|
9
|
+
"proposedMap",
|
|
10
|
+
"reason",
|
|
11
|
+
"hash",
|
|
12
|
+
]);
|
|
13
|
+
if (proposal.schemaVersion !== 1 || proposal.kind !== "map-proposal") {
|
|
14
|
+
throw new Error("Unsupported map proposal schema");
|
|
15
|
+
}
|
|
16
|
+
const result = {
|
|
17
|
+
schemaVersion: 1,
|
|
18
|
+
kind: "map-proposal",
|
|
19
|
+
baseHash: mapHash(proposal.baseHash, "Proposal base hash"),
|
|
20
|
+
proposedHash: mapHash(proposal.proposedHash, "Proposed map hash"),
|
|
21
|
+
proposedMap: validateMapContract(proposal.proposedMap),
|
|
22
|
+
reason: mapText(proposal.reason, "Proposal reason", 8000),
|
|
23
|
+
hash: mapHash(proposal.hash, "Proposal record hash"),
|
|
24
|
+
};
|
|
25
|
+
const { hash, ...body } = result;
|
|
26
|
+
if (hash !== observationHash(body) ||
|
|
27
|
+
result.proposedHash !== hashMapContract(result.proposedMap)) {
|
|
28
|
+
throw new Error("Map proposal evidence hash mismatch");
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
export function createMapProposal(current, proposed, reason) {
|
|
33
|
+
const base = validateMapContract(current);
|
|
34
|
+
const proposedMap = validateMapContract(proposed);
|
|
35
|
+
if (base.kind !== proposedMap.kind || base.id !== proposedMap.id) {
|
|
36
|
+
throw new Error("A proposal must preserve map identity and kind");
|
|
37
|
+
}
|
|
38
|
+
if (hashMapContract(base) === hashMapContract(proposedMap)) {
|
|
39
|
+
throw new Error("A proposal requires a semantic contract change");
|
|
40
|
+
}
|
|
41
|
+
const body = {
|
|
42
|
+
schemaVersion: 1,
|
|
43
|
+
kind: "map-proposal",
|
|
44
|
+
baseHash: hashMapContract(base),
|
|
45
|
+
proposedHash: hashMapContract(proposedMap),
|
|
46
|
+
proposedMap,
|
|
47
|
+
reason,
|
|
48
|
+
};
|
|
49
|
+
return validateMapProposal({
|
|
50
|
+
...body,
|
|
51
|
+
hash: observationHash(body),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Accepting changes the canonical revision; it neither creates nor carries forward owner approval. */
|
|
55
|
+
export function acceptMapProposal(current, raw) {
|
|
56
|
+
const base = validateMapContract(current);
|
|
57
|
+
const proposal = validateMapProposal(raw);
|
|
58
|
+
if (hashMapContract(base) !== proposal.baseHash ||
|
|
59
|
+
base.id !== proposal.proposedMap.id ||
|
|
60
|
+
base.kind !== proposal.proposedMap.kind) {
|
|
61
|
+
throw new Error("Map proposal is stale or belongs to another map; review against the current contract");
|
|
62
|
+
}
|
|
63
|
+
if (proposal.proposedHash === proposal.baseHash) {
|
|
64
|
+
throw new Error("A proposal must change the canonical revision before it can be accepted");
|
|
65
|
+
}
|
|
66
|
+
return structuredClone(proposal.proposedMap);
|
|
67
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { MapContract, MapDrawingPlan } from "./types.js";
|
|
2
|
+
export declare function renderMapMermaid(raw: MapContract): string;
|
|
3
|
+
export declare function renderMapMarkdown(raw: MapContract): string;
|
|
4
|
+
export declare function buildMapDrawingPlan(raw: MapContract, detailMaps?: Record<string, MapContract>): MapDrawingPlan;
|