@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.
Files changed (142) hide show
  1. package/dist/adapters.d.ts +1 -1
  2. package/dist/adapters.js +4 -0
  3. package/dist/approvals.d.ts +4 -1
  4. package/dist/approvals.js +7 -4
  5. package/dist/baseline-coverage.js +3 -3
  6. package/dist/baselines-config.d.ts +3 -0
  7. package/dist/baselines-config.js +3 -0
  8. package/dist/baselines.d.ts +7 -0
  9. package/dist/baselines.js +90 -21
  10. package/dist/bounded-file.d.ts +2 -0
  11. package/dist/bounded-file.js +38 -0
  12. package/dist/browser-wake.d.ts +22 -0
  13. package/dist/browser-wake.js +241 -0
  14. package/dist/cli-main.js +193 -18
  15. package/dist/design-commands/context.d.ts +9 -0
  16. package/dist/design-commands/context.js +1 -0
  17. package/dist/design-commands/index.d.ts +3 -0
  18. package/dist/design-commands/index.js +7 -0
  19. package/dist/design-commands/map-command.d.ts +4 -0
  20. package/dist/design-commands/map-command.js +685 -0
  21. package/dist/design-commands/release-command.d.ts +3 -0
  22. package/dist/design-commands/release-command.js +239 -0
  23. package/dist/design-releases/artifact-paths.d.ts +6 -0
  24. package/dist/design-releases/artifact-paths.js +82 -0
  25. package/dist/design-releases/assemble.d.ts +3 -0
  26. package/dist/design-releases/assemble.js +176 -0
  27. package/dist/design-releases/check.d.ts +16 -0
  28. package/dist/design-releases/check.js +323 -0
  29. package/dist/design-releases/context.d.ts +2 -0
  30. package/dist/design-releases/context.js +119 -0
  31. package/dist/design-releases/contract-graph.d.ts +13 -0
  32. package/dist/design-releases/contract-graph.js +75 -0
  33. package/dist/design-releases/fidelity.d.ts +23 -0
  34. package/dist/design-releases/fidelity.js +328 -0
  35. package/dist/design-releases/finalize.d.ts +4 -0
  36. package/dist/design-releases/finalize.js +201 -0
  37. package/dist/design-releases/index.d.ts +8 -0
  38. package/dist/design-releases/index.js +7 -0
  39. package/dist/design-releases/live.d.ts +22 -0
  40. package/dist/design-releases/live.js +97 -0
  41. package/dist/design-releases/snippets.d.ts +5 -0
  42. package/dist/design-releases/snippets.js +89 -0
  43. package/dist/design-releases/store.d.ts +10 -0
  44. package/dist/design-releases/store.js +205 -0
  45. package/dist/design-releases/types.d.ts +161 -0
  46. package/dist/design-releases/types.js +1 -0
  47. package/dist/design-releases/validation.d.ts +6 -0
  48. package/dist/design-releases/validation.js +384 -0
  49. package/dist/design-releases/verify.d.ts +3 -0
  50. package/dist/design-releases/verify.js +148 -0
  51. package/dist/errors.js +1 -1
  52. package/dist/image-codec.d.ts +3 -0
  53. package/dist/image-codec.js +78 -0
  54. package/dist/image-evidence.d.ts +15 -0
  55. package/dist/image-evidence.js +121 -0
  56. package/dist/index.d.ts +5 -0
  57. package/dist/index.js +5 -0
  58. package/dist/inspection.js +13 -5
  59. package/dist/instance/config-schema.d.ts +4 -0
  60. package/dist/instance/config-schema.js +7 -0
  61. package/dist/instance/connection.d.ts +2 -0
  62. package/dist/instance/connection.js +15 -3
  63. package/dist/kit-source.js +1 -1
  64. package/dist/layout-policy.js +21 -11
  65. package/dist/layout-settling.d.ts +1 -0
  66. package/dist/layout-settling.js +51 -17
  67. package/dist/layout.d.ts +7 -1
  68. package/dist/layout.js +19 -3
  69. package/dist/map-penpot/content-validation.d.ts +3 -0
  70. package/dist/map-penpot/content-validation.js +57 -0
  71. package/dist/map-penpot/content.d.ts +22 -0
  72. package/dist/map-penpot/content.js +173 -0
  73. package/dist/map-penpot/details.d.ts +4 -0
  74. package/dist/map-penpot/details.js +84 -0
  75. package/dist/map-penpot/evidence-error.d.ts +5 -0
  76. package/dist/map-penpot/evidence-error.js +9 -0
  77. package/dist/map-penpot/icons.d.ts +5 -0
  78. package/dist/map-penpot/icons.js +79 -0
  79. package/dist/map-penpot/import-content.d.ts +2 -0
  80. package/dist/map-penpot/import-content.js +150 -0
  81. package/dist/map-penpot/import.d.ts +5 -0
  82. package/dist/map-penpot/import.js +255 -0
  83. package/dist/map-penpot/index.d.ts +11 -0
  84. package/dist/map-penpot/index.js +10 -0
  85. package/dist/map-penpot/path-geometry.d.ts +5 -0
  86. package/dist/map-penpot/path-geometry.js +51 -0
  87. package/dist/map-penpot/presentation.d.ts +20 -0
  88. package/dist/map-penpot/presentation.js +205 -0
  89. package/dist/map-penpot/projection.d.ts +8 -0
  90. package/dist/map-penpot/projection.js +176 -0
  91. package/dist/map-penpot/publish.d.ts +4 -0
  92. package/dist/map-penpot/publish.js +301 -0
  93. package/dist/map-penpot/release-maps.d.ts +21 -0
  94. package/dist/map-penpot/release-maps.js +104 -0
  95. package/dist/map-penpot/snippets.d.ts +4 -0
  96. package/dist/map-penpot/snippets.js +164 -0
  97. package/dist/map-penpot/status-labels.d.ts +7 -0
  98. package/dist/map-penpot/status-labels.js +7 -0
  99. package/dist/map-penpot/types.d.ts +161 -0
  100. package/dist/map-penpot/types.js +1 -0
  101. package/dist/maps/annotation-layout.d.ts +24 -0
  102. package/dist/maps/annotation-layout.js +85 -0
  103. package/dist/maps/context.d.ts +13 -0
  104. package/dist/maps/context.js +207 -0
  105. package/dist/maps/index.d.ts +9 -0
  106. package/dist/maps/index.js +9 -0
  107. package/dist/maps/layout.d.ts +35 -0
  108. package/dist/maps/layout.js +364 -0
  109. package/dist/maps/proposal.d.ts +5 -0
  110. package/dist/maps/proposal.js +67 -0
  111. package/dist/maps/render.d.ts +4 -0
  112. package/dist/maps/render.js +331 -0
  113. package/dist/maps/review.d.ts +16 -0
  114. package/dist/maps/review.js +90 -0
  115. package/dist/maps/store.d.ts +21 -0
  116. package/dist/maps/store.js +222 -0
  117. package/dist/maps/types.d.ts +206 -0
  118. package/dist/maps/types.js +1 -0
  119. package/dist/maps/validation.d.ts +10 -0
  120. package/dist/maps/validation.js +375 -0
  121. package/dist/plugin-health.d.ts +13 -0
  122. package/dist/plugin-health.js +39 -0
  123. package/dist/skill-routing.js +4 -0
  124. package/dist/version.d.ts +1 -1
  125. package/dist/version.js +1 -1
  126. package/package.json +2 -1
  127. package/skills/penpair-design-release/SKILL.md +55 -0
  128. package/skills/penpair-design-release/agents/openai.yaml +6 -0
  129. package/skills/penpair-design-release/references/workflow.md +55 -0
  130. package/skills/penpair-diagrams/SKILL.md +52 -0
  131. package/skills/penpair-diagrams/agents/openai.yaml +6 -0
  132. package/skills/penpair-diagrams/references/workflow.md +84 -0
  133. package/skills/penpair-handoff/SKILL.md +7 -0
  134. package/skills/penpair-handoff/references/handoff-flow.md +7 -1
  135. package/skills/penpair-integration-map/SKILL.md +44 -0
  136. package/skills/penpair-integration-map/agents/openai.yaml +6 -0
  137. package/skills/penpair-integration-map/references/workflow.md +33 -0
  138. package/skills/penpair-journey-map/SKILL.md +42 -0
  139. package/skills/penpair-journey-map/agents/openai.yaml +6 -0
  140. package/skills/penpair-journey-map/references/workflow.md +35 -0
  141. package/skills/penpair-verify/SKILL.md +7 -0
  142. package/skills/penpair-verify/references/browser-evidence.md +10 -0
@@ -0,0 +1,176 @@
1
+ import { nativeMapIcons } from "./icons.js";
2
+ import { validateMapDrawingContent } from "./content-validation.js";
3
+ import { nativeMapRoles } from "./content.js";
4
+ import { observationHash } from "../journal.js";
5
+ import { selectPage, withPageSession } from "../page-session.js";
6
+ import { runSnippet } from "../snippet.js";
7
+ import { inspectMapSnippet } from "./snippets.js";
8
+ export function drawingFromPlan(plan) {
9
+ if (!plan.complete) {
10
+ throw new Error(`Map requires smaller detail boards: ${plan.reasons.join("; ")}`);
11
+ }
12
+ return {
13
+ mapId: `${plan.mapKind}:${plan.mapId}`,
14
+ contractHash: plan.mapHash,
15
+ title: plan.title,
16
+ goal: plan.goal,
17
+ ...(plan.summary !== undefined
18
+ ? {
19
+ summary: plan.summary,
20
+ }
21
+ : {}),
22
+ ...(plan.description !== undefined
23
+ ? {
24
+ description: plan.description,
25
+ }
26
+ : {}),
27
+ ...(plan.headerSummary !== undefined
28
+ ? {
29
+ headerSummary: plan.headerSummary,
30
+ }
31
+ : {}),
32
+ header: plan.header,
33
+ annotations: plan.annotations,
34
+ fontFamily: plan.fontFamily,
35
+ nodes: plan.nodes,
36
+ edges: plan.edges,
37
+ };
38
+ }
39
+ export function validateDrawing(drawing) {
40
+ validateMapDrawingContent(drawing);
41
+ if (!drawing.mapId ||
42
+ !/^[a-f0-9]{64}$/.test(drawing.contractHash) ||
43
+ !drawing.title ||
44
+ drawing.title.length > 1000) {
45
+ throw new Error("Map identity, title and SHA-256 contract hash are required");
46
+ }
47
+ if (drawing.nodes.length > 1000 || drawing.edges.length > 2000) {
48
+ throw new Error("Map exceeds graph limits");
49
+ }
50
+ if (drawing.origin &&
51
+ ![
52
+ drawing.origin.x,
53
+ drawing.origin.y,
54
+ ].every(Number.isFinite)) {
55
+ throw new Error("Map origin must be finite");
56
+ }
57
+ const identifiers = new Set(drawing.nodes.map((node) => node.id));
58
+ if (identifiers.size !== drawing.nodes.length ||
59
+ drawing.nodes.some((node) => !node.id ||
60
+ (node.icon !== undefined && !Object.hasOwn(nativeMapIcons, node.icon)) ||
61
+ (node.summary !== undefined &&
62
+ (typeof node.summary !== "string" || node.summary.length > 240)) ||
63
+ (node.description !== undefined &&
64
+ (typeof node.description !== "string" || node.description.length > 4000)) ||
65
+ (node.detail !== undefined &&
66
+ (node.kind !== "detail-link" ||
67
+ !node.detail.fileId ||
68
+ !node.detail.pageId ||
69
+ !node.detail.frameId ||
70
+ !node.detail.path ||
71
+ !/^[a-f0-9]{64}$/.test(node.detail.sha256))) ||
72
+ (node.origin !== undefined &&
73
+ ![
74
+ "observed",
75
+ "proposed",
76
+ "owner-decision",
77
+ "unknown",
78
+ ].includes(node.origin)) ||
79
+ !node.label ||
80
+ node.label.length > 4000 ||
81
+ ![
82
+ node.x,
83
+ node.y,
84
+ node.width,
85
+ node.height,
86
+ ].every(Number.isFinite) ||
87
+ node.width < 64 ||
88
+ node.height < 32)) {
89
+ throw new Error("Map nodes need unique IDs, bounded labels and finite positive geometry");
90
+ }
91
+ if (new Set(drawing.edges.map((edge) => edge.id)).size !== drawing.edges.length ||
92
+ drawing.edges.some((edge) => !edge.id ||
93
+ !identifiers.has(edge.from) ||
94
+ !identifiers.has(edge.to) ||
95
+ typeof edge.label !== "string" ||
96
+ edge.label.length > 4000 ||
97
+ (edge.labelBox !== undefined &&
98
+ (![
99
+ edge.labelBox.x,
100
+ edge.labelBox.y,
101
+ edge.labelBox.width,
102
+ edge.labelBox.height,
103
+ ].every(Number.isFinite) ||
104
+ edge.labelBox.width <= 0 ||
105
+ edge.labelBox.height <= 0)) ||
106
+ (edge.points !== undefined &&
107
+ (!Array.isArray(edge.points) ||
108
+ edge.points.length < 2 ||
109
+ edge.points.length > 32 ||
110
+ edge.points.some((point) => ![
111
+ point.x,
112
+ point.y,
113
+ ].every(Number.isFinite)))))) {
114
+ throw new Error("Map edges need unique IDs and existing endpoints");
115
+ }
116
+ }
117
+ export function projection(target, mapId, result) {
118
+ if (!Array.isArray(result.shapes) ||
119
+ !Array.isArray(result.conflicts) ||
120
+ !(result.boardId === null || typeof result.boardId === "string") ||
121
+ !(result.boardBounds === null ||
122
+ (typeof result.boardBounds === "object" &&
123
+ result.boardBounds &&
124
+ [
125
+ "x",
126
+ "y",
127
+ "width",
128
+ "height",
129
+ ].every((key) => Number.isFinite(result.boardBounds[key])))) ||
130
+ !(result.contractHash === null || typeof result.contractHash === "string")) {
131
+ throw new Error("Native map inspection returned incomplete evidence");
132
+ }
133
+ if (result.shapes.some((shape) => !shape ||
134
+ typeof shape !== "object" ||
135
+ typeof shape.shapeId !== "string" ||
136
+ typeof shape.logicalId !== "string" ||
137
+ !nativeMapRoles.includes(shape.role) ||
138
+ ![
139
+ shape.x,
140
+ shape.y,
141
+ shape.width,
142
+ shape.height,
143
+ ].every(Number.isFinite) ||
144
+ !(shape.label === null || typeof shape.label === "string") ||
145
+ !(shape.content === null || typeof shape.content === "string")) ||
146
+ result.conflicts.some((conflict) => typeof conflict !== "string")) {
147
+ throw new Error("Native map inspection returned malformed shape evidence");
148
+ }
149
+ const observed = {
150
+ boardId: result.boardId,
151
+ boardBounds: result.boardBounds,
152
+ contractHash: result.contractHash,
153
+ shapes: result.shapes,
154
+ conflicts: result.conflicts,
155
+ };
156
+ return {
157
+ schemaVersion: 1,
158
+ kind: "native-map-projection",
159
+ ...target,
160
+ boardId: result.boardId,
161
+ boardBounds: result.boardBounds,
162
+ mapId,
163
+ contractHash: result.contractHash,
164
+ fingerprint: observationHash(observed),
165
+ shapes: result.shapes,
166
+ conflicts: result.conflicts,
167
+ };
168
+ }
169
+ export async function readProjection(client, target, mapId, options = {}) {
170
+ return withPageSession(client, async () => {
171
+ await selectPage(client, {
172
+ id: target.pageId,
173
+ }, options);
174
+ return projection(target, mapId, await runSnippet(client, inspectMapSnippet(target, mapId), options));
175
+ });
176
+ }
@@ -0,0 +1,4 @@
1
+ import type { PenpotToolClient } from "../client.js";
2
+ import type { MapDrawing, MapTarget, NativeMapProjection, PublishMapOptions, PublishMapResult } from "./types.js";
3
+ export declare function verificationProblems(drawing: MapDrawing, observed: NativeMapProjection): string[];
4
+ export declare function publishMap(client: PenpotToolClient, target: MapTarget, drawing: MapDrawing, options?: PublishMapOptions): Promise<PublishMapResult>;
@@ -0,0 +1,301 @@
1
+ import { nativeMapShapeKeys, nativeMapTextEntries } from "./content.js";
2
+ import { defaultMapIcon, identifyMapIcon } from "./icons.js";
3
+ import { mapOriginLabels, mapOriginLegend } from "./status-labels.js";
4
+ import { redactEndpoint } from "../endpoint.js";
5
+ import { MapExportEvidenceError } from "./evidence-error.js";
6
+ import { resolveMapDetails } from "./details.js";
7
+ import { exportShapePng } from "../export.js";
8
+ import { runOperation } from "../operation.js";
9
+ import { routeMapEdges, mapDrawingGeometryProblems } from "../maps/layout.js";
10
+ import { preserveMapPresentation } from "./presentation.js";
11
+ import { withPageSession } from "../page-session.js";
12
+ import { readMapPathPoints } from "./path-geometry.js";
13
+ import { inspectMapSnippet, publishMapSnippet } from "./snippets.js";
14
+ import { validateDrawing, projection, readProjection } from "./projection.js";
15
+ export function verificationProblems(drawing, observed) {
16
+ const problems = [
17
+ ...observed.conflicts,
18
+ ];
19
+ if (!observed.boardId || observed.contractHash !== drawing.contractHash) {
20
+ problems.push("Map board or contract identity differs");
21
+ }
22
+ const expectedKeys = nativeMapShapeKeys(drawing);
23
+ for (const shape of observed.shapes) {
24
+ if (!expectedKeys.delete(`${shape.role}:${shape.logicalId}`)) {
25
+ problems.push(`Unexpected shape ${shape.shapeId}`);
26
+ }
27
+ }
28
+ if (expectedKeys.size) {
29
+ problems.push("Managed shapes are missing");
30
+ }
31
+ if (drawing.nodes.some((node) => node.origin) &&
32
+ observed.shapes.find((shape) => shape.role === "legend")?.label !== mapOriginLegend) {
33
+ problems.push("Origin legend differs");
34
+ }
35
+ for (const shape of observed.shapes) {
36
+ const style = shape.appearance;
37
+ const radius = shape.role === "node" || shape.role === "annotation-panel"
38
+ ? 12
39
+ : shape.role === "icon-well"
40
+ ? 8
41
+ : undefined;
42
+ if (radius !== undefined &&
43
+ [
44
+ "borderRadiusTopLeft",
45
+ "borderRadiusTopRight",
46
+ "borderRadiusBottomLeft",
47
+ "borderRadiusBottomRight",
48
+ ].some((corner) => typeof style?.[corner] === "number" && style[corner] !== radius)) {
49
+ problems.push(`Native rounded corners differ: ${shape.role}:${shape.logicalId}`);
50
+ }
51
+ if ((typeof style?.opacity === "number" && style.opacity !== 1) ||
52
+ (typeof style?.blendMode === "string" && style.blendMode !== "normal")) {
53
+ problems.push(`Native decorative opacity/blend differs: ${shape.role}:${shape.logicalId}`);
54
+ }
55
+ }
56
+ const bounds = observed.boardBounds;
57
+ if (bounds) {
58
+ for (const shape of observed.shapes.filter((shape) => shape.role !== "edge")) {
59
+ if (shape.x < bounds.x ||
60
+ shape.y < bounds.y ||
61
+ shape.x + shape.width > bounds.x + bounds.width + 0.01 ||
62
+ shape.y + shape.height > bounds.y + bounds.height + 0.01) {
63
+ problems.push(`Map content exceeds board bounds: ${shape.logicalId}`);
64
+ }
65
+ }
66
+ }
67
+ for (const node of drawing.nodes) {
68
+ const label = observed.shapes.find((shape) => shape.role === "label" && shape.logicalId === node.id);
69
+ for (const clickable of observed.shapes.filter((shape) => shape.logicalId === node.id &&
70
+ [
71
+ "node",
72
+ "label",
73
+ "badge",
74
+ "summary",
75
+ "icon-well",
76
+ "icon",
77
+ "port-in",
78
+ "port-out",
79
+ ].includes(shape.role))) {
80
+ if (node.detail &&
81
+ (clickable.interactions?.length !== 1 ||
82
+ clickable.interactions[0]?.trigger !== "click" ||
83
+ clickable.interactions[0]?.type !== "navigate-to" ||
84
+ clickable.interactions[0]?.destinationId !== node.detail.frameId ||
85
+ clickable.interactions[0]?.destinationPageId !== node.detail.pageId ||
86
+ clickable.interactions[0]?.destinationMapHash !== node.detail.contractHash ||
87
+ clickable.interactions[0]?.destinationMapId !== node.detail.mapId)) {
88
+ problems.push(`Detail link destination differs or is unobservable: ${node.id}`);
89
+ }
90
+ if (!node.detail && clickable.interactions?.length) {
91
+ problems.push(`Unexpected native interaction: ${node.id}`);
92
+ }
93
+ }
94
+ if (node.origin) {
95
+ const status = mapOriginLabels[node.origin];
96
+ if (observed.shapes.find((shape) => shape.role === "badge" && shape.logicalId === node.id)
97
+ ?.label !== status) {
98
+ problems.push(`Origin badge differs: ${node.id}`);
99
+ }
100
+ }
101
+ const icon = observed.shapes.find((shape) => shape.role === "icon" && shape.logicalId === node.id);
102
+ if (!icon || identifyMapIcon(icon) !== (node.icon ?? defaultMapIcon(node.kind))) {
103
+ problems.push(`Native icon differs: ${node.id}`);
104
+ }
105
+ const card = observed.shapes.find((shape) => shape.role === "node" && shape.logicalId === node.id);
106
+ if (card &&
107
+ label &&
108
+ (label.x < card.x ||
109
+ label.y < card.y ||
110
+ label.x + label.width > card.x + card.width + 0.01 ||
111
+ label.y + label.height > card.y + card.height + 0.01)) {
112
+ problems.push(`Node label exceeds card bounds: ${node.id}`);
113
+ }
114
+ }
115
+ if (observed.shapes.find((shape) => shape.role === "title")?.label !== drawing.title) {
116
+ problems.push("Map title differs");
117
+ }
118
+ for (const entry of nativeMapTextEntries(drawing)) {
119
+ const actual = observed.shapes.find((shape) => shape.role === entry.role && shape.logicalId === entry.logicalId);
120
+ if (actual?.label !== entry.text) {
121
+ problems.push(`Native text differs: ${entry.role}:${entry.logicalId}`);
122
+ }
123
+ if (actual && actual.height > entry.height + 0.01) {
124
+ problems.push(`Native text exceeds reserved space: ${entry.role}:${entry.logicalId}`);
125
+ }
126
+ }
127
+ const title = observed.shapes.find((shape) => shape.role === "title");
128
+ const legend = observed.shapes.find((shape) => shape.role === "legend");
129
+ if (title &&
130
+ (title.height > (drawing.header?.titleBox.height ?? 48) ||
131
+ (legend && title.y + title.height > legend.y))) {
132
+ problems.push("Map title exceeds the reserved header space");
133
+ }
134
+ for (const edge of drawing.edges) {
135
+ const path = observed.shapes.find((shape) => shape.role === "edge" && shape.logicalId === edge.id);
136
+ if (path?.from !== edge.from || path.to !== edge.to) {
137
+ problems.push(`Relationship differs: ${edge.id}`);
138
+ }
139
+ const edgeLabel = observed.shapes.find((shape) => shape.role === "edge-label" && shape.logicalId === edge.id);
140
+ if (edge.label && edgeLabel?.label !== edge.label) {
141
+ problems.push(`Relationship label differs: ${edge.id}`);
142
+ }
143
+ if (edge.labelBox && edgeLabel && edgeLabel.height > edge.labelBox.height + 0.01) {
144
+ problems.push(`Relationship label exceeds reserved space: ${edge.id}`);
145
+ }
146
+ const from = observed.shapes.find((shape) => shape.role === "node" && shape.logicalId === edge.from);
147
+ const to = observed.shapes.find((shape) => shape.role === "node" && shape.logicalId === edge.to);
148
+ const vertices = readMapPathPoints(path?.content);
149
+ const start = vertices?.[0];
150
+ const end = vertices?.at(-1);
151
+ const close = (left, right) => left !== undefined && Number.isFinite(left) && Math.abs(left - right) < 0.01;
152
+ if (!from ||
153
+ !to ||
154
+ !vertices ||
155
+ !(close(start?.x, from.x + from.width) || close(start?.x, from.x)) ||
156
+ !close(start?.y, from.y + from.height / 2) ||
157
+ !((close(end?.x, to.x) && close(end?.y, to.y + to.height / 2)) ||
158
+ (edge.from === edge.to && close(end?.x, to.x + to.width / 2) && close(end?.y, to.y)))) {
159
+ problems.push(`Relationship native geometry differs: ${edge.id}`);
160
+ }
161
+ }
162
+ if (bounds) {
163
+ const nodes = observed.shapes
164
+ .filter((shape) => shape.role === "node")
165
+ .map((shape) => ({
166
+ id: shape.logicalId,
167
+ x: shape.x - bounds.x,
168
+ y: shape.y - bounds.y,
169
+ width: shape.width,
170
+ height: shape.height,
171
+ }));
172
+ const edges = drawing.edges.flatMap((edge) => {
173
+ const path = observed.shapes.find((shape) => shape.role === "edge" && shape.logicalId === edge.id);
174
+ const points = readMapPathPoints(path?.content);
175
+ const label = observed.shapes.find((shape) => shape.role === "edge-label" && shape.logicalId === edge.id);
176
+ return points && label
177
+ ? [
178
+ {
179
+ id: edge.id,
180
+ points: points.map((point) => ({
181
+ x: point.x - bounds.x,
182
+ y: point.y - bounds.y,
183
+ })),
184
+ labelBox: {
185
+ x: label.x - bounds.x,
186
+ y: label.y - bounds.y,
187
+ width: label.width,
188
+ height: label.height,
189
+ },
190
+ },
191
+ ]
192
+ : [];
193
+ });
194
+ problems.push(...mapDrawingGeometryProblems(nodes, edges));
195
+ }
196
+ return problems;
197
+ }
198
+ export async function publishMap(client, target, drawing, options = {}) {
199
+ const configuredRedact = options.redact ?? options.journal?.redact;
200
+ const redact = (text) => redactEndpoint(configuredRedact ? configuredRedact(text) : text);
201
+ const requireProtectedValuesAbsent = (value) => {
202
+ const encoded = JSON.stringify(value);
203
+ if (redact(encoded) !== encoded) {
204
+ throw new Error("Native map contains protected values; publication refused");
205
+ }
206
+ };
207
+ requireProtectedValuesAbsent(drawing);
208
+ validateDrawing(drawing);
209
+ if (drawing.nodes.some((node) => node.detail)) {
210
+ if (!options.journal?.root) {
211
+ throw new Error("Pinned detail publication needs its project root");
212
+ }
213
+ drawing = await resolveMapDetails(options.journal.root, drawing);
214
+ }
215
+ drawing = {
216
+ ...drawing,
217
+ edges: drawing.edges.every((edge) => edge.points && edge.labelBox)
218
+ ? drawing.edges
219
+ : routeMapEdges(drawing.nodes, drawing.edges),
220
+ };
221
+ requireProtectedValuesAbsent(drawing);
222
+ const geometryProblems = mapDrawingGeometryProblems(drawing.nodes, drawing.edges);
223
+ if (geometryProblems.length) {
224
+ throw new Error(`Map layout requires review: ${geometryProblems.join("; ")}`);
225
+ }
226
+ if (options.apply && !options.journal) {
227
+ throw new Error("Map publication requires a durable operation journal");
228
+ }
229
+ return withPageSession(client, async () => {
230
+ const before = await readProjection(client, target, drawing.mapId, options);
231
+ requireProtectedValuesAbsent(before);
232
+ if (before.conflicts.length) {
233
+ throw new Error(before.conflicts.join("; "));
234
+ }
235
+ if (options.apply &&
236
+ before.boardId &&
237
+ !options.journal?.resume &&
238
+ (!options.expected ||
239
+ options.expected.fingerprint !== before.fingerprint ||
240
+ options.expected.mapId !== drawing.mapId ||
241
+ options.expected.fileId !== target.fileId ||
242
+ options.expected.pageId !== target.pageId)) {
243
+ throw new Error("Existing map requires matching prior projection; import and reconcile edits first");
244
+ }
245
+ // Build the full write during preview too; size limits fail before a journal or remote mutation.
246
+ publishMapSnippet(target, before.boardId ? preserveMapPresentation(drawing, before) : drawing, before);
247
+ const operation = {
248
+ name: "publish-map",
249
+ page: () => undefined,
250
+ inspect: () => inspectMapSnippet(target, drawing.mapId),
251
+ apply: (_drawing, inspected) => {
252
+ const observed = inspected
253
+ ? projection(target, drawing.mapId, inspected)
254
+ : {
255
+ ...before,
256
+ boardId: null,
257
+ boardBounds: null,
258
+ contractHash: null,
259
+ shapes: [],
260
+ conflicts: [],
261
+ };
262
+ const projectedDrawing = observed.boardId
263
+ ? preserveMapPresentation(drawing, observed)
264
+ : drawing;
265
+ requireProtectedValuesAbsent(projectedDrawing);
266
+ requireProtectedValuesAbsent(observed);
267
+ return publishMapSnippet(target, projectedDrawing, observed);
268
+ },
269
+ readback: () => inspectMapSnippet(target, drawing.mapId),
270
+ verify: ({ readback }) => verificationProblems(drawing, projection(target, drawing.mapId, readback)),
271
+ };
272
+ const report = await runOperation(client, operation, drawing, options);
273
+ const observed = report.readback ? projection(target, drawing.mapId, report.readback) : before;
274
+ let image;
275
+ if (options.apply && report.verified && options.export !== false && observed.boardId) {
276
+ try {
277
+ image = await exportShapePng(client, observed.boardId, {
278
+ ...options,
279
+ stable: true,
280
+ });
281
+ }
282
+ catch (error) {
283
+ const detail = redact(error instanceof Error ? error.message : String(error));
284
+ if (report.journal?.runId) {
285
+ throw new MapExportEvidenceError(report.journal.runId, detail);
286
+ }
287
+ throw error;
288
+ }
289
+ }
290
+ return {
291
+ report,
292
+ projection: observed,
293
+ ...(image
294
+ ? {
295
+ image,
296
+ }
297
+ : {}),
298
+ visualReviewRequired: Boolean(options.apply),
299
+ };
300
+ });
301
+ }
@@ -0,0 +1,21 @@
1
+ import type { PenpotToolClient } from "../client.js";
2
+ import type { MapContract } from "../maps/types.js";
3
+ import type { MapTarget, PublishMapOptions, PublishMapResult } from "./types.js";
4
+ export interface ReleaseMapPublication {
5
+ mapKind: MapContract["kind"];
6
+ mapId: string;
7
+ contractHash: string;
8
+ publication: PublishMapResult;
9
+ }
10
+ /** Each contract has its own recoverable journal; an interrupted batch is never replayed blindly. */
11
+ export declare function assembleReleaseMaps(client: PenpotToolClient, root: string, target: MapTarget, contracts: MapContract[], options?: Omit<PublishMapOptions, "journal" | "expected"> & {
12
+ origin?: {
13
+ x: number;
14
+ y: number;
15
+ };
16
+ journals?: Record<string, {
17
+ runId: string;
18
+ resume?: boolean;
19
+ }>;
20
+ projections?: Record<string, NonNullable<PublishMapOptions["expected"]>>;
21
+ }): Promise<ReleaseMapPublication[]>;
@@ -0,0 +1,104 @@
1
+ import { observeMapSources } from "../maps/store.js";
2
+ import { buildMapDrawingPlan } from "../maps/render.js";
3
+ import { resolveMapDetails } from "./details.js";
4
+ import { drawingFromPlan } from "./projection.js";
5
+ import { publishMap } from "./publish.js";
6
+ /** Each contract has its own recoverable journal; an interrupted batch is never replayed blindly. */
7
+ export async function assembleReleaseMaps(client, root, target, contracts, options = {}) {
8
+ if (contracts.length > 100 ||
9
+ new Set(contracts.map((contract) => `${contract.kind}:${contract.id}`)).size !==
10
+ contracts.length) {
11
+ throw new Error("Release maps require at most 100 unique contract identities");
12
+ }
13
+ const prepared = new Map();
14
+ for (const contract of contracts) {
15
+ const observation = await observeMapSources(root, contract);
16
+ const plan = buildMapDrawingPlan(contract, observation.detailMaps);
17
+ const drawing = await resolveMapDetails(root, drawingFromPlan(plan));
18
+ prepared.set(drawing.mapId, {
19
+ contract,
20
+ drawing,
21
+ width: plan.width,
22
+ });
23
+ }
24
+ const ordered = [];
25
+ const visiting = new Set();
26
+ const completed = new Set();
27
+ const visit = (id) => {
28
+ if (visiting.has(id)) {
29
+ throw new Error("Release detail-map dependencies contain a cycle");
30
+ }
31
+ if (completed.has(id)) {
32
+ return;
33
+ }
34
+ const current = prepared.get(id);
35
+ visiting.add(id);
36
+ for (const node of current.drawing.nodes.filter((node) => node.detail)) {
37
+ const detail = node.detail;
38
+ const child = prepared.get(detail.mapId);
39
+ if (!child || child.drawing.contractHash !== detail.contractHash) {
40
+ throw new Error("Every detail contract must be explicitly included at its pinned revision in the release");
41
+ }
42
+ visit(child.drawing.mapId);
43
+ }
44
+ visiting.delete(id);
45
+ completed.add(id);
46
+ ordered.push(id);
47
+ };
48
+ for (const id of prepared.keys()) {
49
+ visit(id);
50
+ }
51
+ const results = [];
52
+ const published = new Map();
53
+ let nextX = options.origin?.x ?? 0;
54
+ for (const id of ordered) {
55
+ const entry = prepared.get(id);
56
+ const contract = entry.contract;
57
+ const drawing = {
58
+ ...entry.drawing,
59
+ origin: {
60
+ x: nextX,
61
+ y: options.origin?.y ?? 0,
62
+ },
63
+ nodes: entry.drawing.nodes.map((node) => {
64
+ if (!node.detail) {
65
+ return node;
66
+ }
67
+ const child = published.get(node.detail.mapId);
68
+ if (options.apply && !child?.projection.boardId) {
69
+ throw new Error("Release child map has no verified native target");
70
+ }
71
+ return child?.projection.boardId
72
+ ? {
73
+ ...node,
74
+ detail: {
75
+ ...node.detail,
76
+ fileId: target.fileId,
77
+ pageId: target.pageId,
78
+ frameId: child.projection.boardId,
79
+ },
80
+ }
81
+ : node;
82
+ }),
83
+ };
84
+ const recovery = options.journals?.[drawing.mapId];
85
+ const publication = await publishMap(client, target, drawing, {
86
+ ...options,
87
+ expected: options.projections?.[drawing.mapId],
88
+ journal: {
89
+ root,
90
+ redact: options.redact,
91
+ ...recovery,
92
+ },
93
+ });
94
+ nextX += Math.max(entry.width, publication.projection.boardBounds?.width ?? 964) + 160;
95
+ published.set(drawing.mapId, publication);
96
+ results.push({
97
+ mapKind: contract.kind,
98
+ mapId: contract.id,
99
+ contractHash: drawing.contractHash,
100
+ publication,
101
+ });
102
+ }
103
+ return results;
104
+ }
@@ -0,0 +1,4 @@
1
+ import { type Snippet } from "../snippet.js";
2
+ import type { MapDrawing, MapTarget, NativeMapProjection } from "./types.js";
3
+ export declare function inspectMapSnippet(target: MapTarget, mapId: string): Snippet;
4
+ export declare function publishMapSnippet(target: MapTarget, drawing: MapDrawing, expected: NativeMapProjection): Snippet;