@penpair/core 0.3.7 → 0.4.1
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 +4 -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/browser-wake.d.ts +22 -0
- package/dist/browser-wake.js +241 -0
- package/dist/cli-main.js +193 -18
- 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/errors.js +1 -1
- 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/instance/connection.d.ts +2 -0
- package/dist/instance/connection.js +15 -3
- package/dist/kit-source.js +1 -1
- package/dist/layout-policy.js +21 -11
- package/dist/layout-settling.d.ts +1 -0
- package/dist/layout-settling.js +51 -17
- package/dist/layout.d.ts +7 -1
- package/dist/layout.js +19 -3
- 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/plugin-health.d.ts +13 -0
- package/dist/plugin-health.js +39 -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,255 @@
|
|
|
1
|
+
import { nativeMapShapeKeys, nativeMapTextEntries } from "./content.js";
|
|
2
|
+
import { importNativeContent } from "./import-content.js";
|
|
3
|
+
import { requireResolvedMapDetails } from "./details.js";
|
|
4
|
+
import { mapOriginLabels, mapOriginLegend } from "./status-labels.js";
|
|
5
|
+
import { hashMapContract } from "../maps/validation.js";
|
|
6
|
+
import { createMapProposal } from "../maps/proposal.js";
|
|
7
|
+
import { readMapPathPoints } from "./path-geometry.js";
|
|
8
|
+
import { validateDrawing } from "./projection.js";
|
|
9
|
+
export function importMapProposal(drawing, observed, baseline) {
|
|
10
|
+
validateDrawing(drawing);
|
|
11
|
+
requireResolvedMapDetails(drawing);
|
|
12
|
+
if (observed.mapId !== drawing.mapId || observed.contractHash !== drawing.contractHash) {
|
|
13
|
+
throw new Error("Projection does not match base contract");
|
|
14
|
+
}
|
|
15
|
+
const presentation = [];
|
|
16
|
+
const edgeChanges = [];
|
|
17
|
+
const findings = [
|
|
18
|
+
...observed.conflicts,
|
|
19
|
+
];
|
|
20
|
+
const expectedKeys = nativeMapShapeKeys(drawing);
|
|
21
|
+
for (const shape of observed.shapes) {
|
|
22
|
+
if (!expectedKeys.delete(`${shape.role}:${shape.logicalId}`) &&
|
|
23
|
+
!([
|
|
24
|
+
"summary",
|
|
25
|
+
"full-summary",
|
|
26
|
+
"description",
|
|
27
|
+
].includes(shape.role) && drawing.nodes.some((node) => node.id === shape.logicalId)) &&
|
|
28
|
+
!([
|
|
29
|
+
"map-summary",
|
|
30
|
+
"map-goal",
|
|
31
|
+
"map-description",
|
|
32
|
+
].includes(shape.role) && shape.logicalId === drawing.mapId)) {
|
|
33
|
+
findings.push(`Unmapped managed object requires reconciliation: ${shape.shapeId}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const missing of expectedKeys) {
|
|
37
|
+
if (![
|
|
38
|
+
"summary",
|
|
39
|
+
"full-summary",
|
|
40
|
+
"description",
|
|
41
|
+
"map-summary",
|
|
42
|
+
"map-description",
|
|
43
|
+
].some((role) => missing.startsWith(role + ":"))) {
|
|
44
|
+
findings.push(`Missing managed object: ${missing}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (drawing.nodes.some((node) => node.origin) &&
|
|
48
|
+
observed.shapes.find((shape) => shape.role === "legend")?.label !== mapOriginLegend) {
|
|
49
|
+
findings.push("Origin legend changed; reconcile against the canonical evidence contract");
|
|
50
|
+
}
|
|
51
|
+
for (const node of drawing.nodes) {
|
|
52
|
+
for (const clickable of observed.shapes.filter((shape) => shape.logicalId === node.id &&
|
|
53
|
+
[
|
|
54
|
+
"node",
|
|
55
|
+
"label",
|
|
56
|
+
"badge",
|
|
57
|
+
"summary",
|
|
58
|
+
"icon-well",
|
|
59
|
+
"icon",
|
|
60
|
+
"port-in",
|
|
61
|
+
"port-out",
|
|
62
|
+
].includes(shape.role))) {
|
|
63
|
+
if ((node.detail &&
|
|
64
|
+
(clickable.interactions?.length !== 1 ||
|
|
65
|
+
clickable.interactions[0]?.trigger !== "click" ||
|
|
66
|
+
clickable.interactions[0]?.type !== "navigate-to" ||
|
|
67
|
+
clickable.interactions[0]?.destinationId !== node.detail.frameId ||
|
|
68
|
+
clickable.interactions[0]?.destinationPageId !== node.detail.pageId ||
|
|
69
|
+
clickable.interactions[0]?.destinationMapHash !== node.detail.contractHash ||
|
|
70
|
+
clickable.interactions[0]?.destinationMapId !== node.detail.mapId)) ||
|
|
71
|
+
(!node.detail && clickable.interactions?.length)) {
|
|
72
|
+
findings.push(`Native interaction changed; reconcile the pinned detail contract: ${node.id}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (node.origin) {
|
|
76
|
+
const status = mapOriginLabels[node.origin];
|
|
77
|
+
if (observed.shapes.find((shape) => shape.role === "badge" && shape.logicalId === node.id)
|
|
78
|
+
?.label !== status) {
|
|
79
|
+
findings.push(`Origin badge changed; update evidence in canonical contract: ${node.id}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const labels = observed.shapes.filter((shape) => shape.role === "label" && shape.logicalId === node.id);
|
|
83
|
+
const card = observed.shapes.find((shape) => shape.role === "node" && shape.logicalId === node.id);
|
|
84
|
+
if (labels.length !== 1 || !card || !labels[0]?.label) {
|
|
85
|
+
findings.push(`Missing or ambiguous node: ${node.id}`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
presentation.push({
|
|
89
|
+
nodeId: node.id,
|
|
90
|
+
x: card.x,
|
|
91
|
+
y: card.y,
|
|
92
|
+
width: card.width,
|
|
93
|
+
height: card.height,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
for (const edge of drawing.edges) {
|
|
97
|
+
const paths = observed.shapes.filter((shape) => shape.role === "edge" && shape.logicalId === edge.id);
|
|
98
|
+
const path = paths[0];
|
|
99
|
+
const previous = baseline?.shapes.find((shape) => shape.role === "edge" && shape.logicalId === edge.id);
|
|
100
|
+
const labels = observed.shapes.filter((shape) => shape.role === "edge-label" && shape.logicalId === edge.id);
|
|
101
|
+
if (paths.length !== 1 || labels.length > 1 || !path) {
|
|
102
|
+
findings.push(`Relationship needs explicit reconciliation: ${edge.id}`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
let from = path.from;
|
|
106
|
+
let to = path.to;
|
|
107
|
+
if (!previous || path.content !== previous.content) {
|
|
108
|
+
const vertices = readMapPathPoints(path.content);
|
|
109
|
+
const start = vertices?.[0];
|
|
110
|
+
const end = vertices?.at(-1);
|
|
111
|
+
const matchEndpoint = (coordinates, side) => observed.shapes.filter((shape) => shape.role === "node" &&
|
|
112
|
+
(((Math.abs(shape.x + (side === "from" ? shape.width : 0) - coordinates?.x) < 0.01 ||
|
|
113
|
+
(side === "from" && Math.abs(shape.x - coordinates?.x) < 0.01)) &&
|
|
114
|
+
Math.abs(shape.y + shape.height / 2 - coordinates?.y) < 0.01) ||
|
|
115
|
+
(side === "to" &&
|
|
116
|
+
Math.abs(shape.x + shape.width / 2 - coordinates?.x) < 0.01 &&
|
|
117
|
+
Math.abs(shape.y - coordinates?.y) < 0.01)));
|
|
118
|
+
const validPath = vertices !== undefined;
|
|
119
|
+
const starts = validPath ? matchEndpoint(start, "from") : [];
|
|
120
|
+
const ends = validPath ? matchEndpoint(end, "to") : [];
|
|
121
|
+
if (starts.length !== 1 || ends.length !== 1) {
|
|
122
|
+
findings.push(`Relationship endpoints are ambiguous: ${edge.id}`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
from = starts[0]?.logicalId;
|
|
126
|
+
to = ends[0]?.logicalId;
|
|
127
|
+
}
|
|
128
|
+
if (!drawing.nodes.some((node) => node.id === from) ||
|
|
129
|
+
!drawing.nodes.some((node) => node.id === to)) {
|
|
130
|
+
findings.push(`Relationship references unknown nodes: ${edge.id}`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const label = labels[0]?.label ?? "";
|
|
134
|
+
if (from !== edge.from || to !== edge.to || label !== edge.label) {
|
|
135
|
+
edgeChanges.push({
|
|
136
|
+
edgeId: edge.id,
|
|
137
|
+
from: from,
|
|
138
|
+
to: to,
|
|
139
|
+
label,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const entry of nativeMapTextEntries(drawing).filter((entry) => !entry.semantic &&
|
|
144
|
+
![
|
|
145
|
+
"edge-label",
|
|
146
|
+
"badge",
|
|
147
|
+
"legend",
|
|
148
|
+
].includes(entry.role))) {
|
|
149
|
+
if (observed.shapes.find((shape) => shape.role === entry.role && shape.logicalId === entry.logicalId)?.label !== entry.text) {
|
|
150
|
+
findings.push(`Derived annotation label changed: ${entry.role}:${entry.logicalId}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const content = importNativeContent(drawing, observed, findings);
|
|
154
|
+
return {
|
|
155
|
+
schemaVersion: 1,
|
|
156
|
+
kind: "native-map-proposal",
|
|
157
|
+
baseContractHash: drawing.contractHash,
|
|
158
|
+
observedProjectionHash: observed.fingerprint,
|
|
159
|
+
mapId: drawing.mapId,
|
|
160
|
+
...(observed.shapes.find((shape) => shape.role === "title")?.label !== drawing.title
|
|
161
|
+
? {
|
|
162
|
+
title: observed.shapes.find((shape) => shape.role === "title")?.label ?? drawing.title,
|
|
163
|
+
}
|
|
164
|
+
: {}),
|
|
165
|
+
...content,
|
|
166
|
+
edgeChanges,
|
|
167
|
+
presentation,
|
|
168
|
+
findings,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/** Convert a native observation into the canonical reviewed proposal format. */
|
|
172
|
+
export function nativeProposalToMapProposal(map, drawing, observed, baseline) {
|
|
173
|
+
if (hashMapContract(map) !== drawing.contractHash ||
|
|
174
|
+
(map.id !== drawing.mapId && `${map.kind}:${map.id}` !== drawing.mapId)) {
|
|
175
|
+
throw new Error("Native import requires the exact canonical base contract");
|
|
176
|
+
}
|
|
177
|
+
const imported = importMapProposal(drawing, observed, baseline);
|
|
178
|
+
const proposedMap = {
|
|
179
|
+
...map,
|
|
180
|
+
title: imported.title ?? map.title,
|
|
181
|
+
...(imported.metadataChanges?.goal !== undefined
|
|
182
|
+
? {
|
|
183
|
+
goal: imported.metadataChanges.goal,
|
|
184
|
+
}
|
|
185
|
+
: {}),
|
|
186
|
+
nodes: map.nodes.map((node) => {
|
|
187
|
+
const change = imported.changes.find((change) => change.nodeId === node.id);
|
|
188
|
+
if (!change) {
|
|
189
|
+
return {
|
|
190
|
+
...node,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const next = {
|
|
194
|
+
...node,
|
|
195
|
+
origin: "proposed",
|
|
196
|
+
...(change.label !== undefined
|
|
197
|
+
? {
|
|
198
|
+
label: change.label,
|
|
199
|
+
}
|
|
200
|
+
: {}),
|
|
201
|
+
};
|
|
202
|
+
for (const field of [
|
|
203
|
+
"summary",
|
|
204
|
+
"description",
|
|
205
|
+
"icon",
|
|
206
|
+
]) {
|
|
207
|
+
if (change[field] === null) {
|
|
208
|
+
delete next[field];
|
|
209
|
+
}
|
|
210
|
+
else if (change[field] !== undefined) {
|
|
211
|
+
Object.assign(next, {
|
|
212
|
+
[field]: change[field],
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return next;
|
|
217
|
+
}),
|
|
218
|
+
edges: map.edges.map((edge) => {
|
|
219
|
+
const change = imported.edgeChanges.find((change) => change.edgeId === edge.id);
|
|
220
|
+
return change
|
|
221
|
+
? {
|
|
222
|
+
...edge,
|
|
223
|
+
from: change.from,
|
|
224
|
+
to: change.to,
|
|
225
|
+
label: change.label,
|
|
226
|
+
origin: "proposed",
|
|
227
|
+
}
|
|
228
|
+
: {
|
|
229
|
+
...edge,
|
|
230
|
+
};
|
|
231
|
+
}),
|
|
232
|
+
findings: [
|
|
233
|
+
...map.findings,
|
|
234
|
+
...imported.findings.map((message, index) => ({
|
|
235
|
+
id: `native-${observed.fingerprint.slice(0, 12)}-${index}`,
|
|
236
|
+
message,
|
|
237
|
+
severity: "blocking",
|
|
238
|
+
status: "open",
|
|
239
|
+
})),
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
for (const field of [
|
|
243
|
+
"summary",
|
|
244
|
+
"description",
|
|
245
|
+
]) {
|
|
246
|
+
const value = imported.metadataChanges?.[field];
|
|
247
|
+
if (value === null) {
|
|
248
|
+
delete proposedMap[field];
|
|
249
|
+
}
|
|
250
|
+
else if (value !== undefined) {
|
|
251
|
+
proposedMap[field] = value;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return createMapProposal(map, proposedMap, `Imported native map edits from projection ${observed.fingerprint}; presentation remains separate`);
|
|
255
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type * from "./types.js";
|
|
2
|
+
export { drawingFromPlan, readProjection } from "./projection.js";
|
|
3
|
+
export { publishMap } from "./publish.js";
|
|
4
|
+
export { importMapProposal, nativeProposalToMapProposal } from "./import.js";
|
|
5
|
+
export { inspectMapSnippet, publishMapSnippet } from "./snippets.js";
|
|
6
|
+
export { assembleReleaseMaps, type ReleaseMapPublication } from "./release-maps.js";
|
|
7
|
+
export { createMapPresentationProposal, validateMapPresentationProposal, acceptMapPresentationProposal, preserveMapPresentation, validateNativeMapProjection, type MapPresentationProposal, } from "./presentation.js";
|
|
8
|
+
export { resolveMapDetails } from "./details.js";
|
|
9
|
+
export { MapExportEvidenceError } from "./evidence-error.js";
|
|
10
|
+
export { nativeMapTextEntries, nativeMapShapeKeys, type NativeMapTextEntry } from "./content.js";
|
|
11
|
+
export { nativeMapIcons, defaultMapIcon } from "./icons.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { drawingFromPlan, readProjection } from "./projection.js";
|
|
2
|
+
export { publishMap } from "./publish.js";
|
|
3
|
+
export { importMapProposal, nativeProposalToMapProposal } from "./import.js";
|
|
4
|
+
export { inspectMapSnippet, publishMapSnippet } from "./snippets.js";
|
|
5
|
+
export { assembleReleaseMaps } from "./release-maps.js";
|
|
6
|
+
export { createMapPresentationProposal, validateMapPresentationProposal, acceptMapPresentationProposal, preserveMapPresentation, validateNativeMapProjection, } from "./presentation.js";
|
|
7
|
+
export { resolveMapDetails } from "./details.js";
|
|
8
|
+
export { MapExportEvidenceError } from "./evidence-error.js";
|
|
9
|
+
export { nativeMapTextEntries, nativeMapShapeKeys } from "./content.js";
|
|
10
|
+
export { nativeMapIcons, defaultMapIcon } from "./icons.js";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Parse the absolute M/L subset emitted by Penpot, including compact/comma SVG normalization. */
|
|
2
|
+
export function readMapPathPoints(content) {
|
|
3
|
+
if (typeof content !== "string" || content.length > 65536) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
let offset = 0;
|
|
7
|
+
const number = /[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/y;
|
|
8
|
+
const skipSeparators = () => {
|
|
9
|
+
while (offset < content.length && /[\s,]/.test(content[offset])) {
|
|
10
|
+
offset++;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const readCoordinate = () => {
|
|
14
|
+
skipSeparators();
|
|
15
|
+
number.lastIndex = offset;
|
|
16
|
+
const match = number.exec(content);
|
|
17
|
+
if (!match) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
offset = number.lastIndex;
|
|
21
|
+
const value = Number(match[0]);
|
|
22
|
+
return Number.isFinite(value) ? value : undefined;
|
|
23
|
+
};
|
|
24
|
+
const points = [];
|
|
25
|
+
while (offset < content.length) {
|
|
26
|
+
skipSeparators();
|
|
27
|
+
if (offset === content.length) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
const command = content[offset];
|
|
31
|
+
if (command === "M" || command === "L") {
|
|
32
|
+
if (points.length === 0 ? command !== "M" : command !== "L") {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
offset++;
|
|
36
|
+
}
|
|
37
|
+
else if (points.length === 0) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const x = readCoordinate();
|
|
41
|
+
const y = readCoordinate();
|
|
42
|
+
if (x === undefined || y === undefined || points.length >= 32) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
points.push({
|
|
46
|
+
x,
|
|
47
|
+
y,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return points.length >= 2 ? points : undefined;
|
|
51
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MapContract } from "../maps/types.js";
|
|
2
|
+
import type { MapDrawing, NativeMapProjection } from "./types.js";
|
|
3
|
+
export interface MapPresentationProposal {
|
|
4
|
+
schemaVersion: 1;
|
|
5
|
+
kind: "map-presentation-proposal";
|
|
6
|
+
mapKind: MapContract["kind"];
|
|
7
|
+
mapId: string;
|
|
8
|
+
baseContractHash: string;
|
|
9
|
+
baselineFingerprint: string;
|
|
10
|
+
observedProjectionHash: string;
|
|
11
|
+
projection: NativeMapProjection;
|
|
12
|
+
hash: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function validateNativeMapProjection(raw: unknown): NativeMapProjection;
|
|
15
|
+
export declare function validateMapPresentationProposal(raw: unknown): MapPresentationProposal;
|
|
16
|
+
export declare function createMapPresentationProposal(map: MapContract, drawing: MapDrawing, observed: NativeMapProjection, baseline: NativeMapProjection): MapPresentationProposal;
|
|
17
|
+
/** The caller must independently observe the live projection before adopting this saved evidence. */
|
|
18
|
+
export declare function acceptMapPresentationProposal(current: MapContract, baseline: NativeMapProjection, raw: unknown, resolvedDrawing?: MapDrawing): NativeMapProjection;
|
|
19
|
+
/** Reuse accepted node geometry while preserving the new contract's semantic graph. */
|
|
20
|
+
export declare function preserveMapPresentation(drawing: MapDrawing, savedProjection: NativeMapProjection): MapDrawing;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { observationHash } from "../journal.js";
|
|
2
|
+
import { routeMapEdges, mapDrawingGeometryProblems } from "../maps/layout.js";
|
|
3
|
+
import { buildMapDrawingPlan } from "../maps/render.js";
|
|
4
|
+
import { hashMapContract } from "../maps/validation.js";
|
|
5
|
+
import { importMapProposal } from "./import.js";
|
|
6
|
+
import { drawingFromPlan, projection, validateDrawing } from "./projection.js";
|
|
7
|
+
export function validateNativeMapProjection(raw) {
|
|
8
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
9
|
+
throw new Error("Saved map projection must be an object");
|
|
10
|
+
}
|
|
11
|
+
const record = raw;
|
|
12
|
+
if (record.schemaVersion !== 1 ||
|
|
13
|
+
record.kind !== "native-map-projection" ||
|
|
14
|
+
typeof record.fileId !== "string" ||
|
|
15
|
+
!record.fileId ||
|
|
16
|
+
typeof record.pageId !== "string" ||
|
|
17
|
+
!record.pageId ||
|
|
18
|
+
typeof record.mapId !== "string" ||
|
|
19
|
+
!record.mapId ||
|
|
20
|
+
typeof record.fingerprint !== "string") {
|
|
21
|
+
throw new Error("Saved map projection is missing identity or fingerprint");
|
|
22
|
+
}
|
|
23
|
+
const checked = projection({
|
|
24
|
+
fileId: record.fileId,
|
|
25
|
+
pageId: record.pageId,
|
|
26
|
+
}, record.mapId, record);
|
|
27
|
+
if (checked.fingerprint !== record.fingerprint) {
|
|
28
|
+
throw new Error("Saved map projection fingerprint differs from its observations");
|
|
29
|
+
}
|
|
30
|
+
return structuredClone(checked);
|
|
31
|
+
}
|
|
32
|
+
function sameMap(left, right) {
|
|
33
|
+
return (left.fileId === right.fileId &&
|
|
34
|
+
left.pageId === right.pageId &&
|
|
35
|
+
left.boardId === right.boardId &&
|
|
36
|
+
left.mapId === right.mapId);
|
|
37
|
+
}
|
|
38
|
+
export function validateMapPresentationProposal(raw) {
|
|
39
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
40
|
+
throw new Error("Map presentation proposal must be an object");
|
|
41
|
+
}
|
|
42
|
+
const record = raw;
|
|
43
|
+
if (record.schemaVersion !== 1 ||
|
|
44
|
+
record.kind !== "map-presentation-proposal" ||
|
|
45
|
+
![
|
|
46
|
+
"journey-map",
|
|
47
|
+
"integration-map",
|
|
48
|
+
].includes(String(record.mapKind)) ||
|
|
49
|
+
typeof record.mapId !== "string" ||
|
|
50
|
+
!record.mapId ||
|
|
51
|
+
[
|
|
52
|
+
record.baseContractHash,
|
|
53
|
+
record.baselineFingerprint,
|
|
54
|
+
record.observedProjectionHash,
|
|
55
|
+
record.hash,
|
|
56
|
+
].some((hash) => typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash))) {
|
|
57
|
+
throw new Error("Unsupported map presentation proposal or missing hashes");
|
|
58
|
+
}
|
|
59
|
+
const observed = validateNativeMapProjection(record.projection);
|
|
60
|
+
if (observed.fingerprint !== record.observedProjectionHash ||
|
|
61
|
+
observed.contractHash !== record.baseContractHash ||
|
|
62
|
+
!observed.boardId ||
|
|
63
|
+
observed.conflicts.length) {
|
|
64
|
+
throw new Error("Presentation proposal observations conflict with their base contract");
|
|
65
|
+
}
|
|
66
|
+
const { hash, ...body } = record;
|
|
67
|
+
if (hash !== observationHash(body)) {
|
|
68
|
+
throw new Error("Map presentation proposal record hash differs");
|
|
69
|
+
}
|
|
70
|
+
return structuredClone(record);
|
|
71
|
+
}
|
|
72
|
+
export function createMapPresentationProposal(map, drawing, observed, baseline) {
|
|
73
|
+
const baseHash = hashMapContract(map);
|
|
74
|
+
const before = validateNativeMapProjection(baseline);
|
|
75
|
+
const after = validateNativeMapProjection(observed);
|
|
76
|
+
if (drawing.contractHash !== baseHash ||
|
|
77
|
+
before.contractHash !== baseHash ||
|
|
78
|
+
after.contractHash !== baseHash ||
|
|
79
|
+
!sameMap(before, after) ||
|
|
80
|
+
before.fingerprint === after.fingerprint) {
|
|
81
|
+
throw new Error("Presentation proposal requires a changed projection of the same exact contract and board");
|
|
82
|
+
}
|
|
83
|
+
const semantic = importMapProposal(drawing, after, before);
|
|
84
|
+
if (semantic.metadataChanges ||
|
|
85
|
+
semantic.changes.length ||
|
|
86
|
+
semantic.edgeChanges.length ||
|
|
87
|
+
semantic.title !== undefined ||
|
|
88
|
+
semantic.findings.length ||
|
|
89
|
+
before.conflicts.length) {
|
|
90
|
+
throw new Error("Semantic edits or unresolved findings require a canonical map proposal");
|
|
91
|
+
}
|
|
92
|
+
const priorShapes = new Map(before.shapes.map((shape) => [
|
|
93
|
+
shape.shapeId,
|
|
94
|
+
shape,
|
|
95
|
+
]));
|
|
96
|
+
if (priorShapes.size !== after.shapes.length ||
|
|
97
|
+
after.shapes.some((shape) => {
|
|
98
|
+
const prior = priorShapes.get(shape.shapeId);
|
|
99
|
+
return (!prior ||
|
|
100
|
+
prior.role !== shape.role ||
|
|
101
|
+
prior.logicalId !== shape.logicalId ||
|
|
102
|
+
observationHash(prior.appearance) !== observationHash(shape.appearance));
|
|
103
|
+
})) {
|
|
104
|
+
throw new Error("Presentation adoption supports geometry edits of existing managed objects; style or ownership changes need reconciliation");
|
|
105
|
+
}
|
|
106
|
+
const body = {
|
|
107
|
+
schemaVersion: 1,
|
|
108
|
+
kind: "map-presentation-proposal",
|
|
109
|
+
mapKind: map.kind,
|
|
110
|
+
mapId: map.id,
|
|
111
|
+
baseContractHash: baseHash,
|
|
112
|
+
baselineFingerprint: before.fingerprint,
|
|
113
|
+
observedProjectionHash: after.fingerprint,
|
|
114
|
+
projection: after,
|
|
115
|
+
};
|
|
116
|
+
return validateMapPresentationProposal({
|
|
117
|
+
...body,
|
|
118
|
+
hash: observationHash(body),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** The caller must independently observe the live projection before adopting this saved evidence. */
|
|
122
|
+
export function acceptMapPresentationProposal(current, baseline, raw, resolvedDrawing) {
|
|
123
|
+
const proposal = validateMapPresentationProposal(raw);
|
|
124
|
+
const before = validateNativeMapProjection(baseline);
|
|
125
|
+
if (proposal.mapId !== current.id ||
|
|
126
|
+
proposal.mapKind !== current.kind ||
|
|
127
|
+
hashMapContract(current) !== proposal.baseContractHash ||
|
|
128
|
+
before.contractHash !== proposal.baseContractHash ||
|
|
129
|
+
before.fingerprint !== proposal.baselineFingerprint ||
|
|
130
|
+
!sameMap(before, proposal.projection)) {
|
|
131
|
+
throw new Error("Presentation proposal is stale or belongs to another map");
|
|
132
|
+
}
|
|
133
|
+
const revalidated = createMapPresentationProposal(current, resolvedDrawing ?? drawingFromPlan(buildMapDrawingPlan(current)), proposal.projection, before);
|
|
134
|
+
if (revalidated.hash !== proposal.hash) {
|
|
135
|
+
throw new Error("Presentation proposal contains unsupported changes or metadata");
|
|
136
|
+
}
|
|
137
|
+
return structuredClone(proposal.projection);
|
|
138
|
+
}
|
|
139
|
+
/** Reuse accepted node geometry while preserving the new contract's semantic graph. */
|
|
140
|
+
export function preserveMapPresentation(drawing, savedProjection) {
|
|
141
|
+
validateDrawing(drawing);
|
|
142
|
+
const saved = validateNativeMapProjection(savedProjection);
|
|
143
|
+
if (saved.mapId !== drawing.mapId || !saved.boardBounds || saved.conflicts.length) {
|
|
144
|
+
throw new Error("Cannot preserve presentation from a different or conflicted map");
|
|
145
|
+
}
|
|
146
|
+
const bounds = saved.boardBounds;
|
|
147
|
+
const nodes = drawing.nodes.map((node) => {
|
|
148
|
+
const cards = saved.shapes.filter((shape) => shape.role === "node" && shape.logicalId === node.id);
|
|
149
|
+
const card = cards[0];
|
|
150
|
+
if (!card) {
|
|
151
|
+
return {
|
|
152
|
+
...node,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const appearance = card.appearance;
|
|
156
|
+
if (cards.length !== 1 ||
|
|
157
|
+
typeof appearance?.name !== "string" ||
|
|
158
|
+
!appearance.name.startsWith(`${node.kind}: `)) {
|
|
159
|
+
throw new Error(`Node kind changed or presentation is ambiguous: ${node.id}`);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
...node,
|
|
163
|
+
x: card.x - bounds.x,
|
|
164
|
+
y: card.y - bounds.y,
|
|
165
|
+
width: card.width,
|
|
166
|
+
height: card.height,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
const edges = routeMapEdges(nodes, drawing.edges);
|
|
170
|
+
const problems = mapDrawingGeometryProblems(nodes, edges);
|
|
171
|
+
if (problems.length) {
|
|
172
|
+
throw new Error(`Preserved map layout needs reconciliation: ${problems.join("; ")}`);
|
|
173
|
+
}
|
|
174
|
+
const flowBottom = (flow) => Math.max(0, ...flow.nodes.map((node) => node.y + node.height), ...flow.edges.flatMap((edge) => [
|
|
175
|
+
...(edge.points ?? []).map((point) => point.y),
|
|
176
|
+
...(edge.labelBox
|
|
177
|
+
? [
|
|
178
|
+
edge.labelBox.y + edge.labelBox.height,
|
|
179
|
+
]
|
|
180
|
+
: []),
|
|
181
|
+
]));
|
|
182
|
+
const noteOffset = Math.max(0, flowBottom({
|
|
183
|
+
nodes,
|
|
184
|
+
edges,
|
|
185
|
+
}) - flowBottom(drawing));
|
|
186
|
+
const preserved = {
|
|
187
|
+
...drawing,
|
|
188
|
+
...(drawing.annotations
|
|
189
|
+
? {
|
|
190
|
+
annotations: drawing.annotations.map((annotation) => ({
|
|
191
|
+
...annotation,
|
|
192
|
+
y: annotation.y + noteOffset,
|
|
193
|
+
})),
|
|
194
|
+
}
|
|
195
|
+
: {}),
|
|
196
|
+
origin: {
|
|
197
|
+
x: bounds.x,
|
|
198
|
+
y: bounds.y,
|
|
199
|
+
},
|
|
200
|
+
nodes,
|
|
201
|
+
edges,
|
|
202
|
+
};
|
|
203
|
+
validateDrawing(preserved);
|
|
204
|
+
return preserved;
|
|
205
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PenpotToolClient } from "../client.js";
|
|
2
|
+
import { type SnippetResult } from "../snippet.js";
|
|
3
|
+
import type { MapDrawingPlan } from "../maps/types.js";
|
|
4
|
+
import type { MapDrawing, MapTarget, NativeMapProjection, ProjectionReadOptions } from "./types.js";
|
|
5
|
+
export declare function drawingFromPlan(plan: MapDrawingPlan): MapDrawing;
|
|
6
|
+
export declare function validateDrawing(drawing: MapDrawing): void;
|
|
7
|
+
export declare function projection(target: MapTarget, mapId: string, result: SnippetResult): NativeMapProjection;
|
|
8
|
+
export declare function readProjection(client: PenpotToolClient, target: MapTarget, mapId: string, options?: ProjectionReadOptions): Promise<NativeMapProjection>;
|