openplanr 2.3.0 → 2.5.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 (127) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/dashboard/.vite/manifest.json +1 -1
  3. package/dist/dashboard/assets/{index-B74ddyXA.js → index-BaARCc1f.js} +2 -2
  4. package/dist/dashboard/assets/{index-B74ddyXA.js.map → index-BaARCc1f.js.map} +1 -1
  5. package/dist/dashboard/dashboard-manifest.json +8 -8
  6. package/dist/dashboard/index.html +1 -1
  7. package/dist/services/diagram-pipeline-service.d.ts +6 -0
  8. package/dist/services/diagram-pipeline-service.d.ts.map +1 -1
  9. package/dist/services/diagram-pipeline-service.js +45 -2
  10. package/dist/services/diagram-pipeline-service.js.map +1 -1
  11. package/lib/host-packages/adapter-registry.json +5 -5
  12. package/lib/host-packages/capability-map.json +1 -1
  13. package/lib/host-packages/claude/.claude-plugin/marketplace.json +2 -2
  14. package/lib/host-packages/claude/openplanr/.claude-plugin/plugin.json +1 -1
  15. package/lib/host-packages/claude/openplanr/.openplanr-content.json +81 -25
  16. package/lib/host-packages/claude/openplanr/skills/design/SKILL.md +3 -0
  17. package/lib/host-packages/claude/openplanr/skills/design/openplanr.skill.json +34 -1
  18. package/lib/host-packages/claude/openplanr/skills/design/references/handoff.md +35 -0
  19. package/lib/host-packages/claude/openplanr/skills/design/scripts/design.mjs +2227 -336
  20. package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/protocol/package.json +5 -1
  21. package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  22. package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  23. package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  24. package/lib/host-packages/claude/openplanr/skills/design-loop/openplanr.skill.json +33 -0
  25. package/lib/host-packages/claude/openplanr/skills/design-loop/references/handoff.md +35 -0
  26. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design.mjs +2227 -336
  27. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/protocol/package.json +5 -1
  28. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  29. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  30. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  31. package/lib/host-packages/claude/openplanr/skills/design-review/SKILL.md +3 -0
  32. package/lib/host-packages/claude/openplanr/skills/design-review/openplanr.skill.json +34 -1
  33. package/lib/host-packages/claude/openplanr/skills/design-review/references/handoff.md +35 -0
  34. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design.mjs +2227 -336
  35. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/protocol/package.json +5 -1
  36. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  37. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  38. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  39. package/lib/host-packages/claude/openplanr/skills/diagram/SKILL.md +12 -0
  40. package/lib/host-packages/claude/openplanr/skills/diagram/references/diagram-document.md +4 -0
  41. package/lib/host-packages/claude/openplanr/skills/diagram/references/diagram-intent-to-ir.md +4 -0
  42. package/lib/host-packages/claude/openplanr/skills/plan/SKILL.md +12 -0
  43. package/lib/host-packages/claude/openplanr/skills/plan/openplanr.skill.json +45 -1
  44. package/lib/host-packages/claude/openplanr/skills/plan/references/design-handoff.md +33 -0
  45. package/lib/host-packages/claude/openplanr/skills/plan/scripts/design.mjs +2227 -336
  46. package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/protocol/package.json +5 -1
  47. package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  48. package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  49. package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  50. package/lib/host-packages/claude/openplanr/skills/ship/SKILL.md +9 -0
  51. package/lib/host-packages/claude/openplanr/skills/ship/openplanr.skill.json +12 -1
  52. package/lib/host-packages/claude/openplanr/skills/ship/references/design-lineage.md +26 -0
  53. package/lib/host-packages/cursor/openplanr/.openplanr-content.json +76 -20
  54. package/lib/host-packages/cursor/openplanr/manifest.json +1 -1
  55. package/lib/host-packages/cursor/openplanr/rules/planr-design/references/handoff.md +35 -0
  56. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design.mjs +2227 -336
  57. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/protocol/package.json +5 -1
  58. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  59. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  60. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  61. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/references/handoff.md +35 -0
  62. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design.mjs +2227 -336
  63. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/protocol/package.json +5 -1
  64. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  65. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  66. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  67. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/references/handoff.md +35 -0
  68. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design.mjs +2227 -336
  69. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/protocol/package.json +5 -1
  70. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  71. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  72. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  73. package/lib/host-packages/cursor/openplanr/rules/planr-design-review.mdc +3 -0
  74. package/lib/host-packages/cursor/openplanr/rules/planr-design.mdc +3 -0
  75. package/lib/host-packages/cursor/openplanr/rules/planr-diagram/references/diagram-document.md +4 -0
  76. package/lib/host-packages/cursor/openplanr/rules/planr-diagram/references/diagram-intent-to-ir.md +4 -0
  77. package/lib/host-packages/cursor/openplanr/rules/planr-diagram.mdc +12 -0
  78. package/lib/host-packages/cursor/openplanr/rules/planr-plan/references/design-handoff.md +33 -0
  79. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design.mjs +2227 -336
  80. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/protocol/package.json +5 -1
  81. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  82. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  83. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  84. package/lib/host-packages/cursor/openplanr/rules/planr-plan.mdc +12 -0
  85. package/lib/host-packages/cursor/openplanr/rules/planr-ship/references/design-lineage.md +26 -0
  86. package/lib/host-packages/cursor/openplanr/rules/planr-ship.mdc +9 -0
  87. package/lib/host-packages/openai/.claude-plugin/marketplace.json +2 -2
  88. package/lib/host-packages/openai/openplanr/.codex-plugin/plugin.json +1 -1
  89. package/lib/host-packages/openai/openplanr/.openplanr-content.json +81 -25
  90. package/lib/host-packages/openai/openplanr/skills/design/SKILL.md +3 -0
  91. package/lib/host-packages/openai/openplanr/skills/design/openplanr.skill.json +34 -1
  92. package/lib/host-packages/openai/openplanr/skills/design/references/handoff.md +35 -0
  93. package/lib/host-packages/openai/openplanr/skills/design/scripts/design.mjs +2227 -336
  94. package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/protocol/package.json +5 -1
  95. package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  96. package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  97. package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  98. package/lib/host-packages/openai/openplanr/skills/design-loop/openplanr.skill.json +33 -0
  99. package/lib/host-packages/openai/openplanr/skills/design-loop/references/handoff.md +35 -0
  100. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design.mjs +2227 -336
  101. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/protocol/package.json +5 -1
  102. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  103. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  104. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  105. package/lib/host-packages/openai/openplanr/skills/design-review/SKILL.md +3 -0
  106. package/lib/host-packages/openai/openplanr/skills/design-review/openplanr.skill.json +34 -1
  107. package/lib/host-packages/openai/openplanr/skills/design-review/references/handoff.md +35 -0
  108. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design.mjs +2227 -336
  109. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/protocol/package.json +5 -1
  110. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  111. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  112. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  113. package/lib/host-packages/openai/openplanr/skills/diagram/SKILL.md +12 -0
  114. package/lib/host-packages/openai/openplanr/skills/diagram/references/diagram-document.md +4 -0
  115. package/lib/host-packages/openai/openplanr/skills/diagram/references/diagram-intent-to-ir.md +4 -0
  116. package/lib/host-packages/openai/openplanr/skills/plan/SKILL.md +12 -0
  117. package/lib/host-packages/openai/openplanr/skills/plan/openplanr.skill.json +45 -1
  118. package/lib/host-packages/openai/openplanr/skills/plan/references/design-handoff.md +33 -0
  119. package/lib/host-packages/openai/openplanr/skills/plan/scripts/design.mjs +2227 -336
  120. package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/protocol/package.json +5 -1
  121. package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  122. package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  123. package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  124. package/lib/host-packages/openai/openplanr/skills/ship/SKILL.md +9 -0
  125. package/lib/host-packages/openai/openplanr/skills/ship/openplanr.skill.json +12 -1
  126. package/lib/host-packages/openai/openplanr/skills/ship/references/design-lineage.md +26 -0
  127. package/package.json +2 -2
@@ -2,8 +2,8 @@
2
2
 
3
3
  // packages/design/lib/design/utility.mjs
4
4
  import { spawn } from "node:child_process";
5
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync15, realpathSync as realpathSync6, writeFileSync as writeFileSync7 } from "node:fs";
6
- import { dirname as dirname12, join as join11, resolve as resolve6 } from "node:path";
5
+ import { existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync17, realpathSync as realpathSync7, writeFileSync as writeFileSync9 } from "node:fs";
6
+ import { dirname as dirname14, join as join13, resolve as resolve7 } from "node:path";
7
7
  import { fileURLToPath as fileURLToPath3 } from "node:url";
8
8
 
9
9
  // packages/design/lib/design/document.mjs
@@ -189,7 +189,7 @@ function normalizeArtifactInspection(value, viewport) {
189
189
  const exact = (entry, keys) => plain(entry) && Object.keys(entry).length === keys.length && Object.keys(entry).every((key) => keys.includes(key));
190
190
  if (!exact(value, ["tagName", "anchor", "rect", "viewport", "styles", "accessibility"])) return null;
191
191
  const tagName = own(value, "tagName");
192
- const anchor = own(value, "anchor");
192
+ const anchor2 = own(value, "anchor");
193
193
  const rect = own(value, "rect");
194
194
  const size = own(value, "viewport");
195
195
  const styles = own(value, "styles");
@@ -199,15 +199,15 @@ function normalizeArtifactInspection(value, viewport) {
199
199
  if (!["width", "height"].every((key) => Number.isInteger(own(size, key)) && own(size, key) >= 1 && own(size, key) <= 16384 && own(size, key) === viewport?.[key])) return null;
200
200
  if (!["x", "y", "width", "height"].every((key) => typeof own(rect, key) === "number" && Number.isFinite(own(rect, key)) && own(rect, key) >= 0)) return null;
201
201
  if (rect.x + rect.width > size.width || rect.y + rect.height > size.height) return null;
202
- if (anchor !== null && (!exact(anchor, own(anchor, "screen") === void 0 ? ["planrId"] : ["planrId", "screen"]) || typeof own(anchor, "planrId") !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,511}$/.test(anchor.planrId) || own(anchor, "screen") !== void 0 && (typeof anchor.screen !== "string" || !/^[^\u0000-\u001f\u007f]{1,128}$/.test(anchor.screen)))) return null;
202
+ if (anchor2 !== null && (!exact(anchor2, own(anchor2, "screen") === void 0 ? ["planrId"] : ["planrId", "screen"]) || typeof own(anchor2, "planrId") !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,511}$/.test(anchor2.planrId) || own(anchor2, "screen") !== void 0 && (typeof anchor2.screen !== "string" || !/^[^\u0000-\u001f\u007f]{1,128}$/.test(anchor2.screen)))) return null;
203
203
  if (!exact(styles, ARTIFACT_INSPECTION_PROPERTIES) || !ARTIFACT_INSPECTION_PROPERTIES.every((key) => {
204
- const text3 = own(styles, key);
205
- return typeof text3 === "string" && text3.length <= 256 && !/[\u0000-\u001f\u007f]/.test(text3) && !/(?:url\s*\(|https?:|file:)/i.test(text3);
204
+ const text4 = own(styles, key);
205
+ return typeof text4 === "string" && text4.length <= 256 && !/[\u0000-\u001f\u007f]/.test(text4) && !/(?:url\s*\(|https?:|file:)/i.test(text4);
206
206
  })) return null;
207
207
  if (!exact(accessibility, ["role", "ariaLabel", "alt", "tabIndex", "disabled"]) || !["role", "ariaLabel", "alt"].every((key) => typeof own(accessibility, key) === "string" && own(accessibility, key).length <= 256 && !/[\u0000-\u001f\u007f]/.test(own(accessibility, key))) || !Number.isInteger(own(accessibility, "tabIndex")) || accessibility.tabIndex < -1 || accessibility.tabIndex > 32767 || typeof own(accessibility, "disabled") !== "boolean") return null;
208
208
  return Object.freeze({
209
209
  tagName,
210
- anchor: anchor === null ? null : Object.freeze({ ...anchor }),
210
+ anchor: anchor2 === null ? null : Object.freeze({ ...anchor2 }),
211
211
  rect: Object.freeze({ ...rect }),
212
212
  viewport: Object.freeze({ ...size }),
213
213
  styles: Object.freeze({ ...styles }),
@@ -220,19 +220,19 @@ function normalizeArtifactThumbnail(value) {
220
220
  if (typeof dataUrl !== "string" || dataUrl.length > ARTIFACT_THUMBNAIL_MAX_DATA_URL || !/^data:image\/png;base64,[A-Za-z0-9+/]+=*$/.test(dataUrl) || !Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width > ARTIFACT_THUMBNAIL_MAX_EDGE || height > ARTIFACT_THUMBNAIL_MAX_EDGE || typeof label !== "string" || label.length < 1 || label.length > 128) return null;
221
221
  return Object.freeze({ dataUrl, width, height, label });
222
222
  }
223
- function normalizeArtifactBridgeToolResult(requestType, message, viewport) {
223
+ function normalizeArtifactBridgeToolResult(requestType, message2, viewport) {
224
224
  const base = ["channel", "schemaVersion", "type", "nonce", "artifactId", "requestId"];
225
- const exact = (keys) => message && typeof message === "object" && !Array.isArray(message) && Object.keys(message).length === keys.length && Object.keys(message).every((key) => keys.includes(key)) && Object.values(Object.getOwnPropertyDescriptors(message)).every((descriptor) => Object.hasOwn(descriptor, "value"));
225
+ const exact = (keys) => message2 && typeof message2 === "object" && !Array.isArray(message2) && Object.keys(message2).length === keys.length && Object.keys(message2).every((key) => keys.includes(key)) && Object.values(Object.getOwnPropertyDescriptors(message2)).every((descriptor) => Object.hasOwn(descriptor, "value"));
226
226
  if (requestType === "inspect.point" || requestType === "inspect.anchor") {
227
- if (exact(base) && message.type === "inspect.miss") return { valid: true, value: null };
228
- if (!exact([...base, "inspection"]) || message.type !== "inspect.result") return { valid: false };
229
- const value = normalizeArtifactInspection(message.inspection, viewport);
227
+ if (exact(base) && message2.type === "inspect.miss") return { valid: true, value: null };
228
+ if (!exact([...base, "inspection"]) || message2.type !== "inspect.result") return { valid: false };
229
+ const value = normalizeArtifactInspection(message2.inspection, viewport);
230
230
  return value ? { valid: true, value } : { valid: false };
231
231
  }
232
232
  if (requestType === "thumbnail.request") {
233
- if (exact([...base, "reason"]) && message.type === "thumbnail.error" && typeof message.reason === "string" && message.reason.length <= 256) return { valid: true, value: null };
234
- if (!exact([...base, "dataUrl", "width", "height", "label"]) || message.type !== "thumbnail.result") return { valid: false };
235
- const value = normalizeArtifactThumbnail({ dataUrl: message.dataUrl, width: message.width, height: message.height, label: message.label });
233
+ if (exact([...base, "reason"]) && message2.type === "thumbnail.error" && typeof message2.reason === "string" && message2.reason.length <= 256) return { valid: true, value: null };
234
+ if (!exact([...base, "dataUrl", "width", "height", "label"]) || message2.type !== "thumbnail.result") return { valid: false };
235
+ const value = normalizeArtifactThumbnail({ dataUrl: message2.dataUrl, width: message2.width, height: message2.height, label: message2.label });
236
236
  return value ? { valid: true, value } : { valid: false };
237
237
  }
238
238
  return { valid: false };
@@ -253,7 +253,7 @@ function createArtifactBridgeTools(document2, window2) {
253
253
  const Serializer = window2.XMLSerializer;
254
254
  const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
255
255
  const get = (element2, key) => attr2.call(element2, key);
256
- const validId = (id3) => typeof id3 === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,511}$/.test(id3);
256
+ const validId = (id4) => typeof id4 === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,511}$/.test(id4);
257
257
  const screen = (element2) => {
258
258
  const owner = closest.call(element2, "[data-planr-screen]");
259
259
  const value = owner && get(owner, "data-planr-screen");
@@ -295,12 +295,12 @@ function createArtifactBridgeTools(document2, window2) {
295
295
  if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || y < 0 || x > window2.innerWidth || y > window2.innerHeight) return null;
296
296
  return inspectElement(fromPoint(x, y));
297
297
  },
298
- inspect(anchor) {
299
- if (!anchor || !validId(anchor.planrId) || Object.keys(anchor).some((key) => !["planrId", "screen"].includes(key)) || anchor.screen !== void 0 && (typeof anchor.screen !== "string" || !/^[^\u0000-\u001f\u007f]{1,128}$/.test(anchor.screen))) return null;
298
+ inspect(anchor2) {
299
+ if (!anchor2 || !validId(anchor2.planrId) || Object.keys(anchor2).some((key) => !["planrId", "screen"].includes(key)) || anchor2.screen !== void 0 && (typeof anchor2.screen !== "string" || !/^[^\u0000-\u001f\u007f]{1,128}$/.test(anchor2.screen))) return null;
300
300
  let count = 0;
301
301
  for (const element2 of query("[data-planr-id]")) {
302
302
  if (++count > 1e4) return null;
303
- if (get(element2, "data-planr-id") === anchor.planrId && (anchor.screen === void 0 || screen(element2) === anchor.screen)) return inspectElement(element2);
303
+ if (get(element2, "data-planr-id") === anchor2.planrId && (anchor2.screen === void 0 || screen(element2) === anchor2.screen)) return inspectElement(element2);
304
304
  }
305
305
  return null;
306
306
  },
@@ -346,19 +346,19 @@ function createArtifactBridgeTools(document2, window2) {
346
346
  if (source.nodeType !== 1 || source.tagName !== "TEXTAREA") for (let child = source.firstChild; child; child = child.nextSibling) append.call(target, cloneStyled(child));
347
347
  return target;
348
348
  };
349
- const clone3 = cloneStyled(document2.documentElement);
350
- setAttribute.call(clone3, "xmlns", "http://www.w3.org/1999/xhtml");
351
- const markup = serialize3.call(new Serializer(), clone3);
349
+ const clone7 = cloneStyled(document2.documentElement);
350
+ setAttribute.call(clone7, "xmlns", "http://www.w3.org/1999/xhtml");
351
+ const markup = serialize3.call(new Serializer(), clone7);
352
352
  if (markup.length > 4 * 1024 * 1024) throw new Error("Thumbnail markup limit exceeded.");
353
353
  const scale = Math.min(1, ARTIFACT_THUMBNAIL_MAX_EDGE / Math.max(width, height));
354
354
  const outputWidth = Math.max(1, Math.round(width * scale)), outputHeight = Math.max(1, Math.round(height * scale));
355
355
  const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' + outputWidth + '" height="' + outputHeight + '" viewBox="0 0 ' + width + " " + height + '"><foreignObject width="' + width + '" height="' + height + '">' + markup + "</foreignObject></svg>";
356
356
  image = new Image();
357
- await new Promise((resolve7, reject) => {
357
+ await new Promise((resolve8, reject) => {
358
358
  const timer = setTimeout(() => reject(new Error("Thumbnail capture timed out.")), Math.max(1, deadline - Date.now()));
359
359
  image.onload = () => {
360
360
  clearTimeout(timer);
361
- resolve7();
361
+ resolve8();
362
362
  };
363
363
  image.onerror = () => {
364
364
  clearTimeout(timer);
@@ -582,13 +582,13 @@ var Preprocessor = class {
582
582
  return this.droppedBufferSize + this.pos;
583
583
  }
584
584
  getError(code, cpOffset) {
585
- const { line, col, offset } = this;
585
+ const { line: line2, col, offset } = this;
586
586
  const startCol = col + cpOffset;
587
587
  const startOffset = offset + cpOffset;
588
588
  return {
589
589
  code,
590
- startLine: line,
591
- endLine: line,
590
+ startLine: line2,
591
+ endLine: line2,
592
592
  startCol,
593
593
  endCol: startCol,
594
594
  startOffset,
@@ -4818,22 +4818,22 @@ var defaultTreeAdapter = {
4818
4818
  node.parentNode = null;
4819
4819
  }
4820
4820
  },
4821
- insertText(parentNode, text3) {
4821
+ insertText(parentNode, text4) {
4822
4822
  if (parentNode.childNodes.length > 0) {
4823
4823
  const prevNode = parentNode.childNodes[parentNode.childNodes.length - 1];
4824
4824
  if (defaultTreeAdapter.isTextNode(prevNode)) {
4825
- prevNode.value += text3;
4825
+ prevNode.value += text4;
4826
4826
  return;
4827
4827
  }
4828
4828
  }
4829
- defaultTreeAdapter.appendChild(parentNode, defaultTreeAdapter.createTextNode(text3));
4829
+ defaultTreeAdapter.appendChild(parentNode, defaultTreeAdapter.createTextNode(text4));
4830
4830
  },
4831
- insertTextBefore(parentNode, text3, referenceNode) {
4831
+ insertTextBefore(parentNode, text4, referenceNode) {
4832
4832
  const prevNode = parentNode.childNodes[parentNode.childNodes.indexOf(referenceNode) - 1];
4833
4833
  if (prevNode && defaultTreeAdapter.isTextNode(prevNode)) {
4834
- prevNode.value += text3;
4834
+ prevNode.value += text4;
4835
4835
  } else {
4836
- defaultTreeAdapter.insertBefore(parentNode, defaultTreeAdapter.createTextNode(text3), referenceNode);
4836
+ defaultTreeAdapter.insertBefore(parentNode, defaultTreeAdapter.createTextNode(text4), referenceNode);
4837
4837
  }
4838
4838
  },
4839
4839
  adoptAttributes(recipient, attrs) {
@@ -5800,9 +5800,9 @@ var Parser = class {
5800
5800
  }
5801
5801
  //Special elements
5802
5802
  /** @protected */
5803
- _isSpecialElement(element2, id3) {
5803
+ _isSpecialElement(element2, id4) {
5804
5804
  const ns = this.treeAdapter.getNamespaceURI(element2);
5805
- return SPECIAL_ELEMENTS[ns].has(id3);
5805
+ return SPECIAL_ELEMENTS[ns].has(id4);
5806
5806
  }
5807
5807
  /** @internal */
5808
5808
  onCharacter(token) {
@@ -8534,8 +8534,8 @@ var PROTOCOL_ERROR_CODES = Object.freeze({
8534
8534
  SORT_ORDER_INVALID: "E_PROTOCOL_SORT_ORDER_INVALID"
8535
8535
  });
8536
8536
  var PipelineError = class extends Error {
8537
- constructor(code, message, fix = "", details = void 0) {
8538
- super(message);
8537
+ constructor(code, message2, fix = "", details = void 0) {
8538
+ super(message2);
8539
8539
  this.name = "PipelineError";
8540
8540
  this.code = code;
8541
8541
  this.fix = fix;
@@ -8599,8 +8599,8 @@ var URL_ATTRIBUTES = /* @__PURE__ */ new Set([
8599
8599
  "xlink:href"
8600
8600
  ]);
8601
8601
  var REMOTE_URL_RE = /^(?:https?:|file:|ftp:|wss?:|\/\/)/i;
8602
- function pipelineError(code, message, details) {
8603
- return new PipelineError(code, message, "", details);
8602
+ function pipelineError(code, message2, details) {
8603
+ return new PipelineError(code, message2, "", details);
8604
8604
  }
8605
8605
  function getAttr(node, name) {
8606
8606
  return node.attrs?.find((attribute) => attribute.name.toLowerCase() === name)?.value;
@@ -8709,7 +8709,7 @@ function assertSandboxableTree(document2, { allowLocalForms = false } = {}) {
8709
8709
  }
8710
8710
  }
8711
8711
  function artifactGuardAndBridgeSource({ artifactId, nonce, parentOrigin }) {
8712
- const contract = JSON.stringify({
8712
+ const contract2 = JSON.stringify({
8713
8713
  channel: ARTIFACT_BRIDGE_CHANNEL,
8714
8714
  schemaVersion: ARTIFACT_BRIDGE_VERSION,
8715
8715
  artifactId,
@@ -8746,7 +8746,7 @@ function artifactGuardAndBridgeSource({ artifactId, nonce, parentOrigin }) {
8746
8746
  'use strict';
8747
8747
  const injectedScript=document.currentScript;
8748
8748
  injectedScript?.remove();
8749
- const contract=${contract};
8749
+ const contract=${contract2};
8750
8750
  ${renderArtifactBridgeToolsSource()}
8751
8751
  const inspectionTools=createArtifactBridgeTools(document,globalThis);
8752
8752
  const postToParent=parent.postMessage.bind(parent);
@@ -9586,8 +9586,8 @@ var MAX_REPLIES = 1e4;
9586
9586
  var MAX_PASTE_BYTES = 5 * 1024 * 1024;
9587
9587
  var ARTIFACT_PRESENTATIONS = Object.freeze(["document", "canvas"]);
9588
9588
  var ARTIFACT_DOCUMENT_MAX_HEIGHT = 262144;
9589
- function invalid(message, details = void 0) {
9590
- throw new PipelineError(ARTIFACT_ERROR_CODES.ENVELOPE_INVALID, message, "", details);
9589
+ function invalid(message2, details = void 0) {
9590
+ throw new PipelineError(ARTIFACT_ERROR_CODES.ENVELOPE_INVALID, message2, "", details);
9591
9591
  }
9592
9592
  function assertBoundedString(value, label, { min = 0, max, pattern } = {}) {
9593
9593
  if (typeof value !== "string" || value.length < min || max !== void 0 && value.length > max || pattern && !pattern.test(value)) {
@@ -9642,46 +9642,46 @@ function normalizeArtifact(artifact) {
9642
9642
  if (!artifact || typeof artifact !== "object") {
9643
9643
  throw new PipelineError(ARTIFACT_ERROR_CODES.ENVELOPE_INVALID, "Each artifact must be an object.");
9644
9644
  }
9645
- const id3 = artifact.id;
9646
- const title = artifact.title;
9647
- if (typeof id3 !== "string" || !ID_RE2.test(id3) || id3.length > 128) {
9648
- throw new PipelineError(ARTIFACT_ERROR_CODES.ENVELOPE_INVALID, `Invalid artifact id: ${String(id3)}`);
9645
+ const id4 = artifact.id;
9646
+ const title2 = artifact.title;
9647
+ if (typeof id4 !== "string" || !ID_RE2.test(id4) || id4.length > 128) {
9648
+ throw new PipelineError(ARTIFACT_ERROR_CODES.ENVELOPE_INVALID, `Invalid artifact id: ${String(id4)}`);
9649
9649
  }
9650
- if (typeof title !== "string" || title.length === 0 || title.length > 512) {
9651
- throw new PipelineError(ARTIFACT_ERROR_CODES.ENVELOPE_INVALID, `Artifact ${id3} requires a title.`);
9650
+ if (typeof title2 !== "string" || title2.length === 0 || title2.length > 512) {
9651
+ throw new PipelineError(ARTIFACT_ERROR_CODES.ENVELOPE_INVALID, `Artifact ${id4} requires a title.`);
9652
9652
  }
9653
9653
  const html = normalizeUtf8Text(artifact.html);
9654
9654
  if (html.length === 0) {
9655
- throw new PipelineError(ARTIFACT_ERROR_CODES.ENVELOPE_INVALID, `Artifact ${id3} HTML is empty.`);
9655
+ throw new PipelineError(ARTIFACT_ERROR_CODES.ENVELOPE_INVALID, `Artifact ${id4} HTML is empty.`);
9656
9656
  }
9657
9657
  if (Buffer.byteLength(html, "utf8") > MAX_ARTIFACT_HTML_BYTES) {
9658
- invalid(`Artifact ${id3} exceeds ${MAX_ARTIFACT_HTML_BYTES} UTF-8 bytes.`);
9658
+ invalid(`Artifact ${id4} exceeds ${MAX_ARTIFACT_HTML_BYTES} UTF-8 bytes.`);
9659
9659
  }
9660
- const sha256 = digestArtifact(html);
9661
- if (artifact.sha256 !== void 0 && artifact.sha256 !== sha256) {
9660
+ const sha2562 = digestArtifact(html);
9661
+ if (artifact.sha256 !== void 0 && artifact.sha256 !== sha2562) {
9662
9662
  throw new PipelineError(
9663
9663
  ARTIFACT_ERROR_CODES.ENVELOPE_INVALID,
9664
- `Artifact ${id3} digest does not match its canonical bundled HTML.`,
9664
+ `Artifact ${id4} digest does not match its canonical bundled HTML.`,
9665
9665
  "",
9666
- { expected: sha256, actual: artifact.sha256 }
9666
+ { expected: sha2562, actual: artifact.sha256 }
9667
9667
  );
9668
9668
  }
9669
9669
  const colorScheme = artifact.colorScheme ?? "light";
9670
9670
  if (!["light", "dark"].includes(colorScheme)) {
9671
- throw new PipelineError(ARTIFACT_ERROR_CODES.ENVELOPE_INVALID, `Artifact ${id3} has an invalid color scheme.`);
9671
+ throw new PipelineError(ARTIFACT_ERROR_CODES.ENVELOPE_INVALID, `Artifact ${id4} has an invalid color scheme.`);
9672
9672
  }
9673
9673
  return {
9674
- id: id3,
9674
+ id: id4,
9675
9675
  kind: "html",
9676
- title,
9677
- sha256,
9676
+ title: title2,
9677
+ sha256: sha2562,
9678
9678
  html,
9679
9679
  viewport: normalizeViewport(artifact.viewport),
9680
9680
  colorScheme
9681
9681
  };
9682
9682
  }
9683
9683
  function normalizeViewer(viewer, artifacts) {
9684
- const ids = new Set(artifacts.map(({ id: id3 }) => id3));
9684
+ const ids = new Set(artifacts.map(({ id: id4 }) => id4));
9685
9685
  const normalized = {
9686
9686
  mode: viewer?.mode ?? (artifacts.length > 1 ? "variants" : "single"),
9687
9687
  activeArtifactId: viewer?.activeArtifactId ?? artifacts[0].id
@@ -9777,7 +9777,7 @@ function validateArtifactEnvelope(envelope) {
9777
9777
  if (!Array.isArray(envelope.artifacts) || envelope.artifacts.length < 1 || envelope.artifacts.length > MAX_ARTIFACTS) {
9778
9778
  invalid(`Envelope requires 1 through ${MAX_ARTIFACTS} artifacts.`);
9779
9779
  }
9780
- const ids = envelope.artifacts.map(({ id: id3 }) => id3);
9780
+ const ids = envelope.artifacts.map(({ id: id4 }) => id4);
9781
9781
  if (new Set(ids).size !== ids.length) {
9782
9782
  throw new PipelineError(ARTIFACT_ERROR_CODES.ENVELOPE_INVALID, "Artifact ids must be unique.");
9783
9783
  }
@@ -9830,7 +9830,7 @@ function createArtifactEnvelope({ artifacts, viewer, review } = {}) {
9830
9830
  }
9831
9831
  normalizedArtifacts.push(normalized);
9832
9832
  }
9833
- if (new Set(normalizedArtifacts.map(({ id: id3 }) => id3)).size !== normalizedArtifacts.length) {
9833
+ if (new Set(normalizedArtifacts.map(({ id: id4 }) => id4)).size !== normalizedArtifacts.length) {
9834
9834
  throw new PipelineError(ARTIFACT_ERROR_CODES.ENVELOPE_INVALID, "Artifact ids must be unique.");
9835
9835
  }
9836
9836
  const envelope = {
@@ -9859,8 +9859,8 @@ import {
9859
9859
  } from "node:fs";
9860
9860
  import { dirname as dirname2, join as join3 } from "node:path";
9861
9861
  var LOOPBACK_HOST = "127.0.0.1";
9862
- function codedError(code, message, details) {
9863
- const error = new Error(message);
9862
+ function codedError(code, message2, details) {
9863
+ const error = new Error(message2);
9864
9864
  error.code = code;
9865
9865
  if (details !== void 0) error.details = details;
9866
9866
  return error;
@@ -10125,10 +10125,10 @@ function bundleLocalDocument({ root: inputRoot, source, sharedStyles = [], scree
10125
10125
  let cssExpansions = 0;
10126
10126
  let bytes = 0;
10127
10127
  function read(path, from = root) {
10128
- const checked = readSource?.(path, from);
10129
- const file = checked?.file ?? resolveLocalDocumentFile(root, path, from);
10128
+ const checked2 = readSource?.(path, from);
10129
+ const file = checked2?.file ?? resolveLocalDocumentFile(root, path, from);
10130
10130
  if (!files.has(file)) {
10131
- const value = checked?.value ?? readFileSync3(file);
10131
+ const value = checked2?.value ?? readFileSync3(file);
10132
10132
  bytes += value.length;
10133
10133
  if (bytes > maxBytes || files.size >= 1e3) throw new Error("Design source exceeds the local asset budget.");
10134
10134
  files.set(file, value);
@@ -10465,25 +10465,25 @@ function validateDesignDocument(value) {
10465
10465
  seen.add(item2.id);
10466
10466
  }
10467
10467
  }
10468
- const screenIds = new Set(value.screens.map(({ id: id3 }) => id3));
10468
+ const screenIds = new Set(value.screens.map(({ id: id4 }) => id4));
10469
10469
  const orderedIds = new Set(value.screenOrder);
10470
- for (const id3 of value.screenOrder) {
10471
- if (!screenIds.has(id3)) errors.push(`$.screenOrder: unknown screen '${id3}'`);
10470
+ for (const id4 of value.screenOrder) {
10471
+ if (!screenIds.has(id4)) errors.push(`$.screenOrder: unknown screen '${id4}'`);
10472
10472
  }
10473
- for (const id3 of screenIds) {
10474
- if (!orderedIds.has(id3)) errors.push(`$.screenOrder: missing screen '${id3}'`);
10473
+ for (const id4 of screenIds) {
10474
+ if (!orderedIds.has(id4)) errors.push(`$.screenOrder: missing screen '${id4}'`);
10475
10475
  }
10476
10476
  for (const [index, flow] of (value.flows ?? []).entries()) {
10477
- for (const id3 of flow.screens) {
10478
- if (!screenIds.has(id3)) errors.push(`$.flows[${index}].screens: unknown screen '${id3}'`);
10477
+ for (const id4 of flow.screens) {
10478
+ if (!screenIds.has(id4)) errors.push(`$.flows[${index}].screens: unknown screen '${id4}'`);
10479
10479
  }
10480
10480
  }
10481
10481
  for (const [index, variant] of value.variants.entries()) {
10482
- for (const id3 of Object.keys(variant.sources ?? {})) {
10483
- if (!screenIds.has(id3)) errors.push(`$.variants[${index}].sources: unknown screen '${id3}'`);
10482
+ for (const id4 of Object.keys(variant.sources ?? {})) {
10483
+ if (!screenIds.has(id4)) errors.push(`$.variants[${index}].sources: unknown screen '${id4}'`);
10484
10484
  }
10485
10485
  }
10486
- const selected = value.variants.find(({ id: id3 }) => id3 === value.selectedVariant);
10486
+ const selected = value.variants.find(({ id: id4 }) => id4 === value.selectedVariant);
10487
10487
  if (!selected) errors.push(`$.selectedVariant: unknown variant '${value.selectedVariant}'`);
10488
10488
  else if (selected.status !== "ready") errors.push("$.selectedVariant: selected variant must be ready");
10489
10489
  for (const [index, spacing] of (value.designSystem?.spacing ?? []).entries()) {
@@ -10788,9 +10788,9 @@ var DESIGN_REVIEW_BUNDLE_SCHEMA = schema("design-review-bundle", {
10788
10788
  verification: { type: ["object", "null"], additionalProperties: false, properties: { status: { enum: ["verified", "unverified", "failed", "pending"] } } }
10789
10789
  }, ["kind", "schemaVersion", "design", "envelope", "entries", "revision"]);
10790
10790
  DESIGN_REVIEW_BUNDLE_SCHEMA.$defs = structuredClone(DESIGN_DOCUMENT_SCHEMA.$defs);
10791
- function assertWorkspaceContract(value, contract) {
10792
- const errors = validateJson(value, contract);
10793
- if (errors.length) throw new TypeError(`Invalid ${contract["x-openplanr-contract"].id}: ${errors.slice(0, 5).map(({ path, detail }) => `${path}: ${detail}`).join("; ")}`);
10791
+ function assertWorkspaceContract(value, contract2) {
10792
+ const errors = validateJson(value, contract2);
10793
+ if (errors.length) throw new TypeError(`Invalid ${contract2["x-openplanr-contract"].id}: ${errors.slice(0, 5).map(({ path, detail }) => `${path}: ${detail}`).join("; ")}`);
10794
10794
  return value;
10795
10795
  }
10796
10796
  var DESIGN_WORKSPACE_SCHEMAS = Object.freeze({
@@ -10881,9 +10881,9 @@ Object.assign(DESIGN_REVIEW_METADATA_PAYLOAD_V11_SCHEMA.properties, {
10881
10881
  schemaVersion: { const: "1.1.0" },
10882
10882
  category: { enum: ["question", "suggestion", "change-request", "blocker"] }
10883
10883
  });
10884
- function assertReviewExperience(value, contract) {
10885
- const errors = validateJson(value, contract);
10886
- if (errors.length) throw new TypeError(`Invalid ${contract["x-openplanr-contract"]?.id ?? "review data"}: ${errors.slice(0, 4).map((item2) => `${item2.path} ${item2.detail}`).join("; ")}`);
10884
+ function assertReviewExperience(value, contract2) {
10885
+ const errors = validateJson(value, contract2);
10886
+ if (errors.length) throw new TypeError(`Invalid ${contract2["x-openplanr-contract"]?.id ?? "review data"}: ${errors.slice(0, 4).map((item2) => `${item2.path} ${item2.detail}`).join("; ")}`);
10887
10887
  return value;
10888
10888
  }
10889
10889
  function assertDesignReviewMetadata(value) {
@@ -10933,7 +10933,7 @@ function loadReviewContext(root, document2, { readSource } = {}) {
10933
10933
  for (const component of context.implementation.components) {
10934
10934
  if (ids.has(component.id)) throw new Error("Review component identities must be unique.");
10935
10935
  ids.add(component.id);
10936
- if (component.screenIds?.some((id3) => !screens.has(id3))) throw new Error(`Component ${component.id} references an unknown screen.`);
10936
+ if (component.screenIds?.some((id4) => !screens.has(id4))) throw new Error(`Component ${component.id} references an unknown screen.`);
10937
10937
  }
10938
10938
  if (/(?:file:\/\/|\/(?:Users|home|private|tmp|var|etc|opt|Volumes)\/|[A-Za-z]:\\\\|\\\\\\\\)/u.test(JSON.stringify(context))) throw new Error("Review context contains a local filesystem path. Use share-safe implementation guidance.");
10939
10939
  return context;
@@ -11452,8 +11452,8 @@ var ARTIFACT_THEME_ERROR_CODES = Object.freeze({
11452
11452
  CONTRAST: "E_ARTIFACT_THEME_CONTRAST"
11453
11453
  });
11454
11454
  var ArtifactThemeError = class extends Error {
11455
- constructor(code, message, details = {}) {
11456
- super(message);
11455
+ constructor(code, message2, details = {}) {
11456
+ super(message2);
11457
11457
  this.name = "ArtifactThemeError";
11458
11458
  this.code = code;
11459
11459
  this.details = details;
@@ -11518,12 +11518,12 @@ function readJson(path, label) {
11518
11518
  );
11519
11519
  }
11520
11520
  }
11521
- function schemaCode(issue) {
11522
- if (issue.rule === "required") return ARTIFACT_THEME_ERROR_CODES.TOKEN_MISSING;
11523
- if (issue.rule === "additionalProperties") return ARTIFACT_THEME_ERROR_CODES.TOKEN_UNKNOWN;
11524
- if (issue.rule === "pattern") return ARTIFACT_THEME_ERROR_CODES.FORMAT;
11525
- if (issue.path.includes(".layout.motion")) return ARTIFACT_THEME_ERROR_CODES.MOTION;
11526
- if (issue.path.includes(".layout.")) return ARTIFACT_THEME_ERROR_CODES.LAYOUT;
11521
+ function schemaCode(issue2) {
11522
+ if (issue2.rule === "required") return ARTIFACT_THEME_ERROR_CODES.TOKEN_MISSING;
11523
+ if (issue2.rule === "additionalProperties") return ARTIFACT_THEME_ERROR_CODES.TOKEN_UNKNOWN;
11524
+ if (issue2.rule === "pattern") return ARTIFACT_THEME_ERROR_CODES.FORMAT;
11525
+ if (issue2.path.includes(".layout.motion")) return ARTIFACT_THEME_ERROR_CODES.MOTION;
11526
+ if (issue2.path.includes(".layout.")) return ARTIFACT_THEME_ERROR_CODES.LAYOUT;
11527
11527
  return ARTIFACT_THEME_ERROR_CODES.SCHEMA;
11528
11528
  }
11529
11529
  function assertSchema(theme, schema3) {
@@ -11596,8 +11596,8 @@ function assertContrast(themes) {
11596
11596
  }
11597
11597
  }
11598
11598
  function validateArtifactTheme(theme, { schema: schema3 } = {}) {
11599
- const contract = schema3 ?? readJson(ARTIFACT_THEME_SCHEMA_PATH, "artifact theme schema");
11600
- assertSchema(theme, contract);
11599
+ const contract2 = schema3 ?? readJson(ARTIFACT_THEME_SCHEMA_PATH, "artifact theme schema");
11600
+ assertSchema(theme, contract2);
11601
11601
  assertLayout(theme.layout);
11602
11602
  assertMotion(theme.layout);
11603
11603
  assertContrast(theme.themes);
@@ -11772,9 +11772,9 @@ function resolveArtifactPresentation(value, { mode = "single", artifactCount = 1
11772
11772
  }
11773
11773
  function normalizeArtifact2(value, index) {
11774
11774
  const artifact = value && typeof value === "object" ? value : {};
11775
- const id3 = plainText(artifact.id, `artifact-${index + 1}`) || `artifact-${index + 1}`;
11775
+ const id4 = plainText(artifact.id, `artifact-${index + 1}`) || `artifact-${index + 1}`;
11776
11776
  return Object.freeze({
11777
- id: id3,
11777
+ id: id4,
11778
11778
  domId: `planr-artifact-${index + 1}`,
11779
11779
  title: plainText(artifact.title, `Artifact ${index + 1}`) || `Artifact ${index + 1}`,
11780
11780
  kind: plainText(artifact.kind, "html") || "html",
@@ -11938,8 +11938,8 @@ ${model.presentation === "canvas" ? ` <div class="planr-stage-heading"><span>AR
11938
11938
  </main>`;
11939
11939
  }
11940
11940
  function renderArtifactRail(model) {
11941
- const closed2 = !model.railOpen;
11942
- return `<aside class="planr-review-rail" id="planr-review-rail" aria-label="Review comments"${closed2 ? ' inert aria-hidden="true"' : ""}>
11941
+ const closed3 = !model.railOpen;
11942
+ return `<aside class="planr-review-rail" id="planr-review-rail" aria-label="Review comments"${closed3 ? ' inert aria-hidden="true"' : ""}>
11943
11943
  <header><h2>Review comments</h2><div class="planr-review-header-actions"><div class="planr-review-metrics" aria-label="Review metrics"><span data-planr-metric="open">0 open</span><span class="planr-count" data-planr-metric="total" aria-label="${commentsLabel(model.feedbackCount)}">${model.feedbackCount}</span></div><button class="planr-rail-close" type="button" data-planr-close-feedback aria-label="Close comments">\xD7</button></div></header>
11944
11944
  <section class="planr-identity" aria-label="Reviewer identity"><label for="planr-reviewer-name">Your name<input id="planr-reviewer-name" type="text" maxlength="256" autocomplete="name" aria-describedby="planr-identity-status planr-review-error" data-planr-reviewer-name></label><p class="planr-identity-status" id="planr-identity-status" aria-live="polite" data-planr-identity-status>Used to sign your comments.</p><p class="planr-field-error" id="planr-review-error" role="alert" hidden></p></section>
11945
11945
  <div class="planr-feedback-slot" data-planr-slot="feedback-rail" role="region" aria-label="Comment threads"><p data-planr-empty-feedback>No comments yet. Choose Add comment, then click or drag on the artifact.</p><div class="planr-thread-list" data-planr-thread-list></div></div>
@@ -12028,7 +12028,7 @@ function requestedArtifactId(value) {
12028
12028
  return typeof value?.id === "string" ? value.id : "";
12029
12029
  }
12030
12030
  function availableId(artifacts, requested, fallback = "") {
12031
- return artifacts.some(({ id: id3 }) => id3 === requested) ? requested : fallback;
12031
+ return artifacts.some(({ id: id4 }) => id4 === requested) ? requested : fallback;
12032
12032
  }
12033
12033
  function normalizeViewMode(value, artifactCount) {
12034
12034
  if (artifactCount < 2) return "single";
@@ -13048,7 +13048,7 @@ function renderArtifactShellDocument(input = DEFAULT_ARTIFACT_SHELL_INPUT, { the
13048
13048
  const stagePayload = createArtifactStagePayload(input.envelope, {
13049
13049
  viewer: input.viewer ?? input.envelope?.viewer
13050
13050
  });
13051
- const reviewState = Object.freeze({
13051
+ const reviewState2 = Object.freeze({
13052
13052
  schemaVersion: "1.0.0",
13053
13053
  reviewOf: digestArtifactEnvelope({
13054
13054
  schemaVersion: input.envelope?.schemaVersion ?? "1.0.0",
@@ -13073,7 +13073,7 @@ ${themeCss}${ARTIFACT_SHELL_CSS}</style>
13073
13073
  ${shellMarkup}
13074
13074
  <script type="application/json" id="planr-artifact-shell-model">${modelData}</script>
13075
13075
  <script type="application/json" id="planr-artifact-stage-payload">${embedJson(stagePayload)}</script>
13076
- <script type="application/json" id="planr-artifact-review-state">${embedJson(reviewState)}</script>
13076
+ <script type="application/json" id="planr-artifact-review-state">${embedJson(reviewState2)}</script>
13077
13077
  <script src="${escapeHtml(stageRuntimeUrl)}" defer></script>
13078
13078
  </body>
13079
13079
  </html>
@@ -13162,7 +13162,7 @@ function navigator(document2) {
13162
13162
  <label class="design-field" for="design-variant">Direction<select id="design-variant" data-design-variant>${document2.variants.map((variant) => `<option value="${escapeHtml(variant.id)}"${variant.status !== "ready" ? " disabled" : ""}>${escapeHtml(variant.label)}${variant.status === "failed" ? " \u2014 unavailable" : ""}</option>`).join("")}</select></label>
13163
13163
  ${document2.variants.filter(({ status }) => status === "ready").length > 1 ? `<label class="design-compare"><input type="checkbox" data-design-compare> Compare directions</label>` : ""}
13164
13164
  <div class="design-screen-list">${document2.screenOrder.map((screenId, index) => {
13165
- const screen = document2.screens.find(({ id: id3 }) => id3 === screenId);
13165
+ const screen = document2.screens.find(({ id: id4 }) => id4 === screenId);
13166
13166
  return `<button type="button" class="design-screen" data-design-screen="${escapeHtml(screen.id)}" aria-current="${index === 0 ? "page" : "false"}" title="${escapeHtml(screen.title)}"><span class="design-screen-number" aria-hidden="true">${String(index + 1).padStart(2, "0")}</span><span>${escapeHtml(screen.title)}</span></button>`;
13167
13167
  }).join("")}</div>
13168
13168
  <div class="design-nav-footer"><span data-design-verification>Browser inspection pending</span><span class="design-local">Local workspace</span></div>
@@ -13177,7 +13177,7 @@ function designNotes(document2) {
13177
13177
  ).join("")}</div></details>`;
13178
13178
  }
13179
13179
  function stageDetails(document2) {
13180
- return `<div class="design-stage-context"><div><strong data-design-screen-title>${escapeHtml(document2.screens.find(({ id: id3 }) => id3 === document2.screenOrder[0]).title)}</strong><span data-design-stage-description>Every screen, one connected design</span></div><div class="design-stage-actions">${designNotes(document2)}<label class="design-frame-picker">Frame<select aria-label="Responsive frame" data-design-frame>${document2.frames.map((frame) => `<option value="${escapeHtml(frame.id)}">${escapeHtml(frame.label)} \xB7 ${frame.width} \xD7 ${frame.height}</option>`).join("")}</select></label></div></div>
13180
+ return `<div class="design-stage-context"><div><strong data-design-screen-title>${escapeHtml(document2.screens.find(({ id: id4 }) => id4 === document2.screenOrder[0]).title)}</strong><span data-design-stage-description>Every screen, one connected design</span></div><div class="design-stage-actions">${designNotes(document2)}<label class="design-frame-picker">Frame<select aria-label="Responsive frame" data-design-frame>${document2.frames.map((frame) => `<option value="${escapeHtml(frame.id)}">${escapeHtml(frame.label)} \xB7 ${frame.width} \xD7 ${frame.height}</option>`).join("")}</select></label></div></div>
13181
13181
  <div class="design-canvas-tools" role="group" aria-label="Canvas controls"><div class="planr-segment design-interaction-picker" role="group" aria-label="Review mode">${iconButton("Interact", "pointer", 'data-planr-mode="interact" aria-pressed="true" aria-keyshortcuts="I"')}${iconButton("Annotate", "comment", 'data-planr-mode="comment" aria-pressed="false" aria-keyshortcuts="C"')}</div><span></span>${button("\u2212", 'data-design-zoom="out" aria-label="Zoom out"')}${button("100%", 'data-design-zoom="reset" aria-label="Reset zoom"')}${button("+", 'data-design-zoom="in" aria-label="Zoom in"')}<span></span>${button("Fit", 'data-design-fit aria-label="Fit all visible artboards"')}${button("Pan", 'data-design-pan aria-pressed="false" aria-label="Pan canvas" aria-keyshortcuts="H Space" title="Pan canvas (H). Hold Space to pan temporarily; Escape returns to Interact."')}</div>
13182
13182
  <div class="design-walkthrough-caption" hidden><div><span data-design-step></span><h2 data-design-narrative-title></h2><p data-design-narrative></p></div><div>${button("Previous", 'data-design-step-change="-1"')}${button("Next", 'data-design-step-change="1"')}</div></div>
13183
13183
  <div class="design-notice" role="status" aria-live="polite" hidden></div>`;
@@ -13205,11 +13205,11 @@ function renderDesignStudioMarkup({
13205
13205
  throw new TypeError(
13206
13206
  "Design studio requires a design document and artifact envelope."
13207
13207
  );
13208
- const artifactIds = new Set(envelope.artifacts.map(({ id: id3 }) => id3));
13208
+ const artifactIds = new Set(envelope.artifacts.map(({ id: id4 }) => id4));
13209
13209
  for (const entry of entries) {
13210
- if (!artifactIds.has(entry.artifactId) || !document2.screens.some(({ id: id3 }) => id3 === entry.screenId) || !document2.variants.some(
13211
- ({ id: id3, status }) => id3 === entry.variantId && status === "ready"
13212
- ) || !document2.frames.some(({ id: id3 }) => id3 === entry.frameId)) {
13210
+ if (!artifactIds.has(entry.artifactId) || !document2.screens.some(({ id: id4 }) => id4 === entry.screenId) || !document2.variants.some(
13211
+ ({ id: id4, status }) => id4 === entry.variantId && status === "ready"
13212
+ ) || !document2.frames.some(({ id: id4 }) => id4 === entry.frameId)) {
13213
13213
  throw new TypeError(`Invalid design studio entry: ${entry.artifactId}.`);
13214
13214
  }
13215
13215
  }
@@ -13295,11 +13295,11 @@ function renderDesignStudioMarkup({
13295
13295
  // packages/design/lib/design/review-export.mjs
13296
13296
  function reviewExportTools() {
13297
13297
  const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
13298
- const list2 = (value) => Array.isArray(value) ? value : [];
13298
+ const list3 = (value) => Array.isArray(value) ? value : [];
13299
13299
  const localPath = /(?:file:\/\/|\/(?:Users|home|private|tmp|var|etc|opt|Volumes)\/|[A-Za-z]:\\|\\\\)/u;
13300
13300
  const field = (value) => typeof value === "string" && value.length <= 1024 && !localPath.test(value) ? value : null;
13301
- const digest3 = (value) => typeof value === "string" && /^[a-f0-9]{64}$/u.test(value) ? value : null;
13302
- const timestamp = (value) => typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/u.test(value) && Number.isFinite(Date.parse(value)) ? value : null;
13301
+ const digest4 = (value) => typeof value === "string" && /^[a-f0-9]{64}$/u.test(value) ? value : null;
13302
+ const timestamp3 = (value) => typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/u.test(value) && Number.isFinite(Date.parse(value)) ? value : null;
13303
13303
  const quote = (value) => {
13304
13304
  if (typeof value !== "string" || value.length > 16384) throw new TypeError("Review text must be a string of at most 16384 characters.");
13305
13305
  return value;
@@ -13307,20 +13307,20 @@ function reviewExportTools() {
13307
13307
  const identity = (value) => ({ id: field(value?.id), name: typeof value?.name === "string" ? quote(value.name) : "Unknown reviewer" });
13308
13308
  const dimensions = (value) => Number.isInteger(value?.width) && value.width > 0 && value.width <= 16384 && Number.isInteger(value?.height) && value.height > 0 && value.height <= 16384 ? { width: value.width, height: value.height } : null;
13309
13309
  const rounded = (value) => Math.round(value * 1e6) / 1e6;
13310
- const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
13311
- const order = (a, b) => compare(a.createdAt ?? "", b.createdAt ?? "") || compare(a.id ?? "", b.id ?? "");
13310
+ const compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
13311
+ const order = (a, b) => compare2(a.createdAt ?? "", b.createdAt ?? "") || compare2(a.id ?? "", b.id ?? "");
13312
13312
  const fragment = (values) => "#" + Object.entries(values).filter(([, value]) => value !== null && value !== void 0).map(([key, value]) => `${key}=${encodeURIComponent(value).replace(/[!'()*]/gu, (char) => "%" + char.charCodeAt(0).toString(16).toUpperCase())}`).join("&");
13313
13313
  function location(pin) {
13314
13314
  const region = pin.region;
13315
13315
  if (!region || ["x", "y", "w", "h"].some((key) => !Number.isFinite(region[key]) || region[key] < 0 || region[key] > 1) || region.x + region.w > 1.000001 || region.y + region.h > 1.000001) throw new TypeError("Review pin has an invalid normalized region.");
13316
- const anchor = field(pin.anchor?.planrId) ? { planrId: field(pin.anchor.planrId), screen: field(pin.anchor.screen) } : null;
13316
+ const anchor2 = field(pin.anchor?.planrId) ? { planrId: field(pin.anchor.planrId), screen: field(pin.anchor.screen) } : null;
13317
13317
  const viewport = dimensions(pin.viewport);
13318
13318
  const normalizedRegion = { x: region.x, y: region.y, w: region.w, h: region.h };
13319
13319
  const point = { x: rounded(region.x + region.w / 2), y: rounded(region.y + region.h / 2) };
13320
13320
  return {
13321
13321
  kind: region.w > 0 || region.h > 0 ? "region" : "point",
13322
13322
  coordinateSpace: pin.anchor ? "anchor-normalized" : "viewport-normalized",
13323
- anchor,
13323
+ anchor: anchor2,
13324
13324
  region: normalizedRegion,
13325
13325
  point,
13326
13326
  capturedViewport: viewport,
@@ -13329,21 +13329,21 @@ function reviewExportTools() {
13329
13329
  }
13330
13330
  function sourceBundle(value, fallback = {}) {
13331
13331
  const bundle = value?.bundle ?? value;
13332
- return { bundle: object(bundle), revisionId: field(value?.revisionId ?? fallback.revisionId ?? bundle?.revision), reviewOf: digest3(value?.reviewOf ?? fallback.reviewOf ?? bundle?.reviewOf) };
13332
+ return { bundle: object(bundle), revisionId: field(value?.revisionId ?? fallback.revisionId ?? bundle?.revision), reviewOf: digest4(value?.reviewOf ?? fallback.reviewOf ?? bundle?.reviewOf) };
13333
13333
  }
13334
13334
  function flatten(input) {
13335
13335
  if (Array.isArray(input.feedback?.pins)) return input.feedback.pins;
13336
- if (input.review) return list2(input.review.pins).map((pin) => ({ ...pin, reviewId: input.review.reviewId, reviewOf: input.review.reviewOf, revisionId: pin.revisionId ?? input.revisionId }));
13337
- return list2(input.feedback?.ledger?.reviews).flatMap((entry) => list2(entry.review?.pins).map((pin) => ({ ...pin, reviewId: entry.review.reviewId, reviewOf: entry.review.reviewOf, stale: pin.stale || entry.stale })));
13336
+ if (input.review) return list3(input.review.pins).map((pin) => ({ ...pin, reviewId: input.review.reviewId, reviewOf: input.review.reviewOf, revisionId: pin.revisionId ?? input.revisionId }));
13337
+ return list3(input.feedback?.ledger?.reviews).flatMap((entry) => list3(entry.review?.pins).map((pin) => ({ ...pin, reviewId: entry.review.reviewId, reviewOf: entry.review.reviewOf, stale: pin.stale || entry.stale })));
13338
13338
  }
13339
13339
  function revisionFor(pin) {
13340
13340
  return field(pin.revisionId) ?? (pin.reviewId?.startsWith("shared-") ? field(pin.reviewId.slice(7)) : null);
13341
13341
  }
13342
13342
  function resolveSource(pin, sources) {
13343
- const revisionId = revisionFor(pin), reviewOf = digest3(pin.reviewOf);
13343
+ const revisionId = revisionFor(pin), reviewOf = digest4(pin.reviewOf);
13344
13344
  const matches = sources.filter((source) => revisionId ? source.revisionId === revisionId && (!source.reviewOf || !reviewOf || source.reviewOf === reviewOf) : reviewOf && source.reviewOf === reviewOf);
13345
- const distinct = matches.filter((item2, index) => matches.findIndex((other) => other.revisionId === item2.revisionId && other.reviewOf === item2.reviewOf) === index);
13346
- return distinct.length === 1 ? distinct[0] : null;
13345
+ const distinct2 = matches.filter((item2, index) => matches.findIndex((other) => other.revisionId === item2.revisionId && other.reviewOf === item2.reviewOf) === index);
13346
+ return distinct2.length === 1 ? distinct2[0] : null;
13347
13347
  }
13348
13348
  function pinMetadata(metadata2, pin, current) {
13349
13349
  const key = revisionFor(pin) ?? pin.reviewId;
@@ -13352,7 +13352,7 @@ function reviewExportTools() {
13352
13352
  function createDesignReviewExport2(input = {}) {
13353
13353
  const current = sourceBundle(input.bundle ?? {}, { revisionId: input.revisionId, reviewOf: input.reviewOf ?? input.review?.reviewOf });
13354
13354
  const design = current.bundle.design ?? current.bundle.document ?? {};
13355
- const sources = [current, ...list2(input.revisions).map((value) => sourceBundle(value))];
13355
+ const sources = [current, ...list3(input.revisions).map((value) => sourceBundle(value))];
13356
13356
  const pins = flatten(input);
13357
13357
  if (pins.length > 1e4) throw new TypeError("Review export exceeds 10000 threads.");
13358
13358
  const metadata2 = object(input.metadata ?? input.feedback?.metadata);
@@ -13361,12 +13361,12 @@ function reviewExportTools() {
13361
13361
  if (!field(pin.id) || !field(pin.artifactId)) throw new TypeError("Review pin is missing a share-safe identity.");
13362
13362
  const source = resolveSource(pin, sources), original = source?.bundle;
13363
13363
  const originalDesign = original?.design ?? original?.document;
13364
- const entry = list2(original?.entries).find((item2) => item2.artifactId === pin.artifactId);
13365
- const screen = list2(originalDesign?.screens).find((item2) => item2.id === entry?.screenId);
13366
- const direction = list2(originalDesign?.variants).find((item2) => item2.id === entry?.variantId);
13367
- const frame = list2(originalDesign?.frames).find((item2) => item2.id === entry?.frameId);
13364
+ const entry = list3(original?.entries).find((item2) => item2.artifactId === pin.artifactId);
13365
+ const screen = list3(originalDesign?.screens).find((item2) => item2.id === entry?.screenId);
13366
+ const direction = list3(originalDesign?.variants).find((item2) => item2.id === entry?.variantId);
13367
+ const frame = list3(originalDesign?.frames).find((item2) => item2.id === entry?.frameId);
13368
13368
  const sourceRevisionId = revisionFor(pin) ?? source?.revisionId ?? null;
13369
- const reviewId = field(pin.reviewId), reviewOf = digest3(pin.reviewOf);
13369
+ const reviewId = field(pin.reviewId), reviewOf = digest4(pin.reviewOf);
13370
13370
  const threadKey = JSON.stringify([sourceRevisionId, reviewId, reviewOf, pin.id]);
13371
13371
  if (seen.has(threadKey)) throw new TypeError("Review export contains a duplicate thread identity.");
13372
13372
  seen.add(threadKey);
@@ -13379,7 +13379,7 @@ function reviewExportTools() {
13379
13379
  if (!source || !entry) staleReasons.push("Original screen mapping is unavailable; do not relocate this pin.");
13380
13380
  if (pin.anchor?.planrId && screen?.anchors?.length && !screen.anchors.includes(pin.anchor.planrId) && pin.anchor.planrId !== screen.id) staleReasons.push("The stable element anchor is not declared in the original screen.");
13381
13381
  const refs = { revision: sourceRevisionId, review: reviewId, screen: field(entry?.screenId) ?? field(pin.screenId), direction: field(entry?.variantId) ?? field(pin.variantId), frame: field(entry?.frameId) ?? field(pin.frameId), pin: pin.id };
13382
- const replies = list2(pin.replies);
13382
+ const replies = list3(pin.replies);
13383
13383
  if (replies.length > 1e3) throw new TypeError("Review export exceeds 1000 replies in one thread.");
13384
13384
  const thread = {
13385
13385
  id: pin.id,
@@ -13391,12 +13391,12 @@ function reviewExportTools() {
13391
13391
  stale: staleReasons.length > 0,
13392
13392
  staleReasons,
13393
13393
  author: identity(pin.author),
13394
- createdAt: timestamp(pin.createdAt),
13395
- updatedAt: timestamp(pin.updatedAt),
13394
+ createdAt: timestamp3(pin.createdAt),
13395
+ updatedAt: timestamp3(pin.updatedAt),
13396
13396
  comment: quote(pin.comment),
13397
13397
  location: location(pin),
13398
- disposition: field(decision.disposition) ? { value: field(decision.disposition), explanation: quote(decision.reason ?? ""), author: typeof decision.author === "string" ? { id: null, name: quote(decision.author) } : identity(decision.author), updatedAt: timestamp(decision.updatedAt) } : null,
13399
- replies: [...replies].sort(order).map((reply) => ({ id: field(reply.id), author: identity(reply.author), createdAt: timestamp(reply.createdAt), comment: quote(reply.comment) }))
13398
+ disposition: field(decision.disposition) ? { value: field(decision.disposition), explanation: quote(decision.reason ?? ""), author: typeof decision.author === "string" ? { id: null, name: quote(decision.author) } : identity(decision.author), updatedAt: timestamp3(decision.updatedAt) } : null,
13399
+ replies: [...replies].sort(order).map((reply) => ({ id: field(reply.id), author: identity(reply.author), createdAt: timestamp3(reply.createdAt), comment: quote(reply.comment) }))
13400
13400
  };
13401
13401
  const groupKey = JSON.stringify([sourceRevisionId, reviewId, reviewOf, refs.screen, refs.direction, refs.frame, pin.artifactId]);
13402
13402
  if (!groups.has(groupKey)) groups.set(groupKey, {
@@ -13412,17 +13412,17 @@ function reviewExportTools() {
13412
13412
  });
13413
13413
  groups.get(groupKey).threads.push(thread);
13414
13414
  }
13415
- const orderedGroups = [...groups.entries()].sort(([a], [b]) => compare(a, b)).map(([, value]) => value);
13415
+ const orderedGroups = [...groups.entries()].sort(([a], [b]) => compare2(a, b)).map(([, value]) => value);
13416
13416
  const threads = orderedGroups.flatMap((group) => group.threads);
13417
- const reviews = input.review ? [{ review: input.review }] : list2(input.feedback?.ledger?.reviews);
13418
- const overallNotes = reviews.filter((entry) => entry.review?.overall).map(({ review }) => ({ reviewId: field(review.reviewId), reviewOf: digest3(review.reviewOf), comment: quote(review.overall) })).sort((a, b) => compare(a.reviewId ?? "", b.reviewId ?? ""));
13417
+ const reviews = input.review ? [{ review: input.review }] : list3(input.feedback?.ledger?.reviews);
13418
+ const overallNotes = reviews.filter((entry) => entry.review?.overall).map(({ review }) => ({ reviewId: field(review.reviewId), reviewOf: digest4(review.reviewOf), comment: quote(review.overall) })).sort((a, b) => compare2(a.reviewId ?? "", b.reviewId ?? ""));
13419
13419
  return {
13420
13420
  kind: "openplanr-design-review-export",
13421
13421
  schemaVersion: "1.0.0",
13422
13422
  design: { id: field(design.id), title: field(design.title) ?? "Design review" },
13423
13423
  currentRevisionId: current.revisionId,
13424
13424
  currentArtifactDigest: current.reviewOf,
13425
- ...timestamp(input.generatedAt) ? { generatedAt: timestamp(input.generatedAt) } : {},
13425
+ ...timestamp3(input.generatedAt) ? { generatedAt: timestamp3(input.generatedAt) } : {},
13426
13426
  completeness: { historyComplete: input.historyComplete === true, olderPagesLoading: input.olderPagesLoading === true, includesUnsentLocalChanges: input.includesUnsentLocalChanges === true },
13427
13427
  summary: { threads: threads.length, replies: threads.reduce((count, thread) => count + thread.replies.length, 0), open: threads.filter((thread) => !thread.resolved).length, resolved: threads.filter((thread) => thread.resolved).length, stale: threads.filter((thread) => thread.stale).length },
13428
13428
  resolutionGuidance: [
@@ -13505,16 +13505,16 @@ function renderDesignReviewExportSource() {
13505
13505
  // packages/design/lib/design/studio.mjs
13506
13506
  var templateRoot = new URL("../../templates/studio/", new URL("./runtime/packages/design/lib/design/studio.mjs", import.meta.url).href);
13507
13507
  function designStudioArtifactId(variantId, screenId, frameId) {
13508
- const id3 = [
13508
+ const id4 = [
13509
13509
  "design",
13510
13510
  ...[variantId, screenId, frameId].map(
13511
13511
  (value) => `${value.length}-${value}`
13512
13512
  )
13513
13513
  ].join(".");
13514
- return id3.length <= 128 ? id3 : `design.${createHash4("sha256").update(JSON.stringify([variantId, screenId, frameId])).digest("hex")}`;
13514
+ return id4.length <= 128 ? id4 : `design.${createHash4("sha256").update(JSON.stringify([variantId, screenId, frameId])).digest("hex")}`;
13515
13515
  }
13516
13516
  function createDesignStudioEntries(document2, envelope) {
13517
- const ids = new Set(envelope.artifacts.map(({ id: id3 }) => id3));
13517
+ const ids = new Set(envelope.artifacts.map(({ id: id4 }) => id4));
13518
13518
  const entries = [];
13519
13519
  for (const variant of document2.variants.filter(
13520
13520
  ({ status }) => status === "ready"
@@ -13637,9 +13637,9 @@ function recoverDesignPublication(root, { ownsRenderLock = false } = {}) {
13637
13637
  }
13638
13638
  }
13639
13639
  function loadDesignDocument(file, { readSource } = {}) {
13640
- const checked = readSource?.(file, process.cwd());
13641
- const path = checked?.file ?? realpathSync3(resolve3(file));
13642
- const document2 = assertDesignDocument(checked ? JSON.parse(checked.value.toString("utf8")) : readJson2(path));
13640
+ const checked2 = readSource?.(file, process.cwd());
13641
+ const path = checked2?.file ?? realpathSync3(resolve3(file));
13642
+ const document2 = assertDesignDocument(checked2 ? JSON.parse(checked2.value.toString("utf8")) : readJson2(path));
13643
13643
  return { path, root: dirname5(path), document: document2 };
13644
13644
  }
13645
13645
  function designSpecPath(root) {
@@ -13731,10 +13731,10 @@ function prepareDesignDocument(file, { readSource, passive = false, maxBytes } =
13731
13731
  designSystem: document2.designSystem
13732
13732
  });
13733
13733
  lint.push({ variantId: variant.id, screenId, ...report });
13734
- for (const anchor of screen.anchors ?? []) {
13735
- if (!bundled.html.includes(`data-planr-id="${anchor}"`) && !bundled.html.includes(`id="${anchor}"`))
13734
+ for (const anchor2 of screen.anchors ?? []) {
13735
+ if (!bundled.html.includes(`data-planr-id="${anchor2}"`) && !bundled.html.includes(`id="${anchor2}"`))
13736
13736
  throw new Error(
13737
- `Screen ${screenId} is missing its declared anchor ${anchor}.`
13737
+ `Screen ${screenId} is missing its declared anchor ${anchor2}.`
13738
13738
  );
13739
13739
  }
13740
13740
  for (const frame of document2.frames) {
@@ -13913,7 +13913,7 @@ ${errors.join("\n")}`);
13913
13913
  contentProvenance: prepared.document.brief.provenance,
13914
13914
  generatedAt,
13915
13915
  screens: prepared.document.screenOrder.map(
13916
- (id3) => prepared.document.screens.find((item2) => item2.id === id3).title
13916
+ (id4) => prepared.document.screens.find((item2) => item2.id === id4).title
13917
13917
  ),
13918
13918
  iterations: previous && previous.revision !== prepared.revision ? (previous.iterations ?? 0) + 1 : previous?.iterations ?? 0,
13919
13919
  htmlFile: `.design/revisions/${prepared.revision}/${prepared.document.defaultView}.html`
@@ -13993,8 +13993,8 @@ ${errors.join("\n")}`);
13993
13993
  }
13994
13994
 
13995
13995
  // packages/design/lib/design/review.mjs
13996
- import { existsSync as existsSync9, readFileSync as readFileSync14 } from "node:fs";
13997
- import { dirname as dirname11, join as join10 } from "node:path";
13996
+ import { existsSync as existsSync11, readFileSync as readFileSync16 } from "node:fs";
13997
+ import { dirname as dirname13, join as join12 } from "node:path";
13998
13998
 
13999
13999
  // packages/artifact/lib/artifact/import.mjs
14000
14000
  import {
@@ -14019,10 +14019,10 @@ var ARTIFACT_REVIEW_STATE_VERSION = "1.0.0";
14019
14019
  var ARTIFACT_REVIEW_STATE_KIND = "artifact-review-state";
14020
14020
  var SHA256_RE2 = /^[a-f0-9]{64}$/;
14021
14021
  var ARTIFACT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
14022
- function conflict(message, details = void 0) {
14022
+ function conflict(message2, details = void 0) {
14023
14023
  throw new PipelineError(
14024
14024
  ARTIFACT_ERROR_CODES.MERGE_CONFLICT,
14025
- message,
14025
+ message2,
14026
14026
  "Keep both reviews under different stable IDs or resolve the conflicting item explicitly.",
14027
14027
  details
14028
14028
  );
@@ -14364,13 +14364,13 @@ function writeArtifactReviewState(path, ledger, {
14364
14364
  }
14365
14365
  return ledger;
14366
14366
  }
14367
- function withArtifactReviewLock(path, action) {
14367
+ function withArtifactReviewLock(path, action3) {
14368
14368
  const previous = reviewPathQueues.get(path) ?? Promise.resolve();
14369
14369
  const run = async () => {
14370
14370
  let release;
14371
14371
  try {
14372
14372
  release = await acquireStartLock(`${path}.lock`, { timeout: 15e3, stale: 3e4 });
14373
- return await action();
14373
+ return await action3();
14374
14374
  } catch (error) {
14375
14375
  if (error instanceof PipelineError) throw error;
14376
14376
  throw new PipelineError(
@@ -14480,8 +14480,8 @@ ${reviewMarkdown(normalized)}
14480
14480
 
14481
14481
  // packages/artifact/lib/artifact/import.mjs
14482
14482
  var ARTIFACT_ID_RE2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
14483
- function pathError(code, message) {
14484
- throw new PipelineError(code, message, "Choose a real, non-symlinked project or user review destination.");
14483
+ function pathError(code, message2) {
14484
+ throw new PipelineError(code, message2, "Choose a real, non-symlinked project or user review destination.");
14485
14485
  }
14486
14486
  function pathEntry(path, fs = { lstatSync: lstatSync3 }) {
14487
14487
  try {
@@ -14679,8 +14679,8 @@ var PERMISSIONS_POLICY = [
14679
14679
  "web-share=()",
14680
14680
  "xr-spatial-tracking=()"
14681
14681
  ].join(", ");
14682
- function artifactError(code, message, fix = "", details) {
14683
- return new PipelineError(code, message, fix, details);
14682
+ function artifactError(code, message2, fix = "", details) {
14683
+ return new PipelineError(code, message2, fix, details);
14684
14684
  }
14685
14685
  function statusForError(error) {
14686
14686
  if (error?.code === "E_REQUEST_BODY_LIMIT" || error?.code === ARTIFACT_ERROR_CODES.REQUEST_LIMIT) return 413;
@@ -14769,9 +14769,9 @@ function normalizeRegistration(value) {
14769
14769
  throw artifactError(ARTIFACT_ERROR_CODES.REQUEST_INVALID, "Artifact session registration must be an object.");
14770
14770
  }
14771
14771
  const envelope = cloneAndValidateEnvelope(value.envelope);
14772
- const title = value.title ?? envelope.artifacts[0]?.title ?? "Artifact review";
14772
+ const title2 = value.title ?? envelope.artifacts[0]?.title ?? "Artifact review";
14773
14773
  const theme = value.theme ?? "auto";
14774
- if (typeof title !== "string" || title.length < 1 || title.length > TITLE_LIMIT) {
14774
+ if (typeof title2 !== "string" || title2.length < 1 || title2.length > TITLE_LIMIT) {
14775
14775
  throw artifactError(ARTIFACT_ERROR_CODES.REQUEST_INVALID, `Artifact title must be 1 through ${TITLE_LIMIT} characters.`);
14776
14776
  }
14777
14777
  if (!THEME_VALUES.has(theme)) {
@@ -14785,7 +14785,7 @@ function normalizeRegistration(value) {
14785
14785
  if (reviewKey !== void 0 && (typeof reviewKey !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(reviewKey))) {
14786
14786
  throw artifactError(ARTIFACT_ERROR_CODES.REQUEST_INVALID, "Artifact review storage key is invalid.");
14787
14787
  }
14788
- return { envelope, title, theme, cwd, ...reviewKey ? { reviewKey } : {} };
14788
+ return { envelope, title: title2, theme, cwd, ...reviewKey ? { reviewKey } : {} };
14789
14789
  }
14790
14790
  function assertReviewStateSize(ledger) {
14791
14791
  const bytes = Buffer.byteLength(JSON.stringify(ledger), "utf8");
@@ -14864,7 +14864,7 @@ function safeSessionId(value) {
14864
14864
  return isCapabilityToken(value, { bytes: SESSION_ID_BYTES });
14865
14865
  }
14866
14866
  function artifactFor(session, artifactId) {
14867
- return session.envelope.artifacts.find(({ id: id3 }) => id3 === artifactId) ?? null;
14867
+ return session.envelope.artifacts.find(({ id: id4 }) => id4 === artifactId) ?? null;
14868
14868
  }
14869
14869
  function publicBase(session) {
14870
14870
  return `/r/${session.id}/${session.capability}/`;
@@ -14978,22 +14978,22 @@ function createArtifactReviewServer({
14978
14978
  if (draining) {
14979
14979
  throw artifactError(ARTIFACT_ERROR_CODES.LOOPBACK_STATE, "Artifact review server is restarting.");
14980
14980
  }
14981
- const id3 = mintCapabilityToken({ bytes: SESSION_ID_BYTES });
14982
- const reviewState = await initializeSessionReview(registration, env);
14981
+ const id4 = mintCapabilityToken({ bytes: SESSION_ID_BYTES });
14982
+ const reviewState2 = await initializeSessionReview(registration, env);
14983
14983
  const session2 = {
14984
14984
  ...registration,
14985
- id: id3,
14985
+ id: id4,
14986
14986
  capability: mintCapabilityToken({ bytes: SESSION_TOKEN_BYTES }),
14987
14987
  bridgeNonce: createArtifactBridgeNonce(),
14988
14988
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
14989
- reviewState: reviewState.ledger,
14990
- reviewPath: reviewState.path,
14989
+ reviewState: reviewState2.ledger,
14990
+ reviewPath: reviewState2.path,
14991
14991
  writeQueue: Promise.resolve()
14992
14992
  };
14993
- sessions.set(id3, session2);
14993
+ sessions.set(id4, session2);
14994
14994
  sendJson(res, 201, {
14995
14995
  ok: true,
14996
- sessionId: id3,
14996
+ sessionId: id4,
14997
14997
  capability: session2.capability,
14998
14998
  path: publicBase(session2)
14999
14999
  });
@@ -15081,11 +15081,11 @@ function createArtifactReviewServer({
15081
15081
  }
15082
15082
  const value = JSON.parse(body || "{}");
15083
15083
  const review = value?.review ?? value;
15084
- const reviewState = await queueSessionReviewWrite(session, review);
15084
+ const reviewState2 = await queueSessionReviewWrite(session, review);
15085
15085
  sendJson(res, 200, {
15086
15086
  ok: true,
15087
15087
  reviewId: review.reviewId,
15088
- effectiveDecision: effectiveReviewDecision(reviewState)
15088
+ effectiveDecision: effectiveReviewDecision(reviewState2)
15089
15089
  });
15090
15090
  return;
15091
15091
  }
@@ -15272,13 +15272,13 @@ function workspaceReviewUrl(access) {
15272
15272
  if (!idPattern.test(access.id)) throw new TypeError("Invalid design workspace identity.");
15273
15273
  return `${normalizeWorkspaceBase(access.baseUrl)}/d/${access.id}`;
15274
15274
  }
15275
- async function tokenMaterial(token, id3, purpose) {
15276
- if (!tokenPattern.test(token) || decodeWorkspaceBytes(token).length !== 32 || !idPattern.test(id3)) throw new TypeError("Enter the complete generated access token.");
15275
+ async function tokenMaterial(token, id4, purpose) {
15276
+ if (!tokenPattern.test(token) || decodeWorkspaceBytes(token).length !== 32 || !idPattern.test(id4)) throw new TypeError("Enter the complete generated access token.");
15277
15277
  const key = await crypto.subtle.importKey("raw", decodeWorkspaceBytes(token), "HKDF", false, ["deriveBits"]);
15278
- return new Uint8Array(await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt: encoder.encode(id3), info: encoder.encode(`openplanr-design-workspace/v1/${purpose}`) }, key, 256));
15278
+ return new Uint8Array(await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt: encoder.encode(id4), info: encoder.encode(`openplanr-design-workspace/v1/${purpose}`) }, key, 256));
15279
15279
  }
15280
- async function deriveWorkspaceAuthentication(token, id3) {
15281
- return encodeWorkspaceBytes(await tokenMaterial(token, id3, "reviewer-auth"));
15280
+ async function deriveWorkspaceAuthentication(token, id4) {
15281
+ return encodeWorkspaceBytes(await tokenMaterial(token, id4, "reviewer-auth"));
15282
15282
  }
15283
15283
  function canonicalWorkspacePublicKey(value) {
15284
15284
  if (!value || value.kty !== "EC" || value.crv !== "P-256" || !tokenPattern.test(value.x ?? "") || !tokenPattern.test(value.y ?? "") || "d" in value) {
@@ -15318,9 +15318,9 @@ async function unseal(value, rawKey, context, limit = DESIGN_WORKSPACE_MAX_BYTES
15318
15318
  const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv: decodeWorkspaceBytes(value.iv), additionalData: encoder.encode(canonicalizeJson(context)) }, key, bytes);
15319
15319
  return JSON.parse(decoder.decode(plaintext));
15320
15320
  }
15321
- var keyringContext = (id3, epoch2) => ({ type: "keyring", workspaceId: id3, epoch: epoch2 });
15322
- var revisionContext = (id3, revision) => ({ type: "revision", workspaceId: id3, id: revision.id, epoch: revision.epoch, reviewOf: revision.reviewOf });
15323
- var eventContext = (id3, event) => ({ type: "event", workspaceId: id3, id: event.id, epoch: event.epoch, revisionId: event.revisionId, reviewOf: event.reviewOf });
15321
+ var keyringContext = (id4, epoch2) => ({ type: "keyring", workspaceId: id4, epoch: epoch2 });
15322
+ var revisionContext = (id4, revision) => ({ type: "revision", workspaceId: id4, id: revision.id, epoch: revision.epoch, reviewOf: revision.reviewOf });
15323
+ var eventContext = (id4, event) => ({ type: "event", workspaceId: id4, id: event.id, epoch: event.epoch, revisionId: event.revisionId, reviewOf: event.reviewOf });
15324
15324
  function workspaceEnvelopeDigest(envelope) {
15325
15325
  return sha256Hex2(canonicalizeJson({ schemaVersion: envelope.schemaVersion, artifacts: envelope.artifacts, viewer: envelope.viewer }));
15326
15326
  }
@@ -15415,22 +15415,22 @@ async function decryptWorkspaceRevision(access, revisionId = access.currentRevis
15415
15415
  if (workspaceEnvelopeDigest(bundle.envelope) !== revision.reviewOf) throw new Error("Published design content does not match its revision.");
15416
15416
  return { ...bundle, workspaceRevision: revision.id, reviewOf: revision.reviewOf };
15417
15417
  }
15418
- async function prepareWorkspaceMutation(custody, action, payload = void 0) {
15418
+ async function prepareWorkspaceMutation(custody, action3, payload = void 0) {
15419
15419
  if (custody.pendingCreate) throw new Error("Finish creating this shared review before changing it.");
15420
15420
  if (custody.pendingMutation) throw new Error("A sharing operation is pending. Retry it before making another change.");
15421
15421
  const body = { operationId: newWorkspaceId(), expectedVersion: custody.version, epoch: custody.epoch };
15422
15422
  let next = {};
15423
- if (action === "publish") body.revision = await prepareRevision(custody, payload);
15424
- else if (action === "rotate") {
15423
+ if (action3 === "publish") body.revision = await prepareRevision(custody, payload);
15424
+ else if (action3 === "rotate") {
15425
15425
  const token = newWorkspaceToken();
15426
15426
  const epoch2 = custody.epoch + 1;
15427
15427
  const keys = { ...custody.keys, [epoch2]: newWorkspaceToken() };
15428
15428
  const keyring = await wrapKeyring(custody, token, epoch2, keys);
15429
15429
  Object.assign(body, { epoch: epoch2, reviewerAuthHash: sha256Hex2(await deriveWorkspaceAuthentication(token, custody.id)), keyring });
15430
15430
  next = { token, epoch: epoch2, keys, keyring };
15431
- } else if (["pause", "resume", "revoke", "delete"].includes(action)) body.action = action;
15431
+ } else if (["pause", "resume", "revoke", "delete"].includes(action3)) body.action = action3;
15432
15432
  else throw new TypeError("Unknown design sharing operation.");
15433
- custody.pendingMutation = { action, body: await signWorkspaceValue(body, custody.ownerPrivateKey), next };
15433
+ custody.pendingMutation = { action: action3, body: await signWorkspaceValue(body, custody.ownerPrivateKey), next };
15434
15434
  return custody.pendingMutation;
15435
15435
  }
15436
15436
  async function commitWorkspaceMutation(custody, options = {}) {
@@ -15465,18 +15465,27 @@ async function prepareWorkspaceEvent(access, payload, { revisionId = access.curr
15465
15465
  }
15466
15466
  async function appendWorkspaceEvent(access, payload, { preparedEvent, signer, revisionId, reviewOf, ...options } = {}) {
15467
15467
  const event = preparedEvent ?? await prepareWorkspaceEvent(access, payload, { signer, revisionId, reviewOf: reviewOf ?? payload.reviewOf });
15468
- return request(access, "/events", { ...options, method: "POST", body: event });
15468
+ const result = await request(access, "/events", { ...options, method: "POST", body: event });
15469
+ if (!result || typeof result !== "object" || Array.isArray(result) || !Number.isSafeInteger(result.sequence) || result.sequence < 1 || !result.event || typeof result.event !== "object" || Array.isArray(result.event)) throw new Error("The feedback receipt is invalid. Retry the saved operation.");
15470
+ const { sequence: eventSequence, ...received } = result.event;
15471
+ if (eventSequence !== void 0 && eventSequence !== result.sequence || canonicalizeJson(received) !== canonicalizeJson(event)) throw new Error("The feedback receipt does not match the saved event. Retry the saved operation.");
15472
+ return { event: { ...received, sequence: result.sequence }, sequence: result.sequence };
15469
15473
  }
15470
15474
  async function readWorkspaceEvents(access, { after = 0, ...options } = {}) {
15471
15475
  if (!Number.isSafeInteger(after) || after < 0) throw new TypeError("Invalid feedback cursor.");
15472
15476
  if (!access.keys) await getWorkspace(access, options);
15473
15477
  const page = await request(access, `/events?after=${after}`, options);
15474
- if (!Array.isArray(page.events) || page.events.length > 100 || !Number.isSafeInteger(page.cursor) || page.cursor < after) throw new Error("Invalid shared feedback page.");
15478
+ if (!Array.isArray(page.events) || page.events.length > 100 || !Number.isSafeInteger(page.cursor) || page.cursor < after || typeof page.hasMore !== "boolean") throw new Error("Invalid shared feedback page.");
15475
15479
  const events = [];
15476
15480
  const issues = [];
15481
+ let previousSequence = after;
15482
+ const pageIds = /* @__PURE__ */ new Set();
15477
15483
  for (const item2 of page.events) {
15478
15484
  const { sequence, ...record } = item2?.event ? { ...item2.event, sequence: item2.sequence } : item2 ?? {};
15479
- if (!Number.isSafeInteger(sequence) || sequence <= after || sequence > page.cursor) throw new Error("Invalid shared feedback sequence.");
15485
+ if (!Number.isSafeInteger(sequence) || sequence <= previousSequence || sequence > page.cursor) throw new Error("Invalid shared feedback sequence.");
15486
+ previousSequence = sequence;
15487
+ if (typeof record.id !== "string" || pageIds.has(record.id)) throw new Error("Invalid shared feedback event identity.");
15488
+ pageIds.add(record.id);
15480
15489
  try {
15481
15490
  assertWorkspaceContract(record, DESIGN_WORKSPACE_EVENT_SCHEMA);
15482
15491
  if (!await verifyWorkspaceSignature(record, record.publicKey)) throw new Error("Signature verification failed.");
@@ -15489,6 +15498,7 @@ async function readWorkspaceEvents(access, { after = 0, ...options } = {}) {
15489
15498
  issues.push({ sequence, id: typeof record.id === "string" && idPattern.test(record.id) ? record.id : null, reason: "Invalid encrypted feedback; not imported." });
15490
15499
  }
15491
15500
  }
15501
+ if (previousSequence > page.cursor) throw new Error("Invalid shared feedback cursor.");
15492
15502
  return { ...page, events, issues };
15493
15503
  }
15494
15504
 
@@ -15526,14 +15536,14 @@ var ARTIFACT_REVIEW_INTENTS = Object.freeze(["fix", "improve", "question"]);
15526
15536
  var ARTIFACT_REVIEW_STATUSES = Object.freeze(["open", "addressed", "resolved"]);
15527
15537
  var REVIEW_OF_RE = /^[a-f0-9]{64}$/;
15528
15538
  var ArtifactReviewStateError = class extends Error {
15529
- constructor(code, message) {
15530
- super(message);
15539
+ constructor(code, message2) {
15540
+ super(message2);
15531
15541
  this.name = "ArtifactReviewStateError";
15532
15542
  this.code = code;
15533
15543
  }
15534
15544
  };
15535
- function invalid2(message) {
15536
- throw new ArtifactReviewStateError("E_ARTIFACT_REVIEW_INVALID", message);
15545
+ function invalid2(message2) {
15546
+ throw new ArtifactReviewStateError("E_ARTIFACT_REVIEW_INVALID", message2);
15537
15547
  }
15538
15548
  function identityRequired() {
15539
15549
  throw new ArtifactReviewStateError(
@@ -15564,11 +15574,11 @@ function enumValue(value, values, label) {
15564
15574
  return value;
15565
15575
  }
15566
15576
  function isoTimestamp(value, label) {
15567
- const timestamp = value instanceof Date ? value.toISOString() : value;
15568
- if (typeof timestamp !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(timestamp) || !Number.isFinite(Date.parse(timestamp))) {
15577
+ const timestamp3 = value instanceof Date ? value.toISOString() : value;
15578
+ if (typeof timestamp3 !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(timestamp3) || !Number.isFinite(Date.parse(timestamp3))) {
15569
15579
  invalid2(`${label} must be an ISO-8601 date-time.`);
15570
15580
  }
15571
- return timestamp;
15581
+ return timestamp3;
15572
15582
  }
15573
15583
  function normalizeArtifactReviewIdentity(value, { allowEmpty = false } = {}) {
15574
15584
  if (value === null || value === void 0 || value === "") {
@@ -15582,12 +15592,12 @@ function normalizeArtifactReviewIdentity(value, { allowEmpty = false } = {}) {
15582
15592
  max: ARTIFACT_REVIEW_LIMITS.authorName,
15583
15593
  trim: true
15584
15594
  });
15585
- const id3 = optionalString(source.id, "author.id", {
15595
+ const id4 = optionalString(source.id, "author.id", {
15586
15596
  min: 1,
15587
15597
  max: ARTIFACT_REVIEW_LIMITS.id,
15588
15598
  trim: true
15589
15599
  });
15590
- return deepFreezeArtifactReview(id3 === void 0 ? { name } : { id: id3, name });
15600
+ return deepFreezeArtifactReview(id4 === void 0 ? { name } : { id: id4, name });
15591
15601
  }
15592
15602
  function normalizeRegion(region) {
15593
15603
  if (!region || typeof region !== "object" || Array.isArray(region)) {
@@ -15618,15 +15628,15 @@ function normalizeViewport2(viewport) {
15618
15628
  height: dimension(viewport.height, "pin.viewport.height")
15619
15629
  };
15620
15630
  }
15621
- function normalizeAnchor(anchor) {
15622
- if (anchor === void 0 || anchor === null) return void 0;
15623
- if (typeof anchor !== "object" || Array.isArray(anchor)) invalid2("pin.anchor must be an object.");
15624
- const planrId = boundedString(anchor.planrId, "pin.anchor.planrId", {
15631
+ function normalizeAnchor(anchor2) {
15632
+ if (anchor2 === void 0 || anchor2 === null) return void 0;
15633
+ if (typeof anchor2 !== "object" || Array.isArray(anchor2)) invalid2("pin.anchor must be an object.");
15634
+ const planrId = boundedString(anchor2.planrId, "pin.anchor.planrId", {
15625
15635
  min: 1,
15626
15636
  max: ARTIFACT_REVIEW_LIMITS.anchor,
15627
15637
  trim: true
15628
15638
  });
15629
- const screen = optionalString(anchor.screen, "pin.anchor.screen", {
15639
+ const screen = optionalString(anchor2.screen, "pin.anchor.screen", {
15630
15640
  min: 1,
15631
15641
  max: ARTIFACT_REVIEW_LIMITS.screen,
15632
15642
  trim: true
@@ -15660,7 +15670,7 @@ function normalizePin(pin, label = "pin") {
15660
15670
  invalid2(`${label}.replies must contain no more than ${ARTIFACT_REVIEW_LIMITS.replies} items.`);
15661
15671
  }
15662
15672
  const replies = pin.replies.map((reply, index) => normalizeReply(reply, `${label}.replies[${index}]`));
15663
- const replyIds = new Set(replies.map(({ id: id3 }) => id3));
15673
+ const replyIds = new Set(replies.map(({ id: id4 }) => id4));
15664
15674
  if (replyIds.size !== replies.length) invalid2(`${label}.replies must have unique ids.`);
15665
15675
  const normalized = {
15666
15676
  id: boundedString(pin.id, `${label}.id`, {
@@ -15692,9 +15702,9 @@ function normalizePin(pin, label = "pin") {
15692
15702
  max: ARTIFACT_REVIEW_LIMITS.variant,
15693
15703
  trim: true
15694
15704
  });
15695
- const anchor = normalizeAnchor(pin.anchor);
15705
+ const anchor2 = normalizeAnchor(pin.anchor);
15696
15706
  if (variant !== void 0) normalized.variant = variant;
15697
- if (anchor !== void 0) normalized.anchor = anchor;
15707
+ if (anchor2 !== void 0) normalized.anchor = anchor2;
15698
15708
  return normalized;
15699
15709
  }
15700
15710
  function normalizeArtifactReview2(review) {
@@ -15705,7 +15715,7 @@ function normalizeArtifactReview2(review) {
15705
15715
  invalid2(`review.pins must contain no more than ${ARTIFACT_REVIEW_LIMITS.pins} items.`);
15706
15716
  }
15707
15717
  const pins = review.pins.map((pin, index) => normalizePin(pin, `review.pins[${index}]`));
15708
- const pinIds = new Set(pins.map(({ id: id3 }) => id3));
15718
+ const pinIds = new Set(pins.map(({ id: id4 }) => id4));
15709
15719
  if (pinIds.size !== pins.length) invalid2("review.pins must have unique ids.");
15710
15720
  const normalized = {
15711
15721
  schemaVersion: enumValue(review.schemaVersion, ["1.0.0"], "review.schemaVersion"),
@@ -15742,22 +15752,27 @@ function mergeWorkspaceFeedback(events, { revisionId, reviewOf, ownerPublicKey }
15742
15752
  const overall = /* @__PURE__ */ new Map();
15743
15753
  const directions = /* @__PURE__ */ new Map();
15744
15754
  const seen = /* @__PURE__ */ new Map();
15755
+ const revisionBases = /* @__PURE__ */ new Map();
15745
15756
  const acceptedEventIds = [];
15746
15757
  const issues = [];
15747
15758
  const categories = {}, dispositions = {};
15748
15759
  for (const event of [...events].sort((a, b) => a.sequence - b.sequence)) {
15749
- if (event.revisionId !== revisionId) continue;
15750
15760
  try {
15751
- if (event.reviewOf !== reviewOf || !event.publicKey?.x || !event.publicKey?.y || !Number.isSafeInteger(event.sequence) || event.sequence < 1) throw new TypeError("Shared feedback has an invalid revision or signer.");
15752
- const signerId = workspaceReviewerId(event.publicKey);
15753
- const payload = event.payload;
15754
- if (!payload || typeof payload.author !== "string" || !payload.author.trim() || payload.author.length > 160 || payload.reviewOf !== reviewOf) throw new TypeError("Shared feedback has an invalid author or digest.");
15761
+ if (!event || typeof event.id !== "string" || !event.id || typeof event.revisionId !== "string" || !/^[a-f0-9]{64}$/u.test(event.reviewOf ?? "") || !Number.isSafeInteger(event.sequence) || event.sequence < 1) throw new TypeError("Shared feedback has an invalid event identity or revision.");
15755
15762
  const eventHash = sha256Hex2(canonicalizeJson(event));
15756
15763
  if (seen.has(event.id)) {
15757
- if (seen.get(event.id) !== eventHash) throw new TypeError("Shared feedback reuses an event identity.");
15764
+ if (seen.get(event.id) !== eventHash) throw new TypeError("Shared feedback reuses an event identity with changed bytes.");
15758
15765
  continue;
15759
15766
  }
15760
15767
  seen.set(event.id, eventHash);
15768
+ const boundReviewOf = revisionBases.get(event.revisionId);
15769
+ if (boundReviewOf && boundReviewOf !== event.reviewOf) throw new TypeError("A shared revision identity is bound to conflicting design content.");
15770
+ revisionBases.set(event.revisionId, event.reviewOf);
15771
+ if (event.revisionId !== revisionId) continue;
15772
+ if (event.reviewOf !== reviewOf || !event.publicKey?.x || !event.publicKey?.y) throw new TypeError("Shared feedback has an invalid revision or signer.");
15773
+ const signerId = workspaceReviewerId(event.publicKey);
15774
+ const payload = event.payload;
15775
+ if (!payload || typeof payload.author !== "string" || !payload.author.trim() || payload.author.length > 160 || payload.reviewOf !== reviewOf) throw new TypeError("Shared feedback has an invalid author or digest.");
15761
15776
  const author = { id: signerId, name: payload.author.trim() };
15762
15777
  if (["category", "disposition"].includes(payload.kind)) {
15763
15778
  assertDesignReviewMetadata(payload);
@@ -15774,11 +15789,11 @@ function mergeWorkspaceFeedback(events, { revisionId, reviewOf, ownerPublicKey }
15774
15789
  for (const value of [payload.ratings ?? {}, payload.remix ?? {}]) if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).length > 256) throw new TypeError("Shared direction feedback is too large or invalid.");
15775
15790
  const ratings = {};
15776
15791
  const remix = {};
15777
- for (const [id3, rating] of Object.entries(payload.ratings ?? {})) {
15778
- if (Number.isInteger(rating) && rating >= 1 && rating <= 5) ratings[id3] = rating;
15792
+ for (const [id4, rating] of Object.entries(payload.ratings ?? {})) {
15793
+ if (Number.isInteger(rating) && rating >= 1 && rating <= 5) ratings[id4] = rating;
15779
15794
  }
15780
- for (const [id3, note] of Object.entries(payload.remix ?? {})) {
15781
- if (typeof note === "string" && note.length <= 8192) remix[id3] = note;
15795
+ for (const [id4, note] of Object.entries(payload.remix ?? {})) {
15796
+ if (typeof note === "string" && note.length <= 8192) remix[id4] = note;
15782
15797
  }
15783
15798
  directions.set(signerId, { signerId, author: author.name, revisionId, ratings, remix });
15784
15799
  acceptedEventIds.push(event.id);
@@ -15898,13 +15913,13 @@ function writeCustody(path, record) {
15898
15913
  throw error;
15899
15914
  }
15900
15915
  }
15901
- async function withCustody(file, options, action) {
15916
+ async function withCustody(file, options, action3) {
15902
15917
  const location = custodyLocation(file, options);
15903
15918
  ensurePrivateDirectory(location.root);
15904
15919
  const unlock = await acquireStartLock(`${location.path}.lock`);
15905
15920
  try {
15906
15921
  let record = readCustody(location.path);
15907
- return await action({ ...location, record, save(value = record) {
15922
+ return await action3({ ...location, record, save(value = record) {
15908
15923
  record = value;
15909
15924
  writeCustody(location.path, value);
15910
15925
  } });
@@ -15952,8 +15967,9 @@ var safeStatus = (record, current) => ({
15952
15967
  commentsPaused: Boolean(record.custody.commentsPaused ?? record.commentsPaused),
15953
15968
  revoked: Boolean(record.revoked),
15954
15969
  deleted: Boolean(record.deleted),
15955
- pending: Boolean(record.custody.pendingCreate || record.custody.pendingMutation),
15956
- pendingAction: record.custody.pendingCreate ? "create" : record.custody.pendingMutation?.action ?? null
15970
+ pending: Boolean(record.custody.pendingCreate || record.custody.pendingMutation || record.pendingReviewMetadata?.length),
15971
+ pendingAction: record.custody.pendingCreate ? "create" : record.custody.pendingMutation?.action ?? (record.pendingReviewMetadata?.length ? "review-metadata" : null),
15972
+ pendingReviewMetadata: Boolean(record.pendingReviewMetadata?.length)
15957
15973
  } : {}
15958
15974
  });
15959
15975
  function getDesignShareStatus(file, options = {}) {
@@ -16000,25 +16016,25 @@ async function publishDesignShare(file, options = {}) {
16000
16016
  return safeStatus(record, current);
16001
16017
  });
16002
16018
  }
16003
- async function manageDesignShare(file, action, options = {}) {
16004
- if (!["rotate", "pause", "resume", "revoke", "delete", "access"].includes(action)) throw new Error("Unknown design sharing action.");
16019
+ async function manageDesignShare(file, action3, options = {}) {
16020
+ if (!["rotate", "pause", "resume", "revoke", "delete", "access"].includes(action3)) throw new Error("Unknown design sharing action.");
16005
16021
  return withCustody(file, options, async ({ record, current, save }) => {
16006
16022
  if (!record || record.custody.pendingCreate) throw new Error("Create the shared review first.");
16007
- if (action === "access") {
16023
+ if (action3 === "access") {
16008
16024
  if (record.revoked || record.deleted) throw new Error("This review is no longer accessible.");
16009
16025
  return { ...safeStatus(record, current), token: record.custody.token };
16010
16026
  }
16011
- if (action === "rotate") await flushOwnerMetadata(record, save, options);
16012
- if (record.custody.pendingMutation && record.custody.pendingMutation.action !== action) throw new Error(`Retry the pending ${record.custody.pendingMutation.action} operation first.`);
16027
+ if (action3 === "rotate") await flushOwnerMetadata(record, save, options);
16028
+ if (record.custody.pendingMutation && record.custody.pendingMutation.action !== action3) throw new Error(`Retry the pending ${record.custody.pendingMutation.action} operation first.`);
16013
16029
  if (!record.custody.pendingMutation) {
16014
16030
  if (!record.revoked) await getWorkspace(record.custody, { fetchImpl: options.fetchImpl });
16015
- await prepareWorkspaceMutation(record.custody, action);
16031
+ await prepareWorkspaceMutation(record.custody, action3);
16016
16032
  save(record);
16017
16033
  }
16018
16034
  await commitMutation(record, save, options);
16019
- if (action === "pause" || action === "resume") record.commentsPaused = action === "pause";
16020
- if (action === "revoke") record.revoked = true;
16021
- if (action === "delete") record.deleted = true;
16035
+ if (action3 === "pause" || action3 === "resume") record.commentsPaused = action3 === "pause";
16036
+ if (action3 === "revoke") record.revoked = true;
16037
+ if (action3 === "delete") record.deleted = true;
16022
16038
  save(record);
16023
16039
  return safeStatus(record, current);
16024
16040
  });
@@ -16101,22 +16117,39 @@ async function syncDesignShare(file, options = {}) {
16101
16117
  const page = await readWorkspaceEvents(record.custody, { after: record.lastEvent ?? 0, fetchImpl: options.fetchImpl });
16102
16118
  const earlier = readJson2(ledgerPath, { schemaVersion: "1.0.0", events: [], issues: [], importedReviews: {} });
16103
16119
  const events = [...earlier.events];
16104
- const ids = new Set(events.map((event) => event.id));
16120
+ const eventBytes = new Map(events.map((event) => [event.id, canonicalizeJson(event)]));
16121
+ const revisionBases = /* @__PURE__ */ new Map();
16122
+ for (const event of events) {
16123
+ const previous = revisionBases.get(event.revisionId);
16124
+ if (previous && previous !== event.reviewOf) throw new Error("Saved shared feedback binds one revision to conflicting design content.");
16125
+ revisionBases.set(event.revisionId, event.reviewOf);
16126
+ }
16105
16127
  const pageIssues = [...page.issues ?? []];
16106
16128
  for (const event of page.events ?? []) {
16107
- if (ids.has(event.id)) continue;
16129
+ if (eventBytes.has(event.id)) {
16130
+ if (eventBytes.get(event.id) !== canonicalizeJson(event)) pageIssues.push({ id: event.id, sequence: event.sequence, reason: "Shared feedback reuses an event identity with changed bytes." });
16131
+ continue;
16132
+ }
16108
16133
  try {
16134
+ const boundReviewOf = revisionBases.get(event.revisionId);
16135
+ if (boundReviewOf && boundReviewOf !== event.reviewOf) throw new Error("A shared revision identity is bound to conflicting design content.");
16109
16136
  const candidate = mergeWorkspaceFeedback([...events, event], { revisionId: event.revisionId, reviewOf: event.reviewOf, ownerPublicKey: record.custody.ownerPublicKey });
16110
- const invalid3 = candidate.issues?.find((issue) => (issue.id ?? issue.eventId) === event.id);
16137
+ const invalid3 = candidate.issues?.find((issue2) => (issue2.id ?? issue2.eventId) === event.id);
16111
16138
  if (invalid3) throw new Error(invalid3.reason);
16112
16139
  events.push(event);
16113
- ids.add(event.id);
16140
+ eventBytes.set(event.id, canonicalizeJson(event));
16141
+ revisionBases.set(event.revisionId, event.reviewOf);
16114
16142
  imported++;
16115
16143
  } catch (error) {
16116
16144
  pageIssues.push({ id: event.id, sequence: event.sequence, reason: error.message });
16117
16145
  }
16118
16146
  }
16119
- const revisions = new Map(events.map((event) => [event.revisionId, event.reviewOf]));
16147
+ const revisions = /* @__PURE__ */ new Map();
16148
+ for (const event of events) {
16149
+ const previous = revisions.get(event.revisionId);
16150
+ if (previous && previous !== event.reviewOf) throw new Error("Saved shared feedback binds one revision to conflicting design content.");
16151
+ revisions.set(event.revisionId, event.reviewOf);
16152
+ }
16120
16153
  const importedReviews = { ...earlier.importedReviews ?? {} }, directions = [], metadataByRevision = {};
16121
16154
  await withArtifactReviewLock(reviewPath, () => {
16122
16155
  const ledger = readArtifactReviewState(reviewPath, { allowMissing: true }) ?? createReviewLedger({ artifactId: reviewKey, currentReviewOf: currentDigest });
@@ -16145,7 +16178,7 @@ async function syncDesignShare(file, options = {}) {
16145
16178
  }
16146
16179
  writeArtifactReviewState(reviewPath, createReviewLedger({ artifactId: reviewKey, currentReviewOf: currentDigest, reviews: [...entries.values()] }));
16147
16180
  });
16148
- issues = [...new Map([...earlier.issues ?? [], ...pageIssues].map((issue) => [`${issue.id ?? issue.eventId}:${issue.sequence}`, issue])).values()];
16181
+ issues = [...new Map([...earlier.issues ?? [], ...pageIssues].map((issue2) => [`${issue2.id ?? issue2.eventId}:${issue2.sequence}`, issue2])).values()];
16149
16182
  atomicJson(ledgerPath, { schemaVersion: "1.0.0", workspaceId: record.custody.id, events, issues, directions, metadataByRevision, importedReviews });
16150
16183
  const next = page.cursor;
16151
16184
  hasMore = Boolean(page.hasMore) && next > record.lastEvent;
@@ -16159,11 +16192,883 @@ async function syncDesignShare(file, options = {}) {
16159
16192
  // packages/design/lib/design/handoff.mjs
16160
16193
  import { existsSync as existsSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "node:fs";
16161
16194
  import { dirname as dirname10, join as join9 } from "node:path";
16162
- var conflict2 = (message) => Object.assign(new Error(message), { statusCode: 409 });
16195
+
16196
+ // packages/design/lib/design/handoff-resolution.mjs
16197
+ var OUTCOMES = /* @__PURE__ */ new Set([
16198
+ "accepted",
16199
+ "open",
16200
+ "blocking",
16201
+ "deferred",
16202
+ "declined"
16203
+ ]);
16204
+ var CATEGORIES = /* @__PURE__ */ new Set([
16205
+ "question",
16206
+ "suggestion",
16207
+ "change-request",
16208
+ "blocker"
16209
+ ]);
16210
+ var DISPOSITIONS = /* @__PURE__ */ new Set(["accepted", "deferred", "rejected", "declined"]);
16211
+ var MAX_COMMENTS = 1e4;
16212
+ var MAX_ISSUES = 1e4;
16213
+ var digestPattern = /^[a-f0-9]{64}$/u;
16214
+ var revisionOf = (pin) => pin.revisionId ?? pin.reviewId;
16215
+ var keyOf = (pin) => `${revisionOf(pin)}:${pin.id}`;
16216
+ var compare = (left, right) => left.localeCompare(right, "en");
16217
+ function assertPlainData(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
16218
+ if (depth > 64)
16219
+ throw new TypeError(
16220
+ "Review resolution data exceeds the maximum nesting depth."
16221
+ );
16222
+ if (value === null || ["string", "boolean"].includes(typeof value)) return;
16223
+ if (typeof value === "number" && Number.isFinite(value)) return;
16224
+ if (typeof value !== "object" || seen.has(value))
16225
+ throw new TypeError("Review resolution data must be finite, acyclic JSON.");
16226
+ if (!Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
16227
+ throw new TypeError(
16228
+ "Review resolution data must contain only plain JSON objects."
16229
+ );
16230
+ seen.add(value);
16231
+ for (const [key, descriptor] of Object.entries(
16232
+ Object.getOwnPropertyDescriptors(value)
16233
+ )) {
16234
+ if (["__proto__", "prototype", "constructor"].includes(key) || !Object.hasOwn(descriptor, "value"))
16235
+ throw new TypeError(
16236
+ "Review resolution data contains a forbidden property."
16237
+ );
16238
+ assertPlainData(descriptor.value, depth + 1, seen);
16239
+ }
16240
+ seen.delete(value);
16241
+ }
16242
+ var clone3 = (value) => {
16243
+ assertPlainData(value);
16244
+ return JSON.parse(canonicalizeJson(value));
16245
+ };
16246
+ function issue(code, severity, message2, { pinId, revisionId, recoveryAction } = {}) {
16247
+ return {
16248
+ code,
16249
+ severity,
16250
+ message: message2,
16251
+ ...revisionId ? { revisionId } : {},
16252
+ ...pinId ? { pinId } : {},
16253
+ recoveryAction: recoveryAction ?? {
16254
+ id: "refresh-review-resolution",
16255
+ label: "Refresh review decisions"
16256
+ }
16257
+ };
16258
+ }
16259
+ function validatePin(pin) {
16260
+ if (!pin || typeof pin !== "object" || Array.isArray(pin))
16261
+ throw new TypeError("Every review comment must be an object.");
16262
+ if (typeof pin.id !== "string" || !pin.id || pin.id.length > 160)
16263
+ throw new TypeError("Every review comment requires a stable identity.");
16264
+ const revisionId = revisionOf(pin);
16265
+ if (typeof revisionId !== "string" || !revisionId || revisionId.length > 160)
16266
+ throw new TypeError(
16267
+ "Every review comment requires its original revision identity."
16268
+ );
16269
+ if (typeof pin.reviewOf !== "string" || !digestPattern.test(pin.reviewOf))
16270
+ throw new TypeError(
16271
+ "Every review comment requires its original review basis."
16272
+ );
16273
+ if (pin.screenId !== void 0 && (typeof pin.screenId !== "string" || !pin.screenId))
16274
+ throw new TypeError("Review screen references must be stable identities.");
16275
+ if (pin.elementId !== void 0 && (typeof pin.elementId !== "string" || !pin.elementId || !pin.screenId))
16276
+ throw new TypeError(
16277
+ "Review element references require a stable screen identity."
16278
+ );
16279
+ }
16280
+ function scopedMetadata(metadata2, pin, duplicatePinIds, diagnostics) {
16281
+ const revisionId = revisionOf(pin);
16282
+ const scoped = metadata2.byRevision?.[revisionId] ?? {};
16283
+ let category = scoped.categories?.[pin.id];
16284
+ let disposition = scoped.dispositions?.[pin.id];
16285
+ if (category === void 0 && Object.hasOwn(metadata2.categories ?? {}, pin.id)) {
16286
+ if (duplicatePinIds.has(pin.id))
16287
+ diagnostics.push(
16288
+ issue(
16289
+ "AMBIGUOUS_LEGACY_CATEGORY",
16290
+ "blocked",
16291
+ "A legacy category cannot be matched to one original revision.",
16292
+ { pinId: pin.id, revisionId }
16293
+ )
16294
+ );
16295
+ else category = metadata2.categories[pin.id];
16296
+ }
16297
+ if (disposition === void 0 && Object.hasOwn(metadata2.dispositions ?? {}, pin.id)) {
16298
+ if (duplicatePinIds.has(pin.id))
16299
+ diagnostics.push(
16300
+ issue(
16301
+ "AMBIGUOUS_LEGACY_DISPOSITION",
16302
+ "blocked",
16303
+ "A legacy owner decision cannot be matched to one original revision.",
16304
+ { pinId: pin.id, revisionId }
16305
+ )
16306
+ );
16307
+ else disposition = metadata2.dispositions[pin.id];
16308
+ }
16309
+ if (category !== void 0 && !CATEGORIES.has(category)) {
16310
+ diagnostics.push(
16311
+ issue(
16312
+ "UNKNOWN_CATEGORY",
16313
+ "blocked",
16314
+ "The comment category is not supported.",
16315
+ { pinId: pin.id, revisionId }
16316
+ )
16317
+ );
16318
+ category = void 0;
16319
+ }
16320
+ const dispositionValue = typeof disposition === "string" ? disposition : disposition?.disposition;
16321
+ if (dispositionValue !== void 0 && !DISPOSITIONS.has(dispositionValue)) {
16322
+ diagnostics.push(
16323
+ issue(
16324
+ "UNKNOWN_DISPOSITION",
16325
+ "blocked",
16326
+ "The owner decision is not supported.",
16327
+ { pinId: pin.id, revisionId }
16328
+ )
16329
+ );
16330
+ disposition = void 0;
16331
+ }
16332
+ return {
16333
+ category,
16334
+ disposition,
16335
+ dispositionValue: typeof disposition === "string" ? disposition : disposition?.disposition
16336
+ };
16337
+ }
16338
+ function unknownMetadata(metadata2, known, diagnostics) {
16339
+ for (const [revisionId, value] of Object.entries(
16340
+ metadata2.byRevision ?? {}
16341
+ ).sort(([left], [right]) => compare(left, right))) {
16342
+ for (const field of ["categories", "dispositions"]) {
16343
+ for (const pinId of Object.keys(value?.[field] ?? {}).sort(compare)) {
16344
+ if (!known.has(`${revisionId}:${pinId}`))
16345
+ diagnostics.push(
16346
+ issue(
16347
+ "UNKNOWN_COMMENT_METADATA",
16348
+ "blocked",
16349
+ "Review metadata targets a comment that is not present in the recorded review history.",
16350
+ { revisionId, pinId }
16351
+ )
16352
+ );
16353
+ }
16354
+ }
16355
+ }
16356
+ }
16357
+ function compileDesignHandoffResolution(input) {
16358
+ const source = clone3(input ?? {});
16359
+ const pins = Array.isArray(source.pins) ? source.pins : [];
16360
+ if (pins.length > MAX_COMMENTS)
16361
+ throw new RangeError("Review resolution exceeds the comment limit.");
16362
+ const metadata2 = source.metadata && typeof source.metadata === "object" && !Array.isArray(source.metadata) ? source.metadata : {};
16363
+ const diagnostics = [];
16364
+ const counts = /* @__PURE__ */ new Map();
16365
+ for (const pin of pins) {
16366
+ validatePin(pin);
16367
+ counts.set(pin.id, (counts.get(pin.id) ?? 0) + 1);
16368
+ }
16369
+ const duplicatePinIds = new Set(
16370
+ [...counts].filter(([, count]) => count > 1).map(([id4]) => id4)
16371
+ );
16372
+ const known = /* @__PURE__ */ new Set();
16373
+ for (const pin of pins) {
16374
+ const key = keyOf(pin);
16375
+ if (known.has(key))
16376
+ throw new TypeError(`Duplicate review comment identity: ${key}.`);
16377
+ known.add(key);
16378
+ }
16379
+ unknownMetadata(metadata2, known, diagnostics);
16380
+ const items = [...pins].sort((left, right) => compare(keyOf(left), keyOf(right))).map((pin) => {
16381
+ const revisionId = revisionOf(pin);
16382
+ const current = pin.stale !== true && (!source.currentReviewOf || pin.reviewOf === source.currentReviewOf);
16383
+ const resolved = scopedMetadata(
16384
+ metadata2,
16385
+ pin,
16386
+ duplicatePinIds,
16387
+ diagnostics
16388
+ );
16389
+ let outcome;
16390
+ if (resolved.dispositionValue === "accepted") outcome = "accepted";
16391
+ else if (resolved.dispositionValue === "deferred") outcome = "deferred";
16392
+ else if (["rejected", "declined"].includes(resolved.dispositionValue))
16393
+ outcome = "declined";
16394
+ else if (["blocker", "change-request"].includes(resolved.category))
16395
+ outcome = "blocking";
16396
+ else outcome = "open";
16397
+ if (!OUTCOMES.has(outcome))
16398
+ throw new Error("Review resolution produced an invalid outcome.");
16399
+ if (!current && outcome === "accepted")
16400
+ diagnostics.push(
16401
+ issue(
16402
+ "STALE_ACCEPTED_DECISION",
16403
+ "stale",
16404
+ "An accepted change belongs to an earlier design revision and must be reviewed again.",
16405
+ {
16406
+ pinId: pin.id,
16407
+ revisionId,
16408
+ recoveryAction: {
16409
+ id: "review-stale-decision",
16410
+ label: "Review this decision against the current design"
16411
+ }
16412
+ }
16413
+ )
16414
+ );
16415
+ if (current && outcome === "blocking")
16416
+ diagnostics.push(
16417
+ issue(
16418
+ "UNRESOLVED_BLOCKING_COMMENT",
16419
+ "blocked",
16420
+ "A blocking comment still needs an owner decision.",
16421
+ {
16422
+ pinId: pin.id,
16423
+ revisionId,
16424
+ recoveryAction: {
16425
+ id: "resolve-blocking-comment",
16426
+ label: "Record the owner decision"
16427
+ }
16428
+ }
16429
+ )
16430
+ );
16431
+ return {
16432
+ id: keyOf(pin),
16433
+ pinId: pin.id,
16434
+ reviewId: pin.reviewId,
16435
+ revisionId,
16436
+ reviewOf: pin.reviewOf,
16437
+ current,
16438
+ category: resolved.category ?? "question",
16439
+ outcome,
16440
+ implementationScope: current && outcome === "accepted",
16441
+ anchor: pin.elementId ? { screenId: pin.screenId, elementId: pin.elementId } : pin.screenId ? { screenId: pin.screenId } : pin.anchor?.planrId ? { planrId: pin.anchor.planrId } : { artifactId: pin.artifactId },
16442
+ source: {
16443
+ text: pin.comment,
16444
+ author: clone3(pin.author),
16445
+ status: pin.status,
16446
+ ...resolved.disposition && typeof resolved.disposition === "object" ? { decision: clone3(resolved.disposition) } : {}
16447
+ }
16448
+ };
16449
+ });
16450
+ if (source.historyComplete === false)
16451
+ diagnostics.push(
16452
+ issue(
16453
+ "INCOMPLETE_REVIEW_HISTORY",
16454
+ "blocked",
16455
+ "The complete review history is unavailable.",
16456
+ {
16457
+ recoveryAction: {
16458
+ id: "restore-review-history",
16459
+ label: "Restore the complete review history"
16460
+ }
16461
+ }
16462
+ )
16463
+ );
16464
+ if (source.synchronizationPending === true)
16465
+ diagnostics.push(
16466
+ issue(
16467
+ "SYNCHRONIZATION_PENDING",
16468
+ "blocked",
16469
+ "A review decision is still waiting to synchronize.",
16470
+ {
16471
+ recoveryAction: {
16472
+ id: "retry-review-sync",
16473
+ label: "Retry review synchronization"
16474
+ }
16475
+ }
16476
+ )
16477
+ );
16478
+ for (const value of (source.synchronizationIssues ?? []).slice(0, MAX_ISSUES))
16479
+ diagnostics.push(
16480
+ issue(
16481
+ "UNTRUSTED_HOSTED_FEEDBACK",
16482
+ "blocked",
16483
+ value?.reason || "Hosted feedback could not be validated.",
16484
+ {
16485
+ pinId: value?.pinId,
16486
+ revisionId: value?.revisionId,
16487
+ recoveryAction: {
16488
+ id: "inspect-review-sync",
16489
+ label: "Inspect the rejected hosted feedback"
16490
+ }
16491
+ }
16492
+ )
16493
+ );
16494
+ if ((source.synchronizationIssues ?? []).length > MAX_ISSUES)
16495
+ throw new RangeError(
16496
+ "Review resolution exceeds the synchronization issue limit."
16497
+ );
16498
+ diagnostics.sort(
16499
+ (left, right) => compare(
16500
+ `${left.code}:${left.revisionId ?? ""}:${left.pinId ?? ""}:${left.message}`,
16501
+ `${right.code}:${right.revisionId ?? ""}:${right.pinId ?? ""}:${right.message}`
16502
+ )
16503
+ );
16504
+ const severity = new Set(diagnostics.map((value) => value.severity));
16505
+ const status = severity.has("stale") ? "stale" : severity.has("blocked") ? "blocked" : items.some((item2) => item2.outcome === "open") ? "attention" : "ready";
16506
+ return {
16507
+ kind: "openplanr-design-handoff-resolution",
16508
+ schemaVersion: "1.0.0",
16509
+ currentReviewOf: source.currentReviewOf ?? null,
16510
+ status,
16511
+ complete: source.historyComplete !== false && !["blocked", "stale"].includes(status),
16512
+ items,
16513
+ implementationScope: items.filter((item2) => item2.implementationScope).map((item2) => item2.id),
16514
+ diagnostics
16515
+ };
16516
+ }
16517
+ function canApproveDesignHandoffResolution(value) {
16518
+ return Boolean(
16519
+ value?.complete && ["ready", "attention"].includes(value.status)
16520
+ );
16521
+ }
16522
+
16523
+ // packages/protocol/src/design-handoff-contracts.mjs
16524
+ var DESIGN_HANDOFF_PROTOCOL_VERSION = "1.11.0";
16525
+ var DESIGN_HANDOFF_CONTRACT_VERSION = "1.0.0";
16526
+ var DESIGN_HANDOFF_AUTHORITY = "prepare-plan";
16527
+ var DESIGN_HANDOFF_CHECK_IDS = Object.freeze([
16528
+ "current-revision",
16529
+ "selected-direction",
16530
+ "design-specification",
16531
+ "rendered-verification",
16532
+ "review-freshness",
16533
+ "review-dispositions",
16534
+ "unresolved-blockers",
16535
+ "approved-review-handoff"
16536
+ ]);
16537
+ var DESIGN_HANDOFF_SOURCE_KINDS = Object.freeze([
16538
+ "design-revision",
16539
+ "selected-direction",
16540
+ "design-specification",
16541
+ "rendered-verification",
16542
+ "review-context",
16543
+ "review-feedback",
16544
+ "review-metadata",
16545
+ "review-handoff",
16546
+ "screen",
16547
+ "frame",
16548
+ "component",
16549
+ "state",
16550
+ "flow",
16551
+ "token",
16552
+ "review-decision",
16553
+ "element-anchor"
16554
+ ]);
16555
+ var DESIGN_HANDOFF_REQUIREMENT_KINDS = Object.freeze([
16556
+ "behavior",
16557
+ "visual-state",
16558
+ "responsive",
16559
+ "accessibility",
16560
+ "content-data-assumption",
16561
+ "constraint",
16562
+ "verification-intent"
16563
+ ]);
16564
+ var DESIGN_HANDOFF_CONTRACT_FILES = Object.freeze({
16565
+ "design-handoff-readiness": "design-handoff-readiness.schema.json",
16566
+ "design-implementation-handoff": "design-implementation-handoff.schema.json",
16567
+ "design-planning-lineage": "design-planning-lineage.schema.json"
16568
+ });
16569
+ var text3 = { type: "string", minLength: 1, maxLength: 16384 };
16570
+ var title = { ...text3, maxLength: 240 };
16571
+ var id3 = { type: "string", minLength: 1, maxLength: 160, pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]*$" };
16572
+ var digest3 = { type: "string", pattern: "^sha256:[a-f0-9]{64}$" };
16573
+ var relativePath = { type: "string", minLength: 1, maxLength: 4096 };
16574
+ var timestamp = { type: "string", format: "date-time" };
16575
+ var list2 = (items, maxItems = 1e3) => ({ type: "array", items, maxItems });
16576
+ var closed2 = (properties, required = Object.keys(properties)) => ({
16577
+ type: "object",
16578
+ additionalProperties: false,
16579
+ properties,
16580
+ required
16581
+ });
16582
+ var contract = (name, body) => ({
16583
+ $schema: "https://json-schema.org/draft/2020-12/schema",
16584
+ $id: `https://openplanr.dev/schemas/v1.11.0/${name}.schema.json`,
16585
+ "x-openplanr-contract": { id: name, version: DESIGN_HANDOFF_PROTOCOL_VERSION },
16586
+ ...body
16587
+ });
16588
+ var action = closed2({ id: id3, label: title });
16589
+ var anchor = {
16590
+ oneOf: [
16591
+ closed2({ section: id3 }),
16592
+ closed2({ screenId: id3 }),
16593
+ closed2({ screenId: id3, elementId: id3 }),
16594
+ closed2({ reviewId: id3, pinId: id3 })
16595
+ ]
16596
+ };
16597
+ var evidenceReference = closed2({
16598
+ id: id3,
16599
+ kind: { enum: DESIGN_HANDOFF_SOURCE_KINDS },
16600
+ path: relativePath,
16601
+ revision: digest3,
16602
+ digest: digest3,
16603
+ anchor
16604
+ }, ["id", "kind", "path"]);
16605
+ var readinessCheck = closed2({
16606
+ id: { enum: DESIGN_HANDOFF_CHECK_IDS },
16607
+ status: { enum: ["pass", "attention", "blocked", "stale"] },
16608
+ message: title,
16609
+ evidenceRefs: list2(id3, 64),
16610
+ recoveryAction: action
16611
+ }, ["id", "status", "message", "evidenceRefs"]);
16612
+ var readinessRecord = closed2({
16613
+ kind: { const: "openplanr-design-handoff-readiness" },
16614
+ schemaVersion: { const: DESIGN_HANDOFF_CONTRACT_VERSION },
16615
+ scope: { const: "design-originated" },
16616
+ authority: { const: "none" },
16617
+ designId: id3,
16618
+ sourceRevision: { anyOf: [digest3, { type: "null" }] },
16619
+ selectedVariant: { anyOf: [id3, { type: "null" }] },
16620
+ status: { enum: ["ready", "attention", "blocked", "stale"] },
16621
+ continuation: closed2({ action: { const: DESIGN_HANDOFF_AUTHORITY }, available: { type: "boolean" } }),
16622
+ checks: list2(readinessCheck, DESIGN_HANDOFF_CHECK_IDS.length),
16623
+ evidence: list2(evidenceReference, 1e4),
16624
+ blockers: list2({ enum: DESIGN_HANDOFF_CHECK_IDS }, DESIGN_HANDOFF_CHECK_IDS.length),
16625
+ nextActions: list2(action, DESIGN_HANDOFF_CHECK_IDS.length)
16626
+ });
16627
+ var readinessAbsence = closed2({
16628
+ kind: { const: "openplanr-design-handoff-readiness-absence" },
16629
+ schemaVersion: { const: DESIGN_HANDOFF_CONTRACT_VERSION },
16630
+ status: { const: "absent" },
16631
+ reason: { enum: ["not-computed", "not-applicable", "unavailable"] },
16632
+ message: title,
16633
+ nextAction: action
16634
+ });
16635
+ var DESIGN_HANDOFF_READINESS_SCHEMA = contract("design-handoff-readiness", {
16636
+ oneOf: [readinessRecord, readinessAbsence]
16637
+ });
16638
+ var requirement = closed2({
16639
+ id: { type: "string", pattern: "^REQ-[0-9]{3,}$" },
16640
+ kind: { enum: DESIGN_HANDOFF_REQUIREMENT_KINDS },
16641
+ statement: text3,
16642
+ sourceRefs: { ...list2(id3, 256), minItems: 1 },
16643
+ verification: { ...list2(text3, 256), minItems: 1 }
16644
+ });
16645
+ var implementationHandoff = closed2({
16646
+ kind: { const: "openplanr-design-implementation-handoff" },
16647
+ schemaVersion: { const: DESIGN_HANDOFF_CONTRACT_VERSION },
16648
+ id: id3,
16649
+ version: { type: "integer", minimum: 1 },
16650
+ status: { enum: ["draft", "approved", "superseded", "revoked"] },
16651
+ authority: { const: DESIGN_HANDOFF_AUTHORITY },
16652
+ title,
16653
+ basis: closed2({
16654
+ designId: id3,
16655
+ sourceRevision: digest3,
16656
+ selectedVariant: id3,
16657
+ readiness: closed2({ status: { enum: ["ready", "attention", "blocked", "stale"] }, digest: digest3 }),
16658
+ reviewHandoff: closed2({ version: { type: "integer", minimum: 1 }, contentDigest: digest3 })
16659
+ }),
16660
+ sources: list2(evidenceReference, 1e4),
16661
+ requirements: { ...list2(requirement, 1e4), minItems: 1 },
16662
+ contentDigest: digest3,
16663
+ markdown: { type: "string", maxLength: 2097152 },
16664
+ approval: closed2({ actorId: id3, approvedAt: timestamp, contentDigest: digest3, authority: { const: DESIGN_HANDOFF_AUTHORITY } }),
16665
+ supersededBy: closed2({ id: id3, version: { type: "integer", minimum: 1 }, contentDigest: digest3 }),
16666
+ revocation: closed2({ actorId: id3, revokedAt: timestamp, reason: text3 })
16667
+ }, ["kind", "schemaVersion", "id", "version", "status", "authority", "title", "basis", "sources", "requirements", "contentDigest", "markdown"]);
16668
+ implementationHandoff.allOf = [
16669
+ {
16670
+ if: { properties: { status: { const: "approved" } }, required: ["status"] },
16671
+ then: { required: ["approval"], not: { anyOf: [{ required: ["supersededBy"] }, { required: ["revocation"] }] } }
16672
+ },
16673
+ {
16674
+ if: { properties: { status: { const: "superseded" } }, required: ["status"] },
16675
+ then: { required: ["approval", "supersededBy"], not: { required: ["revocation"] } }
16676
+ },
16677
+ {
16678
+ if: { properties: { status: { const: "revoked" } }, required: ["status"] },
16679
+ then: { required: ["approval", "revocation"], not: { required: ["supersededBy"] } }
16680
+ },
16681
+ {
16682
+ if: { properties: { status: { const: "draft" } }, required: ["status"] },
16683
+ then: { not: { anyOf: [{ required: ["approval"] }, { required: ["supersededBy"] }, { required: ["revocation"] }] } }
16684
+ }
16685
+ ];
16686
+ var DESIGN_IMPLEMENTATION_HANDOFF_SCHEMA = contract(
16687
+ "design-implementation-handoff",
16688
+ implementationHandoff
16689
+ );
16690
+ var lineageMapping = closed2({
16691
+ requirementId: { type: "string", pattern: "^REQ-[0-9]{3,}$" },
16692
+ acceptanceRefs: { ...list2(closed2({
16693
+ storyId: { type: "string", pattern: "^US-[0-9]{3,}$" },
16694
+ acceptanceId: { type: "string", pattern: "^AC-[0-9]{3,}$" }
16695
+ }), 256), minItems: 1 },
16696
+ taskIds: { ...list2({ type: "string", pattern: "^T-[0-9]{3,}$" }, 256), minItems: 1 }
16697
+ });
16698
+ var DESIGN_PLANNING_LINEAGE_SCHEMA = contract("design-planning-lineage", closed2({
16699
+ kind: { const: "openplanr-design-planning-lineage" },
16700
+ schemaVersion: { const: DESIGN_HANDOFF_CONTRACT_VERSION },
16701
+ handoff: closed2({ id: id3, version: { type: "integer", minimum: 1 }, contentDigest: digest3 }),
16702
+ specId: { type: "string", pattern: "^SPEC-[0-9]{3,}$" },
16703
+ mappings: { ...list2(lineageMapping, 1e4), minItems: 1 }
16704
+ }));
16705
+ var DESIGN_HANDOFF_SCHEMAS = deepFreeze({
16706
+ "design-handoff-readiness": DESIGN_HANDOFF_READINESS_SCHEMA,
16707
+ "design-implementation-handoff": DESIGN_IMPLEMENTATION_HANDOFF_SCHEMA,
16708
+ "design-planning-lineage": DESIGN_PLANNING_LINEAGE_SCHEMA
16709
+ });
16710
+ function deepFreeze(value) {
16711
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
16712
+ for (const nested of Object.values(value)) deepFreeze(nested);
16713
+ Object.freeze(value);
16714
+ }
16715
+ return value;
16716
+ }
16717
+ function assertPlainData2(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
16718
+ if (depth > 64) throw new TypeError("Design handoff data exceeds the maximum nesting depth.");
16719
+ if (value === null || ["string", "boolean"].includes(typeof value)) return;
16720
+ if (typeof value === "number" && Number.isFinite(value)) return;
16721
+ if (typeof value !== "object" || seen.has(value)) throw new TypeError("Design handoff data must be finite, acyclic JSON.");
16722
+ if (!Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value))) throw new TypeError("Design handoff data must contain only plain JSON objects.");
16723
+ seen.add(value);
16724
+ for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
16725
+ if (["__proto__", "prototype", "constructor"].includes(key) || !Object.hasOwn(descriptor, "value")) throw new TypeError("Design handoff data contains a forbidden property.");
16726
+ assertPlainData2(descriptor.value, depth + 1, seen);
16727
+ }
16728
+ seen.delete(value);
16729
+ }
16730
+ function distinct(items, select, label) {
16731
+ const values = items.map(select);
16732
+ if (new Set(values).size !== values.length) throw new TypeError(`Duplicate ${label}.`);
16733
+ }
16734
+ function isDesignHandoffRelativePath(value) {
16735
+ return typeof value === "string" && value.length > 0 && value.length <= 4096 && !/^(?:[A-Za-z]:|\/|[A-Za-z][A-Za-z0-9+.-]*:)|[\\?#%\u0000-\u001f\u007f]/u.test(value) && value.split("/").every((part) => part && part !== "." && part !== ".." && !["__proto__", "prototype", "constructor"].includes(part));
16736
+ }
16737
+ function assertDesignHandoffContract(value, schemaOrName) {
16738
+ const schema3 = typeof schemaOrName === "string" ? DESIGN_HANDOFF_SCHEMAS[schemaOrName] : schemaOrName;
16739
+ if (!schema3) throw new TypeError("Unknown design handoff contract.");
16740
+ assertPlainData2(value);
16741
+ canonicalizeJson(value);
16742
+ const errors = validateJson(value, schema3);
16743
+ if (errors.length) throw new TypeError(`Invalid ${schema3["x-openplanr-contract"]?.id ?? "design handoff data"}: ${errors.slice(0, 5).map((error) => `${error.path} (${error.rule})`).join("; ")}`);
16744
+ return value;
16745
+ }
16746
+ function assertEvidenceReferences(evidence2) {
16747
+ distinct(evidence2, (item2) => item2.id, "evidence reference identity");
16748
+ for (const item2 of evidence2) if (!isDesignHandoffRelativePath(item2.path)) throw new TypeError("Design handoff evidence requires a repository-relative logical path.");
16749
+ }
16750
+ function assertProductCopy(value) {
16751
+ if (/\b(?:hash|digest|checksum|sha[- ]?256|canonical(?:ize|ization)?)\b/iu.test(value)) throw new TypeError("Design readiness guidance must use product language.");
16752
+ }
16753
+ function assertDesignHandoffReadiness(value) {
16754
+ assertDesignHandoffContract(value, DESIGN_HANDOFF_READINESS_SCHEMA);
16755
+ if (value.kind.endsWith("-absence")) {
16756
+ assertProductCopy(`${value.message} ${value.nextAction.label}`);
16757
+ return value;
16758
+ }
16759
+ assertEvidenceReferences(value.evidence);
16760
+ distinct(value.checks, (item2) => item2.id, "readiness check identity");
16761
+ const expected = DESIGN_HANDOFF_CHECK_IDS.join("\n");
16762
+ if (value.checks.map((item2) => item2.id).join("\n") !== expected) throw new TypeError("Design readiness must contain every stable check in canonical order.");
16763
+ const evidenceIds = new Set(value.evidence.map((item2) => item2.id));
16764
+ for (const check of value.checks) {
16765
+ distinct(check.evidenceRefs, (item2) => item2, "readiness evidence reference");
16766
+ if (check.evidenceRefs.some((reference) => !evidenceIds.has(reference))) throw new TypeError("Design readiness references missing evidence.");
16767
+ assertProductCopy(`${check.message} ${check.recoveryAction?.label ?? ""}`);
16768
+ }
16769
+ const priority2 = { pass: 0, attention: 1, blocked: 2, stale: 3 };
16770
+ const worst = value.checks.reduce((current, check) => priority2[check.status] > priority2[current] ? check.status : current, "pass");
16771
+ const expectedStatus = worst === "pass" ? "ready" : worst;
16772
+ if (value.status !== expectedStatus) throw new TypeError("Design readiness summary does not match its checks.");
16773
+ const blockingIds = value.checks.filter((check) => ["blocked", "stale"].includes(check.status)).map((check) => check.id);
16774
+ if (value.blockers.join("\n") !== blockingIds.join("\n")) throw new TypeError("Design readiness blockers do not match its blocking checks.");
16775
+ const actionable = value.checks.filter((check) => check.status !== "pass");
16776
+ if (value.nextActions.length !== actionable.length || value.nextActions.some((actionValue, index) => {
16777
+ const recoveryAction = actionable[index].recoveryAction;
16778
+ return actionValue.id !== recoveryAction?.id || actionValue.label !== recoveryAction?.label;
16779
+ })) throw new TypeError("Design readiness next actions do not match its checks.");
16780
+ if (value.continuation.available !== ["ready", "attention"].includes(value.status)) throw new TypeError("Design readiness continuation availability does not match its status.");
16781
+ return value;
16782
+ }
16783
+ function designImplementationHandoffDigest(value) {
16784
+ const projection = {
16785
+ kind: value.kind,
16786
+ schemaVersion: value.schemaVersion,
16787
+ id: value.id,
16788
+ version: value.version,
16789
+ authority: value.authority,
16790
+ title: value.title,
16791
+ basis: value.basis,
16792
+ sources: value.sources,
16793
+ requirements: value.requirements,
16794
+ markdown: value.markdown
16795
+ };
16796
+ return `sha256:${sha256Hex2(canonicalizeJson(projection))}`;
16797
+ }
16798
+ function assertDesignImplementationHandoff(value) {
16799
+ assertDesignHandoffContract(value, DESIGN_IMPLEMENTATION_HANDOFF_SCHEMA);
16800
+ assertEvidenceReferences(value.sources);
16801
+ distinct(value.requirements, (item2) => item2.id, "implementation requirement identity");
16802
+ const sourceIds = new Set(value.sources.map((item2) => item2.id));
16803
+ for (const item2 of value.requirements) {
16804
+ distinct(item2.sourceRefs, (reference) => reference, "requirement source reference");
16805
+ if (item2.sourceRefs.some((reference) => !sourceIds.has(reference))) throw new TypeError("Implementation requirement references missing evidence.");
16806
+ }
16807
+ if (value.contentDigest !== designImplementationHandoffDigest(value)) throw new TypeError("Implementation handoff content does not match its recorded integrity value.");
16808
+ if (value.approval?.contentDigest !== void 0 && value.approval.contentDigest !== value.contentDigest) throw new TypeError("Implementation handoff approval does not match its content.");
16809
+ if (value.status !== "draft" && value.basis.readiness.status !== "ready") throw new TypeError("Only a ready implementation handoff can be approved or retained as approved history.");
16810
+ if (value.supersededBy && value.supersededBy.id === value.id && value.supersededBy.version <= value.version) throw new TypeError("A superseding handoff must identify a newer package version.");
16811
+ return value;
16812
+ }
16813
+
16814
+ // packages/design/lib/design/handoff-readiness.mjs
16815
+ var CHECKS = Object.freeze([
16816
+ "current-revision",
16817
+ "selected-direction",
16818
+ "design-specification",
16819
+ "rendered-verification",
16820
+ "review-freshness",
16821
+ "review-dispositions",
16822
+ "unresolved-blockers",
16823
+ "approved-review-handoff"
16824
+ ]);
16825
+ var action2 = Object.freeze({
16826
+ "current-revision": Object.freeze({ id: "render-design", label: "Render the current design" }),
16827
+ "selected-direction": Object.freeze({ id: "select-direction", label: "Choose one ready direction" }),
16828
+ "design-specification": Object.freeze({ id: "complete-design-specification", label: "Complete the design specification" }),
16829
+ "rendered-verification": Object.freeze({ id: "verify-rendered-design", label: "Verify the rendered design" }),
16830
+ "review-freshness": Object.freeze({ id: "refresh-review", label: "Refresh review against the current design" }),
16831
+ "review-dispositions": Object.freeze({ id: "resolve-review-decisions", label: "Record the remaining review decisions" }),
16832
+ "unresolved-blockers": Object.freeze({ id: "resolve-review-blockers", label: "Resolve the blocking feedback" }),
16833
+ "approved-review-handoff": Object.freeze({ id: "approve-review-handoff", label: "Prepare and approve the current review handoff" })
16834
+ });
16835
+ var message = Object.freeze({
16836
+ "current-revision": Object.freeze({
16837
+ pass: "The current design has a completed render.",
16838
+ blocked: "Render the design before preparing work for engineering."
16839
+ }),
16840
+ "selected-direction": Object.freeze({
16841
+ pass: "One ready design direction is selected.",
16842
+ blocked: "Choose one ready design direction before continuing.",
16843
+ stale: "The selected direction changed after the review handoff was prepared."
16844
+ }),
16845
+ "design-specification": Object.freeze({
16846
+ pass: "The design specification is complete for this revision.",
16847
+ blocked: "Complete the design specification before continuing.",
16848
+ stale: "The design specification belongs to an earlier revision."
16849
+ }),
16850
+ "rendered-verification": Object.freeze({
16851
+ pass: "The rendered design is verified for this revision.",
16852
+ blocked: "Complete rendered design verification before continuing.",
16853
+ stale: "Rendered verification belongs to an earlier revision."
16854
+ }),
16855
+ "review-freshness": Object.freeze({
16856
+ pass: "Review feedback is current for this revision.",
16857
+ blocked: "Review feedback contains ambiguous identities or anchors.",
16858
+ stale: "Review feedback belongs to an earlier revision."
16859
+ }),
16860
+ "review-dispositions": Object.freeze({
16861
+ pass: "Every current review comment has a recorded outcome.",
16862
+ attention: "Some non-blocking review comments still need an owner decision.",
16863
+ blocked: "Review decisions are incomplete or ambiguous."
16864
+ }),
16865
+ "unresolved-blockers": Object.freeze({
16866
+ pass: "No blocking feedback remains open.",
16867
+ blocked: "Blocking feedback must be resolved before continuing.",
16868
+ stale: "A prior decision must be reviewed against the current design."
16869
+ }),
16870
+ "approved-review-handoff": Object.freeze({
16871
+ pass: "The current review handoff is approved.",
16872
+ blocked: "Prepare and approve the review handoff before continuing.",
16873
+ stale: "The approved review handoff no longer matches the current design."
16874
+ })
16875
+ });
16876
+ var priority = Object.freeze({ pass: 0, attention: 1, blocked: 2, stale: 3 });
16877
+ var digestPattern2 = /^(?:sha256:)?[a-f0-9]{64}$/u;
16878
+ var normalizeDigest = (value, label) => {
16879
+ if (typeof value !== "string" || !digestPattern2.test(value)) throw new TypeError(`${label} must be a lowercase SHA-256 value.`);
16880
+ return value.startsWith("sha256:") ? value : `sha256:${value}`;
16881
+ };
16882
+ var optionalDigest = (value, label) => value === void 0 || value === null ? void 0 : normalizeDigest(value, label);
16883
+ var plainClone = (value) => value === void 0 ? void 0 : JSON.parse(canonicalizeJson(value));
16884
+ var logicalPath = (value, fallback, label) => {
16885
+ const result = value ?? fallback;
16886
+ if (!isDesignHandoffRelativePath(result) || /[?#%]/u.test(result)) throw new TypeError(`${label} must be a repository-relative logical path.`);
16887
+ return result;
16888
+ };
16889
+ var evidenceDigest = (value) => `sha256:${sha256Hex2(canonicalizeJson(value))}`;
16890
+ function ensureUnique(items, select, label) {
16891
+ const values = items.map(select);
16892
+ if (new Set(values).size !== values.length) throw new TypeError(`Duplicate ${label}.`);
16893
+ }
16894
+ function evidence(id4, kind, path, value, { revision, anchor: anchor2 } = {}) {
16895
+ return {
16896
+ id: id4,
16897
+ kind,
16898
+ path,
16899
+ ...revision ? { revision: normalizeDigest(revision, `${id4} revision`) } : {},
16900
+ digest: optionalDigest(value?.digest, `${id4} integrity`) ?? evidenceDigest(value),
16901
+ ...anchor2 ? { anchor: anchor2 } : {}
16902
+ };
16903
+ }
16904
+ function checked(id4, status, evidenceRefs = []) {
16905
+ return {
16906
+ id: id4,
16907
+ status,
16908
+ message: message[id4][status],
16909
+ evidenceRefs,
16910
+ ...status === "pass" ? {} : { recoveryAction: action2[id4] }
16911
+ };
16912
+ }
16913
+ function selectedDirection(document2, studioState) {
16914
+ const selected = studioState?.selectedVariant ?? document2?.selectedVariant ?? null;
16915
+ const variants = Array.isArray(document2?.variants) ? document2.variants : [];
16916
+ ensureUnique(variants, (item2) => item2?.id, "design direction identity");
16917
+ const matches = variants.filter((item2) => item2?.id === selected && item2?.status === "ready");
16918
+ return { selected, ready: matches.length === 1 };
16919
+ }
16920
+ function normalizedPins(review, revision) {
16921
+ const pins = Array.isArray(review?.pins) ? review.pins.map((pin) => ({ ...pin })) : [];
16922
+ ensureUnique(pins, (pin) => `${pin.revisionId ?? pin.reviewId ?? ""}:${pin.id ?? ""}`, "review comment identity");
16923
+ for (const pin of pins) {
16924
+ if (typeof pin.id !== "string" || !pin.id) throw new TypeError("Review comments require stable identities.");
16925
+ if (pin.elementId && !pin.screenId) throw new TypeError("An element review anchor must identify its screen.");
16926
+ if (pin.anchorCount !== void 0 && pin.anchorCount !== 1) throw new TypeError("Review comments must identify exactly one anchor.");
16927
+ }
16928
+ return pins.sort((left, right) => `${left.revisionId ?? left.reviewId}:${left.id}`.localeCompare(`${right.revisionId ?? right.reviewId}:${right.id}`)).map((pin) => ({
16929
+ ...pin,
16930
+ current: !pin.stale && (!revision || !pin.revisionId || normalizeDigest(pin.revisionId, "review revision") === revision)
16931
+ }));
16932
+ }
16933
+ function reviewState(input, revision) {
16934
+ if (!input) return { pins: [], stale: false, ambiguous: false };
16935
+ const pins = normalizedPins(input, revision);
16936
+ const recordedRevision = optionalDigest(input.revision, "review revision");
16937
+ return {
16938
+ pins,
16939
+ stale: input.current === false || recordedRevision !== void 0 && recordedRevision !== revision || pins.some((pin) => !pin.current),
16940
+ ambiguous: input.ambiguous === true
16941
+ };
16942
+ }
16943
+ function handoffState(handoff, designId, revision, selectedVariant) {
16944
+ if (!handoff) return "blocked";
16945
+ if (handoff.status !== "approved" || !handoff.approval || handoff.approval.contentHash !== handoff.contentHash) return "blocked";
16946
+ const basisRevision = optionalDigest(handoff.basis?.sourceRevision, "review handoff revision");
16947
+ if (handoff.current === false || handoff.basis?.designId !== designId || basisRevision !== revision || handoff.basis?.selectedVariant !== selectedVariant) return "stale";
16948
+ return "pass";
16949
+ }
16950
+ function compileDesignHandoffReadiness(input) {
16951
+ if (input === null || input === void 0) return designHandoffReadinessAbsence();
16952
+ const source = plainClone(input);
16953
+ const document2 = source.document;
16954
+ if (!document2 || document2.kind !== "openplanr-design-document" || document2.schemaVersion !== "1.0.0" || typeof document2.id !== "string" || !document2.id) throw new TypeError("Readiness requires one supported design document.");
16955
+ const revision = source.sourceRevision === null || source.sourceRevision === void 0 ? null : normalizeDigest(source.sourceRevision, "design revision");
16956
+ const direction = selectedDirection(document2, source.studioState);
16957
+ const evidenceItems = [];
16958
+ if (revision) evidenceItems.push(evidence("design-revision", "design-revision", logicalPath(source.documentPath, "design-document.json", "design document path"), document2, { revision }));
16959
+ const directionEvidence = direction.selected && revision ? evidence(
16960
+ "selected-direction",
16961
+ "selected-direction",
16962
+ logicalPath(source.studioStatePath, ".design/studio-state.json", "Studio state path"),
16963
+ { selectedVariant: direction.selected },
16964
+ { revision, anchor: { section: direction.selected } }
16965
+ ) : null;
16966
+ if (directionEvidence) evidenceItems.push(directionEvidence);
16967
+ const specification = source.specification;
16968
+ if (specification) evidenceItems.push(evidence(
16969
+ "design-specification",
16970
+ "design-specification",
16971
+ logicalPath(specification.path, "design-spec.md", "design specification path"),
16972
+ specification,
16973
+ { revision: specification.revision }
16974
+ ));
16975
+ const verification = source.verification;
16976
+ if (verification) evidenceItems.push(evidence(
16977
+ "rendered-verification",
16978
+ "rendered-verification",
16979
+ logicalPath(verification.path, ".design/verification/current.json", "verification path"),
16980
+ verification,
16981
+ { revision: verification.revision }
16982
+ ));
16983
+ const review = reviewState(source.review, revision);
16984
+ if (source.review) evidenceItems.push(evidence(
16985
+ "review-feedback",
16986
+ "review-feedback",
16987
+ logicalPath(source.review.path, ".design/review.json", "review feedback path"),
16988
+ source.review,
16989
+ { revision: source.review.revision }
16990
+ ));
16991
+ const handoff = source.reviewHandoff;
16992
+ if (handoff) evidenceItems.push(evidence(
16993
+ "review-handoff",
16994
+ "review-handoff",
16995
+ logicalPath(handoff.path, "review-handoff.json", "review handoff path"),
16996
+ handoff,
16997
+ { revision: handoff.basis?.sourceRevision }
16998
+ ));
16999
+ const checks = [];
17000
+ checks.push(checked("current-revision", revision ? "pass" : "blocked", revision ? ["design-revision"] : []));
17001
+ const handoffDirection = handoff?.basis?.selectedVariant;
17002
+ const directionStatus = !direction.ready ? "blocked" : handoffDirection && handoffDirection !== direction.selected ? "stale" : "pass";
17003
+ checks.push(checked("selected-direction", directionStatus, directionEvidence ? ["selected-direction"] : []));
17004
+ let specificationStatus = "blocked";
17005
+ if (specification?.complete === true) {
17006
+ const specificationRevision = optionalDigest(specification.revision, "design specification revision");
17007
+ specificationStatus = revision && specificationRevision && specificationRevision !== revision ? "stale" : "pass";
17008
+ }
17009
+ checks.push(checked("design-specification", specificationStatus, specification ? ["design-specification"] : []));
17010
+ let verificationStatus = "blocked";
17011
+ if (verification) {
17012
+ const verificationRevision = optionalDigest(verification.revision, "rendered verification revision");
17013
+ if (revision && verificationRevision && verificationRevision !== revision) verificationStatus = "stale";
17014
+ else if (verification.status === "verified") verificationStatus = "pass";
17015
+ }
17016
+ checks.push(checked("rendered-verification", verificationStatus, verification ? ["rendered-verification"] : []));
17017
+ const reviewRefs = source.review ? ["review-feedback"] : [];
17018
+ const freshnessStatus = review.ambiguous ? "blocked" : review.stale ? "stale" : "pass";
17019
+ checks.push(checked("review-freshness", freshnessStatus, reviewRefs));
17020
+ const undecided = review.pins.filter((pin) => pin.current && !["accepted", "deferred", "rejected"].includes(pin.disposition));
17021
+ const invalidDisposition = review.pins.some((pin) => pin.disposition && !["accepted", "deferred", "rejected"].includes(pin.disposition));
17022
+ const dispositionStatus = invalidDisposition || review.ambiguous ? "blocked" : undecided.length ? "attention" : "pass";
17023
+ checks.push(checked("review-dispositions", dispositionStatus, reviewRefs));
17024
+ const openBlockers = review.pins.filter((pin) => pin.current && ["blocker", "change-request"].includes(pin.category) && !["accepted", "deferred", "rejected"].includes(pin.disposition));
17025
+ const staleAccepted = review.pins.some((pin) => !pin.current && pin.disposition === "accepted");
17026
+ const blockerStatus = staleAccepted ? "stale" : openBlockers.length ? "blocked" : "pass";
17027
+ checks.push(checked("unresolved-blockers", blockerStatus, reviewRefs));
17028
+ const approvalStatus = handoffState(handoff, document2.id, revision, direction.selected);
17029
+ checks.push(checked("approved-review-handoff", approvalStatus, handoff ? ["review-handoff"] : []));
17030
+ if (checks.map((item2) => item2.id).join("\n") !== CHECKS.join("\n")) throw new TypeError("Readiness checks are not in canonical order.");
17031
+ const worst = checks.reduce((current, item2) => priority[item2.status] > priority[current] ? item2.status : current, "pass");
17032
+ const status = worst === "pass" ? "ready" : worst;
17033
+ const blockers = checks.filter((item2) => ["blocked", "stale"].includes(item2.status)).map((item2) => item2.id);
17034
+ const nextActions = checks.filter((item2) => item2.status !== "pass").map((item2) => item2.recoveryAction);
17035
+ return assertDesignHandoffReadiness({
17036
+ kind: "openplanr-design-handoff-readiness",
17037
+ schemaVersion: "1.0.0",
17038
+ scope: "design-originated",
17039
+ authority: "none",
17040
+ designId: document2.id,
17041
+ sourceRevision: revision,
17042
+ selectedVariant: direction.selected,
17043
+ status,
17044
+ continuation: { action: "prepare-plan", available: ["ready", "attention"].includes(status) },
17045
+ checks,
17046
+ evidence: evidenceItems.sort((left, right) => left.id.localeCompare(right.id)),
17047
+ blockers,
17048
+ nextActions
17049
+ });
17050
+ }
17051
+ function designHandoffReadinessAbsence(reason = "not-computed") {
17052
+ return assertDesignHandoffReadiness({
17053
+ kind: "openplanr-design-handoff-readiness-absence",
17054
+ schemaVersion: "1.0.0",
17055
+ status: "absent",
17056
+ reason,
17057
+ message: "No design handoff readiness has been prepared.",
17058
+ nextAction: { id: "inspect-design", label: "Open the design when you want to prepare a handoff" }
17059
+ });
17060
+ }
17061
+ function designHandoffReadinessDigest(value) {
17062
+ assertDesignHandoffReadiness(value);
17063
+ return `sha256:${sha256Hex2(canonicalizeJson(value))}`;
17064
+ }
17065
+
17066
+ // packages/design/lib/design/handoff.mjs
17067
+ var conflict2 = (message2) => Object.assign(new Error(message2), { statusCode: 409 });
16163
17068
  var sections = ["agreedChanges", "openQuestions", "deferred", "rejected"];
16164
17069
  var metadataPath = (current) => join9(current.root, ".design/review-metadata.json");
16165
- var revisionOf = (pin) => pin.revisionId ?? pin.reviewId;
16166
- var pinKey = (pin) => `${revisionOf(pin)}:${pin.id}`;
17070
+ var revisionOf2 = (pin) => pin.revisionId ?? pin.reviewId;
17071
+ var pinKey = (pin) => `${revisionOf2(pin)}:${pin.id}`;
16167
17072
  function metadata(current, feedback) {
16168
17073
  const local = readJson2(metadataPath(current), { version: 0, byRevision: {} });
16169
17074
  const byRevision = structuredClone(feedback.shared?.metadataByRevision ?? {});
@@ -16175,21 +17080,45 @@ function metadata(current, feedback) {
16175
17080
  for (const pin of feedback.pins) counts.set(pin.id, (counts.get(pin.id) ?? 0) + 1);
16176
17081
  const categories = {}, dispositions = {};
16177
17082
  for (const pin of feedback.pins) {
16178
- const value = byRevision[revisionOf(pin)];
17083
+ const value = byRevision[revisionOf2(pin)];
16179
17084
  if (counts.get(pin.id) === 1 && value) {
16180
17085
  if (Object.hasOwn(value.categories ?? {}, pin.id)) Object.defineProperty(categories, pin.id, { value: value.categories[pin.id], enumerable: true });
16181
17086
  if (Object.hasOwn(value.dispositions ?? {}, pin.id)) Object.defineProperty(dispositions, pin.id, { value: value.dispositions[pin.id], enumerable: true });
16182
17087
  }
17088
+ if (counts.get(pin.id) === 1 && !Object.hasOwn(categories, pin.id) && Object.hasOwn(local.categories ?? {}, pin.id)) Object.defineProperty(categories, pin.id, { value: local.categories[pin.id], enumerable: true });
17089
+ if (counts.get(pin.id) === 1 && !Object.hasOwn(dispositions, pin.id) && Object.hasOwn(local.dispositions ?? {}, pin.id)) Object.defineProperty(dispositions, pin.id, { value: local.dispositions[pin.id], enumerable: true });
16183
17090
  }
16184
- return { version: local.version, categories, dispositions, byRevision };
17091
+ return {
17092
+ version: local.version,
17093
+ categories,
17094
+ dispositions,
17095
+ byRevision,
17096
+ legacy: {
17097
+ categories: structuredClone(local.categories ?? {}),
17098
+ dispositions: structuredClone(local.dispositions ?? {})
17099
+ }
17100
+ };
16185
17101
  }
16186
- var dispositionOf = (metadata2, pin) => metadata2.byRevision[revisionOf(pin)]?.dispositions?.[pin.id]?.disposition;
16187
- function findPin(pins, id3, revision) {
16188
- const candidates = pins.filter((pin) => pin.id === id3 && (!revision || revisionOf(pin) === revision));
17102
+ function findPin(pins, id4, revision) {
17103
+ const candidates = pins.filter((pin) => pin.id === id4 && (!revision || revisionOf2(pin) === revision));
16189
17104
  if (candidates.length !== 1) throw new Error("The comment identity is missing or ambiguous. Include its original revisionId.");
16190
17105
  return candidates[0];
16191
17106
  }
16192
- function snapshot(file, env) {
17107
+ function resolutionFor(value, shareStatus) {
17108
+ return compileDesignHandoffResolution({
17109
+ currentReviewOf: value.basis.reviewOf,
17110
+ pins: value.feedback.pins,
17111
+ metadata: {
17112
+ byRevision: value.metadata.byRevision,
17113
+ categories: value.metadata.legacy.categories,
17114
+ dispositions: value.metadata.legacy.dispositions
17115
+ },
17116
+ historyComplete: !value.feedback.shared?.issues?.length,
17117
+ synchronizationPending: Boolean(shareStatus?.pendingReviewMetadata),
17118
+ synchronizationIssues: value.feedback.shared?.issues ?? []
17119
+ });
17120
+ }
17121
+ function snapshot(file, env, shareOptions = {}) {
16193
17122
  const current = currentDesign(file), feedback = readDesignFeedback(file, env), meta = metadata(current, feedback);
16194
17123
  const reviewContext = current.reviewContext ?? emptyReviewContext(current.document);
16195
17124
  const basis = {
@@ -16198,11 +17127,17 @@ function snapshot(file, env) {
16198
17127
  contextDigest: current.contextDigest ?? reviewDigest(reviewContext),
16199
17128
  reviewOf: digestArtifactEnvelope(current.envelope),
16200
17129
  selectedVariant: feedback.state.selectedVariant ?? current.document.selectedVariant,
16201
- feedbackDigest: reviewDigest({ pins: feedback.pins, metadata: meta.byRevision, overall: (feedback.ledger?.reviews ?? []).map((entry) => ({ reviewId: entry.review.reviewId, reviewOf: entry.review.reviewOf, overall: entry.review.overall })), directions: feedback.shared?.directions ?? [] }),
17130
+ feedbackDigest: reviewDigest({ pins: feedback.pins, metadata: { byRevision: meta.byRevision, categories: meta.categories, dispositions: meta.dispositions }, overall: (feedback.ledger?.reviews ?? []).map((entry) => ({ reviewId: entry.review.reviewId, reviewOf: entry.review.reviewOf, overall: entry.review.overall })), directions: feedback.shared?.directions ?? [] }),
16202
17131
  verificationDigest: reviewDigest(current.verification),
16203
17132
  feedbackWatermark: Math.max(0, ...(feedback.shared?.events ?? []).map((event) => event.sequence ?? 0))
16204
17133
  };
16205
- return { current, feedback, metadata: meta, reviewContext, basis };
17134
+ let shareStatus = null;
17135
+ try {
17136
+ shareStatus = getDesignShareStatus(file, { env, ...shareOptions });
17137
+ } catch {
17138
+ }
17139
+ const value = { current, feedback, metadata: meta, reviewContext, basis };
17140
+ return { ...value, resolution: resolutionFor(value, shareStatus), shareStatus };
16206
17141
  }
16207
17142
  function readDesignExperience(file, { env = process.env } = {}) {
16208
17143
  const value = snapshot(file, env);
@@ -16217,8 +17152,8 @@ function readDesignExperience(file, { env = process.env } = {}) {
16217
17152
  loadingHistory: false
16218
17153
  };
16219
17154
  }
16220
- function readDesignHandoff(file, { env = process.env } = {}) {
16221
- const value = snapshot(file, env);
17155
+ function readDesignHandoff(file, { env = process.env, ...shareOptions } = {}) {
17156
+ const value = snapshot(file, env, shareOptions);
16222
17157
  const path = join9(dirname10(designSpecPath(value.current.root)), "review-handoff.json");
16223
17158
  const draft = readJson2(path, null);
16224
17159
  if (draft) {
@@ -16226,7 +17161,51 @@ function readDesignHandoff(file, { env = process.env } = {}) {
16226
17161
  const markdownPath = path.replace(/\.json$/u, ".md");
16227
17162
  if (!existsSync8(markdownPath) || readFileSync13(markdownPath, "utf8") !== draft.markdown) throw new Error("The handoff Markdown differs from its approved JSON. Rebuild the handoff projection before using it in Plan.");
16228
17163
  }
16229
- return { ok: true, path, revision: value.current.revision, draft, current: Boolean(draft && reviewDigest(draft.basis) === reviewDigest(value.basis)), metadata: value.metadata, feedback: { pins: value.feedback.pins }, basis: value.basis };
17164
+ return { ok: true, path, revision: value.current.revision, draft, current: Boolean(draft && reviewDigest(draft.basis) === reviewDigest(value.basis)), metadata: value.metadata, feedback: { pins: value.feedback.pins }, basis: value.basis, resolution: value.resolution };
17165
+ }
17166
+ function readDesignHandoffReadiness(file, { env = process.env, ...shareOptions } = {}) {
17167
+ const value = snapshot(file, env, shareOptions);
17168
+ const path = join9(dirname10(designSpecPath(value.current.root)), "review-handoff.json");
17169
+ const handoff = readJson2(path, null);
17170
+ if (handoff) assertDraft(handoff);
17171
+ const outcomes = new Map(value.resolution.items.map((item2) => [item2.id, item2]));
17172
+ const pins = value.feedback.pins.map((pin) => {
17173
+ const resolved = outcomes.get(pinKey(pin));
17174
+ const disposition = resolved?.outcome === "accepted" ? "accepted" : resolved?.outcome === "deferred" ? "deferred" : resolved?.outcome === "declined" ? "rejected" : void 0;
17175
+ return {
17176
+ id: pin.id,
17177
+ ...pin.screenId ? { screenId: pin.screenId } : {},
17178
+ ...pin.anchor?.planrId ? { elementId: pin.anchor.planrId } : {},
17179
+ category: resolved?.category,
17180
+ ...disposition ? { disposition } : {},
17181
+ stale: Boolean(pin.stale)
17182
+ };
17183
+ });
17184
+ const specificationPath = designSpecPath(value.current.root);
17185
+ const specification = existsSync8(specificationPath) ? { path: "design-spec.md", revision: value.current.revision, digest: `sha256:${hash(readFileSync13(specificationPath))}`, complete: true } : void 0;
17186
+ const studioState = readJson2(join9(value.current.root, ".design/studio-state.json"), { state: {} }).state;
17187
+ const readiness = compileDesignHandoffReadiness({
17188
+ document: value.current.document,
17189
+ documentPath: "design-document.json",
17190
+ sourceRevision: value.current.revision,
17191
+ studioState,
17192
+ studioStatePath: ".design/studio-state.json",
17193
+ specification,
17194
+ verification: { path: ".design/verification/current.json", ...value.current.verification },
17195
+ review: {
17196
+ path: ".design/review.json",
17197
+ revision: value.current.revision,
17198
+ current: pins.every((pin) => !pin.stale),
17199
+ pins
17200
+ },
17201
+ reviewHandoff: handoff ? {
17202
+ ...handoff,
17203
+ path: "review-handoff.json",
17204
+ digest: `sha256:${handoff.contentHash}`,
17205
+ current: reviewDigest(handoff.basis) === reviewDigest(value.basis)
17206
+ } : void 0
17207
+ });
17208
+ return { ok: true, readiness, digest: designHandoffReadinessDigest(readiness) };
16230
17209
  }
16231
17210
  function assertDraft(draft) {
16232
17211
  assertReviewExperience(draft, DESIGN_HANDOFF_SCHEMA);
@@ -16241,19 +17220,20 @@ function sourceItem(pin, shareUrl) {
16241
17220
  }
16242
17221
  function contentFromFeedback(value, shareUrl) {
16243
17222
  const content = { summary: "", agreedChanges: [], openQuestions: [], deferred: [], rejected: [] };
16244
- for (const pin of value.feedback.pins) {
16245
- const disposition = dispositionOf(value.metadata, pin);
17223
+ const pins = new Map(value.feedback.pins.map((pin) => [pinKey(pin), pin]));
17224
+ for (const resolved of value.resolution.items) {
17225
+ const pin = pins.get(resolved.id);
16246
17226
  const item2 = sourceItem(pin, shareUrl);
16247
- if (disposition === "accepted") content.agreedChanges.push(item2);
16248
- else if (disposition === "deferred") content.deferred.push(item2);
16249
- else if (disposition === "rejected") content.rejected.push(item2);
16250
- else if (pin.status !== "resolved") content.openQuestions.push(item2);
17227
+ if (resolved.outcome === "accepted") content.agreedChanges.push(item2);
17228
+ else if (resolved.outcome === "deferred") content.deferred.push(item2);
17229
+ else if (resolved.outcome === "declined") content.rejected.push(item2);
17230
+ else content.openQuestions.push(item2);
16251
17231
  }
16252
17232
  return content;
16253
17233
  }
16254
- var safeMd = (text3) => String(text3).replaceAll("[", "\\[").replaceAll("]", "\\]").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
16255
- function renderMarkdown(draft, title) {
16256
- const lines = [`# ${safeMd(title)} \u2014 review handoff`, "", `Status: ${draft.status}. Source revision: ${draft.basis.sourceRevision}.`, "", draft.content.summary || "Owner summary has not been written.", ""];
17234
+ var safeMd = (text4) => String(text4).replaceAll("[", "\\[").replaceAll("]", "\\]").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
17235
+ function renderMarkdown(draft, title2) {
17236
+ const lines = [`# ${safeMd(title2)} \u2014 review handoff`, "", `Status: ${draft.status}. Source revision: ${draft.basis.sourceRevision}.`, "", draft.content.summary || "Owner summary has not been written.", ""];
16257
17237
  for (const [key, label] of [["agreedChanges", "Agreed changes"], ["openQuestions", "Open questions"], ["deferred", "Deferred"], ["rejected", "Rejected"]]) {
16258
17238
  lines.push(`## ${label}`, "");
16259
17239
  for (const item2 of draft.content[key]) {
@@ -16275,19 +17255,35 @@ function enrichContent(content, value, shareUrl) {
16275
17255
  const pin = findPin(value.feedback.pins, item2.pinId, item2.revisionId ?? item2.reviewId);
16276
17256
  if (seen.has(pinKey(pin))) throw new Error("Handoff items must cite distinct recorded comments.");
16277
17257
  seen.add(pinKey(pin));
16278
- const expected = key === "agreedChanges" ? "accepted" : key === "deferred" ? "deferred" : key === "rejected" ? "rejected" : void 0;
16279
- if (expected && dispositionOf(value.metadata, pin) !== expected) throw new Error("Record the owner disposition before moving a comment into this handoff section.");
17258
+ const resolved = value.resolution.items.find((itemValue) => itemValue.id === pinKey(pin));
17259
+ const expected = key === "agreedChanges" ? ["accepted"] : key === "deferred" ? ["deferred"] : key === "rejected" ? ["declined"] : ["open", "blocking"];
17260
+ if (!resolved || !expected.includes(resolved.outcome)) throw new Error("Record the owner disposition before moving a comment into this handoff section.");
16280
17261
  const refinement = item2.refinement ?? (item2.text !== pin.comment ? item2.text : void 0);
16281
17262
  return { ...sourceItem(pin, shareUrl), ...refinement !== void 0 ? { refinement } : {} };
16282
17263
  });
16283
- for (const pin of value.feedback.pins) if (pin.status !== "resolved" && !seen.has(pinKey(pin))) throw new Error("Keep every unresolved review comment in the handoff.");
17264
+ for (const pin of value.feedback.pins) if (!seen.has(pinKey(pin))) throw new Error(`Keep every recorded review comment in the handoff; ${pinKey(pin)} is missing.`);
16284
17265
  return enriched;
16285
17266
  }
16286
- async function preserveReviewErrors(path, action) {
17267
+ function normalizedLocalMetadata(local, pins) {
17268
+ const byRevision = structuredClone(local.byRevision ?? {});
17269
+ const counts = /* @__PURE__ */ new Map();
17270
+ for (const pin of pins) counts.set(pin.id, (counts.get(pin.id) ?? 0) + 1);
17271
+ for (const field of ["categories", "dispositions"]) {
17272
+ for (const [pinId, item2] of Object.entries(local[field] ?? {})) {
17273
+ if (counts.get(pinId) !== 1) continue;
17274
+ const pin = pins.find((value) => value.id === pinId);
17275
+ const revision = revisionOf2(pin);
17276
+ const scoped = byRevision[revision] ?? { categories: {}, dispositions: {} };
17277
+ byRevision[revision] = { ...scoped, [field]: { ...scoped[field], [pinId]: item2 } };
17278
+ }
17279
+ }
17280
+ return { version: local.version ?? 0, byRevision };
17281
+ }
17282
+ async function preserveReviewErrors(path, action3) {
16287
17283
  let failure, result;
16288
17284
  await withArtifactReviewLock(path, async () => {
16289
17285
  try {
16290
- result = await action();
17286
+ result = await action3();
16291
17287
  } catch (error) {
16292
17288
  failure = error;
16293
17289
  }
@@ -16304,7 +17300,7 @@ async function updateDesignHandoff(file, input, { env = process.env, fetchImpl =
16304
17300
  const unlock = await acquireStartLock(join9(initial.root, ".design/handoff.lock"));
16305
17301
  try {
16306
17302
  await preserveReviewErrors(designReviewPath(file, env), async () => {
16307
- const value = snapshot(file, env);
17303
+ const value = snapshot(file, env, shareOptions);
16308
17304
  if (input.revision !== value.current.revision) throw conflict2("The design changed. Refresh the review before updating its handoff.");
16309
17305
  if (["category", "disposition"].includes(input.action)) {
16310
17306
  if (input.version !== value.metadata.version) throw conflict2("Review organization changed in another window. Reload before saving.");
@@ -16314,8 +17310,8 @@ async function updateDesignHandoff(file, input, { env = process.env, fetchImpl =
16314
17310
  if (!choices.includes(input[input.action])) throw new Error(`Invalid ${input.action}.`);
16315
17311
  if (typeof (input.reason ?? "") !== "string" || (input.reason ?? "").length > 16384) throw new Error("Disposition reason is too long.");
16316
17312
  const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
16317
- const local = readJson2(metadataPath(value.current), { version: 0, byRevision: {} });
16318
- const revision = revisionOf(pin);
17313
+ const local = normalizedLocalMetadata(readJson2(metadataPath(value.current), { version: 0, byRevision: {} }), value.feedback.pins);
17314
+ const revision = revisionOf2(pin);
16319
17315
  const scoped = local.byRevision?.[revision] ?? { categories: {}, dispositions: {} };
16320
17316
  const valueForRevision = { ...scoped };
16321
17317
  if (input.action === "category") valueForRevision.categories = { ...scoped.categories, [pin.id]: input.category };
@@ -16336,6 +17332,10 @@ async function updateDesignHandoff(file, input, { env = process.env, fetchImpl =
16336
17332
  }
16337
17333
  if (input.action === "approve") {
16338
17334
  if (!previous || input.contentHash !== previous.contentHash || reviewDigest(previous.basis) !== reviewDigest(value.basis)) throw conflict2("The handoff is out of date. Rebuild and review the current draft before approving.");
17335
+ if (!canApproveDesignHandoffResolution(value.resolution)) {
17336
+ const diagnostic = value.resolution.diagnostics[0];
17337
+ throw conflict2(diagnostic?.message ?? "Resolve the blocking review decisions before approving this handoff.");
17338
+ }
16339
17339
  if (!previous.content.summary.trim()) throw new Error("Write or refine the handoff summary before approving it.");
16340
17340
  const approved = { ...previous, version: previous.version + 1, status: "approved", approval: { contentHash: previous.contentHash, at: (/* @__PURE__ */ new Date()).toISOString() } };
16341
17341
  approved.markdown = renderMarkdown(approved, value.current.document.title);
@@ -16350,11 +17350,11 @@ async function updateDesignHandoff(file, input, { env = process.env, fetchImpl =
16350
17350
  if (input.action === "update" && (!previous || reviewDigest(previous.basis) !== reviewDigest(value.basis))) throw conflict2("The review changed. Rebuild the draft before refining it.");
16351
17351
  const affectedScreens = [...new Set(sections.flatMap((key) => content[key].map((item2) => item2.screenId).filter(Boolean)))];
16352
17352
  const verificationGaps = value.current.verification.status === "verified" ? [] : [`Rendered design verification: ${value.current.verification.status}.`];
16353
- for (const issue of value.current.verification.issues ?? []) if (issue.message && verificationGaps.length < 256) verificationGaps.push(issue.message);
17353
+ for (const issue2 of value.current.verification.issues ?? []) if (issue2.message && verificationGaps.length < 256) verificationGaps.push(issue2.message);
16354
17354
  const reviewNotes = (value.feedback.ledger?.reviews ?? []).filter((entry) => entry.review.overall?.trim()).map((entry) => ({ reviewId: entry.review.reviewId, text: entry.review.overall }));
16355
- const title = value.current.document.title;
17355
+ const title2 = value.current.document.title;
16356
17356
  const draft = {
16357
- title,
17357
+ title: title2,
16358
17358
  reviewNotes,
16359
17359
  kind: "openplanr-design-review-handoff",
16360
17360
  schemaVersion: "1.0.0",
@@ -16364,7 +17364,7 @@ async function updateDesignHandoff(file, input, { env = process.env, fetchImpl =
16364
17364
  content,
16365
17365
  affectedScreens,
16366
17366
  verificationGaps,
16367
- contentHash: reviewDigest({ title, reviewNotes, basis: value.basis, content, affectedScreens, verificationGaps }),
17367
+ contentHash: reviewDigest({ title: title2, reviewNotes, basis: value.basis, content, affectedScreens, verificationGaps }),
16368
17368
  markdown: ""
16369
17369
  };
16370
17370
  draft.markdown = renderMarkdown(draft, value.current.document.title);
@@ -16386,7 +17386,783 @@ async function updateDesignHandoff(file, input, { env = process.env, fetchImpl =
16386
17386
  synchronization = { pending: true, error: error.message };
16387
17387
  }
16388
17388
  }
16389
- return { ...readDesignHandoff(file, { env }), ...synchronization ? { synchronization } : {} };
17389
+ return { ...readDesignHandoff(file, { env, ...shareOptions }), ...synchronization ? { synchronization } : {} };
17390
+ }
17391
+
17392
+ // packages/design/lib/design/implementation-handoff.mjs
17393
+ import { createHash as createHash6, randomUUID as randomUUID3 } from "node:crypto";
17394
+ import {
17395
+ existsSync as existsSync9,
17396
+ mkdirSync as mkdirSync7,
17397
+ readFileSync as readFileSync14,
17398
+ realpathSync as realpathSync6,
17399
+ renameSync as renameSync6,
17400
+ rmSync as rmSync6,
17401
+ writeFileSync as writeFileSync7
17402
+ } from "node:fs";
17403
+ import { dirname as dirname11, join as join10, resolve as resolve6, sep } from "node:path";
17404
+
17405
+ // packages/design/lib/design/implementation-handoff-markdown.mjs
17406
+ var line = (value) => String(value).replaceAll("\r\n", "\n").replaceAll("\r", "\n").trim().replaceAll("\n", " ").replace(/([\\`*_[\]<>#|])/gu, "\\$1");
17407
+ var anchorLabel = (anchor2) => {
17408
+ if (!anchor2) return null;
17409
+ if (anchor2.section) return `section ${anchor2.section}`;
17410
+ if (anchor2.reviewId) return `review ${anchor2.reviewId}, comment ${anchor2.pinId}`;
17411
+ if (anchor2.elementId)
17412
+ return `screen ${anchor2.screenId}, element ${anchor2.elementId}`;
17413
+ return `screen ${anchor2.screenId}`;
17414
+ };
17415
+ function renderImplementationHandoffMarkdown(value) {
17416
+ const lines = [
17417
+ `# ${line(value.title)}`,
17418
+ "",
17419
+ `Design: ${line(value.basis.designId)}`,
17420
+ `Selected direction: ${line(value.basis.selectedVariant)}`,
17421
+ `Authority: Prepare Plan`,
17422
+ "",
17423
+ "## Source references",
17424
+ ""
17425
+ ];
17426
+ for (const source of value.sources) {
17427
+ const anchor2 = anchorLabel(source.anchor);
17428
+ lines.push(
17429
+ `- **${line(source.id)}** \u2014 ${line(source.kind)} \xB7 \`${line(source.path)}\`${anchor2 ? ` \xB7 ${line(anchor2)}` : ""}`
17430
+ );
17431
+ }
17432
+ if (!value.sources.length) lines.push("None recorded.");
17433
+ lines.push("", "## Implementation requirements", "");
17434
+ for (const requirement2 of value.requirements) {
17435
+ lines.push(
17436
+ `### ${line(requirement2.id)} \xB7 ${line(requirement2.kind)}`,
17437
+ "",
17438
+ line(requirement2.statement),
17439
+ "",
17440
+ `Sources: ${requirement2.sourceRefs.map((reference) => `\`${line(reference)}\``).join(", ")}`,
17441
+ "",
17442
+ "Verification:",
17443
+ ...requirement2.verification.map((expectation) => `- ${line(expectation)}`),
17444
+ ""
17445
+ );
17446
+ }
17447
+ lines.push(
17448
+ "This package prepares approved design context for Plan. Plan and Ship remain separate user invocations.",
17449
+ ""
17450
+ );
17451
+ return lines.join("\n");
17452
+ }
17453
+
17454
+ // packages/design/lib/design/implementation-handoff.mjs
17455
+ var MAX_PACKAGE_BYTES = 2 * 1024 * 1024;
17456
+ var MAX_SOURCE_BYTES = 16 * 1024 * 1024;
17457
+ var DIGEST = /^sha256:[a-f0-9]{64}$/u;
17458
+ var REQUIREMENT_KINDS = /* @__PURE__ */ new Set([
17459
+ "behavior",
17460
+ "visual-state",
17461
+ "responsive",
17462
+ "accessibility",
17463
+ "content-data-assumption",
17464
+ "constraint",
17465
+ "verification-intent"
17466
+ ]);
17467
+ var INPUT_FIELDS = /* @__PURE__ */ new Set(["id", "version", "title", "basis", "sources", "requirements"]);
17468
+ var REQUIREMENT_FIELDS = /* @__PURE__ */ new Set(["id", "kind", "statement", "sourceRefs", "verification"]);
17469
+ var normalizeText = (value, label) => {
17470
+ if (typeof value !== "string") throw new TypeError(`${label} must be text.`);
17471
+ const normalized = value.replaceAll("\r\n", "\n").replaceAll("\r", "\n").trim();
17472
+ if (!normalized) throw new TypeError(`${label} cannot be empty.`);
17473
+ return normalized;
17474
+ };
17475
+ var clone4 = (value) => JSON.parse(canonicalizeJson(value));
17476
+ var sha256 = (value) => `sha256:${createHash6("sha256").update(value).digest("hex")}`;
17477
+ var jsonBytes = (value) => `${JSON.stringify(value, null, 2)}
17478
+ `;
17479
+ var assertKnownKeys = (value, allowed, label) => {
17480
+ if (Object.keys(value).some((key) => !allowed.has(key)))
17481
+ throw new TypeError(`${label} contains unknown fields.`);
17482
+ };
17483
+ function atomicText(path, value) {
17484
+ mkdirSync7(dirname11(path), { recursive: true });
17485
+ const temporary = `${path}.${randomUUID3()}.tmp`;
17486
+ try {
17487
+ writeFileSync7(temporary, value, { flag: "wx", mode: 384 });
17488
+ renameSync6(temporary, path);
17489
+ } finally {
17490
+ rmSync6(temporary, { force: true });
17491
+ }
17492
+ }
17493
+ function assertPackageSize(value) {
17494
+ if (Buffer.byteLength(jsonBytes(value)) > MAX_PACKAGE_BYTES)
17495
+ throw new TypeError("The implementation handoff JSON exceeds 2 MB.");
17496
+ if (Buffer.byteLength(value.markdown) > MAX_PACKAGE_BYTES)
17497
+ throw new TypeError("The implementation handoff Markdown exceeds 2 MB.");
17498
+ }
17499
+ function normalizeSource(source) {
17500
+ if (!source || typeof source !== "object" || Array.isArray(source))
17501
+ throw new TypeError("Implementation sources must be objects.");
17502
+ if (!isDesignHandoffRelativePath(source.path) || /[?#%]/u.test(source.path))
17503
+ throw new TypeError("Implementation sources require repository-relative logical paths.");
17504
+ if (source.path.includes("`"))
17505
+ throw new TypeError("Implementation source paths cannot contain Markdown delimiters.");
17506
+ if (/(?:^|\/)[^/:\s]+:[^/@\s]+@/u.test(source.path))
17507
+ throw new TypeError("Implementation source paths cannot contain credentials.");
17508
+ if (!DIGEST.test(source.revision ?? "") || !DIGEST.test(source.digest ?? ""))
17509
+ throw new TypeError("Implementation sources require exact revision and integrity values.");
17510
+ return clone4(source);
17511
+ }
17512
+ function requirementFingerprint(requirement2) {
17513
+ return canonicalizeJson({
17514
+ kind: requirement2.kind,
17515
+ statement: normalizeText(requirement2.statement, "Requirement statement"),
17516
+ sourceRefs: requirement2.sourceRefs.map((value) => normalizeText(value, "Source reference")),
17517
+ verification: requirement2.verification.map(
17518
+ (value) => normalizeText(value, "Verification expectation")
17519
+ )
17520
+ });
17521
+ }
17522
+ function deriveImplementationRequirementId(requirement2) {
17523
+ const hexadecimal = createHash6("sha256").update(requirementFingerprint(requirement2)).digest("hex");
17524
+ const numeric = (BigInt(`0x${hexadecimal}`) % 1000000000000n).toString(10).padStart(12, "0");
17525
+ return `REQ-${numeric}`;
17526
+ }
17527
+ function normalizeRequirement(requirement2) {
17528
+ if (!requirement2 || typeof requirement2 !== "object" || Array.isArray(requirement2))
17529
+ throw new TypeError("Implementation requirements must be objects.");
17530
+ assertKnownKeys(
17531
+ requirement2,
17532
+ REQUIREMENT_FIELDS,
17533
+ "Implementation requirement"
17534
+ );
17535
+ if (!REQUIREMENT_KINDS.has(requirement2.kind))
17536
+ throw new TypeError("Unknown implementation requirement kind.");
17537
+ if (!Array.isArray(requirement2.sourceRefs) || !requirement2.sourceRefs.length)
17538
+ throw new TypeError("Implementation requirements need source references.");
17539
+ if (new Set(requirement2.sourceRefs).size !== requirement2.sourceRefs.length)
17540
+ throw new TypeError("Implementation requirement source references must be distinct.");
17541
+ if (!Array.isArray(requirement2.verification) || !requirement2.verification.length)
17542
+ throw new TypeError("Implementation requirements need observable verification expectations.");
17543
+ const normalized = {
17544
+ kind: requirement2.kind,
17545
+ statement: normalizeText(requirement2.statement, "Requirement statement"),
17546
+ sourceRefs: requirement2.sourceRefs.map((value) => normalizeText(value, "Source reference")),
17547
+ verification: requirement2.verification.map(
17548
+ (value) => normalizeText(value, "Verification expectation")
17549
+ )
17550
+ };
17551
+ const id4 = deriveImplementationRequirementId(normalized);
17552
+ if (requirement2.id !== void 0 && requirement2.id !== id4)
17553
+ throw new TypeError("The supplied requirement identity does not match its canonical content.");
17554
+ return { id: id4, ...normalized };
17555
+ }
17556
+ function composeImplementationHandoff(input) {
17557
+ if (!input || typeof input !== "object" || Array.isArray(input))
17558
+ throw new TypeError("Implementation handoff input must be an object.");
17559
+ assertKnownKeys(
17560
+ input,
17561
+ INPUT_FIELDS,
17562
+ "Implementation handoff input"
17563
+ );
17564
+ const sources = (input.sources ?? []).map(normalizeSource).sort(
17565
+ (left, right) => left.id.localeCompare(right.id)
17566
+ );
17567
+ if (new Set(sources.map((source) => source.id)).size !== sources.length)
17568
+ throw new TypeError("Duplicate implementation source identity.");
17569
+ const sourceIds = new Set(sources.map((source) => source.id));
17570
+ const requirements = (input.requirements ?? []).map(normalizeRequirement).sort((left, right) => left.id.localeCompare(right.id));
17571
+ if (!requirements.length)
17572
+ throw new TypeError("An implementation handoff needs at least one requirement.");
17573
+ if (new Set(requirements.map((item2) => item2.id)).size !== requirements.length)
17574
+ throw new TypeError("Duplicate or colliding implementation requirement identity.");
17575
+ for (const requirement2 of requirements) {
17576
+ if (requirement2.sourceRefs.some((reference) => !sourceIds.has(reference)))
17577
+ throw new TypeError("Implementation requirement references missing evidence.");
17578
+ }
17579
+ const base = {
17580
+ kind: "openplanr-design-implementation-handoff",
17581
+ schemaVersion: "1.0.0",
17582
+ id: normalizeText(input.id, "Implementation handoff identity"),
17583
+ version: input.version ?? 1,
17584
+ status: "draft",
17585
+ authority: "prepare-plan",
17586
+ title: normalizeText(input.title, "Implementation handoff title"),
17587
+ basis: clone4(input.basis),
17588
+ sources,
17589
+ requirements
17590
+ };
17591
+ const markdown = renderImplementationHandoffMarkdown(base);
17592
+ const value = { ...base, contentDigest: "sha256:" + "0".repeat(64), markdown };
17593
+ value.contentDigest = designImplementationHandoffDigest(value);
17594
+ assertDesignImplementationHandoff(value);
17595
+ assertPackageSize(value);
17596
+ return value;
17597
+ }
17598
+ function assertImplementationHandoffProjection(value) {
17599
+ assertDesignImplementationHandoff(value);
17600
+ if (value.sources.map((source) => source.id).join("\n") !== [...value.sources].sort((left, right) => left.id.localeCompare(right.id)).map((source) => source.id).join("\n"))
17601
+ throw new TypeError("Implementation handoff sources are not in canonical order.");
17602
+ if (value.requirements.map((item2) => item2.id).join("\n") !== [...value.requirements].sort((left, right) => left.id.localeCompare(right.id)).map((item2) => item2.id).join("\n"))
17603
+ throw new TypeError("Implementation handoff requirements are not in canonical order.");
17604
+ for (const requirement2 of value.requirements)
17605
+ if (requirement2.id !== deriveImplementationRequirementId(requirement2))
17606
+ throw new TypeError("Implementation requirement identity does not match its canonical content.");
17607
+ if (value.markdown !== renderImplementationHandoffMarkdown(value))
17608
+ throw new TypeError("Implementation handoff Markdown differs from its JSON projection.");
17609
+ assertPackageSize(value);
17610
+ return value;
17611
+ }
17612
+ var resolvedBytes = (resolved) => {
17613
+ const value = resolved?.bytes ?? resolved?.value ?? resolved;
17614
+ if (typeof value === "string" || Buffer.isBuffer(value)) return Buffer.from(value);
17615
+ if (value instanceof Uint8Array) return Buffer.from(value);
17616
+ throw new TypeError("The implementation source resolver must return bytes.");
17617
+ };
17618
+ function verifyImplementationHandoffSources(value, resolveSource) {
17619
+ assertImplementationHandoffProjection(value);
17620
+ if (typeof resolveSource !== "function")
17621
+ throw new TypeError("Source verification requires an explicit resolver.");
17622
+ for (const source of value.sources) {
17623
+ const resolved = resolveSource(source.path, clone4(source));
17624
+ const bytes = resolvedBytes(resolved);
17625
+ if (bytes.byteLength > MAX_SOURCE_BYTES)
17626
+ throw new TypeError(`Implementation source ${source.id} exceeds 16 MB.`);
17627
+ if (sha256(bytes) !== source.digest)
17628
+ throw new TypeError(`Implementation source ${source.id} no longer matches its reference.`);
17629
+ if (source.anchor) {
17630
+ if (!Array.isArray(resolved?.anchors))
17631
+ throw new TypeError(`Implementation source ${source.id} did not resolve its anchor.`);
17632
+ const expected = canonicalizeJson(source.anchor);
17633
+ if (resolved.anchors.filter((anchor2) => canonicalizeJson(anchor2) === expected).length !== 1)
17634
+ throw new TypeError(`Implementation source ${source.id} has an unresolved or ambiguous anchor.`);
17635
+ }
17636
+ }
17637
+ return value;
17638
+ }
17639
+ var regexEscape = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
17640
+ function discoverAnchor(bytes, anchor2) {
17641
+ const text4 = bytes.toString("utf8");
17642
+ const values = anchor2.elementId ? [anchor2.elementId] : anchor2.pinId ? [anchor2.reviewId, anchor2.pinId] : anchor2.section ? [anchor2.section] : [anchor2.screenId];
17643
+ const counts = values.map((value) => (text4.match(new RegExp(regexEscape(value), "gu")) ?? []).length);
17644
+ if (counts.every((count) => count === 1)) return [anchor2];
17645
+ if (counts.some((count) => count === 0)) return [];
17646
+ return [anchor2, anchor2];
17647
+ }
17648
+ function createRepositorySourceResolver(root) {
17649
+ const canonicalRoot = realpathSync6(resolve6(root));
17650
+ return (path, source) => {
17651
+ if (!isDesignHandoffRelativePath(path))
17652
+ throw new TypeError("Implementation source path is not repository-relative.");
17653
+ const candidate = realpathSync6(resolve6(canonicalRoot, path));
17654
+ if (candidate !== canonicalRoot && !candidate.startsWith(`${canonicalRoot}${sep}`))
17655
+ throw new TypeError("Implementation source resolves outside the repository root.");
17656
+ const bytes = readFileSync14(candidate);
17657
+ return source?.anchor ? { bytes, anchors: discoverAnchor(bytes, source.anchor) } : bytes;
17658
+ };
17659
+ }
17660
+ function implementationHandoffPaths(root) {
17661
+ const directory = join10(resolve6(root), "implementation-handoff");
17662
+ return Object.freeze({
17663
+ directory,
17664
+ draftJson: join10(directory, "draft.json"),
17665
+ draftMarkdown: join10(directory, "draft.md"),
17666
+ journal: join10(directory, "draft-publication.json"),
17667
+ current: join10(directory, "current.json"),
17668
+ history: join10(directory, "versions")
17669
+ });
17670
+ }
17671
+ function recoverImplementationHandoffDraft(root) {
17672
+ const paths = implementationHandoffPaths(root);
17673
+ if (!existsSync9(paths.journal)) return false;
17674
+ const journal = JSON.parse(readFileSync14(paths.journal, "utf8"));
17675
+ const value = assertImplementationHandoffProjection(journal.package);
17676
+ if (journal.markdown !== value.markdown)
17677
+ throw new TypeError("Implementation handoff recovery journal is inconsistent.");
17678
+ atomicText(paths.draftJson, jsonBytes(value));
17679
+ atomicText(paths.draftMarkdown, value.markdown);
17680
+ rmSync6(paths.journal, { force: true });
17681
+ return true;
17682
+ }
17683
+ function readImplementationHandoffDraft(root, { allowMissing = true } = {}) {
17684
+ const paths = implementationHandoffPaths(root);
17685
+ recoverImplementationHandoffDraft(root);
17686
+ if (!existsSync9(paths.draftJson)) {
17687
+ if (allowMissing) return null;
17688
+ throw new Error("No implementation handoff draft exists.");
17689
+ }
17690
+ const value = assertImplementationHandoffProjection(
17691
+ JSON.parse(readFileSync14(paths.draftJson, "utf8"))
17692
+ );
17693
+ if (!existsSync9(paths.draftMarkdown))
17694
+ throw new Error("Implementation handoff Markdown is missing.");
17695
+ if (readFileSync14(paths.draftMarkdown, "utf8") !== value.markdown)
17696
+ throw new Error("Implementation handoff JSON and Markdown projections differ.");
17697
+ return value;
17698
+ }
17699
+ function writeImplementationHandoffDraft(root, input, { resolveSource } = {}) {
17700
+ const value = input?.kind ? assertImplementationHandoffProjection(clone4(input)) : composeImplementationHandoff(input);
17701
+ if (value.status !== "draft")
17702
+ throw new TypeError("Only editable drafts can be written through the draft composer.");
17703
+ if (resolveSource) verifyImplementationHandoffSources(value, resolveSource);
17704
+ const paths = implementationHandoffPaths(root);
17705
+ atomicText(paths.journal, jsonBytes({ package: value, markdown: value.markdown }));
17706
+ recoverImplementationHandoffDraft(root);
17707
+ return value;
17708
+ }
17709
+ function exportImplementationHandoffPackage(value) {
17710
+ const checked2 = assertImplementationHandoffProjection(clone4(value));
17711
+ return Object.freeze({ json: jsonBytes(checked2), markdown: checked2.markdown });
17712
+ }
17713
+ function importImplementationHandoffPackage(input, { resolveSource } = {}) {
17714
+ if (!input || typeof input.json !== "string" || typeof input.markdown !== "string")
17715
+ throw new TypeError("Portable handoff import requires JSON and Markdown text.");
17716
+ if (Buffer.byteLength(input.json) > MAX_PACKAGE_BYTES || Buffer.byteLength(input.markdown) > MAX_PACKAGE_BYTES)
17717
+ throw new TypeError("Portable handoff import exceeds 2 MB.");
17718
+ const value = assertImplementationHandoffProjection(JSON.parse(input.json));
17719
+ if (input.markdown.replaceAll("\r\n", "\n").replaceAll("\r", "\n") !== value.markdown)
17720
+ throw new TypeError("Imported handoff Markdown does not match its JSON projection.");
17721
+ if (resolveSource) verifyImplementationHandoffSources(value, resolveSource);
17722
+ return value;
17723
+ }
17724
+
17725
+ // packages/design/lib/design/implementation-handoff-approval.mjs
17726
+ import { createHash as createHash7, randomUUID as randomUUID4 } from "node:crypto";
17727
+ import {
17728
+ existsSync as existsSync10,
17729
+ mkdirSync as mkdirSync8,
17730
+ readFileSync as readFileSync15,
17731
+ readdirSync as readdirSync4,
17732
+ renameSync as renameSync7,
17733
+ rmSync as rmSync7,
17734
+ writeFileSync as writeFileSync8
17735
+ } from "node:fs";
17736
+ import { dirname as dirname12, join as join11 } from "node:path";
17737
+ var DIGEST2 = /^sha256:[a-f0-9]{64}$/u;
17738
+ var ID = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/u;
17739
+ var APPROVE_CAPABILITY = "design:implementation-handoff:approve";
17740
+ var REVOKE_CAPABILITY = "design:implementation-handoff:revoke";
17741
+ var MAX_REASON_BYTES = 16 * 1024;
17742
+ var clone5 = (value) => JSON.parse(canonicalizeJson(value));
17743
+ var jsonBytes2 = (value) => `${JSON.stringify(value, null, 2)}
17744
+ `;
17745
+ var requestKey = (requestId) => createHash7("sha256").update(requestId).digest("hex");
17746
+ var packageKey = (value) => {
17747
+ const identity = createHash7("sha256").update(value.id).digest("hex").slice(0, 16);
17748
+ return `${identity}-v${value.version}-${value.contentDigest.slice(7, 23)}`;
17749
+ };
17750
+ function atomicText2(path, value) {
17751
+ mkdirSync8(dirname12(path), { recursive: true });
17752
+ const temporary = `${path}.${randomUUID4()}.tmp`;
17753
+ try {
17754
+ writeFileSync8(temporary, value, { flag: "wx", mode: 384 });
17755
+ renameSync7(temporary, path);
17756
+ } finally {
17757
+ rmSync7(temporary, { force: true });
17758
+ }
17759
+ }
17760
+ function immutableText(path, value) {
17761
+ mkdirSync8(dirname12(path), { recursive: true });
17762
+ try {
17763
+ writeFileSync8(path, value, { flag: "wx", mode: 384 });
17764
+ } catch (error) {
17765
+ if (error.code !== "EEXIST") throw error;
17766
+ if (readFileSync15(path, "utf8") !== value)
17767
+ throw lifecycleConflict("Immutable implementation handoff history conflicts with this operation.");
17768
+ }
17769
+ }
17770
+ function lifecycleConflict(message2) {
17771
+ return Object.assign(new Error(message2), { code: "E_IMPLEMENTATION_HANDOFF_CONFLICT", statusCode: 409 });
17772
+ }
17773
+ function lifecycleForbidden(message2) {
17774
+ return Object.assign(new Error(message2), { code: "E_IMPLEMENTATION_HANDOFF_FORBIDDEN", statusCode: 403 });
17775
+ }
17776
+ function normalizeId(value, label) {
17777
+ if (typeof value !== "string" || value.length > 160 || !ID.test(value))
17778
+ throw new TypeError(`${label} is invalid.`);
17779
+ return value;
17780
+ }
17781
+ function normalizeRequestId(value) {
17782
+ return normalizeId(value, "Implementation handoff request identity");
17783
+ }
17784
+ function timestamp2(clock) {
17785
+ const candidate = typeof clock === "function" ? clock() : /* @__PURE__ */ new Date();
17786
+ const value = candidate instanceof Date ? candidate : new Date(candidate);
17787
+ if (!Number.isFinite(value.getTime())) throw new TypeError("The approval clock returned an invalid timestamp.");
17788
+ return value.toISOString();
17789
+ }
17790
+ function authorizeActor(actor, capability, at) {
17791
+ if (!actor || typeof actor !== "object" || Array.isArray(actor))
17792
+ throw lifecycleForbidden("Implementation handoff approval requires an owner identity.");
17793
+ const actorId = normalizeId(actor.id, "Implementation handoff actor identity");
17794
+ if (!["owner", "maintainer"].includes(actor.role))
17795
+ throw lifecycleForbidden("Only an owner or maintainer can change implementation handoff approval.");
17796
+ if (!Array.isArray(actor.capabilities) || !actor.capabilities.includes(capability))
17797
+ throw lifecycleForbidden("The actor lacks the required implementation handoff capability.");
17798
+ if (actor.sessionExpiresAt === void 0) {
17799
+ if (actorId !== "local-owner")
17800
+ throw lifecycleForbidden("Hosted implementation handoff approval requires a bounded session.");
17801
+ } else {
17802
+ const expiry = new Date(actor.sessionExpiresAt);
17803
+ if (!Number.isFinite(expiry.getTime()) || expiry.getTime() <= new Date(at).getTime())
17804
+ throw lifecycleForbidden("The implementation handoff approval session has expired.");
17805
+ }
17806
+ return Object.freeze({ actorId, role: actor.role, capability });
17807
+ }
17808
+ function implementationHandoffApprovalPaths(root) {
17809
+ const base = implementationHandoffPaths(root);
17810
+ return Object.freeze({
17811
+ ...base,
17812
+ events: join11(base.directory, "events"),
17813
+ journal: join11(base.directory, "lifecycle-publication.json")
17814
+ });
17815
+ }
17816
+ function archivePaths(root, value) {
17817
+ const directory = join11(implementationHandoffPaths(root).history, packageKey(value));
17818
+ return { directory, json: join11(directory, "handoff.json"), markdown: join11(directory, "handoff.md") };
17819
+ }
17820
+ function eventPath(root, requestId) {
17821
+ return join11(implementationHandoffApprovalPaths(root).events, `${requestKey(requestId)}.json`);
17822
+ }
17823
+ function readJson3(path, fallback = void 0) {
17824
+ try {
17825
+ return JSON.parse(readFileSync15(path, "utf8"));
17826
+ } catch (error) {
17827
+ if (error.code === "ENOENT" && fallback !== void 0) return fallback;
17828
+ throw error;
17829
+ }
17830
+ }
17831
+ function readExistingRequest(root, signature2) {
17832
+ const existing = readJson3(eventPath(root, signature2.requestId), null);
17833
+ if (!existing) return null;
17834
+ if (canonicalizeJson(existing.signature) !== canonicalizeJson(signature2))
17835
+ throw lifecycleConflict("This implementation handoff request identity was already used for different input.");
17836
+ return existing;
17837
+ }
17838
+ function pointerFor(value, status, eventId, extra = {}) {
17839
+ return {
17840
+ kind: "openplanr-design-implementation-handoff-current",
17841
+ schemaVersion: "1.0.0",
17842
+ id: value.id,
17843
+ version: value.version,
17844
+ contentDigest: value.contentDigest,
17845
+ status,
17846
+ authority: "prepare-plan",
17847
+ eventId,
17848
+ ...extra
17849
+ };
17850
+ }
17851
+ function writeLifecycleJournal(root, journal) {
17852
+ const paths = implementationHandoffApprovalPaths(root);
17853
+ if (existsSync10(paths.journal)) recoverImplementationHandoffApproval(root);
17854
+ atomicText2(paths.journal, jsonBytes2(journal));
17855
+ return recoverImplementationHandoffApproval(root);
17856
+ }
17857
+ function recoverImplementationHandoffApproval(root) {
17858
+ const paths = implementationHandoffApprovalPaths(root);
17859
+ if (!existsSync10(paths.journal)) return false;
17860
+ const journal = readJson3(paths.journal);
17861
+ if (journal.kind !== "openplanr-design-implementation-handoff-lifecycle-publication" || journal.schemaVersion !== "1.0.0")
17862
+ throw new TypeError("The implementation handoff lifecycle journal is invalid.");
17863
+ if (journal.archive) {
17864
+ const value = assertImplementationHandoffProjection(journal.archive);
17865
+ if (value.status !== "approved") throw new TypeError("Only approved packages belong in immutable history.");
17866
+ const archive = archivePaths(root, value);
17867
+ immutableText(archive.json, jsonBytes2(value));
17868
+ immutableText(archive.markdown, value.markdown);
17869
+ }
17870
+ immutableText(eventPath(root, journal.event.requestId), jsonBytes2(journal.event));
17871
+ atomicText2(paths.current, jsonBytes2(journal.pointer));
17872
+ rmSync7(paths.journal, { force: true });
17873
+ return true;
17874
+ }
17875
+ function readImplementationHandoffVersion(root, identity) {
17876
+ recoverImplementationHandoffApproval(root);
17877
+ const matches = listImplementationHandoffHistory(root).filter((value) => value.id === identity.id && value.version === identity.version && (identity.contentDigest === void 0 || value.contentDigest === identity.contentDigest));
17878
+ if (matches.length !== 1)
17879
+ throw lifecycleConflict(matches.length ? "Implementation handoff version identity is ambiguous." : "Implementation handoff version was not found.");
17880
+ return matches[0];
17881
+ }
17882
+ function listImplementationHandoffHistory(root) {
17883
+ recoverImplementationHandoffApproval(root);
17884
+ const directory = implementationHandoffPaths(root).history;
17885
+ if (!existsSync10(directory)) return [];
17886
+ return readdirSync4(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => assertImplementationHandoffProjection(readJson3(join11(directory, entry.name, "handoff.json")))).sort((left, right) => left.version - right.version || left.id.localeCompare(right.id));
17887
+ }
17888
+ function readImplementationHandoffLifecycle(root) {
17889
+ recoverImplementationHandoffApproval(root);
17890
+ const paths = implementationHandoffApprovalPaths(root);
17891
+ const current = readJson3(paths.current, null);
17892
+ const events = existsSync10(paths.events) ? readdirSync4(paths.events).filter((name) => name.endsWith(".json")).map((name) => readJson3(join11(paths.events, name))).sort((left, right) => left.at.localeCompare(right.at) || left.eventId.localeCompare(right.eventId)) : [];
17893
+ return Object.freeze({ current, history: listImplementationHandoffHistory(root), events });
17894
+ }
17895
+ function previewImplementationHandoffApproval(root) {
17896
+ const draft = readImplementationHandoffDraft(root);
17897
+ const lifecycle = readImplementationHandoffLifecycle(root);
17898
+ if (!draft) return Object.freeze({ available: false, summary: null, approvalRequest: null });
17899
+ const alreadyCurrent = lifecycle.current?.status === "approved" && lifecycle.current.id === draft.id && lifecycle.current.version === draft.version && lifecycle.current.contentDigest === draft.contentDigest;
17900
+ return Object.freeze({
17901
+ available: draft.basis.readiness.status === "ready" && !alreadyCurrent,
17902
+ summary: {
17903
+ title: draft.title,
17904
+ packageVersion: draft.version,
17905
+ selectedVariant: draft.basis.selectedVariant,
17906
+ requirementCount: draft.requirements.length,
17907
+ unresolvedNonblockingItems: 0,
17908
+ effect: "Prepare Plan",
17909
+ description: "Approve this reviewed design context for a later, separate Plan invocation."
17910
+ },
17911
+ approvalRequest: {
17912
+ expectedVersion: draft.version,
17913
+ expectedContentDigest: draft.contentDigest
17914
+ }
17915
+ });
17916
+ }
17917
+ function assertExpectedDraft(draft, request2, currentBasis) {
17918
+ if (!Number.isInteger(request2.expectedVersion) || request2.expectedVersion < 1 || !DIGEST2.test(request2.expectedContentDigest ?? ""))
17919
+ throw new TypeError("Approval requires the expected draft version and content identity.");
17920
+ if (draft.version !== request2.expectedVersion || draft.contentDigest !== request2.expectedContentDigest)
17921
+ throw lifecycleConflict("The implementation package changed after it was loaded. Refresh before approving.");
17922
+ if (currentBasis && canonicalizeJson(draft.basis) !== canonicalizeJson(currentBasis))
17923
+ throw lifecycleConflict("The design basis changed after this implementation package was composed.");
17924
+ if (draft.basis.readiness.status !== "ready")
17925
+ throw lifecycleConflict("Only a ready implementation package can be approved.");
17926
+ }
17927
+ function assertUniqueVersion(root, value) {
17928
+ const existing = listImplementationHandoffHistory(root).find((item2) => item2.id === value.id && item2.version === value.version);
17929
+ if (existing && existing.contentDigest !== value.contentDigest)
17930
+ throw lifecycleConflict("This implementation handoff version already identifies different content.");
17931
+ return existing;
17932
+ }
17933
+ function approveImplementationHandoff(root, request2, options = {}) {
17934
+ const requestId = normalizeRequestId(request2?.requestId);
17935
+ const at = timestamp2(options.clock);
17936
+ const actor = authorizeActor(options.actor, APPROVE_CAPABILITY, at);
17937
+ const signature2 = {
17938
+ operation: "approve",
17939
+ requestId,
17940
+ expectedVersion: request2.expectedVersion,
17941
+ expectedContentDigest: request2.expectedContentDigest,
17942
+ actor
17943
+ };
17944
+ const repeated = readExistingRequest(root, signature2);
17945
+ if (repeated) {
17946
+ return {
17947
+ package: readImplementationHandoffVersion(root, repeated.package),
17948
+ current: readImplementationHandoffLifecycle(root).current,
17949
+ event: repeated,
17950
+ repeated: true
17951
+ };
17952
+ }
17953
+ const draft = readImplementationHandoffDraft(root, { allowMissing: false });
17954
+ assertExpectedDraft(draft, request2, options.currentBasis);
17955
+ if (options.resolveSource) verifyImplementationHandoffSources(draft, options.resolveSource);
17956
+ const approved = assertImplementationHandoffProjection({
17957
+ ...clone5(draft),
17958
+ status: "approved",
17959
+ approval: {
17960
+ actorId: actor.actorId,
17961
+ approvedAt: at,
17962
+ contentDigest: draft.contentDigest,
17963
+ authority: "prepare-plan"
17964
+ }
17965
+ });
17966
+ const existing = assertUniqueVersion(root, approved);
17967
+ if (existing) {
17968
+ const lifecycle = readImplementationHandoffLifecycle(root);
17969
+ if (lifecycle.current?.status === "approved" && lifecycle.current.id === approved.id && lifecycle.current.version === approved.version && lifecycle.current.contentDigest === approved.contentDigest)
17970
+ return { package: existing, current: lifecycle.current, event: lifecycle.events.find((item2) => item2.type === "approved" && item2.package.contentDigest === approved.contentDigest) ?? null, repeated: true };
17971
+ throw lifecycleConflict("This immutable implementation handoff version already exists outside the current approval.");
17972
+ }
17973
+ const eventId = `handoff-approved-${requestKey(requestId).slice(0, 24)}`;
17974
+ const event = {
17975
+ kind: "openplanr-design-implementation-handoff-event",
17976
+ schemaVersion: "1.0.0",
17977
+ eventId,
17978
+ type: "approved",
17979
+ requestId,
17980
+ signature: signature2,
17981
+ package: { id: approved.id, version: approved.version, contentDigest: approved.contentDigest },
17982
+ actor,
17983
+ at,
17984
+ authority: "prepare-plan"
17985
+ };
17986
+ const pointer = pointerFor(approved, "approved", eventId, { updatedAt: at });
17987
+ writeLifecycleJournal(root, {
17988
+ kind: "openplanr-design-implementation-handoff-lifecycle-publication",
17989
+ schemaVersion: "1.0.0",
17990
+ archive: approved,
17991
+ event,
17992
+ pointer
17993
+ });
17994
+ return { package: approved, current: pointer, event, repeated: false };
17995
+ }
17996
+ function supersedeImplementationHandoff(root, replacement, request2, options = {}) {
17997
+ const checked2 = assertImplementationHandoffProjection(clone5(replacement));
17998
+ if (checked2.status !== "draft") throw new TypeError("A superseding package must still be a draft.");
17999
+ const requestId = normalizeRequestId(request2?.requestId);
18000
+ const at = timestamp2(options.clock);
18001
+ const actor = authorizeActor(options.actor, APPROVE_CAPABILITY, at);
18002
+ const lifecycle = readImplementationHandoffLifecycle(root);
18003
+ const currentIdentity = lifecycle.current && {
18004
+ id: lifecycle.current.id,
18005
+ version: lifecycle.current.version,
18006
+ contentDigest: lifecycle.current.contentDigest
18007
+ };
18008
+ const signature2 = {
18009
+ operation: "supersede",
18010
+ requestId,
18011
+ current: currentIdentity,
18012
+ replacement: { id: checked2.id, version: checked2.version, contentDigest: checked2.contentDigest },
18013
+ actor
18014
+ };
18015
+ const repeated = readExistingRequest(root, signature2);
18016
+ if (repeated) return { current: readImplementationHandoffLifecycle(root).current, event: repeated, repeated: true };
18017
+ if (!lifecycle.current || lifecycle.current.status !== "approved") return null;
18018
+ if (lifecycle.current.id === checked2.id && lifecycle.current.version === checked2.version && lifecycle.current.contentDigest === checked2.contentDigest) return null;
18019
+ if (checked2.version <= lifecycle.current.version)
18020
+ throw lifecycleConflict("A regenerated implementation package must use a newer version.");
18021
+ const eventId = `handoff-superseded-${requestKey(requestId).slice(0, 24)}`;
18022
+ const event = {
18023
+ kind: "openplanr-design-implementation-handoff-event",
18024
+ schemaVersion: "1.0.0",
18025
+ eventId,
18026
+ type: "superseded",
18027
+ requestId,
18028
+ signature: signature2,
18029
+ package: signature2.current,
18030
+ supersededBy: signature2.replacement,
18031
+ actor,
18032
+ at,
18033
+ authority: "prepare-plan"
18034
+ };
18035
+ const prior = readImplementationHandoffVersion(root, signature2.current);
18036
+ const pointer = pointerFor(prior, "superseded", eventId, { supersededBy: signature2.replacement, updatedAt: at });
18037
+ writeLifecycleJournal(root, {
18038
+ kind: "openplanr-design-implementation-handoff-lifecycle-publication",
18039
+ schemaVersion: "1.0.0",
18040
+ event,
18041
+ pointer
18042
+ });
18043
+ return { current: pointer, event, repeated: false };
18044
+ }
18045
+ function regenerateImplementationHandoffDraft(root, input, request2, options = {}) {
18046
+ const requestId = normalizeRequestId(request2?.requestId);
18047
+ const existingEvent = readJson3(eventPath(root, requestId), null);
18048
+ if (existingEvent) {
18049
+ const at = timestamp2(options.clock);
18050
+ const actor = authorizeActor(options.actor, APPROVE_CAPABILITY, at);
18051
+ if (existingEvent.type !== "superseded" || existingEvent.requestId !== requestId || canonicalizeJson(existingEvent.actor) !== canonicalizeJson(actor))
18052
+ throw lifecycleConflict("This implementation handoff request identity was already used for a different operation.");
18053
+ const candidate = composeImplementationHandoff({ ...input, version: existingEvent.supersededBy.version });
18054
+ if (options.resolveSource) verifyImplementationHandoffSources(candidate, options.resolveSource);
18055
+ if (candidate.id !== existingEvent.supersededBy.id || candidate.contentDigest !== existingEvent.supersededBy.contentDigest)
18056
+ throw lifecycleConflict("This regeneration request identity was already used for different package content.");
18057
+ const draft2 = readImplementationHandoffDraft(root, { allowMissing: false });
18058
+ if (draft2.id !== candidate.id || draft2.version !== candidate.version || draft2.contentDigest !== candidate.contentDigest)
18059
+ throw lifecycleConflict("The regenerated draft no longer matches this completed request.");
18060
+ return {
18061
+ draft: draft2,
18062
+ supersession: { current: readImplementationHandoffLifecycle(root).current, event: existingEvent, repeated: true }
18063
+ };
18064
+ }
18065
+ const lifecycle = readImplementationHandoffLifecycle(root);
18066
+ const currentDraft = readImplementationHandoffDraft(root);
18067
+ const maximum = Math.max(0, currentDraft?.version ?? 0, ...lifecycle.history.map((item2) => item2.version));
18068
+ const draft = writeImplementationHandoffDraft(root, { ...input, version: maximum + 1 }, { resolveSource: options.resolveSource });
18069
+ const supersession = supersedeImplementationHandoff(root, draft, { requestId }, options);
18070
+ return { draft, supersession };
18071
+ }
18072
+ function revokeImplementationHandoff(root, request2, options = {}) {
18073
+ const requestId = normalizeRequestId(request2.requestId);
18074
+ const reason = typeof request2.reason === "string" ? request2.reason.trim() : "";
18075
+ if (!reason || Buffer.byteLength(reason) > MAX_REASON_BYTES)
18076
+ throw new TypeError("Revocation requires a concise reason.");
18077
+ const at = timestamp2(options.clock);
18078
+ const actor = authorizeActor(options.actor, REVOKE_CAPABILITY, at);
18079
+ const signature2 = {
18080
+ operation: "revoke",
18081
+ requestId,
18082
+ expectedVersion: request2.expectedVersion,
18083
+ expectedContentDigest: request2.expectedContentDigest,
18084
+ reason,
18085
+ actor
18086
+ };
18087
+ const repeated = readExistingRequest(root, signature2);
18088
+ if (repeated) return { current: readImplementationHandoffLifecycle(root).current, event: repeated, repeated: true };
18089
+ const lifecycle = readImplementationHandoffLifecycle(root);
18090
+ if (!lifecycle.current || lifecycle.current.status !== "approved")
18091
+ throw lifecycleConflict("There is no current approved implementation package to revoke.");
18092
+ if (request2?.expectedVersion !== lifecycle.current.version || request2?.expectedContentDigest !== lifecycle.current.contentDigest)
18093
+ throw lifecycleConflict("The current implementation package changed before revocation.");
18094
+ const approved = readImplementationHandoffVersion(root, lifecycle.current);
18095
+ const eventId = `handoff-revoked-${requestKey(requestId).slice(0, 24)}`;
18096
+ const event = {
18097
+ kind: "openplanr-design-implementation-handoff-event",
18098
+ schemaVersion: "1.0.0",
18099
+ eventId,
18100
+ type: "revoked",
18101
+ requestId,
18102
+ signature: signature2,
18103
+ package: { id: approved.id, version: approved.version, contentDigest: approved.contentDigest },
18104
+ actor,
18105
+ at,
18106
+ reason,
18107
+ authority: "prepare-plan"
18108
+ };
18109
+ const pointer = pointerFor(approved, "revoked", eventId, {
18110
+ revocation: { actorId: actor.actorId, revokedAt: at, reason },
18111
+ updatedAt: at
18112
+ });
18113
+ writeLifecycleJournal(root, {
18114
+ kind: "openplanr-design-implementation-handoff-lifecycle-publication",
18115
+ schemaVersion: "1.0.0",
18116
+ event,
18117
+ pointer
18118
+ });
18119
+ return { current: pointer, event, repeated: false };
18120
+ }
18121
+ function compareImplementationHandoffVersions(root, leftIdentity, rightIdentity) {
18122
+ const resolveValue = (identity) => identity === "draft" ? readImplementationHandoffDraft(root, { allowMissing: false }) : readImplementationHandoffVersion(root, identity);
18123
+ const left = resolveValue(leftIdentity);
18124
+ const right = resolveValue(rightIdentity);
18125
+ const sourceIds = (value) => new Set(value.sources.map((item2) => item2.id));
18126
+ const requirementIds = (value) => new Set(value.requirements.map((item2) => item2.id));
18127
+ const difference = (before, after) => ({
18128
+ added: [...after].filter((id4) => !before.has(id4)).sort(),
18129
+ removed: [...before].filter((id4) => !after.has(id4)).sort()
18130
+ });
18131
+ return Object.freeze({
18132
+ left: { id: left.id, version: left.version, contentDigest: left.contentDigest },
18133
+ right: { id: right.id, version: right.version, contentDigest: right.contentDigest },
18134
+ changed: left.contentDigest !== right.contentDigest,
18135
+ basisChanged: canonicalizeJson(left.basis) !== canonicalizeJson(right.basis),
18136
+ titleChanged: left.title !== right.title,
18137
+ sources: difference(sourceIds(left), sourceIds(right)),
18138
+ requirements: difference(requirementIds(left), requirementIds(right))
18139
+ });
18140
+ }
18141
+ var IMPLEMENTATION_HANDOFF_APPROVE_CAPABILITY = APPROVE_CAPABILITY;
18142
+ var IMPLEMENTATION_HANDOFF_REVOKE_CAPABILITY = REVOKE_CAPABILITY;
18143
+
18144
+ // packages/design/lib/design/design-plan-handoff.mjs
18145
+ var clone6 = (value) => JSON.parse(canonicalizeJson(value));
18146
+ function prepareDesignPlanHandoff(handoff, { subject } = {}) {
18147
+ assertDesignImplementationHandoff(handoff);
18148
+ if (handoff.status !== "approved") throw new TypeError("Continue to Plan requires an approved implementation handoff.");
18149
+ const target = String(subject ?? handoff.basis.designId).trim();
18150
+ if (!target || /[\r\n]/u.test(target)) throw new TypeError("Plan subject must be one non-empty line.");
18151
+ return Object.freeze({
18152
+ kind: "openplanr-design-plan-handoff",
18153
+ schemaVersion: "1.0.0",
18154
+ authority: "prepare-plan",
18155
+ handoff: clone6({ id: handoff.id, version: handoff.version, contentDigest: handoff.contentDigest }),
18156
+ subject: target,
18157
+ invocations: Object.freeze({
18158
+ claudeCode: `/planr:plan ${target}`,
18159
+ codex: `$planr:plan ${target}`,
18160
+ chatgpt: `$planr:plan ${target}`,
18161
+ cursor: `$planr:plan ${target}`,
18162
+ fallback: `$planr:plan ${target}`
18163
+ }),
18164
+ effects: Object.freeze({ planningFilesWritten: false, agentDispatched: false, shipStarted: false, gitChanged: false })
18165
+ });
16390
18166
  }
16391
18167
 
16392
18168
  // packages/design/lib/design/review.mjs
@@ -16405,7 +18181,7 @@ function readDesignFeedback(file, env = process.env) {
16405
18181
  const ledger = readArtifactReviewState(designReviewPath(file, env), {
16406
18182
  allowMissing: true
16407
18183
  });
16408
- const digest3 = digestArtifactEnvelope(current.envelope);
18184
+ const digest4 = digestArtifactEnvelope(current.envelope);
16409
18185
  const pins = (ledger?.reviews ?? []).flatMap(
16410
18186
  (entry) => entry.review.pins.map((pin) => {
16411
18187
  const target = current.entries.find(
@@ -16418,7 +18194,7 @@ function readDesignFeedback(file, env = process.env) {
16418
18194
  reviewId: entry.review.reviewId,
16419
18195
  reviewOf: entry.review.reviewOf,
16420
18196
  ...entry.review.reviewId.startsWith("shared-") ? { revisionId: entry.review.reviewId.slice(7) } : {},
16421
- stale: entry.stale || entry.review.reviewOf !== digest3 || !target || Boolean(anchorMissing),
18197
+ stale: entry.stale || entry.review.reviewOf !== digest4 || !target || Boolean(anchorMissing),
16422
18198
  ...target ? { screenId: target.screenId, variantId: target.variantId } : {}
16423
18199
  };
16424
18200
  })
@@ -16427,12 +18203,12 @@ function readDesignFeedback(file, env = process.env) {
16427
18203
  revision: current.revision,
16428
18204
  reviewPath: designReviewPath(file, env),
16429
18205
  pins,
16430
- state: readJson2(join10(current.root, ".design/studio-state.json"), {
18206
+ state: readJson2(join12(current.root, ".design/studio-state.json"), {
16431
18207
  state: {},
16432
18208
  stateVersion: 0
16433
18209
  }).state,
16434
18210
  ledger,
16435
- shared: readJson2(join10(current.root, ".design/shared-feedback.json"), null)
18211
+ shared: readJson2(join12(current.root, ".design/shared-feedback.json"), null)
16436
18212
  };
16437
18213
  }
16438
18214
  function exportDesignReview(file, { scope = "all", env = process.env } = {}) {
@@ -16542,26 +18318,53 @@ function validateState(value, current) {
16542
18318
  for (const viewport of Object.values(value.viewports))
16543
18319
  validateViewport(viewport);
16544
18320
  }
16545
- for (const [id3, rating] of Object.entries(value.ratings ?? {}))
16546
- if (!variants.has(id3) || !Number.isInteger(rating) || rating < 1 || rating > 5)
18321
+ for (const [id4, rating] of Object.entries(value.ratings ?? {}))
18322
+ if (!variants.has(id4) || !Number.isInteger(rating) || rating < 1 || rating > 5)
16547
18323
  throw new Error("Ratings must target available variants and be 1\u20135.");
16548
- for (const [id3, point] of Object.entries(value.positions ?? {}))
16549
- if (!current.entries.some((entry) => entry.artifactId === id3) || !Number.isFinite(point.x) || !Number.isFinite(point.y) || Math.abs(point.x) > 1e7 || Math.abs(point.y) > 1e7)
18324
+ for (const [id4, point] of Object.entries(value.positions ?? {}))
18325
+ if (!current.entries.some((entry) => entry.artifactId === id4) || !Number.isFinite(point.x) || !Number.isFinite(point.y) || Math.abs(point.x) > 1e7 || Math.abs(point.y) > 1e7)
16550
18326
  throw new Error("Invalid artboard arrangement.");
16551
18327
  return structuredClone(value);
16552
18328
  }
16553
18329
  function projectRoot(root) {
16554
18330
  let candidate = root;
16555
18331
  while (true) {
16556
- if (existsSync9(join10(candidate, ".planr")) || existsSync9(join10(candidate, ".git")))
18332
+ if (existsSync11(join12(candidate, ".planr")) || existsSync11(join12(candidate, ".git")))
16557
18333
  return candidate;
16558
- const parent = dirname11(candidate);
18334
+ const parent = dirname13(candidate);
16559
18335
  if (parent === candidate) return root;
16560
18336
  candidate = parent;
16561
18337
  }
16562
18338
  }
18339
+ function currentImplementationBasis(file, env) {
18340
+ const handoff = readDesignHandoff(file, { env });
18341
+ const readiness = readDesignHandoffReadiness(file, { env });
18342
+ if (!handoff.draft || handoff.draft.status !== "approved" || !handoff.current)
18343
+ throw Object.assign(
18344
+ new Error("Approve the current review handoff before composing the implementation package."),
18345
+ { statusCode: 409 }
18346
+ );
18347
+ if (readiness.readiness.status !== "ready")
18348
+ throw Object.assign(
18349
+ new Error("Resolve the remaining design readiness checks before composing the implementation package."),
18350
+ { statusCode: 409 }
18351
+ );
18352
+ return {
18353
+ designId: handoff.basis.designId,
18354
+ sourceRevision: `sha256:${handoff.basis.sourceRevision}`,
18355
+ selectedVariant: handoff.basis.selectedVariant,
18356
+ readiness: {
18357
+ status: readiness.readiness.status,
18358
+ digest: readiness.digest
18359
+ },
18360
+ reviewHandoff: {
18361
+ version: handoff.draft.version,
18362
+ contentDigest: `sha256:${handoff.draft.contentHash}`
18363
+ }
18364
+ };
18365
+ }
16563
18366
  async function persistDesignTaste(current, state) {
16564
- const path = join10(
18367
+ const path = join12(
16565
18368
  projectRoot(current.root),
16566
18369
  ".planr/design-system/taste.json"
16567
18370
  );
@@ -16574,7 +18377,7 @@ async function persistDesignTaste(current, state) {
16574
18377
  );
16575
18378
  const ids = (values) => [
16576
18379
  ...new Set(
16577
- (Array.isArray(values) ? values : []).filter((id3) => validIds.has(id3))
18380
+ (Array.isArray(values) ? values : []).filter((id4) => validIds.has(id4))
16578
18381
  )
16579
18382
  ];
16580
18383
  const explicitSelected = ids(
@@ -16583,7 +18386,7 @@ async function persistDesignTaste(current, state) {
16583
18386
  const selected = explicitSelected.includes(state.selectedVariant) ? [state.selectedVariant] : explicitSelected;
16584
18387
  const rejected = ids(
16585
18388
  state.preferences?.rejected ?? previous.rejected
16586
- ).filter((id3) => !selected.includes(id3));
18389
+ ).filter((id4) => !selected.includes(id4));
16587
18390
  atomicJson(path, {
16588
18391
  ...taste,
16589
18392
  designs: {
@@ -16610,7 +18413,7 @@ async function saveDesignState(file, { state, revision, stateVersion }) {
16610
18413
  new Error("The design changed. Reload before saving feedback."),
16611
18414
  { statusCode: 409 }
16612
18415
  );
16613
- const path = join10(current.root, ".design/studio-state.json");
18416
+ const path = join12(current.root, ".design/studio-state.json");
16614
18417
  const release = await acquireStartLock(`${path}.lock`);
16615
18418
  try {
16616
18419
  current = currentDesign(file);
@@ -16650,9 +18453,12 @@ function respond(res, status, value) {
16650
18453
  var readBody = async (req) => JSON.parse(
16651
18454
  await readRequestBody(req, { maxBytes: 128 * 1024, encoding: "utf8" })
16652
18455
  );
18456
+ var readImplementationBody = async (req) => JSON.parse(
18457
+ await readRequestBody(req, { maxBytes: 5 * 1024 * 1024, encoding: "utf8" })
18458
+ );
16653
18459
  async function startDesignReview(file, options = {}) {
16654
18460
  const { root } = currentDesign(file);
16655
- const release = await acquireStartLock(join10(root, ".design/start.lock"));
18461
+ const release = await acquireStartLock(join12(root, ".design/start.lock"));
16656
18462
  try {
16657
18463
  return await startDesignReviewUnlocked(file, options);
16658
18464
  } finally {
@@ -16665,11 +18471,12 @@ async function startDesignReviewUnlocked(file, {
16665
18471
  noOpen = true,
16666
18472
  view,
16667
18473
  openUrl,
16668
- fetchImpl = fetch
18474
+ fetchImpl = fetch,
18475
+ clock = () => /* @__PURE__ */ new Date()
16669
18476
  } = {}) {
16670
18477
  let current = currentDesign(file);
16671
18478
  if (view !== void 0) {
16672
- const saved = readJson2(join10(current.root, ".design/studio-state.json"), {
18479
+ const saved = readJson2(join12(current.root, ".design/studio-state.json"), {
16673
18480
  state: {},
16674
18481
  stateVersion: 0
16675
18482
  });
@@ -16679,7 +18486,7 @@ async function startDesignReviewUnlocked(file, {
16679
18486
  state: { ...saved.state, view }
16680
18487
  });
16681
18488
  }
16682
- const stateFile = join10(current.root, ".design/server.json");
18489
+ const stateFile = join12(current.root, ".design/server.json");
16683
18490
  const old = readJson2(stateFile, null);
16684
18491
  if (old?.version === VERSION && old.url && /^http:\/\/127\.0\.0\.1:\d+\/r\//u.test(old.url)) {
16685
18492
  try {
@@ -16710,15 +18517,15 @@ async function startDesignReviewUnlocked(file, {
16710
18517
  current = currentDesign(file);
16711
18518
  if (session.designRevision === current.revision) return;
16712
18519
  await session.writeQueue;
16713
- const digest3 = digestArtifactEnvelope(current.envelope);
18520
+ const digest4 = digestArtifactEnvelope(current.envelope);
16714
18521
  await withArtifactReviewLock(session.reviewPath, () => {
16715
18522
  const ledger = readArtifactReviewState(session.reviewPath, { allowMissing: true }) ?? session.reviewState;
16716
18523
  session.reviewState = createReviewLedger({
16717
18524
  artifactId: ledger.artifactId,
16718
- currentReviewOf: digest3,
18525
+ currentReviewOf: digest4,
16719
18526
  reviews: ledger.reviews.map((entry) => ({
16720
18527
  review: entry.review,
16721
- stale: entry.stale || entry.review.reviewOf !== digest3
18528
+ stale: entry.stale || entry.review.reviewOf !== digest4
16722
18529
  }))
16723
18530
  });
16724
18531
  writeArtifactReviewState(session.reviewPath, session.reviewState);
@@ -16727,7 +18534,7 @@ async function startDesignReviewUnlocked(file, {
16727
18534
  session.designRevision = current.revision;
16728
18535
  },
16729
18536
  renderDocument({ model, base }) {
16730
- const state = readJson2(join10(current.root, ".design/studio-state.json"), {
18537
+ const state = readJson2(join12(current.root, ".design/studio-state.json"), {
16731
18538
  state: {}
16732
18539
  }).state;
16733
18540
  const stalePins = readDesignFeedback(file, env).pins.filter(
@@ -16736,7 +18543,7 @@ async function startDesignReviewUnlocked(file, {
16736
18543
  return renderDesignStudio(
16737
18544
  { ...current, envelope: model.envelope, state, stalePins },
16738
18545
  { stageRuntimeUrl: `${base}runtime.js` }
16739
- ).replace("</head>", `<style>${readFileSync14(new URL("../../templates/studio/share.css", new URL("./runtime/packages/design/lib/design/review.mjs", import.meta.url).href), "utf8")}</style></head>`);
18546
+ ).replace("</head>", `<style>${readFileSync16(new URL("../../templates/studio/share.css", new URL("./runtime/packages/design/lib/design/review.mjs", import.meta.url).href), "utf8")}</style></head>`);
16740
18547
  },
16741
18548
  renderRuntime({ options, base }) {
16742
18549
  const settings = {
@@ -16745,11 +18552,13 @@ async function startDesignReviewUnlocked(file, {
16745
18552
  readyUrl: `${base}api/design-ready`,
16746
18553
  shareUrl: `${base}api/design-share`,
16747
18554
  experienceUrl: `${base}api/design-experience`,
18555
+ readinessUrl: `${base}api/design-handoff-readiness`,
16748
18556
  handoffUrl: `${base}api/design-handoff`,
18557
+ implementationHandoffUrl: `${base}api/design-implementation-handoff`,
16749
18558
  revisionsUrl: `${base}api/design-revisions`,
16750
18559
  reviewExportUrl: `${base}api/design-feedback-export`
16751
18560
  };
16752
- return `globalThis.__OPENPLANR_DESIGN_STUDIO_OPTIONS__={...${JSON.stringify(settings)},loadReviewExport:async({scope="all"}={})=>{const r=await fetch(${JSON.stringify(`${base}api/design-feedback-export`)},{method:"POST",headers:{"content-type":"application/json","x-openplanr-design":"1"},body:JSON.stringify({scope})});const value=await r.json();if(!r.ok)throw new Error(value.error||"Review export unavailable");return value},loadExperience:async()=>{const r=await fetch(${JSON.stringify(`${base}api/design-experience`)});if(!r.ok)throw new Error("Review context unavailable");return r.json()},loadHandoff:async()=>{const r=await fetch(${JSON.stringify(`${base}api/design-handoff`)});if(!r.ok)throw new Error("Handoff unavailable");return r.json()},updateHandoff:async(input)=>{const r=await fetch(${JSON.stringify(`${base}api/design-handoff`)},{method:"POST",headers:{"content-type":"application/json","x-openplanr-design":"1"},body:JSON.stringify(input)});const value=await r.json();if(!r.ok)throw new Error(value.error||"Could not update handoff");return value},listRevisions:async()=>{const r=await fetch(${JSON.stringify(`${base}api/design-revisions`)});if(!r.ok)throw new Error("Revision history unavailable");return r.json()},loadRevision:async(revision)=>{const r=await fetch(${JSON.stringify(`${base}api/design-revisions`)},{method:"POST",headers:{"content-type":"application/json","x-openplanr-design":"1"},body:JSON.stringify({revision})});if(!r.ok)throw new Error("Revision unavailable");return r.json()},exportHtml:async()=>{const r=await fetch(${JSON.stringify(`${base}api/design-export`)});if(!r.ok)throw new Error('Export failed');return r.text()}};
18561
+ return `globalThis.__OPENPLANR_DESIGN_STUDIO_OPTIONS__={...${JSON.stringify(settings)},loadReviewExport:async({scope="all"}={})=>{const r=await fetch(${JSON.stringify(`${base}api/design-feedback-export`)},{method:"POST",headers:{"content-type":"application/json","x-openplanr-design":"1"},body:JSON.stringify({scope})});const value=await r.json();if(!r.ok)throw new Error(value.error||"Review export unavailable");return value},loadExperience:async()=>{const r=await fetch(${JSON.stringify(`${base}api/design-experience`)});if(!r.ok)throw new Error("Review context unavailable");return r.json()},loadReadiness:async()=>{const r=await fetch(${JSON.stringify(`${base}api/design-handoff-readiness`)});const value=await r.json();if(!r.ok)throw new Error(value.error||"Handoff readiness unavailable");return value},loadHandoff:async()=>{const r=await fetch(${JSON.stringify(`${base}api/design-handoff`)});if(!r.ok)throw new Error("Handoff unavailable");return r.json()},updateHandoff:async(input)=>{const r=await fetch(${JSON.stringify(`${base}api/design-handoff`)},{method:"POST",headers:{"content-type":"application/json","x-openplanr-design":"1"},body:JSON.stringify(input)});const value=await r.json();if(!r.ok)throw new Error(value.error||"Could not update handoff");return value},loadImplementationHandoff:async()=>{const r=await fetch(${JSON.stringify(`${base}api/design-implementation-handoff`)});const value=await r.json();if(!r.ok)throw new Error(value.error||"Implementation package unavailable");return value},updateImplementationHandoff:async(input)=>{const r=await fetch(${JSON.stringify(`${base}api/design-implementation-handoff`)},{method:"POST",headers:{"content-type":"application/json","x-openplanr-design":"1"},body:JSON.stringify(input)});const value=await r.json();if(!r.ok)throw new Error(value.error||"Could not update implementation package");return value},listRevisions:async()=>{const r=await fetch(${JSON.stringify(`${base}api/design-revisions`)});if(!r.ok)throw new Error("Revision history unavailable");return r.json()},loadRevision:async(revision)=>{const r=await fetch(${JSON.stringify(`${base}api/design-revisions`)},{method:"POST",headers:{"content-type":"application/json","x-openplanr-design":"1"},body:JSON.stringify({revision})});if(!r.ok)throw new Error("Revision unavailable");return r.json()},exportHtml:async()=>{const r=await fetch(${JSON.stringify(`${base}api/design-export`)});if(!r.ok)throw new Error('Export failed');return r.text()}};
16753
18562
  ${renderArtifactParentRuntime({ ...options, adapterRuntimeUrl: `${base}api/design-share-runtime` })}`;
16754
18563
  },
16755
18564
  async handleSessionRequest({ req, res, segments }) {
@@ -16757,17 +18566,99 @@ ${renderArtifactParentRuntime({ ...options, adapterRuntimeUrl: `${base}api/desig
16757
18566
  return false;
16758
18567
  try {
16759
18568
  const route = segments[4];
18569
+ const localImplementationActor = {
18570
+ id: "local-owner",
18571
+ role: "owner",
18572
+ capabilities: [
18573
+ IMPLEMENTATION_HANDOFF_APPROVE_CAPABILITY,
18574
+ IMPLEMENTATION_HANDOFF_REVOKE_CAPABILITY
18575
+ ]
18576
+ };
16760
18577
  if (route === "design-experience" && req.method === "GET") {
16761
18578
  respond(res, 200, readDesignExperience(file, { env }));
18579
+ } else if (route === "design-handoff-readiness" && req.method === "GET") {
18580
+ respond(res, 200, readDesignHandoffReadiness(file, { env }));
16762
18581
  } else if (route === "design-handoff" && req.method === "GET") {
16763
18582
  respond(res, 200, readDesignHandoff(file, { env }));
18583
+ } else if (route === "design-implementation-handoff" && req.method === "GET") {
18584
+ const design = currentDesign(file);
18585
+ const unlock = await acquireStartLock(join12(design.root, ".design/render.lock"));
18586
+ try {
18587
+ const root = dirname13(designSpecPath(design.root));
18588
+ respond(res, 200, { ok: true, draft: readImplementationHandoffDraft(root), ...readImplementationHandoffLifecycle(root), approvalPreview: previewImplementationHandoffApproval(root) });
18589
+ } finally {
18590
+ unlock();
18591
+ }
16764
18592
  } else if (route === "design-revisions" && req.method === "GET") {
16765
18593
  respond(res, 200, listDesignRevisions(file));
16766
- } else if (["design-handoff", "design-revisions", "design-feedback-export"].includes(route) && req.method === "POST") {
18594
+ } else if (["design-handoff", "design-implementation-handoff", "design-revisions", "design-feedback-export"].includes(route) && req.method === "POST") {
16767
18595
  if (req.headers["x-openplanr-design"] !== "1" || !String(req.headers["content-type"] ?? "").startsWith("application/json") || req.headers.origin && req.headers.origin !== `http://127.0.0.1:${server.port}`) throw Object.assign(new Error("Owner actions require a same-origin studio request."), { statusCode: 403 });
16768
- const input = await readBody(req);
18596
+ const input = route === "design-implementation-handoff" ? await readImplementationBody(req) : await readBody(req);
16769
18597
  if (route === "design-handoff") respond(res, 200, await updateDesignHandoff(file, input, { env, fetchImpl }));
16770
- else if (route === "design-feedback-export") {
18598
+ else if (route === "design-implementation-handoff") {
18599
+ if (!input || typeof input !== "object" || Array.isArray(input) || !["draft", "regenerate", "export", "import", "approve", "revoke", "compare", "continue-to-plan"].includes(input.action)) throw new Error("Unknown implementation package action.");
18600
+ const initial = currentDesign(file);
18601
+ const unlock = await acquireStartLock(join12(initial.root, ".design/render.lock"));
18602
+ try {
18603
+ const design = currentDesign(file);
18604
+ const root = dirname13(designSpecPath(design.root));
18605
+ const resolver = createRepositorySourceResolver(projectRoot(design.root));
18606
+ const approvalOptions = {
18607
+ actor: localImplementationActor,
18608
+ clock,
18609
+ resolveSource: resolver,
18610
+ ...["draft", "regenerate", "import", "approve"].includes(input.action) ? { currentBasis: currentImplementationBasis(file, env) } : {}
18611
+ };
18612
+ if (input.action === "draft") {
18613
+ if (!input.package || input.package.kind)
18614
+ throw new Error("Draft composition requires editable package fields, not a lifecycle record.");
18615
+ if (readImplementationHandoffLifecycle(root).history.length)
18616
+ throw Object.assign(new Error("Use regenerate to create a new version after approval."), { statusCode: 409 });
18617
+ const draft = writeImplementationHandoffDraft(root, {
18618
+ ...input.package,
18619
+ version: 1,
18620
+ basis: approvalOptions.currentBasis
18621
+ }, { resolveSource: resolver });
18622
+ respond(res, 200, { ok: true, draft });
18623
+ } else if (input.action === "regenerate") {
18624
+ if (!input.package || input.package.kind)
18625
+ throw new Error("Regeneration requires editable package fields, not a lifecycle record.");
18626
+ const value = regenerateImplementationHandoffDraft(root, {
18627
+ ...input.package,
18628
+ basis: approvalOptions.currentBasis
18629
+ }, { requestId: input.requestId }, approvalOptions);
18630
+ respond(res, 200, { ok: true, ...value });
18631
+ } else if (input.action === "import") {
18632
+ const draft = importImplementationHandoffPackage(input.package, { resolveSource: resolver });
18633
+ if (reviewDigest(draft.basis) !== reviewDigest(approvalOptions.currentBasis))
18634
+ throw Object.assign(new Error("The imported implementation package belongs to a different or earlier design basis."), { statusCode: 409 });
18635
+ const maximumVersion = Math.max(0, ...readImplementationHandoffLifecycle(root).history.map((item2) => item2.version));
18636
+ if (draft.version <= maximumVersion)
18637
+ throw Object.assign(new Error("Imported implementation packages cannot replace immutable version history."), { statusCode: 409 });
18638
+ writeImplementationHandoffDraft(root, draft, { resolveSource: resolver });
18639
+ respond(res, 200, { ok: true, draft });
18640
+ } else if (input.action === "approve") {
18641
+ const value = approveImplementationHandoff(root, input, approvalOptions);
18642
+ respond(res, 200, { ok: true, ...value });
18643
+ } else if (input.action === "revoke") {
18644
+ const value = revokeImplementationHandoff(root, input, approvalOptions);
18645
+ respond(res, 200, { ok: true, ...value });
18646
+ } else if (input.action === "compare") {
18647
+ respond(res, 200, { ok: true, comparison: compareImplementationHandoffVersions(root, input.left, input.right) });
18648
+ } else if (input.action === "continue-to-plan") {
18649
+ const lifecycle = readImplementationHandoffLifecycle(root);
18650
+ if (!lifecycle.current || lifecycle.current.status !== "approved")
18651
+ throw Object.assign(new Error("Continue to Plan requires a current approved implementation package."), { statusCode: 409 });
18652
+ const approved = readImplementationHandoffVersion(root, lifecycle.current);
18653
+ respond(res, 200, { ok: true, handoff: prepareDesignPlanHandoff(approved, { subject: input.subject }) });
18654
+ } else {
18655
+ const draft = readImplementationHandoffDraft(root, { allowMissing: false });
18656
+ respond(res, 200, { ok: true, package: exportImplementationHandoffPackage(draft) });
18657
+ }
18658
+ } finally {
18659
+ unlock();
18660
+ }
18661
+ } else if (route === "design-feedback-export") {
16771
18662
  if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key) => key !== "scope") || input.scope !== void 0 && !["all", "current"].includes(input.scope)) throw new Error("Review export requires scope current or all.");
16772
18663
  await syncDesignShare(file, { env, fetchImpl });
16773
18664
  respond(res, 200, exportDesignReview(file, { scope: input.scope ?? "all", env }));
@@ -16778,25 +18669,25 @@ ${renderArtifactParentRuntime({ ...options, adapterRuntimeUrl: `${base}api/desig
16778
18669
  }
16779
18670
  } else if (route === "design-share-runtime" && req.method === "GET") {
16780
18671
  res.writeHead(200, { "content-type": "application/javascript; charset=utf-8", "cache-control": "no-store", "x-content-type-options": "nosniff" });
16781
- res.end(readFileSync14(new URL("../../templates/studio/share.js", new URL("./runtime/packages/design/lib/design/review.mjs", import.meta.url).href), "utf8"));
18672
+ res.end(readFileSync16(new URL("../../templates/studio/share.js", new URL("./runtime/packages/design/lib/design/review.mjs", import.meta.url).href), "utf8"));
16782
18673
  } else if (route === "design-share" && req.method === "GET") {
16783
18674
  respond(res, 200, getDesignShareStatus(file, { env }));
16784
18675
  } else if (route === "design-share" && req.method === "POST") {
16785
18676
  if (req.headers["x-openplanr-design"] !== "1" || !String(req.headers["content-type"] ?? "").startsWith("application/json")) throw Object.assign(new Error("Sharing requires a same-origin studio request."), { statusCode: 403 });
16786
18677
  const origin = req.headers.origin;
16787
18678
  if (origin && origin !== `http://127.0.0.1:${server.port}`) throw Object.assign(new Error("Sharing requires a same-origin studio request."), { statusCode: 403 });
16788
- const { action } = await readBody(req);
18679
+ const { action: action3 } = await readBody(req);
16789
18680
  const options = { env, fetchImpl };
16790
18681
  let result;
16791
- if (action === "create") result = await shareDesign(file, options);
16792
- else if (action === "publish") result = await publishDesignShare(file, options);
16793
- else if (action === "sync") result = await syncDesignShare(file, options);
16794
- else if (action === "recovery") result = await exportDesignShareRecovery(file, { ...options, output: join10(env.HOME ?? process.env.HOME, "Downloads", `openplanr-design-recovery-${Date.now()}.json`) });
16795
- else result = await manageDesignShare(file, action, options);
18682
+ if (action3 === "create") result = await shareDesign(file, options);
18683
+ else if (action3 === "publish") result = await publishDesignShare(file, options);
18684
+ else if (action3 === "sync") result = await syncDesignShare(file, options);
18685
+ else if (action3 === "recovery") result = await exportDesignShareRecovery(file, { ...options, output: join12(env.HOME ?? process.env.HOME, "Downloads", `openplanr-design-recovery-${Date.now()}.json`) });
18686
+ else result = await manageDesignShare(file, action3, options);
16796
18687
  respond(res, 200, result);
16797
18688
  } else if (route === "design-status" && req.method === "GET") {
16798
18689
  const ready = readJson2(
16799
- join10(current.root, ".design/browser-ready.json"),
18690
+ join12(current.root, ".design/browser-ready.json"),
16800
18691
  null
16801
18692
  );
16802
18693
  respond(res, 200, {
@@ -16808,7 +18699,7 @@ ${renderArtifactParentRuntime({ ...options, adapterRuntimeUrl: `${base}api/desig
16808
18699
  });
16809
18700
  } else if (route === "design-state" && req.method === "GET") {
16810
18701
  respond(res, 200, {
16811
- ...readJson2(join10(current.root, ".design/studio-state.json"), {
18702
+ ...readJson2(join12(current.root, ".design/studio-state.json"), {
16812
18703
  state: {},
16813
18704
  stateVersion: 0
16814
18705
  }),
@@ -16824,7 +18715,7 @@ ${renderArtifactParentRuntime({ ...options, adapterRuntimeUrl: `${base}api/desig
16824
18715
  throw new Error(
16825
18716
  "Browser readiness does not cover every expected design artboard."
16826
18717
  );
16827
- atomicJson(join10(current.root, ".design/browser-ready.json"), {
18718
+ atomicJson(join12(current.root, ".design/browser-ready.json"), {
16828
18719
  status: "ready",
16829
18720
  revision: current.revision,
16830
18721
  checkedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -16832,7 +18723,7 @@ ${renderArtifactParentRuntime({ ...options, adapterRuntimeUrl: `${base}api/desig
16832
18723
  respond(res, 200, { ok: true });
16833
18724
  } else if (route === "design-export" && req.method === "GET") {
16834
18725
  const state = readJson2(
16835
- join10(current.root, ".design/studio-state.json"),
18726
+ join12(current.root, ".design/studio-state.json"),
16836
18727
  { state: {} }
16837
18728
  ).state;
16838
18729
  res.writeHead(200, {
@@ -16962,10 +18853,10 @@ async function resolveDesignPins(file, { pinIds, summary, env = process.env }) {
16962
18853
  createReviewLedger({ ...ledger, reviews: revisions })
16963
18854
  );
16964
18855
  const history = readJson2(
16965
- join10(current.root, ".design/review-history.json"),
18856
+ join12(current.root, ".design/review-history.json"),
16966
18857
  []
16967
18858
  );
16968
- atomicJson(join10(current.root, ".design/review-history.json"), [
18859
+ atomicJson(join12(current.root, ".design/review-history.json"), [
16969
18860
  ...history,
16970
18861
  {
16971
18862
  revision: current.revision,
@@ -16981,7 +18872,7 @@ async function resolveDesignPins(file, { pinIds, summary, env = process.env }) {
16981
18872
  // packages/design/lib/design/browser-audit.mjs
16982
18873
  function auditRenderedScreen() {
16983
18874
  const issues = [];
16984
- const add = (rule, severity, message) => issues.push({ rule, severity, message });
18875
+ const add = (rule, severity, message2) => issues.push({ rule, severity, message: message2 });
16985
18876
  const label = (node) => node.getAttribute("data-planr-id") || node.id || `${node.tagName.toLowerCase()} \u201C${(node.textContent || node.getAttribute("aria-label") || "").trim().replace(/\s+/gu, " ").slice(0, 60)}\u201D`;
16986
18877
  const viewport = { width: window.innerWidth, height: window.innerHeight };
16987
18878
  const screenId = document.body?.getAttribute("data-planr-screen") ?? null;
@@ -17049,7 +18940,7 @@ function auditRenderedScreen() {
17049
18940
  }
17050
18941
  if (node.tabIndex >= 0 && !node.disabled && node.matches('button,a[href],input:not([type="hidden"]),select,textarea,[tabindex],[contenteditable="true"]')) {
17051
18942
  focusTargets.push(node);
17052
- const name = node.getAttribute("aria-label") || (node.getAttribute("aria-labelledby") || "").split(/\s+/u).map((id3) => document.getElementById(id3)?.textContent || "").join(" ").trim() || [...node.labels || []].map((item2) => item2.textContent).join(" ").trim() || node.getAttribute("title") || (node.matches('input[type="button"],input[type="submit"],input[type="reset"]') ? node.value : node.matches("input,select,textarea") ? "" : node.textContent.trim());
18943
+ const name = node.getAttribute("aria-label") || (node.getAttribute("aria-labelledby") || "").split(/\s+/u).map((id4) => document.getElementById(id4)?.textContent || "").join(" ").trim() || [...node.labels || []].map((item2) => item2.textContent).join(" ").trim() || node.getAttribute("title") || (node.matches('input[type="button"],input[type="submit"],input[type="reset"]') ? node.value : node.matches("input,select,textarea") ? "" : node.textContent.trim());
17053
18944
  if (!name) add("unnamed-control", "error", `${label(node)} has no accessible name.`);
17054
18945
  }
17055
18946
  }
@@ -17088,7 +18979,7 @@ function auditRenderedScreen() {
17088
18979
  async function auditDesignPage(page, { revision, entries = [], screenshotPaths = [], scenarios = [] } = {}) {
17089
18980
  const issues = [], checkedArtifacts = [], screens = [];
17090
18981
  const expected = new Map(entries.map((entry) => [entry.artifactId, entry]));
17091
- const add = (artifactId, rule, severity, message) => issues.push({ artifactId, rule, severity, message });
18982
+ const add = (artifactId, rule, severity, message2) => issues.push({ artifactId, rule, severity, message: message2 });
17092
18983
  if (!entries.length || expected.size !== entries.length) add("studio", "invalid-audit-scope", "error", "Audit scope must contain every expected artifact exactly once.");
17093
18984
  if (!page || typeof page.frames !== "function") {
17094
18985
  return { schemaVersion: "1.0.0", revision, status: "unverified", checkedArtifacts, issues: [{ artifactId: "studio", rule: "browser-unavailable", severity: "warning", message: "A browser page is required for rendered verification." }], screenshots: screenshotPaths, scenarios, screens };
@@ -17122,7 +19013,7 @@ async function auditDesignPage(page, { revision, entries = [], screenshotPaths =
17122
19013
  }
17123
19014
  checkedArtifacts.push(artifactId);
17124
19015
  screens.push({ artifactId, ...result });
17125
- for (const issue of result.issues) add(artifactId, issue.rule, issue.severity, issue.message);
19016
+ for (const issue2 of result.issues) add(artifactId, issue2.rule, issue2.severity, issue2.message);
17126
19017
  } catch (error) {
17127
19018
  if (artifactId && expected.has(artifactId)) add(artifactId, "frame-load-error", "error", `The rendered frame could not be inspected: ${error.message}`);
17128
19019
  } finally {
@@ -17175,7 +19066,7 @@ function verifyDesignDocument(file, report) {
17175
19066
  const images = (Array.isArray(report.screenshots) ? report.screenshots : []).filter((item2) => {
17176
19067
  const path = typeof item2 === "string" ? item2 : item2?.path;
17177
19068
  try {
17178
- return typeof path === "string" && isPng(readFileSync15(resolve6(path)));
19069
+ return typeof path === "string" && isPng(readFileSync17(resolve7(path)));
17179
19070
  } catch {
17180
19071
  return false;
17181
19072
  }
@@ -17184,7 +19075,7 @@ function verifyDesignDocument(file, report) {
17184
19075
  const issues = [...Array.isArray(report.issues) ? report.issues : [], ...screenEvidence.flatMap((item2) => Array.isArray(item2?.issues) ? item2.issues : [])];
17185
19076
  const status = issues.some((item2) => item2?.severity === "error") || report.status === "failed" ? "failed" : coverage && images.length > 0 && journeys && report.status !== "unverified" ? "verified" : "unverified";
17186
19077
  const saved = { ...report, issues, schemaVersion: "1.0.0", revision: current.revision, status, checkedAt: (/* @__PURE__ */ new Date()).toISOString(), coverage, screenshotCount: images.length, primaryJourneysChecked: Boolean(journeys) };
17187
- atomicJson(join11(current.root, ".design/verification", `${current.revision}.json`), saved);
19078
+ atomicJson(join13(current.root, ".design/verification", `${current.revision}.json`), saved);
17188
19079
  return saved;
17189
19080
  }
17190
19081
  async function designUtility(argv, { stdout = (value) => process.stdout.write(`${JSON.stringify(value)}
@@ -17204,15 +19095,15 @@ async function designUtility(argv, { stdout = (value) => process.stdout.write(`$
17204
19095
  flags[key] = ["json", "no-open"].includes(key) ? true : args[++i];
17205
19096
  }
17206
19097
  if (flags.view && !["canvas", "prototype", "walkthrough"].includes(flags.view)) throw new Error("View must be canvas, prototype, or walkthrough.");
17207
- const file = resolve6(input);
19098
+ const file = resolve7(input);
17208
19099
  let result;
17209
19100
  if (command === "handoff") {
17210
- const action = flags.action ?? "inspect";
17211
- if (action === "inspect") result = readDesignHandoff(file);
19101
+ const action3 = flags.action ?? "inspect";
19102
+ if (action3 === "inspect") result = readDesignHandoff(file);
17212
19103
  else {
17213
19104
  const snapshot2 = readDesignHandoff(file);
17214
- const input2 = flags.input ? readJson2(resolve6(flags.input)) : {};
17215
- result = await updateDesignHandoff(file, { ...input2, action, revision: input2.revision ?? snapshot2.revision, version: input2.version ?? (snapshot2.draft?.version ?? 0) });
19105
+ const input2 = flags.input ? readJson2(resolve7(flags.input)) : {};
19106
+ result = await updateDesignHandoff(file, { ...input2, action: action3, revision: input2.revision ?? snapshot2.revision, version: input2.version ?? (snapshot2.draft?.version ?? 0) });
17216
19107
  }
17217
19108
  }
17218
19109
  if (command === "share") result = await shareDesign(file);
@@ -17244,39 +19135,39 @@ async function designUtility(argv, { stdout = (value) => process.stdout.write(`$
17244
19135
  }
17245
19136
  if (command === "export") {
17246
19137
  const current = currentDesign(file), view = flags.view ?? current.document.defaultView;
17247
- const state = readJson2(join11(current.root, ".design/studio-state.json"), { state: {} }).state;
19138
+ const state = readJson2(join13(current.root, ".design/studio-state.json"), { state: {} }).state;
17248
19139
  if (flags.format && flags.format !== "html") throw new Error("Portable export supports HTML. Use the studio PNG action for browser-rendered captures.");
17249
- const output = resolve6(flags.output ?? join11(current.root, `${view}-export.html`));
17250
- if (existsSync10(output)) throw new Error(`Export already exists: ${output}. Choose a new --output path.`);
17251
- mkdirSync7(dirname12(output), { recursive: true });
17252
- writeFileSync7(output, standaloneDesignHtml({ ...current, state }, view), { flag: "wx" });
19140
+ const output = resolve7(flags.output ?? join13(current.root, `${view}-export.html`));
19141
+ if (existsSync12(output)) throw new Error(`Export already exists: ${output}. Choose a new --output path.`);
19142
+ mkdirSync9(dirname14(output), { recursive: true });
19143
+ writeFileSync9(output, standaloneDesignHtml({ ...current, state }, view), { flag: "wx" });
17253
19144
  result = { ok: true, output, view, revision: current.revision };
17254
19145
  }
17255
19146
  if (command === "verify") {
17256
19147
  if (!flags.report) throw new Error("verify requires --report <browser-report.json>.");
17257
- result = verifyDesignDocument(file, readJson2(resolve6(flags.report)));
19148
+ result = verifyDesignDocument(file, readJson2(resolve7(flags.report)));
17258
19149
  }
17259
19150
  if (command === "feedback") {
17260
19151
  await syncDesignShare(file, { env, fetchImpl });
17261
- const action = flags.action ?? "inspect";
17262
- if (action === "inspect") result = readDesignFeedback(file, env);
17263
- else if (action === "export") {
19152
+ const action3 = flags.action ?? "inspect";
19153
+ if (action3 === "inspect") result = readDesignFeedback(file, env);
19154
+ else if (action3 === "export") {
17264
19155
  const format = flags.format ?? "json", scope = flags.scope ?? "all";
17265
19156
  if (!["json", "markdown"].includes(format)) throw new Error("Feedback export format must be json or markdown.");
17266
19157
  if (!["current", "all"].includes(scope)) throw new Error("Feedback export scope must be current or all.");
17267
19158
  if (!flags.output) throw new Error("Feedback export requires --output <path>.");
17268
- const output = resolve6(flags.output);
17269
- if (existsSync10(output)) throw new Error("Feedback export already exists. Choose a new --output path.");
19159
+ const output = resolve7(flags.output);
19160
+ if (existsSync12(output)) throw new Error("Feedback export already exists. Choose a new --output path.");
17270
19161
  const snapshot2 = exportDesignReview(file, { scope, env });
17271
- mkdirSync7(dirname12(output), { recursive: true });
17272
- writeFileSync7(output, serializeDesignReviewExport(snapshot2, format), { flag: "wx", mode: 384 });
19162
+ mkdirSync9(dirname14(output), { recursive: true });
19163
+ writeFileSync9(output, serializeDesignReviewExport(snapshot2, format), { flag: "wx", mode: 384 });
17273
19164
  result = { ok: true, output, format, scope, revision: snapshot2.currentRevisionId, summary: snapshot2.summary };
17274
- } else if (action === "resolve") result = await resolveDesignPins(file, { pinIds: flags.pins?.split(",").filter(Boolean), summary: flags.summary, env });
17275
- else if (action === "select") {
17276
- const current = currentDesign(file), saved = readJson2(join11(current.root, ".design/studio-state.json"), { state: {}, stateVersion: 0 });
19165
+ } else if (action3 === "resolve") result = await resolveDesignPins(file, { pinIds: flags.pins?.split(",").filter(Boolean), summary: flags.summary, env });
19166
+ else if (action3 === "select") {
19167
+ const current = currentDesign(file), saved = readJson2(join13(current.root, ".design/studio-state.json"), { state: {}, stateVersion: 0 });
17277
19168
  const variant = flags.variant ?? saved.state.selectedVariant;
17278
19169
  if (!current.document.variants.some((item2) => item2.id === variant && item2.status === "ready")) throw new Error("Select a ready variant with --variant.");
17279
- const selected = [variant], rejected = (saved.state.preferences?.rejected ?? []).filter((id3) => id3 !== variant);
19170
+ const selected = [variant], rejected = (saved.state.preferences?.rejected ?? []).filter((id4) => id4 !== variant);
17280
19171
  result = await saveDesignState(file, { ...saved, revision: current.revision, state: { ...saved.state, selectedVariant: variant, preferences: { selected, rejected } } });
17281
19172
  result = { ok: true, selectedVariant: variant, tastePath: result.tastePath, revision: current.revision };
17282
19173
  } else throw new Error("Feedback action must be inspect, export, select, or resolve.");
@@ -17284,7 +19175,7 @@ async function designUtility(argv, { stdout = (value) => process.stdout.write(`$
17284
19175
  stdout(result);
17285
19176
  return result;
17286
19177
  }
17287
- var main = process.argv[1] && fileURLToPath3(import.meta.url) === realpathSync6(process.argv[1]);
19178
+ var main = process.argv[1] && fileURLToPath3(import.meta.url) === realpathSync7(process.argv[1]);
17288
19179
  if (main) designUtility(process.argv.slice(2), { openUrl: async (url) => {
17289
19180
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
17290
19181
  const child = spawn(command, [url], { stdio: "ignore" });