@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,685 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { atomicWriteFile } from "../atomic-output.js";
5
+ import { readProjectBytes } from "../bounded-file.js";
6
+ import { CommandArgumentError, parseCommandArgs } from "../cli-arguments.js";
7
+ import { observationHash } from "../journal.js";
8
+ import { exportShapePng } from "../export.js";
9
+ import { inspectImageEvidence, maximumImageBytes } from "../image-evidence.js";
10
+ import { acceptMapProposal, buildMapContext, buildMapDrawingPlan, createArtifactReview, findArtifactReview, hashMapContract, maxMapBytes, observeMapSources, readArtifactReviews, readMapContract, readMapProposal, renderMapMarkdown, renderMapMermaid, verifyArtifactReviewEvidence, writeArtifactReview, writeMapContract, writeMapProposal, } from "../maps/index.js";
11
+ import { acceptMapPresentationProposal, createMapPresentationProposal, drawingFromPlan, importMapProposal, preserveMapPresentation, validateMapPresentationProposal, validateNativeMapProjection, nativeProposalToMapProposal, publishMap, readProjection, resolveMapDetails, } from "../map-penpot/index.js";
12
+ import { resolveProjectOutput } from "../project-input.js";
13
+ import { runSnippet, snippet } from "../snippet.js";
14
+ const actions = {
15
+ check: [
16
+ "live",
17
+ "projection",
18
+ "reviews",
19
+ ],
20
+ render: [
21
+ "out",
22
+ ],
23
+ publish: [
24
+ "page-id",
25
+ "board-id",
26
+ "projection",
27
+ "apply",
28
+ "resume",
29
+ ],
30
+ import: [
31
+ "page-id",
32
+ "board-id",
33
+ "projection",
34
+ "out",
35
+ ],
36
+ accept: [
37
+ "projection",
38
+ "proposal",
39
+ "write",
40
+ ],
41
+ approve: [
42
+ "projection",
43
+ "reviews",
44
+ "write",
45
+ "by",
46
+ "note",
47
+ ],
48
+ context: [
49
+ "reviews",
50
+ "out",
51
+ "draft",
52
+ ],
53
+ };
54
+ const booleanOptions = new Set([
55
+ "live",
56
+ "apply",
57
+ "write",
58
+ "draft",
59
+ "json",
60
+ "help",
61
+ ]);
62
+ function sha256(bytes) {
63
+ return createHash("sha256").update(bytes).digest("hex");
64
+ }
65
+ async function savedProjection(root, file, map) {
66
+ let bytes;
67
+ try {
68
+ bytes = await readProjectBytes(root, file, maxMapBytes);
69
+ }
70
+ catch (error) {
71
+ if (error.code === "ENOENT") {
72
+ return undefined;
73
+ }
74
+ throw error;
75
+ }
76
+ return mapProjection(JSON.parse(bytes.toString("utf8")), map);
77
+ }
78
+ function mapProjection(raw, map) {
79
+ const value = validateNativeMapProjection(raw);
80
+ if (value.mapId !== `${map.kind}:${map.id}` || !value.boardId || value.shapes.length > 10000) {
81
+ throw new Error("Saved projection belongs to another map or exceeds the node budget");
82
+ }
83
+ return value;
84
+ }
85
+ async function nativeProposalObservation(context, file, proposal, map) {
86
+ let bytes;
87
+ try {
88
+ bytes = await readProjectBytes(context.root, `${file}.native.json`, maxMapBytes);
89
+ }
90
+ catch (error) {
91
+ if (error.code === "ENOENT") {
92
+ return undefined;
93
+ }
94
+ throw error;
95
+ }
96
+ const raw = JSON.parse(bytes.toString("utf8"));
97
+ if (!raw ||
98
+ raw.schemaVersion !== 1 ||
99
+ raw.kind !== "native-map-proposal-observation" ||
100
+ raw.proposalHash !== proposal.hash ||
101
+ !/^[a-f0-9]{64}$/.test(raw.baselineFingerprint)) {
102
+ throw new Error("Native proposal observation does not match the exact canonical proposal");
103
+ }
104
+ const projection = mapProjection(raw.projection, map);
105
+ if (projection.contractHash !== proposal.baseHash) {
106
+ throw new Error("Native proposal observation differs from the canonical base contract");
107
+ }
108
+ return {
109
+ ...raw,
110
+ projection,
111
+ };
112
+ }
113
+ async function outputTarget(context, file, source) {
114
+ const target = await resolveProjectOutput(context.root, file);
115
+ const map = await readMapContract(context.root, source);
116
+ const inputs = [
117
+ source,
118
+ ...map.sources.flatMap((entry) => entry.path
119
+ ? [
120
+ entry.path,
121
+ ]
122
+ : []),
123
+ ...(map.journey
124
+ ? [
125
+ map.journey.path,
126
+ ]
127
+ : []),
128
+ ];
129
+ for (const input of inputs) {
130
+ if (target === (await resolveProjectOutput(context.root, input))) {
131
+ throw new Error("Generated map output cannot replace the canonical contract or its source evidence");
132
+ }
133
+ }
134
+ return target;
135
+ }
136
+ async function writeOutput(context, file, value, source, replace = false) {
137
+ const target = await outputTarget(context, file, source);
138
+ const bytes = Buffer.isBuffer(value)
139
+ ? value
140
+ : typeof value === "string"
141
+ ? value
142
+ : `${JSON.stringify(value, null, 2)}\n`;
143
+ if (!Buffer.isBuffer(bytes) && context.redact(bytes) !== bytes) {
144
+ throw new Error("Map output requires credential redaction; no output was written");
145
+ }
146
+ await mkdir(path.dirname(target), {
147
+ recursive: true,
148
+ });
149
+ try {
150
+ await atomicWriteFile(target, bytes, {
151
+ replace,
152
+ });
153
+ }
154
+ catch (error) {
155
+ if (error.code !== "EEXIST" ||
156
+ !(await readProjectBytes(context.root, file, Math.max(1, Buffer.byteLength(bytes)))).equals(Buffer.from(bytes))) {
157
+ throw error;
158
+ }
159
+ }
160
+ }
161
+ async function persistNativeEvidence(context, source, projectionFile, map, projection, image) {
162
+ if (!image.stable ||
163
+ inspectImageEvidence(image.png).format !== "png" ||
164
+ image.capture.fileId !== projection.fileId ||
165
+ image.capture.pageId !== projection.pageId ||
166
+ image.capture.frameId !== projection.boardId) {
167
+ throw new Error("Native map evidence requires a stable original PNG bound to the observed target");
168
+ }
169
+ const imageHash = sha256(image.png);
170
+ const evidence = {
171
+ path: path.posix.join(path.posix.dirname(source), `map-evidence/${imageHash}.png`),
172
+ sha256: imageHash,
173
+ };
174
+ await writeOutput(context, evidence.path, image.png, source);
175
+ await writeOutput(context, `${projectionFile}.evidence.json`, {
176
+ projectionHash: projection.fingerprint,
177
+ contractHash: hashMapContract(map),
178
+ image: evidence,
179
+ capture: image.capture,
180
+ visualReviewRequired: true,
181
+ }, source, true);
182
+ await writeOutput(context, projectionFile, projection, source, true);
183
+ return evidence;
184
+ }
185
+ async function liveTarget(client, pageId, boardId) {
186
+ const current = await runSnippet(client, snippet("map-target", "return {ok:true,fileId:penpot.currentFile.id};", {}, "read"));
187
+ if (typeof current.fileId !== "string" || !current.fileId) {
188
+ throw new Error("Map target requires an independently observed file ID");
189
+ }
190
+ return {
191
+ fileId: current.fileId,
192
+ pageId,
193
+ ...(boardId
194
+ ? {
195
+ boardId,
196
+ }
197
+ : {}),
198
+ };
199
+ }
200
+ async function mapEvidence(context, map, reviewsFile, projectionFile) {
201
+ const reviews = await readArtifactReviews(context.root, reviewsFile);
202
+ const review = findArtifactReview(reviews, map.kind, map.id, hashMapContract(map));
203
+ const native = await savedProjection(context.root, projectionFile, map);
204
+ const nativeBound = !native || review?.artifacts?.some((artifact) => artifact.path === projectionFile);
205
+ const reviewEvidenceValid = !!nativeBound && !!review && (await verifyArtifactReviewEvidence(context.root, review));
206
+ const observation = await observeMapSources(context.root, map);
207
+ return {
208
+ reviews,
209
+ ...observation,
210
+ reviewEvidenceValid,
211
+ };
212
+ }
213
+ async function mapStatus(context, map, reviewsFile, projectionFile) {
214
+ const evidence = await mapEvidence(context, map, reviewsFile, projectionFile);
215
+ const detailReadiness = Object.create(null);
216
+ let nestedDetails = false;
217
+ for (const [sourceId, child] of Object.entries(evidence.detailMaps)) {
218
+ if (child.nodes.some((node) => node.kind === "detail-link")) {
219
+ detailReadiness[sourceId] = false;
220
+ nestedDetails = true;
221
+ continue;
222
+ }
223
+ const source = map.sources.find((entry) => entry.id === sourceId);
224
+ const directory = path.posix.dirname(source.path);
225
+ const childEvidence = await mapEvidence(context, child, path.posix.join(directory, "reviews.json"), path.posix.join(directory, "projection.json"));
226
+ const childContext = buildMapContext(child, {
227
+ ...childEvidence,
228
+ maxBytes: 128 * 1024,
229
+ maxItems: 1000,
230
+ });
231
+ detailReadiness[sourceId] = childContext.ready;
232
+ }
233
+ const result = buildMapContext(map, {
234
+ ...evidence,
235
+ detailReadiness,
236
+ maxBytes: 128 * 1024 - (nestedDetails ? 256 : 0),
237
+ maxItems: 1000,
238
+ });
239
+ if (nestedDetails) {
240
+ result.reasons.push("Nested detail review readiness is not assessed by this command; use finalized design-release context.");
241
+ }
242
+ return result;
243
+ }
244
+ /** Explicit local artifact actions remain offline; native publication is preview-only until --apply. */
245
+ export async function runMapCommand(kind, args, context) {
246
+ const help = `penpair ${kind} ACTION --from FILE
247
+ check [--live --projection FILE] [--reviews FILE]
248
+ render [--out DIR]
249
+ publish --page-id ID [--board-id ID] [--projection FILE] [--apply [--resume RUN_ID]]
250
+ import --board-id ID --out PROPOSAL [--page-id ID] [--projection FILE]
251
+ accept --proposal FILE --write [--projection FILE]
252
+ approve [--projection FILE] [--reviews FILE] [--write --by OWNER --note EXACTWORDS]
253
+ context [--reviews FILE] [--out FILE] [--draft]
254
+ Native publication previews by default. Captured source paths establish offline freshness;
255
+ --live checks the managed map projection only. Approval records supplied owner words.`;
256
+ if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
257
+ context.output.log(help);
258
+ return 0;
259
+ }
260
+ const action = args[0];
261
+ if (!Object.hasOwn(actions, action)) {
262
+ throw new CommandArgumentError(`${kind} requires check, render, publish, import, accept, approve or context`);
263
+ }
264
+ const allowed = [
265
+ "from",
266
+ "json",
267
+ "help",
268
+ ...actions[action],
269
+ ];
270
+ const options = Object.fromEntries(allowed.map((name) => [
271
+ name,
272
+ {
273
+ type: booleanOptions.has(name) ? "boolean" : "string",
274
+ },
275
+ ]));
276
+ const parsed = parseCommandArgs({
277
+ args: args.slice(1),
278
+ options,
279
+ strict: true,
280
+ allowPositionals: false,
281
+ });
282
+ const values = parsed.values;
283
+ if (values.help) {
284
+ context.output.log(help);
285
+ return 0;
286
+ }
287
+ if (Object.values(values).some((value) => typeof value === "string" && !value.trim())) {
288
+ throw new CommandArgumentError("Map flags require nonempty values");
289
+ }
290
+ if (action === "check" && values.projection && !values.live) {
291
+ throw new CommandArgumentError("Map check --projection requires --live");
292
+ }
293
+ const stringValue = (name) => typeof values[name] === "string" ? values[name] : undefined;
294
+ const from = stringValue("from");
295
+ if (!from) {
296
+ throw new CommandArgumentError(`${kind} ${action} requires --from FILE`);
297
+ }
298
+ if (action === "approve" &&
299
+ ((values.write && (!stringValue("by")?.trim() || !stringValue("note")?.trim())) ||
300
+ (!values.write && (values.by || values.note)))) {
301
+ throw new CommandArgumentError("Map approval --write requires --by OWNER and --note EXACTWORDS; preview takes neither");
302
+ }
303
+ if (action === "accept" && (!values.write || !stringValue("proposal"))) {
304
+ throw new CommandArgumentError("Map acceptance requires --proposal FILE --write");
305
+ }
306
+ if (action === "publish" && values.resume && !values.apply) {
307
+ throw new CommandArgumentError("Map publication --resume requires --apply and the original target flags");
308
+ }
309
+ if (action === "publish" && !stringValue("page-id")) {
310
+ throw new CommandArgumentError("Map publication requires --page-id ID");
311
+ }
312
+ if (action === "import" && (!stringValue("board-id") || !stringValue("out"))) {
313
+ throw new CommandArgumentError("Native map import requires --board-id ID --out PROPOSAL");
314
+ }
315
+ const map = await readMapContract(context.root, from);
316
+ if (map.kind !== kind) {
317
+ throw new CommandArgumentError(`${kind} cannot process a ${map.kind} contract`);
318
+ }
319
+ const sibling = (name) => path.posix.join(path.posix.dirname(from), name);
320
+ const projectionFile = stringValue("projection") ?? sibling("projection.json");
321
+ const reviewsFile = stringValue("reviews") ?? sibling("reviews.json");
322
+ const emit = (value) => {
323
+ context.output.result(value);
324
+ context.output.log(JSON.stringify(value, null, 2));
325
+ };
326
+ const sourceObservation = await observeMapSources(context.root, map);
327
+ const plan = buildMapDrawingPlan(map, sourceObservation.detailMaps);
328
+ if (action === "render") {
329
+ const result = {
330
+ markdown: renderMapMarkdown(map),
331
+ mermaid: renderMapMermaid(map),
332
+ plan,
333
+ };
334
+ const out = stringValue("out");
335
+ if (out) {
336
+ for (const [name, value] of [
337
+ [
338
+ "map.md",
339
+ result.markdown,
340
+ ],
341
+ [
342
+ "map.mmd",
343
+ result.mermaid,
344
+ ],
345
+ [
346
+ "drawing-plan.json",
347
+ plan,
348
+ ],
349
+ ]) {
350
+ await writeOutput(context, path.posix.join(out, name), value, from, true);
351
+ }
352
+ }
353
+ emit(result);
354
+ return plan.complete ? 0 : 3;
355
+ }
356
+ if (action === "accept") {
357
+ const rawProposal = JSON.parse((await readProjectBytes(context.root, stringValue("proposal"), maxMapBytes)).toString("utf8"));
358
+ if (rawProposal?.kind === "map-presentation-proposal") {
359
+ const proposal = validateMapPresentationProposal(rawProposal);
360
+ const baseline = await savedProjection(context.root, projectionFile, map);
361
+ if (!baseline) {
362
+ throw new Error("Presentation acceptance requires the saved native baseline projection");
363
+ }
364
+ const resolvedDrawing = await resolveMapDetails(context.root, drawingFromPlan(plan));
365
+ const adopted = acceptMapPresentationProposal(map, baseline, proposal, resolvedDrawing);
366
+ await outputTarget(context, projectionFile, from);
367
+ await outputTarget(context, `${projectionFile}.evidence.json`, from);
368
+ return context.withPenpot(async (client) => {
369
+ const target = {
370
+ fileId: adopted.fileId,
371
+ pageId: adopted.pageId,
372
+ boardId: adopted.boardId,
373
+ };
374
+ const current = await readProjection(client, target, adopted.mapId);
375
+ if (current.fingerprint !== proposal.observedProjectionHash) {
376
+ throw new Error("Native presentation changed after import; inspect and import a fresh proposal");
377
+ }
378
+ const image = await exportShapePng(client, adopted.boardId, {
379
+ stable: true,
380
+ });
381
+ const after = await readProjection(client, target, adopted.mapId);
382
+ if (after.fingerprint !== current.fingerprint ||
383
+ hashMapContract(await readMapContract(context.root, from)) !== proposal.baseContractHash) {
384
+ throw new Error("Map changed during presentation acceptance; saved baseline was preserved");
385
+ }
386
+ const evidence = await persistNativeEvidence(context, from, projectionFile, map, adopted, image);
387
+ emit({
388
+ kind: "map-presentation-acceptance",
389
+ mapHash: hashMapContract(map),
390
+ canonicalChanged: false,
391
+ reviewed: false,
392
+ projection: adopted,
393
+ image: evidence,
394
+ visualReviewRequired: true,
395
+ });
396
+ return 0;
397
+ });
398
+ }
399
+ const proposalFile = stringValue("proposal");
400
+ const proposal = await readMapProposal(context.root, proposalFile);
401
+ const native = await nativeProposalObservation(context, proposalFile, proposal, map);
402
+ if (native) {
403
+ const baseline = await savedProjection(context.root, projectionFile, map);
404
+ const currentHash = hashMapContract(map);
405
+ const recovering = currentHash === proposal.proposedHash;
406
+ if (!baseline ||
407
+ ![
408
+ native.baselineFingerprint,
409
+ ...(recovering
410
+ ? [
411
+ native.projection.fingerprint,
412
+ ]
413
+ : []),
414
+ ].includes(baseline.fingerprint) ||
415
+ baseline.fileId !== native.projection.fileId ||
416
+ baseline.pageId !== native.projection.pageId ||
417
+ baseline.boardId !== native.projection.boardId) {
418
+ throw new Error("Native proposal baseline changed; inspect and import a fresh proposal");
419
+ }
420
+ const accepted = recovering ? proposal.proposedMap : acceptMapProposal(map, proposal);
421
+ await outputTarget(context, projectionFile, from);
422
+ return context.withPenpot(async (client) => {
423
+ const observed = native.projection;
424
+ const live = await readProjection(client, {
425
+ fileId: observed.fileId,
426
+ pageId: observed.pageId,
427
+ boardId: observed.boardId,
428
+ }, observed.mapId);
429
+ if (live.fingerprint !== observed.fingerprint || live.conflicts.length) {
430
+ throw new Error("Native map changed after import; canonical contract and saved baseline were preserved");
431
+ }
432
+ if (!recovering) {
433
+ await writeMapContract(context.root, from, accepted, {
434
+ replace: true,
435
+ expectedHash: proposal.baseHash,
436
+ redact: context.redact,
437
+ });
438
+ }
439
+ try {
440
+ await writeOutput(context, projectionFile, observed, from, true);
441
+ }
442
+ catch (error) {
443
+ throw new Error("Canonical proposal accepted but native baseline was not saved; rerun accept with the same proposal to reconcile safely", {
444
+ cause: error,
445
+ });
446
+ }
447
+ emit({
448
+ kind: "map-acceptance",
449
+ mapHash: hashMapContract(accepted),
450
+ previousHash: proposal.baseHash,
451
+ reviewed: false,
452
+ nativeBaselineAdopted: true,
453
+ recovered: recovering,
454
+ nativePublicationRequired: true,
455
+ });
456
+ return 0;
457
+ });
458
+ }
459
+ const accepted = acceptMapProposal(map, proposal);
460
+ await writeMapContract(context.root, from, accepted, {
461
+ replace: true,
462
+ expectedHash: hashMapContract(map),
463
+ redact: context.redact,
464
+ });
465
+ emit({
466
+ kind: "map-acceptance",
467
+ mapHash: hashMapContract(accepted),
468
+ previousHash: hashMapContract(map),
469
+ reviewed: false,
470
+ });
471
+ return 0;
472
+ }
473
+ if (action === "check" || action === "context") {
474
+ const status = await mapStatus(context, map, reviewsFile, projectionFile);
475
+ if (action === "check" && values.live) {
476
+ const expected = await savedProjection(context.root, projectionFile, map);
477
+ if (!expected) {
478
+ throw new Error("Live map checking requires a saved native projection");
479
+ }
480
+ const current = await context.withPenpot((client) => readProjection(client, {
481
+ fileId: expected.fileId,
482
+ pageId: expected.pageId,
483
+ boardId: expected.boardId,
484
+ }, expected.mapId));
485
+ const matches = current.fingerprint === expected.fingerprint &&
486
+ current.contractHash === hashMapContract(map) &&
487
+ !current.conflicts.length;
488
+ emit({
489
+ ...status,
490
+ live: {
491
+ scope: "managed-map-projection",
492
+ sources: "not-live-assessed",
493
+ matches,
494
+ projection: current,
495
+ },
496
+ ready: status.ready && matches,
497
+ });
498
+ return status.ready && matches && !status.truncated ? 0 : 3;
499
+ }
500
+ if (action === "context" && !status.ready && !values.draft) {
501
+ throw new Error(`Approved current map context is unavailable: ${status.reasons.join("; ")}. Use --draft to inspect explicitly unapproved context.`);
502
+ }
503
+ if (stringValue("out")) {
504
+ await writeOutput(context, stringValue("out"), status, from, true);
505
+ }
506
+ emit(status);
507
+ return status.ready && !status.truncated ? 0 : 3;
508
+ }
509
+ const expected = await savedProjection(context.root, projectionFile, map);
510
+ if (action === "publish" || action === "import") {
511
+ if (!plan.complete) {
512
+ throw new Error(`Native drawing plan is incomplete: ${plan.reasons.join("; ")}`);
513
+ }
514
+ const resolvedDrawing = await resolveMapDetails(context.root, drawingFromPlan(plan));
515
+ const drawing = expected ? preserveMapPresentation(resolvedDrawing, expected) : resolvedDrawing;
516
+ if (action === "import" && !expected) {
517
+ throw new Error("Native import requires the last saved native projection");
518
+ }
519
+ const pageId = stringValue("page-id") ?? expected?.pageId;
520
+ if (!pageId) {
521
+ throw new CommandArgumentError("Native map target requires --page-id ID");
522
+ }
523
+ const boardId = stringValue("board-id") ?? expected?.boardId ?? undefined;
524
+ if (expected && (expected.pageId !== pageId || (boardId && expected.boardId !== boardId))) {
525
+ throw new Error("Saved projection belongs to another native target");
526
+ }
527
+ await outputTarget(context, projectionFile, from);
528
+ if (values.apply) {
529
+ await outputTarget(context, `${projectionFile}.evidence.json`, from);
530
+ }
531
+ if (stringValue("out")) {
532
+ await outputTarget(context, stringValue("out"), from);
533
+ await outputTarget(context, `${stringValue("out")}.native.json`, from);
534
+ }
535
+ return context.withPenpot(async (client) => {
536
+ const target = await liveTarget(client, pageId, boardId);
537
+ if (expected && target.fileId !== expected.fileId) {
538
+ throw new Error("Saved projection belongs to another Penpot file");
539
+ }
540
+ if (action === "import") {
541
+ const current = await readProjection(client, target, drawing.mapId);
542
+ const imported = importMapProposal(drawing, current, expected);
543
+ const presentationOnly = !imported.changes.length &&
544
+ !imported.edgeChanges.length &&
545
+ !imported.title &&
546
+ !imported.metadataChanges &&
547
+ !imported.findings.length;
548
+ const proposal = presentationOnly
549
+ ? createMapPresentationProposal(map, drawing, current, expected)
550
+ : nativeProposalToMapProposal(map, drawing, current, expected);
551
+ if (proposal.kind === "map-presentation-proposal") {
552
+ await writeOutput(context, stringValue("out"), proposal, from);
553
+ }
554
+ else {
555
+ await writeOutput(context, `${stringValue("out")}.native.json`, {
556
+ schemaVersion: 1,
557
+ kind: "native-map-proposal-observation",
558
+ proposalHash: proposal.hash,
559
+ baselineFingerprint: expected.fingerprint,
560
+ projection: current,
561
+ }, from);
562
+ await writeMapProposal(context.root, stringValue("out"), proposal, {
563
+ redact: context.redact,
564
+ });
565
+ }
566
+ emit(proposal);
567
+ return 0;
568
+ }
569
+ const result = await publishMap(client, target, drawing, {
570
+ apply: values.apply === true,
571
+ expected,
572
+ journal: {
573
+ root: context.root,
574
+ redact: context.redact,
575
+ ...(stringValue("resume")
576
+ ? {
577
+ runId: stringValue("resume"),
578
+ resume: true,
579
+ }
580
+ : {}),
581
+ },
582
+ });
583
+ let evidence;
584
+ if (values.apply && result.report.verified) {
585
+ if (!result.image) {
586
+ throw new Error("Applied native map has no export evidence; projection was not saved");
587
+ }
588
+ evidence = await persistNativeEvidence(context, from, projectionFile, map, result.projection, result.image);
589
+ }
590
+ const { image: _image, ...report } = result;
591
+ emit({
592
+ ...report,
593
+ ...(evidence
594
+ ? {
595
+ image: evidence,
596
+ }
597
+ : {}),
598
+ });
599
+ return values.apply ? (result.report.verified ? 0 : 3) : 0;
600
+ });
601
+ }
602
+ const status = await mapStatus(context, map, reviewsFile, projectionFile);
603
+ const bundle = {
604
+ mapHash: hashMapContract(map),
605
+ markdown: renderMapMarkdown(map),
606
+ mermaid: renderMapMermaid(map),
607
+ plan,
608
+ };
609
+ const nativeArtifactPaths = [];
610
+ let nativeEvidence;
611
+ if (expected) {
612
+ if (expected.contractHash !== hashMapContract(map) || expected.conflicts.length) {
613
+ throw new Error("Published projection does not match the current map; publish and review it again");
614
+ }
615
+ const evidenceFile = `${projectionFile}.evidence.json`;
616
+ const evidence = JSON.parse((await readProjectBytes(context.root, evidenceFile, maxMapBytes)).toString("utf8"));
617
+ if (evidence.projectionHash !== expected.fingerprint ||
618
+ evidence.contractHash !== hashMapContract(map) ||
619
+ !evidence.image ||
620
+ sha256(await readProjectBytes(context.root, evidence.image.path, maximumImageBytes)) !==
621
+ evidence.image.sha256) {
622
+ throw new Error("Published map render evidence is stale or changed");
623
+ }
624
+ nativeArtifactPaths.push(projectionFile, evidenceFile, evidence.image.path);
625
+ nativeEvidence = {
626
+ projection: expected,
627
+ image: evidence.image,
628
+ evidenceFile,
629
+ };
630
+ }
631
+ if (!values.write) {
632
+ emit({
633
+ kind: "map-review-candidate",
634
+ ...bundle,
635
+ ...(nativeEvidence
636
+ ? {
637
+ native: nativeEvidence,
638
+ }
639
+ : {}),
640
+ context: status,
641
+ recorded: false,
642
+ });
643
+ return 0;
644
+ }
645
+ if (status.freshness !== "matches-supplied-inputs" ||
646
+ map.findings.some((finding) => finding.severity === "blocking" && finding.status === "open")) {
647
+ throw new Error("Map approval requires fresh declared sources and resolved blocking findings");
648
+ }
649
+ const renderFile = sibling(`map-evidence/${observationHash(bundle)}.json`);
650
+ await writeOutput(context, renderFile, bundle, from);
651
+ const artifactPaths = new Set([
652
+ from,
653
+ renderFile,
654
+ ...nativeArtifactPaths,
655
+ ...map.sources.flatMap((source) => source.path
656
+ ? [
657
+ source.path,
658
+ ]
659
+ : []),
660
+ ...(map.journey
661
+ ? [
662
+ map.journey.path,
663
+ ]
664
+ : []),
665
+ ]);
666
+ const artifacts = await Promise.all([
667
+ ...artifactPaths,
668
+ ].map(async (file) => ({
669
+ path: file,
670
+ sha256: sha256(await readProjectBytes(context.root, file, maximumImageBytes)),
671
+ })));
672
+ const review = createArtifactReview({
673
+ artifactKind: kind,
674
+ artifactId: map.id,
675
+ artifactHash: hashMapContract(map),
676
+ by: stringValue("by"),
677
+ note: stringValue("note"),
678
+ artifacts,
679
+ });
680
+ await writeArtifactReview(context.root, reviewsFile, review, {
681
+ redact: context.redact,
682
+ });
683
+ emit(review);
684
+ return 0;
685
+ }
@@ -0,0 +1,3 @@
1
+ import type { DesignCommandContext } from "./context.js";
2
+ /** CLI entrypoint: local planning stays offline and all live work uses a strictly pinned connection. */
3
+ export declare function runDesignReleaseCommand(args: string[], context: DesignCommandContext): Promise<number>;