@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,164 @@
1
+ import { nativeMapRoles, nativeMapTextEntries } from "./content.js";
2
+ import { nativeMapIcons, defaultMapIcon } from "./icons.js";
3
+ import { mapOriginLabels, mapOriginLegend } from "./status-labels.js";
4
+ import { snippet } from "../snippet.js";
5
+ const inspectSource = `
6
+ if (penpot.currentFile.id !== CFG.target.fileId || penpot.currentPage.id !== CFG.target.pageId) throw new Error("Map file/page identity changed");
7
+ const readMeta = shape => {
8
+ if (typeof shape.getSharedPluginData !== "function") throw new Error("Map metadata API unavailable");
9
+ const raw = shape.getSharedPluginData("penpair", "map");
10
+ if (!raw) return null;
11
+ try { return JSON.parse(raw); } catch { throw new Error("Malformed managed map metadata"); }
12
+ };
13
+ const readInteractions = shape => {
14
+ if (!Array.isArray(shape.interactions)) return null;
15
+ return shape.interactions.map(interaction => {
16
+ const destination = interaction.action?.destination;
17
+ const destinationId = destination?.id || null;
18
+ const destinationPageId = destinationId ? penpot.currentFile.pages.find(page => page.getShapeById(destinationId)?.id === destinationId)?.id || null : null;
19
+ const destinationMeta = destination ? readMeta(destination) : null;
20
+ return {trigger:interaction.trigger,type:interaction.action?.type || "unknown",destinationId,destinationPageId,destinationMapHash:destinationMeta?.contractHash || null,destinationMapId:destinationMeta?.mapId || null};
21
+ });
22
+ };
23
+ const all = [], queue = [...penpot.currentPage.root.children], visited = new Set();
24
+ while (queue.length) {
25
+ const shape = queue.shift();
26
+ if (visited.has(shape.id)) throw new Error("Cyclic native hierarchy");
27
+ visited.add(shape.id); all.push(shape);
28
+ if (all.length > 20000) throw new Error("Page traversal exceeds map inspection limit");
29
+ if (shape.children) queue.push(...shape.children);
30
+ }
31
+ const boards = all.filter(shape => { const meta = readMeta(shape); return meta && meta.mapId === CFG.mapId && meta.role === "board"; });
32
+ if (boards.length > 1) throw new Error("Duplicate managed map boards");
33
+ const board = boards[0] || null;
34
+ if (CFG.target.boardId && (!board || board.id !== CFG.target.boardId)) throw new Error("Managed map board identity changed");
35
+ const conflicts = [], shapes = [], keys = new Set();
36
+ if (board) {
37
+ const meta = readMeta(board);
38
+ if (meta.schemaVersion !== 1 || meta.owner !== "penpair") conflicts.push("Edited board ownership");
39
+ for (const shape of board.children) {
40
+ const child = readMeta(shape);
41
+ if (!child || child.owner !== "penpair" || child.schemaVersion !== 1 || child.mapId !== CFG.mapId) { conflicts.push("Unknown or edited ownership: " + shape.id); continue; }
42
+ if (!${JSON.stringify(nativeMapRoles)}.includes(child.role) || typeof child.logicalId !== "string" || !child.logicalId) { conflicts.push("Unknown managed role or identity: " + shape.id); continue; }
43
+ const key = child.role + ":" + child.logicalId;
44
+ if (keys.has(key)) conflicts.push("Duplicate logical ID: " + key);
45
+ keys.add(key);
46
+ if (shape.children && shape.children.length) conflicts.push("Unexpected nested content: " + shape.id);
47
+ shapes.push({shapeId:shape.id,logicalId:child.logicalId,role:child.role,iconKey:child.iconKey || null,interactions:readInteractions(shape),label:shape.type === "text" ? shape.characters : null,x:shape.x,y:shape.y,width:shape.width,height:shape.height,content:shape.type === "path" ? shape.content : null,appearance:{type:shape.type,name:shape.name,fills:shape.fills || [],strokes:shape.strokes || [],fontId:shape.fontId || null,fontFamily:shape.fontFamily || null,fontVariantId:shape.fontVariantId || null,fontSize:shape.fontSize || null,growType:shape.growType || null,borderRadiusTopLeft:shape.borderRadiusTopLeft ?? shape.borderRadius ?? null,borderRadiusTopRight:shape.borderRadiusTopRight ?? shape.borderRadius ?? null,borderRadiusBottomLeft:shape.borderRadiusBottomLeft ?? shape.borderRadius ?? null,borderRadiusBottomRight:shape.borderRadiusBottomRight ?? shape.borderRadius ?? null,opacity:shape.opacity ?? null,blendMode:shape.blendMode ?? null,blur:shape.blur || null},...(child.from ? {from:child.from,to:child.to} : {})});
48
+ }
49
+ const childIds = new Set(board.children.map(shape => shape.id));
50
+ for (const shape of all) { const child = readMeta(shape); if (shape.id !== board.id && child && child.mapId === CFG.mapId && !childIds.has(shape.id)) conflicts.push("Reparented managed shape: " + shape.id); }
51
+ }
52
+ const observation = {boardId:board ? board.id : null,boardBounds:board ? {x:board.x,y:board.y,width:board.width,height:board.height} : null,contractHash:board ? readMeta(board).contractHash : null,shapes,conflicts};
53
+ `;
54
+ export function inspectMapSnippet(target, mapId) {
55
+ return snippet("map-projection-inspect", `${inspectSource}\nreturn {ok:true,...observation};`, {
56
+ target,
57
+ mapId,
58
+ }, "read");
59
+ }
60
+ export function publishMapSnippet(target, drawing, expected) {
61
+ return snippet("map-projection-publish", `${inspectSource}
62
+ if (conflicts.length || JSON.stringify(observation) !== JSON.stringify(CFG.expected)) throw new Error("Map changed after inspection; reconcile before publishing");
63
+ const fontName = CFG.drawing.fontFamily || "Inter";
64
+ const exactFonts = Array.isArray(penpot.fonts.all) ? penpot.fonts.all.filter(font => font.name === fontName) : [];
65
+ if (exactFonts.length !== 1) throw new Error("Map font unavailable or ambiguous: " + fontName);
66
+ const font = exactFonts[0];
67
+ if (typeof font.applyToText !== "function" || !Array.isArray(font.variants)) throw new Error("Map font unavailable");
68
+ const variants = font.variants.filter(variant => String(variant.fontWeight) === "400" && (variant.fontStyle || "normal") === "normal");
69
+ if (variants.length !== 1) throw new Error("Map regular font variant unavailable or ambiguous");
70
+ if (typeof penpot.currentPage.root.setSharedPluginData !== "function" || ["createBoard","createRectangle","createEllipse","createPath","createText"].some(method => typeof penpot[method] !== "function")) throw new Error("Required native map creation/metadata APIs unavailable");
71
+ const detailBoards = new Map();
72
+ for (const node of CFG.drawing.nodes.filter(node => node.detail)) {
73
+ const detail = node.detail;
74
+ if (detail.fileId !== CFG.target.fileId) throw new Error("Detail map target must be in the same pinned file");
75
+ const pages = penpot.currentFile.pages.filter(page => page.id === detail.pageId);
76
+ if (pages.length !== 1 || typeof pages[0].getShapeById !== "function") throw new Error("Detail map target page is missing or cannot be inspected");
77
+ const target = pages[0].getShapeById(detail.frameId);
78
+ const targetMeta = target ? readMeta(target) : null;
79
+ if (!target || target.type !== "board" || !targetMeta || targetMeta.owner !== "penpair" || targetMeta.role !== "board" || !detail.mapId || targetMeta.mapId !== detail.mapId || !detail.contractHash || targetMeta.contractHash !== detail.contractHash) throw new Error("Detail map target is not the existing pinned owned contract board");
80
+ if (typeof target.addInteraction !== "function" || typeof target.removeInteraction !== "function") throw new Error("Native detail interaction API is unavailable");
81
+ detailBoards.set(node.id,target);
82
+ }
83
+ let root = board;
84
+ if (!root) { root = penpot.createBoard(); root.name = CFG.drawing.title; root.fills = [{fillColor:"#F8FAFC",fillOpacity:1}]; root.clipContent = false; root.x = CFG.drawing.origin?.x || 0; root.y = CFG.drawing.origin?.y || 0; root.resize(1600,1200); }
85
+ const metadata = (shape, role, logicalId, extra = {}) => {
86
+ if (typeof shape.setSharedPluginData !== "function") throw new Error("Map metadata write API unavailable");
87
+ shape.setSharedPluginData("penpair", "map", JSON.stringify({schemaVersion:1,owner:"penpair",mapId:CFG.mapId,role,logicalId,...extra}));
88
+ };
89
+ metadata(root,"board",CFG.mapId,{contractHash:CFG.drawing.contractHash});
90
+ const previous = new Map(shapes.map(shape => [shape.role + ":" + shape.logicalId,shape]));
91
+ const retained = new Set();
92
+ const get = (role,id,create) => { const prior = previous.get(role + ":" + id); const shape = prior ? penpot.currentPage.getShapeById(prior.shapeId) : create(); root.appendChild(shape); retained.add(shape.id);shape.opacity=1;shape.blendMode="normal"; metadata(shape,role,id); return shape; };
93
+ const strongVariants = font.variants.filter(variant => ["600","700"].includes(String(variant.fontWeight)) && (variant.fontStyle || "normal") === "normal").sort((left,right)=>Number(left.fontWeight)-Number(right.fontWeight));
94
+ const textShape = entry => {
95
+ const shape = get(entry.role,entry.logicalId,() => penpot.createText(entry.text || " "));
96
+ shape.characters = entry.text || " ";
97
+ font.applyToText(shape,entry.strong && strongVariants.length ? strongVariants[0] : variants[0]);
98
+ shape.fontSize = entry.fontSize; shape.resize(entry.width,entry.height); shape.growType = "auto-height";
99
+ shape.x=root.x+entry.x; shape.y=root.y+entry.y; shape.fills=[{fillColor:entry.color,fillOpacity:1}];
100
+ return shape;
101
+ };
102
+ const plannedNodes = CFG.drawing.nodes;
103
+ const nodes = new Map(plannedNodes.map(node => [node.id,node]));
104
+ for (const edge of CFG.drawing.edges) {
105
+ const from = nodes.get(edge.from), to = nodes.get(edge.to);
106
+ const shape = get("edge",edge.id,() => penpot.createPath());
107
+ const points = edge.points.map(point => ({x:root.x+point.x,y:root.y+point.y}));
108
+ shape.content = points.map((point,index) => (index ? "L" : "M")+point.x+" "+point.y).join(" ");
109
+ shape.fills=[]; shape.strokes=[{strokeColor:"#98A2B3",strokeWidth:1.25,strokeCapEnd:"line-arrow"}];
110
+ metadata(shape,"edge",edge.id,{from:edge.from,to:edge.to});
111
+ }
112
+ for (const node of plannedNodes) {
113
+ const shape = get("node",node.id,() => penpot.createRectangle());
114
+ shape.name=node.kind+": "+node.label;shape.resize(node.width,node.height);shape.x=root.x+node.x;shape.y=root.y+node.y;
115
+ shape.fills=[{fillColor:"#FFFFFF",fillOpacity:1}];shape.strokes=[{strokeColor:"#D0D5DD",strokeWidth:1}];shape.borderRadius=12;
116
+ const well = get("icon-well",node.id,() => penpot.createRectangle());well.resize(32,32);well.x=shape.x+16;well.y=shape.y+16;well.borderRadius=8;well.fills=[{fillColor:"#EEF2FF",fillOpacity:1}];well.strokes=[];
117
+ const iconKey=node.icon || CFG.iconDefaults[node.id];
118
+ const glyph=get("icon",node.id,()=>penpot.createPath());
119
+ glyph.content=CFG.iconPaths[iconKey].replace(/([ML])(-?[0-9.]+) (-?[0-9.]+)/g,(_all,command,x,y)=>command+(shape.x+24+Number(x)*16/24)+" "+(shape.y+24+Number(y)*16/24));
120
+ glyph.resize(16,16);glyph.x=shape.x+24;glyph.y=shape.y+24;
121
+ glyph.fills=[];glyph.strokes=[{strokeColor:"#5B5BD6",strokeWidth:1.6,strokeCapStart:"round",strokeCapEnd:"round",strokeJoinStyle:"round"}];metadata(glyph,"icon",node.id,{iconKey});
122
+ for(const [role,x] of [["port-in",shape.x],["port-out",shape.x+shape.width]]){
123
+ const port=get(role,node.id,()=>penpot.createEllipse());port.resize(8,8);port.x=x-4;port.y=shape.y+shape.height/2-4;port.fills=[{fillColor:"#FFFFFF",fillOpacity:1}];port.strokes=[{strokeColor:"#98A2B3",strokeWidth:1.25}];
124
+ }
125
+ }
126
+ for(const annotation of CFG.drawing.annotations || []){
127
+ const panel=get("annotation-panel",annotation.id,()=>penpot.createRectangle());panel.resize(annotation.width,annotation.height);panel.x=root.x+annotation.x;panel.y=root.y+annotation.y;panel.borderRadius=12;panel.fills=[{fillColor:"#FFFFFF",fillOpacity:1}];panel.strokes=[{strokeColor:"#E4E7EC",strokeWidth:1}];
128
+ }
129
+ for(const entry of CFG.textEntries) textShape(entry);
130
+ for(const node of plannedNodes){
131
+ const clickableRoles=new Set(["node","label","summary","badge","icon-well","icon","port-in","port-out"]);
132
+ for(const id of retained){
133
+ const shape=penpot.currentPage.getShapeById(id);const info=readMeta(shape);
134
+ if(info.logicalId!==node.id || !clickableRoles.has(info.role))continue;
135
+ if(Array.isArray(shape.interactions))for(const interaction of [...shape.interactions])shape.removeInteraction(interaction);
136
+ if(node.detail)shape.addInteraction("click",{type:"navigate-to",destination:detailBoards.get(node.id)});
137
+ }
138
+ }
139
+ if (typeof penpot.waitForLayoutUpdate === "function") await penpot.waitForLayoutUpdate();
140
+ const right = Math.max(900,...root.children.map(shape => shape.x-root.x+shape.width)) + 64;
141
+ const bottom = Math.max(160,...root.children.map(shape => shape.y-root.y+shape.height)) + 64;
142
+ root.resize(right,bottom);
143
+ for (const prior of shapes) if (!retained.has(prior.shapeId)) penpot.currentPage.getShapeById(prior.shapeId).remove();
144
+ return {ok:true,boardId:root.id};`, {
145
+ target,
146
+ mapId: drawing.mapId,
147
+ drawing,
148
+ textEntries: nativeMapTextEntries(drawing),
149
+ iconPaths: nativeMapIcons,
150
+ iconDefaults: Object.fromEntries(drawing.nodes.map((node) => [
151
+ node.id,
152
+ defaultMapIcon(node.kind),
153
+ ])),
154
+ originLabels: mapOriginLabels,
155
+ originLegend: mapOriginLegend,
156
+ expected: {
157
+ boardId: expected.boardId,
158
+ boardBounds: expected.boardBounds,
159
+ contractHash: expected.contractHash,
160
+ shapes: expected.shapes,
161
+ conflicts: expected.conflicts,
162
+ },
163
+ }, "write");
164
+ }
@@ -0,0 +1,7 @@
1
+ export declare const mapOriginLabels: {
2
+ observed: string;
3
+ proposed: string;
4
+ "owner-decision": string;
5
+ unknown: string;
6
+ };
7
+ export declare const mapOriginLegend = "Origin: Observed \u00B7 Proposed \u00B7 Owner decision \u00B7 Unknown. Status labels describe evidence, not implementation acceptance.";
@@ -0,0 +1,7 @@
1
+ export const mapOriginLabels = {
2
+ observed: "Observed",
3
+ proposed: "Proposed",
4
+ "owner-decision": "Owner decision",
5
+ unknown: "Unknown",
6
+ };
7
+ export const mapOriginLegend = "Origin: Observed · Proposed · Owner decision · Unknown. Status labels describe evidence, not implementation acceptance.";
@@ -0,0 +1,161 @@
1
+ import type { MapIcon, MapDrawingPlan } from "../maps/types.js";
2
+ import type { ExecuteOptions } from "../client.js";
3
+ import type { OperationOptions, OperationReport } from "../operation.js";
4
+ import type { ExportResult } from "../export.js";
5
+ export interface NativeDrawingNode {
6
+ id: string;
7
+ label: string;
8
+ summary?: string;
9
+ description?: string;
10
+ icon?: MapIcon;
11
+ cardTitle?: string;
12
+ cardSummary?: string;
13
+ annotationId?: string;
14
+ kind: string;
15
+ requirementIds?: string[];
16
+ detail?: {
17
+ mapId?: string;
18
+ contractHash?: string;
19
+ sourceId: string;
20
+ path: string;
21
+ sha256: string;
22
+ fileId: string;
23
+ pageId: string;
24
+ frameId: string;
25
+ };
26
+ origin?: "observed" | "proposed" | "owner-decision" | "unknown";
27
+ x: number;
28
+ y: number;
29
+ width: number;
30
+ height: number;
31
+ }
32
+ export interface NativeDrawingEdge {
33
+ labelBox?: {
34
+ x: number;
35
+ y: number;
36
+ width: number;
37
+ height: number;
38
+ };
39
+ points?: {
40
+ x: number;
41
+ y: number;
42
+ }[];
43
+ id: string;
44
+ from: string;
45
+ to: string;
46
+ label: string;
47
+ }
48
+ export interface MapDrawing {
49
+ mapId: string;
50
+ contractHash: string;
51
+ title: string;
52
+ goal?: string;
53
+ summary?: string;
54
+ description?: string;
55
+ headerSummary?: string;
56
+ header?: MapDrawingPlan["header"];
57
+ annotations?: MapDrawingPlan["annotations"];
58
+ fontFamily?: string;
59
+ origin?: {
60
+ x: number;
61
+ y: number;
62
+ };
63
+ nodes: NativeDrawingNode[];
64
+ edges: NativeDrawingEdge[];
65
+ }
66
+ export interface MapTarget {
67
+ fileId: string;
68
+ pageId: string;
69
+ boardId?: string;
70
+ }
71
+ export interface ProjectionShape {
72
+ shapeId: string;
73
+ logicalId: string;
74
+ role: "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";
75
+ label: string | null;
76
+ x: number;
77
+ y: number;
78
+ width: number;
79
+ height: number;
80
+ content: string | null;
81
+ appearance?: unknown;
82
+ iconKey?: string | null;
83
+ interactions?: {
84
+ trigger: string;
85
+ type: string;
86
+ destinationId: string | null;
87
+ destinationPageId?: string | null;
88
+ destinationMapHash?: string | null;
89
+ destinationMapId?: string | null;
90
+ }[] | null;
91
+ from?: string;
92
+ to?: string;
93
+ }
94
+ export interface NativeMapProjection {
95
+ schemaVersion: 1;
96
+ kind: "native-map-projection";
97
+ fileId: string;
98
+ pageId: string;
99
+ boardId: string | null;
100
+ boardBounds: {
101
+ x: number;
102
+ y: number;
103
+ width: number;
104
+ height: number;
105
+ } | null;
106
+ mapId: string;
107
+ contractHash: string | null;
108
+ fingerprint: string;
109
+ shapes: ProjectionShape[];
110
+ conflicts: string[];
111
+ }
112
+ export interface PublishMapOptions extends OperationOptions {
113
+ /** Required for writes; the standard operation journal persists the pre-write snapshot. */
114
+ journal?: OperationOptions["journal"];
115
+ /** Last independently observed projection. Required before replacing an existing map. */
116
+ expected?: NativeMapProjection;
117
+ /** Export remains evidence awaiting human/agent visual inspection. */
118
+ export?: boolean;
119
+ }
120
+ export interface PublishMapResult {
121
+ report: OperationReport;
122
+ projection: NativeMapProjection;
123
+ image?: ExportResult;
124
+ visualReviewRequired: boolean;
125
+ }
126
+ export interface NativeMapProposal {
127
+ schemaVersion: 1;
128
+ kind: "native-map-proposal";
129
+ baseContractHash: string;
130
+ observedProjectionHash: string;
131
+ mapId: string;
132
+ title?: string;
133
+ metadataChanges?: {
134
+ title?: string;
135
+ goal?: string;
136
+ summary?: string | null;
137
+ description?: string | null;
138
+ };
139
+ changes: {
140
+ nodeId: string;
141
+ label?: string;
142
+ summary?: string | null;
143
+ description?: string | null;
144
+ icon?: MapIcon | null;
145
+ }[];
146
+ edgeChanges: {
147
+ edgeId: string;
148
+ from: string;
149
+ to: string;
150
+ label: string;
151
+ }[];
152
+ presentation: {
153
+ nodeId: string;
154
+ x: number;
155
+ y: number;
156
+ width: number;
157
+ height: number;
158
+ }[];
159
+ findings: string[];
160
+ }
161
+ export type ProjectionReadOptions = ExecuteOptions;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import type { MapDrawingAnnotation } from "./types.js";
2
+ type AnnotationContent = Pick<MapDrawingAnnotation, "nodeId" | "title" | "goal" | "summary" | "description" | "width">;
3
+ export interface MapAnnotationLayout {
4
+ idBox: {
5
+ y: number;
6
+ height: number;
7
+ };
8
+ titleBox?: {
9
+ y: number;
10
+ height: number;
11
+ };
12
+ fields: Array<{
13
+ field: "goal" | "summary" | "description";
14
+ headingY: number;
15
+ headingHeight: number;
16
+ y: number;
17
+ height: number;
18
+ }>;
19
+ height: number;
20
+ }
21
+ /** Conservative word wrapping for Inter notes; native text uses the same reserved boxes. */
22
+ export declare function mapNoteTextHeight(text: string, width: number, fontSize?: number, lineHeight?: number): number;
23
+ export declare function mapAnnotationLayout(annotation: AnnotationContent): MapAnnotationLayout;
24
+ export {};
@@ -0,0 +1,85 @@
1
+ function wordWidth(word, fontSize) {
2
+ return Array.from(word).reduce((width, character) => {
3
+ const factor = /\s/.test(character)
4
+ ? 0.35
5
+ : /[il.,'!:;]/.test(character)
6
+ ? 0.35
7
+ : /[MW@#]/.test(character)
8
+ ? 1
9
+ : /[A-Z]/.test(character)
10
+ ? 0.78
11
+ : /[a-z0-9]/.test(character)
12
+ ? 0.62
13
+ : 1;
14
+ return width + factor * fontSize;
15
+ }, 0);
16
+ }
17
+ /** Conservative word wrapping for Inter notes; native text uses the same reserved boxes. */
18
+ export function mapNoteTextHeight(text, width, fontSize = 14, lineHeight = 20) {
19
+ let lines = 0;
20
+ for (const paragraph of text.split(/\r?\n/)) {
21
+ let lineWidth = 0;
22
+ let paragraphLines = 1;
23
+ for (const token of paragraph.split(/(\s+)/).filter(Boolean)) {
24
+ const tokenWidth = wordWidth(token, fontSize);
25
+ if (lineWidth && lineWidth + tokenWidth > width) {
26
+ paragraphLines++;
27
+ lineWidth = 0;
28
+ }
29
+ if (!lineWidth && /^\s+$/.test(token)) {
30
+ continue;
31
+ }
32
+ paragraphLines += Math.max(0, Math.ceil(tokenWidth / width) - 1);
33
+ lineWidth += tokenWidth > width ? tokenWidth % width : tokenWidth;
34
+ }
35
+ lines += paragraphLines;
36
+ }
37
+ return Math.max(1, lines) * lineHeight;
38
+ }
39
+ export function mapAnnotationLayout(annotation) {
40
+ const width = annotation.width - 40;
41
+ const idBox = {
42
+ y: 20,
43
+ height: 16,
44
+ };
45
+ let y = idBox.y + idBox.height + 8;
46
+ const titleBox = annotation.nodeId
47
+ ? {
48
+ y,
49
+ height: mapNoteTextHeight(annotation.title, width, 16, 24),
50
+ }
51
+ : undefined;
52
+ if (titleBox) {
53
+ y += titleBox.height + 12;
54
+ }
55
+ const fields = [];
56
+ for (const field of [
57
+ "goal",
58
+ "summary",
59
+ "description",
60
+ ]) {
61
+ const value = annotation[field];
62
+ if (value === undefined) {
63
+ continue;
64
+ }
65
+ const height = mapNoteTextHeight(value, width);
66
+ fields.push({
67
+ field,
68
+ headingY: y,
69
+ headingHeight: 16,
70
+ y: y + 20,
71
+ height,
72
+ });
73
+ y += 20 + height + 14;
74
+ }
75
+ return {
76
+ idBox,
77
+ ...(titleBox
78
+ ? {
79
+ titleBox,
80
+ }
81
+ : {}),
82
+ fields,
83
+ height: y + 6,
84
+ };
85
+ }
@@ -0,0 +1,13 @@
1
+ import type { ArtifactReview, MapContext, MapContract } from "./types.js";
2
+ export interface MapContextOptions {
3
+ reviews?: ArtifactReview[];
4
+ sourceHashes?: Record<string, string>;
5
+ journeyHash?: string;
6
+ journeyRequirementIds?: string[];
7
+ detailMaps?: Record<string, MapContract>;
8
+ detailReadiness?: Record<string, boolean>;
9
+ reviewEvidenceValid?: boolean;
10
+ maxItems?: number;
11
+ maxBytes?: number;
12
+ }
13
+ export declare function buildMapContext(raw: MapContract, options?: MapContextOptions): MapContext;
@@ -0,0 +1,207 @@
1
+ import { findArtifactReview } from "./review.js";
2
+ import { hashMapContract, validateMapContract, validateMapDetail } from "./validation.js";
3
+ export function buildMapContext(raw, options = {}) {
4
+ const contract = validateMapContract(raw);
5
+ const mapHash = hashMapContract(contract);
6
+ const maxItems = options.maxItems ?? 100;
7
+ const maxBytes = options.maxBytes ?? 128 * 1024;
8
+ if (!Number.isInteger(maxItems) ||
9
+ maxItems < 1 ||
10
+ maxItems > 1000 ||
11
+ !Number.isInteger(maxBytes) ||
12
+ maxBytes < 4096 ||
13
+ maxBytes > 1024 * 1024) {
14
+ throw new Error("Map context requires 1–1000 items and a 4 KiB–1 MiB byte budget");
15
+ }
16
+ const reviewed = !!findArtifactReview(options.reviews ?? [], contract.kind, contract.id, mapHash) &&
17
+ options.reviewEvidenceValid !== false;
18
+ const staleSources = contract.sources.filter((source) => options.sourceHashes &&
19
+ Object.hasOwn(options.sourceHashes, source.id) &&
20
+ options.sourceHashes[source.id] !== source.sha256);
21
+ const unknownSources = contract.sources.filter((source) => !options.sourceHashes || !Object.hasOwn(options.sourceHashes, source.id));
22
+ const staleJourney = !!contract.journey &&
23
+ options.journeyHash !== undefined &&
24
+ options.journeyHash !== contract.journey.sha256;
25
+ const missingJourney = contract.kind === "integration-map" && !contract.journey;
26
+ const unknownJourney = !!contract.journey &&
27
+ (options.journeyHash === undefined || options.journeyRequirementIds === undefined);
28
+ const mappedRequirements = new Set([
29
+ ...contract.nodes,
30
+ ...contract.edges,
31
+ ].flatMap((entry) => entry.requirementIds ?? []));
32
+ const unmappedRequirements = contract.requirements.filter((requirement) => !mappedRequirements.has(requirement.id));
33
+ const requirementIds = new Set(contract.requirements.map((requirement) => requirement.id));
34
+ const missingJourneyRequirements = contract.kind === "integration-map"
35
+ ? (options.journeyRequirementIds ?? []).filter((id) => !requirementIds.has(id))
36
+ : [];
37
+ const details = contract.nodes
38
+ .filter((node) => node.kind === "detail-link")
39
+ .map((node) => {
40
+ const source = contract.sources.find((entry) => entry.id === node.sourceIds[0]);
41
+ const supplied = options.detailMaps?.[source.id];
42
+ const child = supplied ? validateMapDetail(contract, node, supplied) : undefined;
43
+ return {
44
+ nodeId: node.id,
45
+ sourceId: source.id,
46
+ path: source.path,
47
+ sha256: source.sha256,
48
+ fileId: source.fileId,
49
+ pageId: source.pageId,
50
+ frameId: source.frameId,
51
+ ...(child
52
+ ? {
53
+ mapId: child.id,
54
+ mapHash: hashMapContract(child),
55
+ }
56
+ : {}),
57
+ ready: !!child && options.detailReadiness?.[source.id] === true,
58
+ review: "separate-artifact",
59
+ contents: "not-inlined",
60
+ };
61
+ });
62
+ const unreadyDetails = details.filter((detail) => !detail.ready);
63
+ const blockers = contract.findings.filter((finding) => finding.severity === "blocking" && finding.status === "open");
64
+ const unknowns = [
65
+ ...contract.nodes,
66
+ ...contract.edges,
67
+ ...contract.requirements,
68
+ ].filter((entry) => entry.origin === "unknown");
69
+ const freshness = staleSources.length || staleJourney
70
+ ? "stale"
71
+ : unknownSources.length || unknownJourney
72
+ ? "unassessed"
73
+ : "matches-supplied-inputs";
74
+ const reasons = [];
75
+ if (!reviewed) {
76
+ reasons.push("Exact current contract owner review is missing or its evidence is invalid");
77
+ }
78
+ if (staleSources.length) {
79
+ reasons.push(`${staleSources.length} changed source(s): ${staleSources
80
+ .slice(0, 5)
81
+ .map((source) => source.id)
82
+ .join(", ")}`);
83
+ }
84
+ if (unknownSources.length || unknownJourney) {
85
+ reasons.push("Source freshness is unassessed for one or more declared references");
86
+ }
87
+ if (staleJourney || missingJourney) {
88
+ reasons.push(staleJourney
89
+ ? "Referenced journey contract changed"
90
+ : "Integration map requires an explicit journey contract reference");
91
+ }
92
+ if (blockers.length) {
93
+ reasons.push(`${blockers.length} unresolved blocking finding(s): ${blockers
94
+ .slice(0, 5)
95
+ .map((finding) => finding.id)
96
+ .join(", ")}`);
97
+ }
98
+ if (unknowns.length) {
99
+ reasons.push(`${unknowns.length} unresolved unknown semantic element(s)`);
100
+ }
101
+ if (unmappedRequirements.length) {
102
+ reasons.push(`${unmappedRequirements.length} unmapped local requirement(s): ${unmappedRequirements
103
+ .slice(0, 5)
104
+ .map((requirement) => requirement.id)
105
+ .join(", ")}`);
106
+ }
107
+ if (missingJourneyRequirements.length) {
108
+ reasons.push(`${missingJourneyRequirements.length} omitted journey requirement(s): ${missingJourneyRequirements.slice(0, 5).join(", ")}`);
109
+ }
110
+ if (unreadyDetails.length) {
111
+ reasons.push(`${unreadyDetails.length} linked detail contract(s) require separate source/readiness/review checks; contents are not inlined`);
112
+ }
113
+ const context = {
114
+ schemaVersion: 1,
115
+ kind: "map-context",
116
+ mapKind: contract.kind,
117
+ mapId: contract.id,
118
+ mapHash,
119
+ title: contract.title,
120
+ goal: contract.goal,
121
+ ...(contract.summary === undefined
122
+ ? {}
123
+ : {
124
+ summary: contract.summary,
125
+ }),
126
+ ...(contract.description === undefined
127
+ ? {}
128
+ : {
129
+ description: contract.description,
130
+ }),
131
+ reviewed,
132
+ ready: reviewed &&
133
+ freshness === "matches-supplied-inputs" &&
134
+ !missingJourney &&
135
+ !blockers.length &&
136
+ !unknowns.length &&
137
+ !unmappedRequirements.length &&
138
+ !missingJourneyRequirements.length &&
139
+ !unreadyDetails.length,
140
+ freshness,
141
+ reasons,
142
+ nodes: contract.nodes.slice(0, maxItems),
143
+ edges: contract.edges.slice(0, maxItems),
144
+ requirements: contract.requirements.slice(0, maxItems),
145
+ findings: [
146
+ ...blockers,
147
+ ...contract.findings.filter((finding) => !blockers.includes(finding)),
148
+ ].slice(0, maxItems),
149
+ sources: contract.sources.slice(0, maxItems),
150
+ details: details.slice(0, maxItems),
151
+ truncated: [
152
+ contract.nodes,
153
+ contract.edges,
154
+ contract.requirements,
155
+ contract.findings,
156
+ contract.sources,
157
+ details,
158
+ ].some((entries) => entries.length > maxItems),
159
+ implementation: "not-assessed",
160
+ };
161
+ const collections = [
162
+ "nodes",
163
+ "edges",
164
+ "requirements",
165
+ "findings",
166
+ "sources",
167
+ "details",
168
+ ];
169
+ const markTruncated = () => {
170
+ context.truncated = true;
171
+ context.ready = false;
172
+ const reason = "Context is truncated; inspect the complete contract before deciding readiness";
173
+ if (!context.reasons.includes(reason)) {
174
+ context.reasons.push(reason);
175
+ }
176
+ };
177
+ if (context.truncated) {
178
+ markTruncated();
179
+ }
180
+ while (true) {
181
+ const visibleNodes = new Set(context.nodes.map((node) => node.id));
182
+ const visibleEdges = context.edges.filter((edge) => visibleNodes.has(edge.from) && visibleNodes.has(edge.to));
183
+ if (visibleEdges.length !== context.edges.length) {
184
+ markTruncated();
185
+ context.edges = visibleEdges;
186
+ }
187
+ if (Buffer.byteLength(JSON.stringify(context)) <= maxBytes) {
188
+ break;
189
+ }
190
+ markTruncated();
191
+ const largest = collections.reduce((previous, key) => JSON.stringify(context[key]).length > JSON.stringify(context[previous]).length
192
+ ? key
193
+ : previous);
194
+ if (!context[largest].length) {
195
+ const textKey = [
196
+ "goal",
197
+ "summary",
198
+ "description",
199
+ ].reduce((previous, key) => (context[key]?.length ?? 0) > (context[previous]?.length ?? 0) ? key : previous);
200
+ context[textKey] = (context[textKey] ?? "").slice(0, Math.floor((context[textKey]?.length ?? 0) / 2));
201
+ }
202
+ else {
203
+ context[largest].splice(Math.floor(context[largest].length / 2));
204
+ }
205
+ }
206
+ return context;
207
+ }