@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,150 @@
|
|
|
1
|
+
import { nativeMapTextEntries } from "./content.js";
|
|
2
|
+
import { defaultMapIcon, identifyMapIcon } from "./icons.js";
|
|
3
|
+
export function importNativeContent(drawing, observed, findings) {
|
|
4
|
+
const entries = nativeMapTextEntries(drawing);
|
|
5
|
+
const changes = [];
|
|
6
|
+
const metadataChanges = {};
|
|
7
|
+
const read = (scope, field, nodeId) => {
|
|
8
|
+
const relevant = entries.filter((entry) => entry.semantic?.scope === scope &&
|
|
9
|
+
entry.semantic.field === field &&
|
|
10
|
+
entry.semantic.nodeId === nodeId);
|
|
11
|
+
const changed = relevant.flatMap((entry) => {
|
|
12
|
+
const actual = observed.shapes.find((shape) => shape.role === entry.role && shape.logicalId === entry.logicalId);
|
|
13
|
+
const text = actual?.label?.trim() ?? "";
|
|
14
|
+
return text !== entry.text
|
|
15
|
+
? [
|
|
16
|
+
{
|
|
17
|
+
text,
|
|
18
|
+
compact: entry.semantic?.compact === true,
|
|
19
|
+
},
|
|
20
|
+
]
|
|
21
|
+
: [];
|
|
22
|
+
});
|
|
23
|
+
if (!relevant.length) {
|
|
24
|
+
const roles = scope === "map"
|
|
25
|
+
? field === "goal"
|
|
26
|
+
? [
|
|
27
|
+
"map-goal",
|
|
28
|
+
]
|
|
29
|
+
: field === "summary"
|
|
30
|
+
? [
|
|
31
|
+
"map-summary",
|
|
32
|
+
]
|
|
33
|
+
: [
|
|
34
|
+
"map-description",
|
|
35
|
+
]
|
|
36
|
+
: field === "label"
|
|
37
|
+
? [
|
|
38
|
+
"label",
|
|
39
|
+
"full-label",
|
|
40
|
+
]
|
|
41
|
+
: field === "summary"
|
|
42
|
+
? [
|
|
43
|
+
"summary",
|
|
44
|
+
"full-summary",
|
|
45
|
+
]
|
|
46
|
+
: [
|
|
47
|
+
"description",
|
|
48
|
+
];
|
|
49
|
+
const actual = observed.shapes.find((shape) => roles.includes(shape.role) &&
|
|
50
|
+
shape.logicalId === (nodeId ?? drawing.mapId) &&
|
|
51
|
+
!entries.some((entry) => entry.role === shape.role &&
|
|
52
|
+
entry.logicalId === shape.logicalId &&
|
|
53
|
+
entry.semantic &&
|
|
54
|
+
entry.semantic.field !== field));
|
|
55
|
+
return actual?.label?.trim() || undefined;
|
|
56
|
+
}
|
|
57
|
+
if (!changed.length) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const full = changed.find((change) => !change.compact);
|
|
61
|
+
const selected = full ?? changed[0];
|
|
62
|
+
if (selected.compact && selected.text.endsWith("…")) {
|
|
63
|
+
findings.push(`Edit the full ${field} annotation for ${nodeId ?? drawing.mapId}; compact text is incomplete`);
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
if (changed.some((change) => change.text !== selected.text && !selected.text.startsWith(change.text.replace(/…$/, "")))) {
|
|
67
|
+
findings.push(`Conflicting compact/full ${field} edits: ${nodeId ?? drawing.mapId}`);
|
|
68
|
+
}
|
|
69
|
+
return selected.text || null;
|
|
70
|
+
};
|
|
71
|
+
for (const field of [
|
|
72
|
+
"title",
|
|
73
|
+
"goal",
|
|
74
|
+
"summary",
|
|
75
|
+
"description",
|
|
76
|
+
]) {
|
|
77
|
+
const value = read("map", field);
|
|
78
|
+
if (value === undefined) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (field === "goal" || field === "title") {
|
|
82
|
+
if (!value) {
|
|
83
|
+
findings.push("Map goal cannot be empty");
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
metadataChanges[field] = value;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
metadataChanges[field] = value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const node of drawing.nodes) {
|
|
94
|
+
const change = {
|
|
95
|
+
nodeId: node.id,
|
|
96
|
+
};
|
|
97
|
+
for (const field of [
|
|
98
|
+
"label",
|
|
99
|
+
"summary",
|
|
100
|
+
"description",
|
|
101
|
+
]) {
|
|
102
|
+
let value = read("node", field, node.id);
|
|
103
|
+
if (value === undefined) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (field === "label") {
|
|
107
|
+
if (!value) {
|
|
108
|
+
findings.push(`Node title cannot be empty: ${node.id}`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (node.cardTitle === undefined) {
|
|
112
|
+
value =
|
|
113
|
+
node.kind === "decision"
|
|
114
|
+
? value.replace(/^Decision: /, "")
|
|
115
|
+
: node.kind === "detail-link"
|
|
116
|
+
? value.replace(/^Details: /, "")
|
|
117
|
+
: value;
|
|
118
|
+
}
|
|
119
|
+
change.label = value;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
change[field] = value;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const icon = observed.shapes.find((shape) => shape.role === "icon" && shape.logicalId === node.id);
|
|
126
|
+
const key = icon ? identifyMapIcon(icon) : undefined;
|
|
127
|
+
if (!key) {
|
|
128
|
+
findings.push(`Unrecognized or missing native icon: ${node.id}`);
|
|
129
|
+
}
|
|
130
|
+
else if (icon?.iconKey &&
|
|
131
|
+
icon.iconKey !== key &&
|
|
132
|
+
icon.iconKey !== (node.icon ?? defaultMapIcon(node.kind))) {
|
|
133
|
+
findings.push(`Icon metadata and glyph disagree: ${node.id}`);
|
|
134
|
+
}
|
|
135
|
+
else if (key !== (node.icon ?? defaultMapIcon(node.kind))) {
|
|
136
|
+
change.icon = key;
|
|
137
|
+
}
|
|
138
|
+
if (Object.keys(change).length > 1) {
|
|
139
|
+
changes.push(change);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
changes,
|
|
144
|
+
...(Object.keys(metadataChanges).length
|
|
145
|
+
? {
|
|
146
|
+
metadataChanges,
|
|
147
|
+
}
|
|
148
|
+
: {}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { MapContract, MapProposal } from "../maps/types.js";
|
|
2
|
+
import type { MapDrawing, NativeMapProjection, NativeMapProposal } from "./types.js";
|
|
3
|
+
export declare function importMapProposal(drawing: MapDrawing, observed: NativeMapProjection, baseline?: NativeMapProjection): NativeMapProposal;
|
|
4
|
+
/** Convert a native observation into the canonical reviewed proposal format. */
|
|
5
|
+
export declare function nativeProposalToMapProposal(map: MapContract, drawing: MapDrawing, observed: NativeMapProjection, baseline?: NativeMapProjection): MapProposal;
|
|
@@ -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;
|