@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.
Files changed (129) hide show
  1. package/dist/adapters.d.ts +1 -1
  2. package/dist/adapters.js +3 -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/cli-main.js +64 -3
  13. package/dist/design-commands/context.d.ts +9 -0
  14. package/dist/design-commands/context.js +1 -0
  15. package/dist/design-commands/index.d.ts +3 -0
  16. package/dist/design-commands/index.js +7 -0
  17. package/dist/design-commands/map-command.d.ts +4 -0
  18. package/dist/design-commands/map-command.js +685 -0
  19. package/dist/design-commands/release-command.d.ts +3 -0
  20. package/dist/design-commands/release-command.js +239 -0
  21. package/dist/design-releases/artifact-paths.d.ts +6 -0
  22. package/dist/design-releases/artifact-paths.js +82 -0
  23. package/dist/design-releases/assemble.d.ts +3 -0
  24. package/dist/design-releases/assemble.js +176 -0
  25. package/dist/design-releases/check.d.ts +16 -0
  26. package/dist/design-releases/check.js +323 -0
  27. package/dist/design-releases/context.d.ts +2 -0
  28. package/dist/design-releases/context.js +119 -0
  29. package/dist/design-releases/contract-graph.d.ts +13 -0
  30. package/dist/design-releases/contract-graph.js +75 -0
  31. package/dist/design-releases/fidelity.d.ts +23 -0
  32. package/dist/design-releases/fidelity.js +328 -0
  33. package/dist/design-releases/finalize.d.ts +4 -0
  34. package/dist/design-releases/finalize.js +201 -0
  35. package/dist/design-releases/index.d.ts +8 -0
  36. package/dist/design-releases/index.js +7 -0
  37. package/dist/design-releases/live.d.ts +22 -0
  38. package/dist/design-releases/live.js +97 -0
  39. package/dist/design-releases/snippets.d.ts +5 -0
  40. package/dist/design-releases/snippets.js +89 -0
  41. package/dist/design-releases/store.d.ts +10 -0
  42. package/dist/design-releases/store.js +205 -0
  43. package/dist/design-releases/types.d.ts +161 -0
  44. package/dist/design-releases/types.js +1 -0
  45. package/dist/design-releases/validation.d.ts +6 -0
  46. package/dist/design-releases/validation.js +384 -0
  47. package/dist/design-releases/verify.d.ts +3 -0
  48. package/dist/design-releases/verify.js +148 -0
  49. package/dist/image-codec.d.ts +3 -0
  50. package/dist/image-codec.js +78 -0
  51. package/dist/image-evidence.d.ts +15 -0
  52. package/dist/image-evidence.js +121 -0
  53. package/dist/index.d.ts +5 -0
  54. package/dist/index.js +5 -0
  55. package/dist/inspection.js +13 -5
  56. package/dist/instance/config-schema.d.ts +4 -0
  57. package/dist/instance/config-schema.js +7 -0
  58. package/dist/map-penpot/content-validation.d.ts +3 -0
  59. package/dist/map-penpot/content-validation.js +57 -0
  60. package/dist/map-penpot/content.d.ts +22 -0
  61. package/dist/map-penpot/content.js +173 -0
  62. package/dist/map-penpot/details.d.ts +4 -0
  63. package/dist/map-penpot/details.js +84 -0
  64. package/dist/map-penpot/evidence-error.d.ts +5 -0
  65. package/dist/map-penpot/evidence-error.js +9 -0
  66. package/dist/map-penpot/icons.d.ts +5 -0
  67. package/dist/map-penpot/icons.js +79 -0
  68. package/dist/map-penpot/import-content.d.ts +2 -0
  69. package/dist/map-penpot/import-content.js +150 -0
  70. package/dist/map-penpot/import.d.ts +5 -0
  71. package/dist/map-penpot/import.js +255 -0
  72. package/dist/map-penpot/index.d.ts +11 -0
  73. package/dist/map-penpot/index.js +10 -0
  74. package/dist/map-penpot/path-geometry.d.ts +5 -0
  75. package/dist/map-penpot/path-geometry.js +51 -0
  76. package/dist/map-penpot/presentation.d.ts +20 -0
  77. package/dist/map-penpot/presentation.js +205 -0
  78. package/dist/map-penpot/projection.d.ts +8 -0
  79. package/dist/map-penpot/projection.js +176 -0
  80. package/dist/map-penpot/publish.d.ts +4 -0
  81. package/dist/map-penpot/publish.js +301 -0
  82. package/dist/map-penpot/release-maps.d.ts +21 -0
  83. package/dist/map-penpot/release-maps.js +104 -0
  84. package/dist/map-penpot/snippets.d.ts +4 -0
  85. package/dist/map-penpot/snippets.js +164 -0
  86. package/dist/map-penpot/status-labels.d.ts +7 -0
  87. package/dist/map-penpot/status-labels.js +7 -0
  88. package/dist/map-penpot/types.d.ts +161 -0
  89. package/dist/map-penpot/types.js +1 -0
  90. package/dist/maps/annotation-layout.d.ts +24 -0
  91. package/dist/maps/annotation-layout.js +85 -0
  92. package/dist/maps/context.d.ts +13 -0
  93. package/dist/maps/context.js +207 -0
  94. package/dist/maps/index.d.ts +9 -0
  95. package/dist/maps/index.js +9 -0
  96. package/dist/maps/layout.d.ts +35 -0
  97. package/dist/maps/layout.js +364 -0
  98. package/dist/maps/proposal.d.ts +5 -0
  99. package/dist/maps/proposal.js +67 -0
  100. package/dist/maps/render.d.ts +4 -0
  101. package/dist/maps/render.js +331 -0
  102. package/dist/maps/review.d.ts +16 -0
  103. package/dist/maps/review.js +90 -0
  104. package/dist/maps/store.d.ts +21 -0
  105. package/dist/maps/store.js +222 -0
  106. package/dist/maps/types.d.ts +206 -0
  107. package/dist/maps/types.js +1 -0
  108. package/dist/maps/validation.d.ts +10 -0
  109. package/dist/maps/validation.js +375 -0
  110. package/dist/skill-routing.js +4 -0
  111. package/dist/version.d.ts +1 -1
  112. package/dist/version.js +1 -1
  113. package/package.json +2 -1
  114. package/skills/penpair-design-release/SKILL.md +55 -0
  115. package/skills/penpair-design-release/agents/openai.yaml +6 -0
  116. package/skills/penpair-design-release/references/workflow.md +55 -0
  117. package/skills/penpair-diagrams/SKILL.md +52 -0
  118. package/skills/penpair-diagrams/agents/openai.yaml +6 -0
  119. package/skills/penpair-diagrams/references/workflow.md +84 -0
  120. package/skills/penpair-handoff/SKILL.md +7 -0
  121. package/skills/penpair-handoff/references/handoff-flow.md +7 -1
  122. package/skills/penpair-integration-map/SKILL.md +44 -0
  123. package/skills/penpair-integration-map/agents/openai.yaml +6 -0
  124. package/skills/penpair-integration-map/references/workflow.md +33 -0
  125. package/skills/penpair-journey-map/SKILL.md +42 -0
  126. package/skills/penpair-journey-map/agents/openai.yaml +6 -0
  127. package/skills/penpair-journey-map/references/workflow.md +35 -0
  128. package/skills/penpair-verify/SKILL.md +7 -0
  129. package/skills/penpair-verify/references/browser-evidence.md +10 -0
@@ -0,0 +1,121 @@
1
+ import { open } from "node:fs/promises";
2
+ import { resolveProjectInput } from "./project-input.js";
3
+ export const maximumImageBytes = 64 * 1024 * 1024;
4
+ function losslessWebpSize(bytes) {
5
+ const malformed = () => new Error("Malformed static lossless WebP evidence");
6
+ if (bytes.readUInt32LE(4) + 8 !== bytes.length) {
7
+ throw malformed();
8
+ }
9
+ let imageSize;
10
+ let canvasSize;
11
+ let offset = 12;
12
+ while (offset < bytes.length) {
13
+ if (offset + 8 > bytes.length) {
14
+ throw malformed();
15
+ }
16
+ const chunk = bytes.subarray(offset, offset + 4).toString("latin1");
17
+ const size = bytes.readUInt32LE(offset + 4);
18
+ const payload = offset + 8;
19
+ const end = payload + size;
20
+ const paddedEnd = end + (size % 2);
21
+ if (paddedEnd > bytes.length || (size % 2 !== 0 && bytes[end] !== 0)) {
22
+ throw malformed();
23
+ }
24
+ if (chunk === "VP8 " || chunk === "ANIM" || chunk === "ANMF" || chunk === "ALPH") {
25
+ throw new Error("Provider evidence must reference PNG or static lossless WebP bytes");
26
+ }
27
+ if (chunk === "VP8X") {
28
+ if (offset !== 12 ||
29
+ canvasSize ||
30
+ size !== 10 ||
31
+ (bytes[payload] & 0xc3) !== 0 ||
32
+ bytes.readUIntLE(payload + 1, 3) !== 0) {
33
+ throw malformed();
34
+ }
35
+ canvasSize = {
36
+ width: bytes.readUIntLE(payload + 4, 3) + 1,
37
+ height: bytes.readUIntLE(payload + 7, 3) + 1,
38
+ };
39
+ }
40
+ else if (chunk === "VP8L") {
41
+ if (imageSize || size <= 5 || bytes[payload] !== 0x2f) {
42
+ throw malformed();
43
+ }
44
+ const dimensions = bytes.readUInt32LE(payload + 1);
45
+ if (dimensions >>> 29 !== 0) {
46
+ throw malformed();
47
+ }
48
+ imageSize = {
49
+ width: (dimensions & 0x3fff) + 1,
50
+ height: ((dimensions >>> 14) & 0x3fff) + 1,
51
+ };
52
+ }
53
+ else if (offset === 12) {
54
+ throw malformed();
55
+ }
56
+ offset = paddedEnd;
57
+ }
58
+ if (!imageSize ||
59
+ (canvasSize && (canvasSize.width !== imageSize.width || canvasSize.height !== imageSize.height))) {
60
+ throw malformed();
61
+ }
62
+ return imageSize;
63
+ }
64
+ /** Header/container assurance only; this does not decode compressed pixels. */
65
+ export function inspectImageEvidence(bytes) {
66
+ if (bytes.length > maximumImageBytes) {
67
+ throw new Error("Image exceeds 64 MiB");
68
+ }
69
+ if (bytes.length >= 24 &&
70
+ bytes.subarray(0, 8).toString("hex") === "89504e470d0a1a0a" &&
71
+ bytes.subarray(12, 16).toString("latin1") === "IHDR") {
72
+ const width = bytes.readUInt32BE(16);
73
+ const height = bytes.readUInt32BE(20);
74
+ if (width > 0 && height > 0) {
75
+ return {
76
+ format: "png",
77
+ width,
78
+ height,
79
+ };
80
+ }
81
+ }
82
+ else if (bytes.length >= 12 &&
83
+ bytes.subarray(0, 4).toString("latin1") === "RIFF" &&
84
+ bytes.subarray(8, 12).toString("latin1") === "WEBP") {
85
+ return {
86
+ format: "webp",
87
+ ...losslessWebpSize(bytes),
88
+ };
89
+ }
90
+ throw new Error("Provider evidence must reference PNG or static lossless WebP bytes");
91
+ }
92
+ export async function readImageEvidence(root, file) {
93
+ const input = await resolveProjectInput(root, file);
94
+ const handle = await open(input, "r");
95
+ try {
96
+ const info = await handle.stat();
97
+ if (!info.isFile() || info.size > maximumImageBytes) {
98
+ throw new Error("Image exceeds 64 MiB or is not a regular file");
99
+ }
100
+ const buffer = Buffer.alloc(info.size + 1);
101
+ let length = 0;
102
+ while (length < buffer.length) {
103
+ const { bytesRead } = await handle.read(buffer, length, buffer.length - length, null);
104
+ if (bytesRead === 0) {
105
+ break;
106
+ }
107
+ length += bytesRead;
108
+ }
109
+ if (length > info.size) {
110
+ throw new Error("Image changed during bounded read");
111
+ }
112
+ const bytes = buffer.subarray(0, length);
113
+ return {
114
+ bytes,
115
+ ...inspectImageEvidence(bytes),
116
+ };
117
+ }
118
+ finally {
119
+ await handle.close();
120
+ }
121
+ }
package/dist/index.d.ts CHANGED
@@ -56,3 +56,8 @@ export * from "./project-status.js";
56
56
  export * from "./audit-rules.js";
57
57
  export * from "./credentials.js";
58
58
  export * from "./token-projection.js";
59
+ export * from "./image-evidence.js";
60
+ export * from "./image-codec.js";
61
+ export * from "./maps/index.js";
62
+ export * from "./map-penpot/index.js";
63
+ export * from "./design-releases/index.js";
package/dist/index.js CHANGED
@@ -55,3 +55,8 @@ export * from "./project-status.js";
55
55
  export * from "./audit-rules.js";
56
56
  export * from "./credentials.js";
57
57
  export * from "./token-projection.js";
58
+ export * from "./image-evidence.js";
59
+ export * from "./image-codec.js";
60
+ export * from "./maps/index.js";
61
+ export * from "./map-penpot/index.js";
62
+ export * from "./design-releases/index.js";
@@ -79,6 +79,7 @@ const clone = (value, unknowns, key) => {
79
79
  } catch { unknowns.push(key); return null; }
80
80
  };
81
81
  const read = (object,key,unknowns,label=key) => { try { return clone(object[key],unknowns,label); } catch { unknowns.push(label); return null; } };
82
+ const optional = (object,key,unknowns,label=key) => { try { const value=object[key]; return value===undefined?null:clone(value,unknowns,label); } catch {unknowns.push(label);return null;} };
82
83
  const pick = (object,keys,unknowns,prefix) => Object.fromEntries(keys.map(key=>[key,read(object,key,unknowns,prefix+"."+key)]));
83
84
  let estimatedBytes = 1024;
84
85
  if (CFG.kind === "shape") {
@@ -97,11 +98,11 @@ if (CFG.kind === "shape") {
97
98
  if (item.depth <= CFG.depth) {
98
99
  const unknowns = [], row = {id:s.id,parentId:item.parentId,index:item.index,depth:item.depth,type:read(s,"type",unknowns),name:read(s,"name",unknowns),childIds:children.slice(0,CFG.maxNodes).map(c=>c.id),childrenTotal:children.length,unknowns};
99
100
  if (children.length > CFG.maxNodes) reasons.add("node-budget");
100
- if (CFG.fields.includes("geometry")) row.geometry = pick(s,["x","y","width","height","rotation","hidden","blocked"],unknowns,"geometry");
101
- if (CFG.fields.includes("text")) row.text = s.type === "text" ? {characters:read(s,"characters",unknowns),typography:pick(s,["fontId","fontFamily","fontVariantId","fontSize","fontWeight","fontStyle","lineHeight","letterSpacing","growType","direction","align","verticalAlign"],unknowns,"typography")} : null;
101
+ if (CFG.fields.includes("geometry")) row.geometry = pick(s,["x","y","width","height","rotation","hidden","blocked","flipX","flipY","proportionLock"],unknowns,"geometry");
102
+ if (CFG.fields.includes("text")) row.text = s.type === "text" ? {characters:read(s,"characters",unknowns),typography:pick(s,["fontId","fontFamily","fontVariantId","fontSize","fontWeight","fontStyle","lineHeight","letterSpacing","growType","direction","align","verticalAlign","textTransform","textDecoration"],unknowns,"typography")} : null;
102
103
  if (row.text && Object.values(row.text.typography).includes("mixed")) unknowns.push("text.mixedRangeStyles");
103
104
  if (CFG.fields.includes("layout")) {
104
- row.layout = {};
105
+ row.layout = {constraints:pick(s,["constraintsHorizontal","constraintsVertical","fixedWhenScrolling"],unknowns,"layout.constraints")};
105
106
  for (const [key,keys] of [["flex",["dir","wrap","alignItems","alignContent","justifyContent","rowGap","columnGap","topPadding","rightPadding","bottomPadding","leftPadding","horizontalSizing","verticalSizing"]],["layoutChild",["absolute","zIndex","horizontalSizing","verticalSizing","alignSelf","topMargin","rightMargin","bottomMargin","leftMargin","minWidth","maxWidth","minHeight","maxHeight"]]]) {
106
107
  try { row.layout[key] = s[key] ? pick(s[key],keys,unknowns,"layout."+key) : null; } catch { row.layout[key]=null; unknowns.push("layout."+key); }
107
108
  }
@@ -111,10 +112,17 @@ if (CFG.kind === "shape") {
111
112
  if(grid)for(const key of ["rows","columns"])row.layout.grid[key]=(grid[key]||[]).map((track,index)=>pick(track,["type","value"],unknowns,"layout.grid."+key+"."+index));
112
113
  } catch {row.layout.grid=null;unknowns.push("layout.grid");}
113
114
  try {row.layout.cell=s.layoutCell?pick(s.layoutCell,["row","rowSpan","column","columnSpan","areaName","position"],unknowns,"layout.cell"):null;} catch {row.layout.cell=null;unknowns.push("layout.cell");}
114
- row.layout.clipContent = read(s,"clipContent",unknowns,"layout.clipContent");
115
+ row.layout.clipContent = s.type === "board" ? read(s,"clipContent",unknowns,"layout.clipContent") : null;
116
+ row.layout.showInViewMode = s.type === "board" ? read(s,"showInViewMode",unknowns,"layout.showInViewMode") : null;
117
+ row.layout.boardSizing = s.type === "board" ? Object.fromEntries(["horizontalSizing","verticalSizing"].map(key=>[key,optional(s,key,unknowns,"layout.boardSizing."+key)])) : null;
115
118
  }
116
119
  if (CFG.fields.includes("tokens")) row.tokens = read(s,"tokens",unknowns);
117
- if (CFG.fields.includes("style")) row.style = pick(s,["fills","strokes","shadows","opacity","blendMode","borderRadiusTopLeft","borderRadiusTopRight","borderRadiusBottomRight","borderRadiusBottomLeft"],unknowns,"style");
120
+ if (CFG.fields.includes("style")) {
121
+ row.style = pick(s,["fills","strokes","shadows","opacity","blendMode","borderRadiusTopLeft","borderRadiusTopRight","borderRadiusBottomRight","borderRadiusBottomLeft"],unknowns,"style");
122
+ for (const key of ["blur","backgroundBlur"]) {
123
+ row.style[key]=optional(s,key,unknowns,"style."+key);
124
+ }
125
+ }
118
126
  if (CFG.fields.includes("component")) {
119
127
  row.component = {};
120
128
  for (const key of ["isComponentInstance","isComponentMainInstance","isComponentCopyInstance","isComponentRoot"]) {
@@ -160,6 +160,10 @@ export declare const projectConfigSchema: {
160
160
  readonly type: "object";
161
161
  readonly required: readonly ["page", "root", "frames"];
162
162
  readonly properties: {
163
+ readonly format: {
164
+ readonly enum: readonly ["png", "webp"];
165
+ readonly description: "Format for new references; existing approved formats are preserved unless explicitly migrated.";
166
+ };
163
167
  readonly page: {
164
168
  readonly type: "string";
165
169
  readonly minLength: 1;
@@ -280,6 +280,13 @@ export const projectConfigSchema = {
280
280
  "frames",
281
281
  ],
282
282
  properties: {
283
+ format: {
284
+ enum: [
285
+ "png",
286
+ "webp",
287
+ ],
288
+ description: "Format for new references; existing approved formats are preserved unless explicitly migrated.",
289
+ },
283
290
  page: {
284
291
  type: "string",
285
292
  minLength: 1,
@@ -0,0 +1,3 @@
1
+ import type { MapDrawing } from "./types.js";
2
+ /** Reject malformed content geometry before a publication can contact Penpot. */
3
+ export declare function validateMapDrawingContent(drawing: MapDrawing): void;
@@ -0,0 +1,57 @@
1
+ function text(value, maximum) {
2
+ return value === undefined || (typeof value === "string" && value.length <= maximum);
3
+ }
4
+ function box(value) {
5
+ if (!value || typeof value !== "object") {
6
+ return false;
7
+ }
8
+ const bounds = value;
9
+ return ([
10
+ "x",
11
+ "y",
12
+ "width",
13
+ "height",
14
+ ].every((key) => Number.isFinite(bounds[key])) &&
15
+ Number(bounds.x) >= 0 &&
16
+ Number(bounds.y) >= 0 &&
17
+ Number(bounds.width) >= 64 &&
18
+ Number(bounds.height) > 0);
19
+ }
20
+ /** Reject malformed content geometry before a publication can contact Penpot. */
21
+ export function validateMapDrawingContent(drawing) {
22
+ if (!text(drawing.goal, 8000) ||
23
+ !text(drawing.summary, 240) ||
24
+ !text(drawing.description, 8000)) {
25
+ throw new Error("Map descriptions exceed their supported text budgets");
26
+ }
27
+ if (drawing.header &&
28
+ (!box(drawing.header.titleBox) ||
29
+ (drawing.header.summaryBox !== undefined && !box(drawing.header.summaryBox)) ||
30
+ !Number.isFinite(drawing.header.legendY) ||
31
+ drawing.header.legendY < 0 ||
32
+ !Number.isFinite(drawing.header.height) ||
33
+ drawing.header.height <= 0)) {
34
+ throw new Error("Map header requires finite positive geometry");
35
+ }
36
+ if (drawing.annotations === undefined) {
37
+ return;
38
+ }
39
+ const nodeIds = new Set(drawing.nodes.map((node) => node.id));
40
+ if (!Array.isArray(drawing.annotations) ||
41
+ drawing.annotations.length > 1001 ||
42
+ new Set(drawing.annotations.map((annotation) => annotation?.id)).size !==
43
+ drawing.annotations.length ||
44
+ drawing.annotations.some((annotation) => !annotation ||
45
+ typeof annotation.id !== "string" ||
46
+ !/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,255}$/.test(annotation.id) ||
47
+ !box(annotation) ||
48
+ (annotation.nodeId !== undefined && !nodeIds.has(annotation.nodeId)) ||
49
+ typeof annotation.title !== "string" ||
50
+ !annotation.title.trim() ||
51
+ annotation.title.length > 4000 ||
52
+ !text(annotation.goal, 8000) ||
53
+ !text(annotation.summary, 240) ||
54
+ !text(annotation.description, annotation.nodeId ? 4000 : 8000))) {
55
+ throw new Error("Map annotations require unique identities, bounded text and finite geometry");
56
+ }
57
+ }
@@ -0,0 +1,22 @@
1
+ import type { MapDrawing, ProjectionShape } from "./types.js";
2
+ export declare const nativeMapRoles: readonly ["node", "label", "edge", "edge-label", "title", "badge", "legend", "map-summary", "map-full-summary", "map-goal", "map-description", "summary", "full-label", "full-summary", "description", "annotation-panel", "annotation-id", "annotation-heading", "icon-well", "icon", "port-in", "port-out"];
3
+ export interface NativeMapTextEntry {
4
+ role: ProjectionShape["role"];
5
+ logicalId: string;
6
+ text: string;
7
+ x: number;
8
+ y: number;
9
+ width: number;
10
+ height: number;
11
+ fontSize: number;
12
+ color: string;
13
+ strong?: boolean;
14
+ semantic?: {
15
+ scope: "map" | "node";
16
+ field: "title" | "goal" | "label" | "summary" | "description";
17
+ nodeId?: string;
18
+ compact?: boolean;
19
+ };
20
+ }
21
+ export declare function nativeMapTextEntries(drawing: MapDrawing): NativeMapTextEntry[];
22
+ export declare function nativeMapShapeKeys(drawing: MapDrawing): Set<string>;
@@ -0,0 +1,173 @@
1
+ import { mapAnnotationLayout } from "../maps/annotation-layout.js";
2
+ import { mapLabelHeight } from "../maps/layout.js";
3
+ import { mapOriginLabels, mapOriginLegend } from "./status-labels.js";
4
+ export const nativeMapRoles = [
5
+ "node",
6
+ "label",
7
+ "edge",
8
+ "edge-label",
9
+ "title",
10
+ "badge",
11
+ "legend",
12
+ "map-summary",
13
+ "map-full-summary",
14
+ "map-goal",
15
+ "map-description",
16
+ "summary",
17
+ "full-label",
18
+ "full-summary",
19
+ "description",
20
+ "annotation-panel",
21
+ "annotation-id",
22
+ "annotation-heading",
23
+ "icon-well",
24
+ "icon",
25
+ "port-in",
26
+ "port-out",
27
+ ];
28
+ export function nativeMapTextEntries(drawing) {
29
+ const result = [];
30
+ const add = (role, logicalId, text, x, y, width, height, fontSize = 14, color = "#101828", semantic, strong = false) => {
31
+ result.push({
32
+ role,
33
+ logicalId,
34
+ text,
35
+ x,
36
+ y,
37
+ width,
38
+ height,
39
+ fontSize,
40
+ color,
41
+ ...(semantic
42
+ ? {
43
+ semantic,
44
+ }
45
+ : {}),
46
+ ...(strong
47
+ ? {
48
+ strong,
49
+ }
50
+ : {}),
51
+ });
52
+ };
53
+ const titleBox = drawing.header?.titleBox ?? {
54
+ x: 32,
55
+ y: 24,
56
+ width: 900,
57
+ height: 40,
58
+ };
59
+ add("title", drawing.mapId, drawing.title, titleBox.x, titleBox.y, titleBox.width, titleBox.height, 24, "#101828", {
60
+ scope: "map",
61
+ field: "title",
62
+ }, true);
63
+ const headerText = drawing.headerSummary ??
64
+ drawing.summary ??
65
+ (drawing.goal && drawing.goal.length <= 240 ? drawing.goal : undefined);
66
+ if (headerText) {
67
+ const box = drawing.header?.summaryBox ?? {
68
+ x: 32,
69
+ y: 72,
70
+ width: 900,
71
+ height: mapLabelHeight(headerText, 900),
72
+ };
73
+ add("map-summary", drawing.mapId, headerText, box.x, box.y, box.width, box.height, 14, "#667085", {
74
+ scope: "map",
75
+ field: drawing.summary !== undefined ? "summary" : "goal",
76
+ });
77
+ }
78
+ if (drawing.nodes.some((node) => node.origin)) {
79
+ add("legend", drawing.mapId, mapOriginLegend, 32, drawing.header?.legendY ?? (headerText ? 132 : 72), 1000, 20, 10, "#667085");
80
+ }
81
+ for (const node of drawing.nodes) {
82
+ const modern = node.cardTitle !== undefined;
83
+ const title = modern
84
+ ? node.cardTitle
85
+ : (node.kind === "decision" ? "Decision: " : node.kind === "detail-link" ? "Details: " : "") +
86
+ node.label;
87
+ add("label", node.id, title, node.x + 64, node.y + 18, node.width - 80, 32, 14, "#101828", {
88
+ scope: "node",
89
+ field: "label",
90
+ nodeId: node.id,
91
+ compact: modern && title !== node.label,
92
+ }, true);
93
+ const summary = node.cardSummary ?? node.summary;
94
+ if (summary) {
95
+ add("summary", node.id, summary, node.x + 64, node.y + 50, node.width - 80, 48, 12, "#667085", {
96
+ scope: "node",
97
+ field: "summary",
98
+ nodeId: node.id,
99
+ compact: summary !== node.summary,
100
+ });
101
+ }
102
+ if (node.origin) {
103
+ add("badge", node.id, mapOriginLabels[node.origin], node.x + 16, node.y + node.height - 24, node.width - 32, 16, 10, "#667085");
104
+ }
105
+ }
106
+ for (const edge of drawing.edges) {
107
+ if (edge.label && edge.labelBox) {
108
+ add("edge-label", edge.id, edge.label, edge.labelBox.x, edge.labelBox.y, edge.labelBox.width, edge.labelBox.height, 12, "#667085");
109
+ }
110
+ }
111
+ for (const annotation of drawing.annotations ?? []) {
112
+ const node = annotation.nodeId
113
+ ? drawing.nodes.find((candidate) => candidate.id === annotation.nodeId)
114
+ : undefined;
115
+ const id = node?.id ?? drawing.mapId;
116
+ const layout = mapAnnotationLayout(annotation);
117
+ const x = annotation.x + 20;
118
+ const width = annotation.width - 40;
119
+ add("annotation-id", annotation.id, node ? `Node · ${node.id}` : "Map context", x, annotation.y + layout.idBox.y, width, layout.idBox.height, 10, "#667085");
120
+ if (node && layout.titleBox) {
121
+ add("full-label", node.id, node.label, x, annotation.y + layout.titleBox.y, width, layout.titleBox.height, 16, "#101828", {
122
+ scope: "node",
123
+ field: "label",
124
+ nodeId: node.id,
125
+ }, true);
126
+ }
127
+ for (const fieldLayout of layout.fields) {
128
+ const field = fieldLayout.field;
129
+ const value = annotation[field];
130
+ add("annotation-heading", `${annotation.id}:${field}`, field[0].toUpperCase() + field.slice(1), x, annotation.y + fieldLayout.headingY, width, fieldLayout.headingHeight, 11, "#667085", undefined, true);
131
+ const role = node
132
+ ? field === "summary"
133
+ ? "full-summary"
134
+ : "description"
135
+ : field === "goal"
136
+ ? "map-goal"
137
+ : field === "summary"
138
+ ? "map-full-summary"
139
+ : "map-description";
140
+ add(role, id, value, x, annotation.y + fieldLayout.y, width, fieldLayout.height, 14, "#344054", {
141
+ scope: node ? "node" : "map",
142
+ field,
143
+ ...(node
144
+ ? {
145
+ nodeId: node.id,
146
+ }
147
+ : {}),
148
+ });
149
+ }
150
+ }
151
+ return result;
152
+ }
153
+ export function nativeMapShapeKeys(drawing) {
154
+ return new Set([
155
+ ...nativeMapTextEntries(drawing).map((entry) => `${entry.role}:${entry.logicalId}`),
156
+ ...drawing.nodes.flatMap((node) => [
157
+ "node",
158
+ "icon-well",
159
+ "icon",
160
+ "port-in",
161
+ "port-out",
162
+ ].map((role) => `${role}:${node.id}`)),
163
+ ...drawing.edges.flatMap((edge) => [
164
+ `edge:${edge.id}`,
165
+ ...(edge.label
166
+ ? [
167
+ `edge-label:${edge.id}`,
168
+ ]
169
+ : []),
170
+ ]),
171
+ ...(drawing.annotations ?? []).map((annotation) => `annotation-panel:${annotation.id}`),
172
+ ]);
173
+ }
@@ -0,0 +1,4 @@
1
+ import type { MapDrawing } from "./types.js";
2
+ /** Resolve only explicitly pinned direct contracts; never recurse into another map's sources. */
3
+ export declare function resolveMapDetails(root: string, drawing: MapDrawing): Promise<MapDrawing>;
4
+ export declare function requireResolvedMapDetails(drawing: MapDrawing): void;
@@ -0,0 +1,84 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readProjectBytes } from "../bounded-file.js";
3
+ import { hashMapContract, maxMapBytes, validateMapContract, validateMapDetail, } from "../maps/validation.js";
4
+ /** Resolve only explicitly pinned direct contracts; never recurse into another map's sources. */
5
+ export async function resolveMapDetails(root, drawing) {
6
+ const references = drawing.nodes.filter((node) => node.detail);
7
+ if (references.length > 64) {
8
+ throw new Error("Native map supports at most 64 direct detail references");
9
+ }
10
+ const identity = drawing.mapId.match(/^(journey-map|integration-map):(.+)$/);
11
+ if (references.length && !identity) {
12
+ throw new Error("Native detail maps require a namespaced canonical parent identity");
13
+ }
14
+ const parent = {
15
+ kind: identity?.[1],
16
+ id: identity?.[2] ?? "",
17
+ sources: references.map((node) => ({
18
+ ...node.detail,
19
+ id: node.detail.sourceId,
20
+ kind: "contract",
21
+ })),
22
+ };
23
+ let totalBytes = 0;
24
+ const cache = new Map();
25
+ const resolved = new Map();
26
+ for (const node of references) {
27
+ const detail = node.detail;
28
+ const key = `${detail.path}:${detail.sha256}`;
29
+ let contract = cache.get(key);
30
+ if (!contract) {
31
+ const bytes = await readProjectBytes(root, detail.path, maxMapBytes);
32
+ totalBytes += bytes.length;
33
+ if (totalBytes > 128 * 1024 * 1024) {
34
+ throw new Error("Direct detail contracts exceed the 128 MiB aggregate budget");
35
+ }
36
+ if (createHash("sha256").update(bytes).digest("hex") !== detail.sha256) {
37
+ throw new Error(`Pinned detail contract changed: ${detail.sourceId}`);
38
+ }
39
+ let raw;
40
+ try {
41
+ raw = JSON.parse(bytes.toString("utf8"));
42
+ }
43
+ catch {
44
+ throw new Error(`Invalid detail contract JSON: ${detail.sourceId}`);
45
+ }
46
+ contract = validateMapContract(raw);
47
+ cache.set(key, contract);
48
+ }
49
+ validateMapDetail(parent, {
50
+ id: node.id,
51
+ sourceIds: [
52
+ detail.sourceId,
53
+ ],
54
+ requirementIds: node.requirementIds,
55
+ }, contract);
56
+ const identity = {
57
+ mapId: `${contract.kind}:${contract.id}`,
58
+ contractHash: hashMapContract(contract),
59
+ };
60
+ if ((detail.mapId !== undefined && detail.mapId !== identity.mapId) ||
61
+ (detail.contractHash !== undefined && detail.contractHash !== identity.contractHash)) {
62
+ throw new Error(`Resolved detail contract identity differs: ${detail.sourceId}`);
63
+ }
64
+ resolved.set(key, identity);
65
+ }
66
+ return {
67
+ ...drawing,
68
+ nodes: drawing.nodes.map((node) => node.detail
69
+ ? {
70
+ ...node,
71
+ detail: {
72
+ ...node.detail,
73
+ ...resolved.get(`${node.detail.path}:${node.detail.sha256}`),
74
+ },
75
+ }
76
+ : node),
77
+ };
78
+ }
79
+ export function requireResolvedMapDetails(drawing) {
80
+ if (drawing.nodes.some((node) => node.detail &&
81
+ (!node.detail.mapId || !/^[a-f0-9]{64}$/.test(node.detail.contractHash ?? "")))) {
82
+ throw new Error("Resolve pinned detail contract identities before using native map evidence");
83
+ }
84
+ }
@@ -0,0 +1,5 @@
1
+ import { PenpairError } from "../errors.js";
2
+ export declare class MapExportEvidenceError extends PenpairError {
3
+ readonly runId: string;
4
+ constructor(runId: string, detail: string);
5
+ }
@@ -0,0 +1,9 @@
1
+ import { PenpairError } from "../errors.js";
2
+ export class MapExportEvidenceError extends PenpairError {
3
+ runId;
4
+ constructor(runId, detail) {
5
+ super("incomplete-output", `Native map write is verified, but export evidence failed for run ${runId}: ${detail}. Inspect the run and rerun the original publish command with --apply --resume ${runId}.`);
6
+ this.runId = runId;
7
+ this.name = "MapExportEvidenceError";
8
+ }
9
+ }
@@ -0,0 +1,5 @@
1
+ import type { MapIcon } from "../maps/types.js";
2
+ import type { ProjectionShape } from "./types.js";
3
+ export declare const nativeMapIcons: Record<MapIcon, string>;
4
+ export declare function defaultMapIcon(kind: string): MapIcon;
5
+ export declare function identifyMapIcon(shape: Pick<ProjectionShape, "content">): MapIcon | undefined;
@@ -0,0 +1,79 @@
1
+ export const nativeMapIcons = {
2
+ screen: "M0 0 L24 0 L24 18 L0 18 L0 0 M6 24 L18 24 M12 18 L12 24",
3
+ action: "M14 0 L2 14 L11 14 L10 24 L22 9 L13 9 L14 0",
4
+ decision: "M12 0 L24 12 L12 24 L0 12 L12 0",
5
+ data: "M0 0 L24 0 L24 24 L0 24 L0 0 M0 8 L24 8 M0 16 L24 16",
6
+ service: "M0 0 L24 0 L24 10 L0 10 L0 0 M0 14 L24 14 L24 24 L0 24 L0 14 M5 5 L8 5 M5 19 L8 19",
7
+ code: "M8 0 L0 12 L8 24 M16 0 L24 12 L16 24",
8
+ check: "M0 12 L8 22 L24 0",
9
+ link: "M0 8 L0 0 L16 0 L16 16 L8 16 M8 8 L24 8 L24 24 L8 24 L8 8",
10
+ };
11
+ export function defaultMapIcon(kind) {
12
+ if (kind === "decision") {
13
+ return "decision";
14
+ }
15
+ if (kind === "detail-link") {
16
+ return "link";
17
+ }
18
+ if (/screen|view|page|route/.test(kind)) {
19
+ return "screen";
20
+ }
21
+ if (/data|store|state/.test(kind)) {
22
+ return "data";
23
+ }
24
+ if (/service|api|request/.test(kind)) {
25
+ return "service";
26
+ }
27
+ if (/component|hook|code/.test(kind)) {
28
+ return "code";
29
+ }
30
+ if (/success|outcome|complete/.test(kind)) {
31
+ return "check";
32
+ }
33
+ return "action";
34
+ }
35
+ function normalizedIcon(content) {
36
+ if (typeof content !== "string" || content.length > 8192) {
37
+ return undefined;
38
+ }
39
+ const tokens = content.match(/[ML]|[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/g);
40
+ if (!tokens || tokens.join("") !== content.replace(/[\s,]/g, "")) {
41
+ return undefined;
42
+ }
43
+ const points = [];
44
+ for (let index = 0; index < tokens.length; index += 3) {
45
+ const command = tokens[index];
46
+ const x = Number(tokens[index + 1]);
47
+ const y = Number(tokens[index + 2]);
48
+ if (![
49
+ "M",
50
+ "L",
51
+ ].includes(command ?? "") ||
52
+ !Number.isFinite(x) ||
53
+ !Number.isFinite(y)) {
54
+ return undefined;
55
+ }
56
+ points.push({
57
+ command: command,
58
+ x,
59
+ y,
60
+ });
61
+ }
62
+ const minX = Math.min(...points.map((point) => point.x));
63
+ const minY = Math.min(...points.map((point) => point.y));
64
+ const width = Math.max(...points.map((point) => point.x)) - minX;
65
+ const height = Math.max(...points.map((point) => point.y)) - minY;
66
+ if (width <= 0 || height <= 0) {
67
+ return undefined;
68
+ }
69
+ return points
70
+ .map((point) => `${point.command}${Math.round(((point.x - minX) / width) * 10000)},${Math.round(((point.y - minY) / height) * 10000)}`)
71
+ .join(" ");
72
+ }
73
+ export function identifyMapIcon(shape) {
74
+ const actual = normalizedIcon(shape.content);
75
+ if (!actual) {
76
+ return undefined;
77
+ }
78
+ return Object.keys(nativeMapIcons).find((icon) => normalizedIcon(nativeMapIcons[icon]) === actual);
79
+ }