openplanr 2.4.0 → 2.6.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 (150) hide show
  1. package/CHANGELOG.md +19 -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/lib/host-packages/adapter-registry.json +5 -5
  8. package/lib/host-packages/capability-map.json +1 -1
  9. package/lib/host-packages/claude/.claude-plugin/marketplace.json +2 -2
  10. package/lib/host-packages/claude/openplanr/.claude-plugin/plugin.json +1 -1
  11. package/lib/host-packages/claude/openplanr/.openplanr-content.json +114 -26
  12. package/lib/host-packages/claude/openplanr/skills/design/SKILL.md +3 -0
  13. package/lib/host-packages/claude/openplanr/skills/design/openplanr.skill.json +56 -1
  14. package/lib/host-packages/claude/openplanr/skills/design/references/handoff.md +35 -0
  15. package/lib/host-packages/claude/openplanr/skills/design/scripts/design.mjs +2285 -341
  16. package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  17. package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  18. package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  19. package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/protocol/package.json +5 -1
  20. package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  21. package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  22. package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  23. package/lib/host-packages/claude/openplanr/skills/design-loop/openplanr.skill.json +55 -0
  24. package/lib/host-packages/claude/openplanr/skills/design-loop/references/handoff.md +35 -0
  25. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design.mjs +2285 -341
  26. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  27. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  28. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  29. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/protocol/package.json +5 -1
  30. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  31. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  32. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  33. package/lib/host-packages/claude/openplanr/skills/design-review/SKILL.md +3 -0
  34. package/lib/host-packages/claude/openplanr/skills/design-review/openplanr.skill.json +56 -1
  35. package/lib/host-packages/claude/openplanr/skills/design-review/references/handoff.md +35 -0
  36. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design.mjs +2285 -341
  37. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  38. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  39. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  40. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/protocol/package.json +5 -1
  41. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  42. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  43. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  44. package/lib/host-packages/claude/openplanr/skills/plan/SKILL.md +12 -0
  45. package/lib/host-packages/claude/openplanr/skills/plan/openplanr.skill.json +67 -1
  46. package/lib/host-packages/claude/openplanr/skills/plan/references/design-handoff.md +33 -0
  47. package/lib/host-packages/claude/openplanr/skills/plan/scripts/design.mjs +2285 -341
  48. package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  49. package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  50. package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  51. package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/protocol/package.json +5 -1
  52. package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  53. package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  54. package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  55. package/lib/host-packages/claude/openplanr/skills/ship/SKILL.md +9 -0
  56. package/lib/host-packages/claude/openplanr/skills/ship/openplanr.skill.json +12 -1
  57. package/lib/host-packages/claude/openplanr/skills/ship/references/design-lineage.md +26 -0
  58. package/lib/host-packages/cursor/openplanr/.openplanr-content.json +109 -21
  59. package/lib/host-packages/cursor/openplanr/manifest.json +1 -1
  60. package/lib/host-packages/cursor/openplanr/rules/planr-design/references/handoff.md +35 -0
  61. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design.mjs +2285 -341
  62. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  63. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  64. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  65. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/protocol/package.json +5 -1
  66. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  67. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  68. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  69. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/references/handoff.md +35 -0
  70. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design.mjs +2285 -341
  71. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  72. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  73. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  74. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/protocol/package.json +5 -1
  75. 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
  76. 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
  77. 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
  78. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/references/handoff.md +35 -0
  79. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design.mjs +2285 -341
  80. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  81. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  82. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  83. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/protocol/package.json +5 -1
  84. 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
  85. 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
  86. 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
  87. package/lib/host-packages/cursor/openplanr/rules/planr-design-review.mdc +3 -0
  88. package/lib/host-packages/cursor/openplanr/rules/planr-design.mdc +3 -0
  89. package/lib/host-packages/cursor/openplanr/rules/planr-plan/references/design-handoff.md +33 -0
  90. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design.mjs +2285 -341
  91. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  92. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  93. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  94. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/protocol/package.json +5 -1
  95. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  96. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  97. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  98. package/lib/host-packages/cursor/openplanr/rules/planr-plan.mdc +12 -0
  99. package/lib/host-packages/cursor/openplanr/rules/planr-ship/references/design-lineage.md +26 -0
  100. package/lib/host-packages/cursor/openplanr/rules/planr-ship.mdc +9 -0
  101. package/lib/host-packages/openai/.claude-plugin/marketplace.json +2 -2
  102. package/lib/host-packages/openai/openplanr/.codex-plugin/plugin.json +1 -1
  103. package/lib/host-packages/openai/openplanr/.openplanr-content.json +114 -26
  104. package/lib/host-packages/openai/openplanr/skills/design/SKILL.md +3 -0
  105. package/lib/host-packages/openai/openplanr/skills/design/openplanr.skill.json +56 -1
  106. package/lib/host-packages/openai/openplanr/skills/design/references/handoff.md +35 -0
  107. package/lib/host-packages/openai/openplanr/skills/design/scripts/design.mjs +2285 -341
  108. package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  109. package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  110. package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  111. package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/protocol/package.json +5 -1
  112. package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  113. package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  114. package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  115. package/lib/host-packages/openai/openplanr/skills/design-loop/openplanr.skill.json +55 -0
  116. package/lib/host-packages/openai/openplanr/skills/design-loop/references/handoff.md +35 -0
  117. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design.mjs +2285 -341
  118. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  119. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  120. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  121. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/protocol/package.json +5 -1
  122. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  123. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  124. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  125. package/lib/host-packages/openai/openplanr/skills/design-review/SKILL.md +3 -0
  126. package/lib/host-packages/openai/openplanr/skills/design-review/openplanr.skill.json +56 -1
  127. package/lib/host-packages/openai/openplanr/skills/design-review/references/handoff.md +35 -0
  128. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design.mjs +2285 -341
  129. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  130. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  131. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  132. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/protocol/package.json +5 -1
  133. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  134. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  135. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  136. package/lib/host-packages/openai/openplanr/skills/plan/SKILL.md +12 -0
  137. package/lib/host-packages/openai/openplanr/skills/plan/openplanr.skill.json +67 -1
  138. package/lib/host-packages/openai/openplanr/skills/plan/references/design-handoff.md +33 -0
  139. package/lib/host-packages/openai/openplanr/skills/plan/scripts/design.mjs +2285 -341
  140. package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  141. package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  142. package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  143. package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/protocol/package.json +5 -1
  144. package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
  145. package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
  146. package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
  147. package/lib/host-packages/openai/openplanr/skills/ship/SKILL.md +9 -0
  148. package/lib/host-packages/openai/openplanr/skills/ship/openplanr.skill.json +12 -1
  149. package/lib/host-packages/openai/openplanr/skills/ship/references/design-lineage.md +26 -0
  150. 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 resolve8 } 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((resolve9, 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
+ resolve9();
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>
@@ -13081,12 +13081,14 @@ ${shellMarkup}
13081
13081
  }
13082
13082
 
13083
13083
  // packages/design/lib/design/studio-render.mjs
13084
- var DESIGN_STUDIO_VERSION = "1.8.5";
13084
+ var DESIGN_STUDIO_VERSION = "1.9.0";
13085
13085
  var DESIGN_STUDIO_ASSETS = Object.freeze({
13086
13086
  style: "templates/studio/studio.css",
13087
13087
  runtime: "templates/studio/studio.js",
13088
13088
  enhancementsStyle: "templates/studio/enhancements.css",
13089
- enhancementsRuntime: "templates/studio/enhancements.js"
13089
+ enhancementsRuntime: "templates/studio/enhancements.js",
13090
+ handoffCenterStyle: "templates/studio/handoff-center.css",
13091
+ handoffCenterRuntime: "templates/studio/handoff-center.js"
13090
13092
  });
13091
13093
  function applyDesignStudioTheme(preference) {
13092
13094
  if (!preference) {
@@ -13162,7 +13164,7 @@ function navigator(document2) {
13162
13164
  <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
13165
  ${document2.variants.filter(({ status }) => status === "ready").length > 1 ? `<label class="design-compare"><input type="checkbox" data-design-compare> Compare directions</label>` : ""}
13164
13166
  <div class="design-screen-list">${document2.screenOrder.map((screenId, index) => {
13165
- const screen = document2.screens.find(({ id: id3 }) => id3 === screenId);
13167
+ const screen = document2.screens.find(({ id: id4 }) => id4 === screenId);
13166
13168
  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
13169
  }).join("")}</div>
13168
13170
  <div class="design-nav-footer"><span data-design-verification>Browser inspection pending</span><span class="design-local">Local workspace</span></div>
@@ -13177,7 +13179,7 @@ function designNotes(document2) {
13177
13179
  ).join("")}</div></details>`;
13178
13180
  }
13179
13181
  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>
13182
+ 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
13183
  <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
13184
  <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
13185
  <div class="design-notice" role="status" aria-live="polite" hidden></div>`;
@@ -13205,11 +13207,11 @@ function renderDesignStudioMarkup({
13205
13207
  throw new TypeError(
13206
13208
  "Design studio requires a design document and artifact envelope."
13207
13209
  );
13208
- const artifactIds = new Set(envelope.artifacts.map(({ id: id3 }) => id3));
13210
+ const artifactIds = new Set(envelope.artifacts.map(({ id: id4 }) => id4));
13209
13211
  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)) {
13212
+ if (!artifactIds.has(entry.artifactId) || !document2.screens.some(({ id: id4 }) => id4 === entry.screenId) || !document2.variants.some(
13213
+ ({ id: id4, status }) => id4 === entry.variantId && status === "ready"
13214
+ ) || !document2.frames.some(({ id: id4 }) => id4 === entry.frameId)) {
13213
13215
  throw new TypeError(`Invalid design studio entry: ${entry.artifactId}.`);
13214
13216
  }
13215
13217
  }
@@ -13295,11 +13297,11 @@ function renderDesignStudioMarkup({
13295
13297
  // packages/design/lib/design/review-export.mjs
13296
13298
  function reviewExportTools() {
13297
13299
  const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
13298
- const list2 = (value) => Array.isArray(value) ? value : [];
13300
+ const list3 = (value) => Array.isArray(value) ? value : [];
13299
13301
  const localPath = /(?:file:\/\/|\/(?:Users|home|private|tmp|var|etc|opt|Volumes)\/|[A-Za-z]:\\|\\\\)/u;
13300
13302
  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;
13303
+ const digest4 = (value) => typeof value === "string" && /^[a-f0-9]{64}$/u.test(value) ? value : null;
13304
+ const timestamp3 = (value) => typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/u.test(value) && Number.isFinite(Date.parse(value)) ? value : null;
13303
13305
  const quote = (value) => {
13304
13306
  if (typeof value !== "string" || value.length > 16384) throw new TypeError("Review text must be a string of at most 16384 characters.");
13305
13307
  return value;
@@ -13307,20 +13309,20 @@ function reviewExportTools() {
13307
13309
  const identity = (value) => ({ id: field(value?.id), name: typeof value?.name === "string" ? quote(value.name) : "Unknown reviewer" });
13308
13310
  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
13311
  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 ?? "");
13312
+ const compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
13313
+ const order = (a, b) => compare2(a.createdAt ?? "", b.createdAt ?? "") || compare2(a.id ?? "", b.id ?? "");
13312
13314
  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
13315
  function location(pin) {
13314
13316
  const region = pin.region;
13315
13317
  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;
13318
+ const anchor2 = field(pin.anchor?.planrId) ? { planrId: field(pin.anchor.planrId), screen: field(pin.anchor.screen) } : null;
13317
13319
  const viewport = dimensions(pin.viewport);
13318
13320
  const normalizedRegion = { x: region.x, y: region.y, w: region.w, h: region.h };
13319
13321
  const point = { x: rounded(region.x + region.w / 2), y: rounded(region.y + region.h / 2) };
13320
13322
  return {
13321
13323
  kind: region.w > 0 || region.h > 0 ? "region" : "point",
13322
13324
  coordinateSpace: pin.anchor ? "anchor-normalized" : "viewport-normalized",
13323
- anchor,
13325
+ anchor: anchor2,
13324
13326
  region: normalizedRegion,
13325
13327
  point,
13326
13328
  capturedViewport: viewport,
@@ -13329,21 +13331,21 @@ function reviewExportTools() {
13329
13331
  }
13330
13332
  function sourceBundle(value, fallback = {}) {
13331
13333
  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) };
13334
+ return { bundle: object(bundle), revisionId: field(value?.revisionId ?? fallback.revisionId ?? bundle?.revision), reviewOf: digest4(value?.reviewOf ?? fallback.reviewOf ?? bundle?.reviewOf) };
13333
13335
  }
13334
13336
  function flatten(input) {
13335
13337
  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 })));
13338
+ if (input.review) return list3(input.review.pins).map((pin) => ({ ...pin, reviewId: input.review.reviewId, reviewOf: input.review.reviewOf, revisionId: pin.revisionId ?? input.revisionId }));
13339
+ 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
13340
  }
13339
13341
  function revisionFor(pin) {
13340
13342
  return field(pin.revisionId) ?? (pin.reviewId?.startsWith("shared-") ? field(pin.reviewId.slice(7)) : null);
13341
13343
  }
13342
13344
  function resolveSource(pin, sources) {
13343
- const revisionId = revisionFor(pin), reviewOf = digest3(pin.reviewOf);
13345
+ const revisionId = revisionFor(pin), reviewOf = digest4(pin.reviewOf);
13344
13346
  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;
13347
+ const distinct2 = matches.filter((item2, index) => matches.findIndex((other) => other.revisionId === item2.revisionId && other.reviewOf === item2.reviewOf) === index);
13348
+ return distinct2.length === 1 ? distinct2[0] : null;
13347
13349
  }
13348
13350
  function pinMetadata(metadata2, pin, current) {
13349
13351
  const key = revisionFor(pin) ?? pin.reviewId;
@@ -13352,7 +13354,7 @@ function reviewExportTools() {
13352
13354
  function createDesignReviewExport2(input = {}) {
13353
13355
  const current = sourceBundle(input.bundle ?? {}, { revisionId: input.revisionId, reviewOf: input.reviewOf ?? input.review?.reviewOf });
13354
13356
  const design = current.bundle.design ?? current.bundle.document ?? {};
13355
- const sources = [current, ...list2(input.revisions).map((value) => sourceBundle(value))];
13357
+ const sources = [current, ...list3(input.revisions).map((value) => sourceBundle(value))];
13356
13358
  const pins = flatten(input);
13357
13359
  if (pins.length > 1e4) throw new TypeError("Review export exceeds 10000 threads.");
13358
13360
  const metadata2 = object(input.metadata ?? input.feedback?.metadata);
@@ -13361,12 +13363,12 @@ function reviewExportTools() {
13361
13363
  if (!field(pin.id) || !field(pin.artifactId)) throw new TypeError("Review pin is missing a share-safe identity.");
13362
13364
  const source = resolveSource(pin, sources), original = source?.bundle;
13363
13365
  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);
13366
+ const entry = list3(original?.entries).find((item2) => item2.artifactId === pin.artifactId);
13367
+ const screen = list3(originalDesign?.screens).find((item2) => item2.id === entry?.screenId);
13368
+ const direction = list3(originalDesign?.variants).find((item2) => item2.id === entry?.variantId);
13369
+ const frame = list3(originalDesign?.frames).find((item2) => item2.id === entry?.frameId);
13368
13370
  const sourceRevisionId = revisionFor(pin) ?? source?.revisionId ?? null;
13369
- const reviewId = field(pin.reviewId), reviewOf = digest3(pin.reviewOf);
13371
+ const reviewId = field(pin.reviewId), reviewOf = digest4(pin.reviewOf);
13370
13372
  const threadKey = JSON.stringify([sourceRevisionId, reviewId, reviewOf, pin.id]);
13371
13373
  if (seen.has(threadKey)) throw new TypeError("Review export contains a duplicate thread identity.");
13372
13374
  seen.add(threadKey);
@@ -13379,7 +13381,7 @@ function reviewExportTools() {
13379
13381
  if (!source || !entry) staleReasons.push("Original screen mapping is unavailable; do not relocate this pin.");
13380
13382
  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
13383
  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);
13384
+ const replies = list3(pin.replies);
13383
13385
  if (replies.length > 1e3) throw new TypeError("Review export exceeds 1000 replies in one thread.");
13384
13386
  const thread = {
13385
13387
  id: pin.id,
@@ -13391,12 +13393,12 @@ function reviewExportTools() {
13391
13393
  stale: staleReasons.length > 0,
13392
13394
  staleReasons,
13393
13395
  author: identity(pin.author),
13394
- createdAt: timestamp(pin.createdAt),
13395
- updatedAt: timestamp(pin.updatedAt),
13396
+ createdAt: timestamp3(pin.createdAt),
13397
+ updatedAt: timestamp3(pin.updatedAt),
13396
13398
  comment: quote(pin.comment),
13397
13399
  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) }))
13400
+ 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,
13401
+ replies: [...replies].sort(order).map((reply) => ({ id: field(reply.id), author: identity(reply.author), createdAt: timestamp3(reply.createdAt), comment: quote(reply.comment) }))
13400
13402
  };
13401
13403
  const groupKey = JSON.stringify([sourceRevisionId, reviewId, reviewOf, refs.screen, refs.direction, refs.frame, pin.artifactId]);
13402
13404
  if (!groups.has(groupKey)) groups.set(groupKey, {
@@ -13412,17 +13414,17 @@ function reviewExportTools() {
13412
13414
  });
13413
13415
  groups.get(groupKey).threads.push(thread);
13414
13416
  }
13415
- const orderedGroups = [...groups.entries()].sort(([a], [b]) => compare(a, b)).map(([, value]) => value);
13417
+ const orderedGroups = [...groups.entries()].sort(([a], [b]) => compare2(a, b)).map(([, value]) => value);
13416
13418
  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 ?? ""));
13419
+ const reviews = input.review ? [{ review: input.review }] : list3(input.feedback?.ledger?.reviews);
13420
+ 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
13421
  return {
13420
13422
  kind: "openplanr-design-review-export",
13421
13423
  schemaVersion: "1.0.0",
13422
13424
  design: { id: field(design.id), title: field(design.title) ?? "Design review" },
13423
13425
  currentRevisionId: current.revisionId,
13424
13426
  currentArtifactDigest: current.reviewOf,
13425
- ...timestamp(input.generatedAt) ? { generatedAt: timestamp(input.generatedAt) } : {},
13427
+ ...timestamp3(input.generatedAt) ? { generatedAt: timestamp3(input.generatedAt) } : {},
13426
13428
  completeness: { historyComplete: input.historyComplete === true, olderPagesLoading: input.olderPagesLoading === true, includesUnsentLocalChanges: input.includesUnsentLocalChanges === true },
13427
13429
  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
13430
  resolutionGuidance: [
@@ -13505,16 +13507,16 @@ function renderDesignReviewExportSource() {
13505
13507
  // packages/design/lib/design/studio.mjs
13506
13508
  var templateRoot = new URL("../../templates/studio/", new URL("./runtime/packages/design/lib/design/studio.mjs", import.meta.url).href);
13507
13509
  function designStudioArtifactId(variantId, screenId, frameId) {
13508
- const id3 = [
13510
+ const id4 = [
13509
13511
  "design",
13510
13512
  ...[variantId, screenId, frameId].map(
13511
13513
  (value) => `${value.length}-${value}`
13512
13514
  )
13513
13515
  ].join(".");
13514
- return id3.length <= 128 ? id3 : `design.${createHash4("sha256").update(JSON.stringify([variantId, screenId, frameId])).digest("hex")}`;
13516
+ return id4.length <= 128 ? id4 : `design.${createHash4("sha256").update(JSON.stringify([variantId, screenId, frameId])).digest("hex")}`;
13515
13517
  }
13516
13518
  function createDesignStudioEntries(document2, envelope) {
13517
- const ids = new Set(envelope.artifacts.map(({ id: id3 }) => id3));
13519
+ const ids = new Set(envelope.artifacts.map(({ id: id4 }) => id4));
13518
13520
  const entries = [];
13519
13521
  for (const variant of document2.variants.filter(
13520
13522
  ({ status }) => status === "ready"
@@ -13543,8 +13545,8 @@ function renderDesignStudio(input = {}, options = {}) {
13543
13545
  return renderDesignStudioMarkup({ ...input, entries: input.entries ?? createDesignStudioEntries(input.document, input.envelope) }, {
13544
13546
  ...options,
13545
13547
  renderShell: renderArtifactShellDocument,
13546
- style: ["studio.css", "enhancements.css"].map((file) => readFileSync7(new URL(file, templateRoot), "utf8")).join("\n"),
13547
- runtime: renderDesignReviewExportSource() + "\n" + ["studio.js", "enhancements.js"].map((file) => readFileSync7(new URL(file, templateRoot), "utf8")).join("\n")
13548
+ style: ["studio.css", "enhancements.css", "handoff-center.css"].map((file) => readFileSync7(new URL(file, templateRoot), "utf8")).join("\n"),
13549
+ runtime: renderDesignReviewExportSource() + "\n" + ["studio.js", "enhancements.js", "handoff-center.js"].map((file) => readFileSync7(new URL(file, templateRoot), "utf8")).join("\n")
13548
13550
  });
13549
13551
  }
13550
13552
 
@@ -13637,9 +13639,9 @@ function recoverDesignPublication(root, { ownsRenderLock = false } = {}) {
13637
13639
  }
13638
13640
  }
13639
13641
  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));
13642
+ const checked2 = readSource?.(file, process.cwd());
13643
+ const path = checked2?.file ?? realpathSync3(resolve3(file));
13644
+ const document2 = assertDesignDocument(checked2 ? JSON.parse(checked2.value.toString("utf8")) : readJson2(path));
13643
13645
  return { path, root: dirname5(path), document: document2 };
13644
13646
  }
13645
13647
  function designSpecPath(root) {
@@ -13731,10 +13733,10 @@ function prepareDesignDocument(file, { readSource, passive = false, maxBytes } =
13731
13733
  designSystem: document2.designSystem
13732
13734
  });
13733
13735
  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}"`))
13736
+ for (const anchor2 of screen.anchors ?? []) {
13737
+ if (!bundled.html.includes(`data-planr-id="${anchor2}"`) && !bundled.html.includes(`id="${anchor2}"`))
13736
13738
  throw new Error(
13737
- `Screen ${screenId} is missing its declared anchor ${anchor}.`
13739
+ `Screen ${screenId} is missing its declared anchor ${anchor2}.`
13738
13740
  );
13739
13741
  }
13740
13742
  for (const frame of document2.frames) {
@@ -13827,7 +13829,7 @@ function designRendererRevision() {
13827
13829
  json({
13828
13830
  version: DESIGN_RENDERER_VERSION,
13829
13831
  stage: hash(stageRuntimeBytes()),
13830
- assets: ["studio.css", "studio.js", "enhancements.css", "enhancements.js"].filter((name) => existsSync3(new URL(`../../templates/studio/${name}`, new URL("./runtime/packages/design/lib/design/document.mjs", import.meta.url).href))).map(
13832
+ assets: ["studio.css", "studio.js", "enhancements.css", "enhancements.js", "handoff-center.css", "handoff-center.js"].filter((name) => existsSync3(new URL(`../../templates/studio/${name}`, new URL("./runtime/packages/design/lib/design/document.mjs", import.meta.url).href))).map(
13831
13833
  (name) => hash(
13832
13834
  readFileSync8(
13833
13835
  new URL(`../../templates/studio/${name}`, new URL("./runtime/packages/design/lib/design/document.mjs", import.meta.url).href)
@@ -13913,7 +13915,7 @@ ${errors.join("\n")}`);
13913
13915
  contentProvenance: prepared.document.brief.provenance,
13914
13916
  generatedAt,
13915
13917
  screens: prepared.document.screenOrder.map(
13916
- (id3) => prepared.document.screens.find((item2) => item2.id === id3).title
13918
+ (id4) => prepared.document.screens.find((item2) => item2.id === id4).title
13917
13919
  ),
13918
13920
  iterations: previous && previous.revision !== prepared.revision ? (previous.iterations ?? 0) + 1 : previous?.iterations ?? 0,
13919
13921
  htmlFile: `.design/revisions/${prepared.revision}/${prepared.document.defaultView}.html`
@@ -13993,8 +13995,8 @@ ${errors.join("\n")}`);
13993
13995
  }
13994
13996
 
13995
13997
  // 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";
13998
+ import { existsSync as existsSync11, readFileSync as readFileSync16 } from "node:fs";
13999
+ import { dirname as dirname13, join as join12, relative as relative4, resolve as resolve7 } from "node:path";
13998
14000
 
13999
14001
  // packages/artifact/lib/artifact/import.mjs
14000
14002
  import {
@@ -14019,10 +14021,10 @@ var ARTIFACT_REVIEW_STATE_VERSION = "1.0.0";
14019
14021
  var ARTIFACT_REVIEW_STATE_KIND = "artifact-review-state";
14020
14022
  var SHA256_RE2 = /^[a-f0-9]{64}$/;
14021
14023
  var ARTIFACT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
14022
- function conflict(message, details = void 0) {
14024
+ function conflict(message2, details = void 0) {
14023
14025
  throw new PipelineError(
14024
14026
  ARTIFACT_ERROR_CODES.MERGE_CONFLICT,
14025
- message,
14027
+ message2,
14026
14028
  "Keep both reviews under different stable IDs or resolve the conflicting item explicitly.",
14027
14029
  details
14028
14030
  );
@@ -14364,13 +14366,13 @@ function writeArtifactReviewState(path, ledger, {
14364
14366
  }
14365
14367
  return ledger;
14366
14368
  }
14367
- function withArtifactReviewLock(path, action) {
14369
+ function withArtifactReviewLock(path, action3) {
14368
14370
  const previous = reviewPathQueues.get(path) ?? Promise.resolve();
14369
14371
  const run = async () => {
14370
14372
  let release;
14371
14373
  try {
14372
14374
  release = await acquireStartLock(`${path}.lock`, { timeout: 15e3, stale: 3e4 });
14373
- return await action();
14375
+ return await action3();
14374
14376
  } catch (error) {
14375
14377
  if (error instanceof PipelineError) throw error;
14376
14378
  throw new PipelineError(
@@ -14480,8 +14482,8 @@ ${reviewMarkdown(normalized)}
14480
14482
 
14481
14483
  // packages/artifact/lib/artifact/import.mjs
14482
14484
  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.");
14485
+ function pathError(code, message2) {
14486
+ throw new PipelineError(code, message2, "Choose a real, non-symlinked project or user review destination.");
14485
14487
  }
14486
14488
  function pathEntry(path, fs = { lstatSync: lstatSync3 }) {
14487
14489
  try {
@@ -14679,8 +14681,8 @@ var PERMISSIONS_POLICY = [
14679
14681
  "web-share=()",
14680
14682
  "xr-spatial-tracking=()"
14681
14683
  ].join(", ");
14682
- function artifactError(code, message, fix = "", details) {
14683
- return new PipelineError(code, message, fix, details);
14684
+ function artifactError(code, message2, fix = "", details) {
14685
+ return new PipelineError(code, message2, fix, details);
14684
14686
  }
14685
14687
  function statusForError(error) {
14686
14688
  if (error?.code === "E_REQUEST_BODY_LIMIT" || error?.code === ARTIFACT_ERROR_CODES.REQUEST_LIMIT) return 413;
@@ -14769,9 +14771,9 @@ function normalizeRegistration(value) {
14769
14771
  throw artifactError(ARTIFACT_ERROR_CODES.REQUEST_INVALID, "Artifact session registration must be an object.");
14770
14772
  }
14771
14773
  const envelope = cloneAndValidateEnvelope(value.envelope);
14772
- const title = value.title ?? envelope.artifacts[0]?.title ?? "Artifact review";
14774
+ const title2 = value.title ?? envelope.artifacts[0]?.title ?? "Artifact review";
14773
14775
  const theme = value.theme ?? "auto";
14774
- if (typeof title !== "string" || title.length < 1 || title.length > TITLE_LIMIT) {
14776
+ if (typeof title2 !== "string" || title2.length < 1 || title2.length > TITLE_LIMIT) {
14775
14777
  throw artifactError(ARTIFACT_ERROR_CODES.REQUEST_INVALID, `Artifact title must be 1 through ${TITLE_LIMIT} characters.`);
14776
14778
  }
14777
14779
  if (!THEME_VALUES.has(theme)) {
@@ -14785,7 +14787,7 @@ function normalizeRegistration(value) {
14785
14787
  if (reviewKey !== void 0 && (typeof reviewKey !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(reviewKey))) {
14786
14788
  throw artifactError(ARTIFACT_ERROR_CODES.REQUEST_INVALID, "Artifact review storage key is invalid.");
14787
14789
  }
14788
- return { envelope, title, theme, cwd, ...reviewKey ? { reviewKey } : {} };
14790
+ return { envelope, title: title2, theme, cwd, ...reviewKey ? { reviewKey } : {} };
14789
14791
  }
14790
14792
  function assertReviewStateSize(ledger) {
14791
14793
  const bytes = Buffer.byteLength(JSON.stringify(ledger), "utf8");
@@ -14864,7 +14866,7 @@ function safeSessionId(value) {
14864
14866
  return isCapabilityToken(value, { bytes: SESSION_ID_BYTES });
14865
14867
  }
14866
14868
  function artifactFor(session, artifactId) {
14867
- return session.envelope.artifacts.find(({ id: id3 }) => id3 === artifactId) ?? null;
14869
+ return session.envelope.artifacts.find(({ id: id4 }) => id4 === artifactId) ?? null;
14868
14870
  }
14869
14871
  function publicBase(session) {
14870
14872
  return `/r/${session.id}/${session.capability}/`;
@@ -14978,22 +14980,22 @@ function createArtifactReviewServer({
14978
14980
  if (draining) {
14979
14981
  throw artifactError(ARTIFACT_ERROR_CODES.LOOPBACK_STATE, "Artifact review server is restarting.");
14980
14982
  }
14981
- const id3 = mintCapabilityToken({ bytes: SESSION_ID_BYTES });
14982
- const reviewState = await initializeSessionReview(registration, env);
14983
+ const id4 = mintCapabilityToken({ bytes: SESSION_ID_BYTES });
14984
+ const reviewState2 = await initializeSessionReview(registration, env);
14983
14985
  const session2 = {
14984
14986
  ...registration,
14985
- id: id3,
14987
+ id: id4,
14986
14988
  capability: mintCapabilityToken({ bytes: SESSION_TOKEN_BYTES }),
14987
14989
  bridgeNonce: createArtifactBridgeNonce(),
14988
14990
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
14989
- reviewState: reviewState.ledger,
14990
- reviewPath: reviewState.path,
14991
+ reviewState: reviewState2.ledger,
14992
+ reviewPath: reviewState2.path,
14991
14993
  writeQueue: Promise.resolve()
14992
14994
  };
14993
- sessions.set(id3, session2);
14995
+ sessions.set(id4, session2);
14994
14996
  sendJson(res, 201, {
14995
14997
  ok: true,
14996
- sessionId: id3,
14998
+ sessionId: id4,
14997
14999
  capability: session2.capability,
14998
15000
  path: publicBase(session2)
14999
15001
  });
@@ -15081,11 +15083,11 @@ function createArtifactReviewServer({
15081
15083
  }
15082
15084
  const value = JSON.parse(body || "{}");
15083
15085
  const review = value?.review ?? value;
15084
- const reviewState = await queueSessionReviewWrite(session, review);
15086
+ const reviewState2 = await queueSessionReviewWrite(session, review);
15085
15087
  sendJson(res, 200, {
15086
15088
  ok: true,
15087
15089
  reviewId: review.reviewId,
15088
- effectiveDecision: effectiveReviewDecision(reviewState)
15090
+ effectiveDecision: effectiveReviewDecision(reviewState2)
15089
15091
  });
15090
15092
  return;
15091
15093
  }
@@ -15272,13 +15274,13 @@ function workspaceReviewUrl(access) {
15272
15274
  if (!idPattern.test(access.id)) throw new TypeError("Invalid design workspace identity.");
15273
15275
  return `${normalizeWorkspaceBase(access.baseUrl)}/d/${access.id}`;
15274
15276
  }
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.");
15277
+ async function tokenMaterial(token, id4, purpose) {
15278
+ if (!tokenPattern.test(token) || decodeWorkspaceBytes(token).length !== 32 || !idPattern.test(id4)) throw new TypeError("Enter the complete generated access token.");
15277
15279
  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));
15280
+ 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
15281
  }
15280
- async function deriveWorkspaceAuthentication(token, id3) {
15281
- return encodeWorkspaceBytes(await tokenMaterial(token, id3, "reviewer-auth"));
15282
+ async function deriveWorkspaceAuthentication(token, id4) {
15283
+ return encodeWorkspaceBytes(await tokenMaterial(token, id4, "reviewer-auth"));
15282
15284
  }
15283
15285
  function canonicalWorkspacePublicKey(value) {
15284
15286
  if (!value || value.kty !== "EC" || value.crv !== "P-256" || !tokenPattern.test(value.x ?? "") || !tokenPattern.test(value.y ?? "") || "d" in value) {
@@ -15318,9 +15320,9 @@ async function unseal(value, rawKey, context, limit = DESIGN_WORKSPACE_MAX_BYTES
15318
15320
  const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv: decodeWorkspaceBytes(value.iv), additionalData: encoder.encode(canonicalizeJson(context)) }, key, bytes);
15319
15321
  return JSON.parse(decoder.decode(plaintext));
15320
15322
  }
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 });
15323
+ var keyringContext = (id4, epoch2) => ({ type: "keyring", workspaceId: id4, epoch: epoch2 });
15324
+ var revisionContext = (id4, revision) => ({ type: "revision", workspaceId: id4, id: revision.id, epoch: revision.epoch, reviewOf: revision.reviewOf });
15325
+ var eventContext = (id4, event) => ({ type: "event", workspaceId: id4, id: event.id, epoch: event.epoch, revisionId: event.revisionId, reviewOf: event.reviewOf });
15324
15326
  function workspaceEnvelopeDigest(envelope) {
15325
15327
  return sha256Hex2(canonicalizeJson({ schemaVersion: envelope.schemaVersion, artifacts: envelope.artifacts, viewer: envelope.viewer }));
15326
15328
  }
@@ -15415,22 +15417,22 @@ async function decryptWorkspaceRevision(access, revisionId = access.currentRevis
15415
15417
  if (workspaceEnvelopeDigest(bundle.envelope) !== revision.reviewOf) throw new Error("Published design content does not match its revision.");
15416
15418
  return { ...bundle, workspaceRevision: revision.id, reviewOf: revision.reviewOf };
15417
15419
  }
15418
- async function prepareWorkspaceMutation(custody, action, payload = void 0) {
15420
+ async function prepareWorkspaceMutation(custody, action3, payload = void 0) {
15419
15421
  if (custody.pendingCreate) throw new Error("Finish creating this shared review before changing it.");
15420
15422
  if (custody.pendingMutation) throw new Error("A sharing operation is pending. Retry it before making another change.");
15421
15423
  const body = { operationId: newWorkspaceId(), expectedVersion: custody.version, epoch: custody.epoch };
15422
15424
  let next = {};
15423
- if (action === "publish") body.revision = await prepareRevision(custody, payload);
15424
- else if (action === "rotate") {
15425
+ if (action3 === "publish") body.revision = await prepareRevision(custody, payload);
15426
+ else if (action3 === "rotate") {
15425
15427
  const token = newWorkspaceToken();
15426
15428
  const epoch2 = custody.epoch + 1;
15427
15429
  const keys = { ...custody.keys, [epoch2]: newWorkspaceToken() };
15428
15430
  const keyring = await wrapKeyring(custody, token, epoch2, keys);
15429
15431
  Object.assign(body, { epoch: epoch2, reviewerAuthHash: sha256Hex2(await deriveWorkspaceAuthentication(token, custody.id)), keyring });
15430
15432
  next = { token, epoch: epoch2, keys, keyring };
15431
- } else if (["pause", "resume", "revoke", "delete"].includes(action)) body.action = action;
15433
+ } else if (["pause", "resume", "revoke", "delete"].includes(action3)) body.action = action3;
15432
15434
  else throw new TypeError("Unknown design sharing operation.");
15433
- custody.pendingMutation = { action, body: await signWorkspaceValue(body, custody.ownerPrivateKey), next };
15435
+ custody.pendingMutation = { action: action3, body: await signWorkspaceValue(body, custody.ownerPrivateKey), next };
15434
15436
  return custody.pendingMutation;
15435
15437
  }
15436
15438
  async function commitWorkspaceMutation(custody, options = {}) {
@@ -15465,18 +15467,27 @@ async function prepareWorkspaceEvent(access, payload, { revisionId = access.curr
15465
15467
  }
15466
15468
  async function appendWorkspaceEvent(access, payload, { preparedEvent, signer, revisionId, reviewOf, ...options } = {}) {
15467
15469
  const event = preparedEvent ?? await prepareWorkspaceEvent(access, payload, { signer, revisionId, reviewOf: reviewOf ?? payload.reviewOf });
15468
- return request(access, "/events", { ...options, method: "POST", body: event });
15470
+ const result = await request(access, "/events", { ...options, method: "POST", body: event });
15471
+ 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.");
15472
+ const { sequence: eventSequence, ...received } = result.event;
15473
+ 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.");
15474
+ return { event: { ...received, sequence: result.sequence }, sequence: result.sequence };
15469
15475
  }
15470
15476
  async function readWorkspaceEvents(access, { after = 0, ...options } = {}) {
15471
15477
  if (!Number.isSafeInteger(after) || after < 0) throw new TypeError("Invalid feedback cursor.");
15472
15478
  if (!access.keys) await getWorkspace(access, options);
15473
15479
  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.");
15480
+ 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
15481
  const events = [];
15476
15482
  const issues = [];
15483
+ let previousSequence = after;
15484
+ const pageIds = /* @__PURE__ */ new Set();
15477
15485
  for (const item2 of page.events) {
15478
15486
  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.");
15487
+ if (!Number.isSafeInteger(sequence) || sequence <= previousSequence || sequence > page.cursor) throw new Error("Invalid shared feedback sequence.");
15488
+ previousSequence = sequence;
15489
+ if (typeof record.id !== "string" || pageIds.has(record.id)) throw new Error("Invalid shared feedback event identity.");
15490
+ pageIds.add(record.id);
15480
15491
  try {
15481
15492
  assertWorkspaceContract(record, DESIGN_WORKSPACE_EVENT_SCHEMA);
15482
15493
  if (!await verifyWorkspaceSignature(record, record.publicKey)) throw new Error("Signature verification failed.");
@@ -15489,6 +15500,7 @@ async function readWorkspaceEvents(access, { after = 0, ...options } = {}) {
15489
15500
  issues.push({ sequence, id: typeof record.id === "string" && idPattern.test(record.id) ? record.id : null, reason: "Invalid encrypted feedback; not imported." });
15490
15501
  }
15491
15502
  }
15503
+ if (previousSequence > page.cursor) throw new Error("Invalid shared feedback cursor.");
15492
15504
  return { ...page, events, issues };
15493
15505
  }
15494
15506
 
@@ -15526,14 +15538,14 @@ var ARTIFACT_REVIEW_INTENTS = Object.freeze(["fix", "improve", "question"]);
15526
15538
  var ARTIFACT_REVIEW_STATUSES = Object.freeze(["open", "addressed", "resolved"]);
15527
15539
  var REVIEW_OF_RE = /^[a-f0-9]{64}$/;
15528
15540
  var ArtifactReviewStateError = class extends Error {
15529
- constructor(code, message) {
15530
- super(message);
15541
+ constructor(code, message2) {
15542
+ super(message2);
15531
15543
  this.name = "ArtifactReviewStateError";
15532
15544
  this.code = code;
15533
15545
  }
15534
15546
  };
15535
- function invalid2(message) {
15536
- throw new ArtifactReviewStateError("E_ARTIFACT_REVIEW_INVALID", message);
15547
+ function invalid2(message2) {
15548
+ throw new ArtifactReviewStateError("E_ARTIFACT_REVIEW_INVALID", message2);
15537
15549
  }
15538
15550
  function identityRequired() {
15539
15551
  throw new ArtifactReviewStateError(
@@ -15564,11 +15576,11 @@ function enumValue(value, values, label) {
15564
15576
  return value;
15565
15577
  }
15566
15578
  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))) {
15579
+ const timestamp3 = value instanceof Date ? value.toISOString() : value;
15580
+ 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
15581
  invalid2(`${label} must be an ISO-8601 date-time.`);
15570
15582
  }
15571
- return timestamp;
15583
+ return timestamp3;
15572
15584
  }
15573
15585
  function normalizeArtifactReviewIdentity(value, { allowEmpty = false } = {}) {
15574
15586
  if (value === null || value === void 0 || value === "") {
@@ -15582,12 +15594,12 @@ function normalizeArtifactReviewIdentity(value, { allowEmpty = false } = {}) {
15582
15594
  max: ARTIFACT_REVIEW_LIMITS.authorName,
15583
15595
  trim: true
15584
15596
  });
15585
- const id3 = optionalString(source.id, "author.id", {
15597
+ const id4 = optionalString(source.id, "author.id", {
15586
15598
  min: 1,
15587
15599
  max: ARTIFACT_REVIEW_LIMITS.id,
15588
15600
  trim: true
15589
15601
  });
15590
- return deepFreezeArtifactReview(id3 === void 0 ? { name } : { id: id3, name });
15602
+ return deepFreezeArtifactReview(id4 === void 0 ? { name } : { id: id4, name });
15591
15603
  }
15592
15604
  function normalizeRegion(region) {
15593
15605
  if (!region || typeof region !== "object" || Array.isArray(region)) {
@@ -15618,15 +15630,15 @@ function normalizeViewport2(viewport) {
15618
15630
  height: dimension(viewport.height, "pin.viewport.height")
15619
15631
  };
15620
15632
  }
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", {
15633
+ function normalizeAnchor(anchor2) {
15634
+ if (anchor2 === void 0 || anchor2 === null) return void 0;
15635
+ if (typeof anchor2 !== "object" || Array.isArray(anchor2)) invalid2("pin.anchor must be an object.");
15636
+ const planrId = boundedString(anchor2.planrId, "pin.anchor.planrId", {
15625
15637
  min: 1,
15626
15638
  max: ARTIFACT_REVIEW_LIMITS.anchor,
15627
15639
  trim: true
15628
15640
  });
15629
- const screen = optionalString(anchor.screen, "pin.anchor.screen", {
15641
+ const screen = optionalString(anchor2.screen, "pin.anchor.screen", {
15630
15642
  min: 1,
15631
15643
  max: ARTIFACT_REVIEW_LIMITS.screen,
15632
15644
  trim: true
@@ -15660,7 +15672,7 @@ function normalizePin(pin, label = "pin") {
15660
15672
  invalid2(`${label}.replies must contain no more than ${ARTIFACT_REVIEW_LIMITS.replies} items.`);
15661
15673
  }
15662
15674
  const replies = pin.replies.map((reply, index) => normalizeReply(reply, `${label}.replies[${index}]`));
15663
- const replyIds = new Set(replies.map(({ id: id3 }) => id3));
15675
+ const replyIds = new Set(replies.map(({ id: id4 }) => id4));
15664
15676
  if (replyIds.size !== replies.length) invalid2(`${label}.replies must have unique ids.`);
15665
15677
  const normalized = {
15666
15678
  id: boundedString(pin.id, `${label}.id`, {
@@ -15692,9 +15704,9 @@ function normalizePin(pin, label = "pin") {
15692
15704
  max: ARTIFACT_REVIEW_LIMITS.variant,
15693
15705
  trim: true
15694
15706
  });
15695
- const anchor = normalizeAnchor(pin.anchor);
15707
+ const anchor2 = normalizeAnchor(pin.anchor);
15696
15708
  if (variant !== void 0) normalized.variant = variant;
15697
- if (anchor !== void 0) normalized.anchor = anchor;
15709
+ if (anchor2 !== void 0) normalized.anchor = anchor2;
15698
15710
  return normalized;
15699
15711
  }
15700
15712
  function normalizeArtifactReview2(review) {
@@ -15705,7 +15717,7 @@ function normalizeArtifactReview2(review) {
15705
15717
  invalid2(`review.pins must contain no more than ${ARTIFACT_REVIEW_LIMITS.pins} items.`);
15706
15718
  }
15707
15719
  const pins = review.pins.map((pin, index) => normalizePin(pin, `review.pins[${index}]`));
15708
- const pinIds = new Set(pins.map(({ id: id3 }) => id3));
15720
+ const pinIds = new Set(pins.map(({ id: id4 }) => id4));
15709
15721
  if (pinIds.size !== pins.length) invalid2("review.pins must have unique ids.");
15710
15722
  const normalized = {
15711
15723
  schemaVersion: enumValue(review.schemaVersion, ["1.0.0"], "review.schemaVersion"),
@@ -15742,22 +15754,27 @@ function mergeWorkspaceFeedback(events, { revisionId, reviewOf, ownerPublicKey }
15742
15754
  const overall = /* @__PURE__ */ new Map();
15743
15755
  const directions = /* @__PURE__ */ new Map();
15744
15756
  const seen = /* @__PURE__ */ new Map();
15757
+ const revisionBases = /* @__PURE__ */ new Map();
15745
15758
  const acceptedEventIds = [];
15746
15759
  const issues = [];
15747
15760
  const categories = {}, dispositions = {};
15748
15761
  for (const event of [...events].sort((a, b) => a.sequence - b.sequence)) {
15749
- if (event.revisionId !== revisionId) continue;
15750
15762
  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.");
15763
+ 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
15764
  const eventHash = sha256Hex2(canonicalizeJson(event));
15756
15765
  if (seen.has(event.id)) {
15757
- if (seen.get(event.id) !== eventHash) throw new TypeError("Shared feedback reuses an event identity.");
15766
+ if (seen.get(event.id) !== eventHash) throw new TypeError("Shared feedback reuses an event identity with changed bytes.");
15758
15767
  continue;
15759
15768
  }
15760
15769
  seen.set(event.id, eventHash);
15770
+ const boundReviewOf = revisionBases.get(event.revisionId);
15771
+ if (boundReviewOf && boundReviewOf !== event.reviewOf) throw new TypeError("A shared revision identity is bound to conflicting design content.");
15772
+ revisionBases.set(event.revisionId, event.reviewOf);
15773
+ if (event.revisionId !== revisionId) continue;
15774
+ if (event.reviewOf !== reviewOf || !event.publicKey?.x || !event.publicKey?.y) throw new TypeError("Shared feedback has an invalid revision or signer.");
15775
+ const signerId = workspaceReviewerId(event.publicKey);
15776
+ const payload = event.payload;
15777
+ 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
15778
  const author = { id: signerId, name: payload.author.trim() };
15762
15779
  if (["category", "disposition"].includes(payload.kind)) {
15763
15780
  assertDesignReviewMetadata(payload);
@@ -15774,11 +15791,11 @@ function mergeWorkspaceFeedback(events, { revisionId, reviewOf, ownerPublicKey }
15774
15791
  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
15792
  const ratings = {};
15776
15793
  const remix = {};
15777
- for (const [id3, rating] of Object.entries(payload.ratings ?? {})) {
15778
- if (Number.isInteger(rating) && rating >= 1 && rating <= 5) ratings[id3] = rating;
15794
+ for (const [id4, rating] of Object.entries(payload.ratings ?? {})) {
15795
+ if (Number.isInteger(rating) && rating >= 1 && rating <= 5) ratings[id4] = rating;
15779
15796
  }
15780
- for (const [id3, note] of Object.entries(payload.remix ?? {})) {
15781
- if (typeof note === "string" && note.length <= 8192) remix[id3] = note;
15797
+ for (const [id4, note] of Object.entries(payload.remix ?? {})) {
15798
+ if (typeof note === "string" && note.length <= 8192) remix[id4] = note;
15782
15799
  }
15783
15800
  directions.set(signerId, { signerId, author: author.name, revisionId, ratings, remix });
15784
15801
  acceptedEventIds.push(event.id);
@@ -15898,13 +15915,13 @@ function writeCustody(path, record) {
15898
15915
  throw error;
15899
15916
  }
15900
15917
  }
15901
- async function withCustody(file, options, action) {
15918
+ async function withCustody(file, options, action3) {
15902
15919
  const location = custodyLocation(file, options);
15903
15920
  ensurePrivateDirectory(location.root);
15904
15921
  const unlock = await acquireStartLock(`${location.path}.lock`);
15905
15922
  try {
15906
15923
  let record = readCustody(location.path);
15907
- return await action({ ...location, record, save(value = record) {
15924
+ return await action3({ ...location, record, save(value = record) {
15908
15925
  record = value;
15909
15926
  writeCustody(location.path, value);
15910
15927
  } });
@@ -15952,8 +15969,9 @@ var safeStatus = (record, current) => ({
15952
15969
  commentsPaused: Boolean(record.custody.commentsPaused ?? record.commentsPaused),
15953
15970
  revoked: Boolean(record.revoked),
15954
15971
  deleted: Boolean(record.deleted),
15955
- pending: Boolean(record.custody.pendingCreate || record.custody.pendingMutation),
15956
- pendingAction: record.custody.pendingCreate ? "create" : record.custody.pendingMutation?.action ?? null
15972
+ pending: Boolean(record.custody.pendingCreate || record.custody.pendingMutation || record.pendingReviewMetadata?.length),
15973
+ pendingAction: record.custody.pendingCreate ? "create" : record.custody.pendingMutation?.action ?? (record.pendingReviewMetadata?.length ? "review-metadata" : null),
15974
+ pendingReviewMetadata: Boolean(record.pendingReviewMetadata?.length)
15957
15975
  } : {}
15958
15976
  });
15959
15977
  function getDesignShareStatus(file, options = {}) {
@@ -16000,25 +16018,25 @@ async function publishDesignShare(file, options = {}) {
16000
16018
  return safeStatus(record, current);
16001
16019
  });
16002
16020
  }
16003
- async function manageDesignShare(file, action, options = {}) {
16004
- if (!["rotate", "pause", "resume", "revoke", "delete", "access"].includes(action)) throw new Error("Unknown design sharing action.");
16021
+ async function manageDesignShare(file, action3, options = {}) {
16022
+ if (!["rotate", "pause", "resume", "revoke", "delete", "access"].includes(action3)) throw new Error("Unknown design sharing action.");
16005
16023
  return withCustody(file, options, async ({ record, current, save }) => {
16006
16024
  if (!record || record.custody.pendingCreate) throw new Error("Create the shared review first.");
16007
- if (action === "access") {
16025
+ if (action3 === "access") {
16008
16026
  if (record.revoked || record.deleted) throw new Error("This review is no longer accessible.");
16009
16027
  return { ...safeStatus(record, current), token: record.custody.token };
16010
16028
  }
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.`);
16029
+ if (action3 === "rotate") await flushOwnerMetadata(record, save, options);
16030
+ if (record.custody.pendingMutation && record.custody.pendingMutation.action !== action3) throw new Error(`Retry the pending ${record.custody.pendingMutation.action} operation first.`);
16013
16031
  if (!record.custody.pendingMutation) {
16014
16032
  if (!record.revoked) await getWorkspace(record.custody, { fetchImpl: options.fetchImpl });
16015
- await prepareWorkspaceMutation(record.custody, action);
16033
+ await prepareWorkspaceMutation(record.custody, action3);
16016
16034
  save(record);
16017
16035
  }
16018
16036
  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;
16037
+ if (action3 === "pause" || action3 === "resume") record.commentsPaused = action3 === "pause";
16038
+ if (action3 === "revoke") record.revoked = true;
16039
+ if (action3 === "delete") record.deleted = true;
16022
16040
  save(record);
16023
16041
  return safeStatus(record, current);
16024
16042
  });
@@ -16101,22 +16119,39 @@ async function syncDesignShare(file, options = {}) {
16101
16119
  const page = await readWorkspaceEvents(record.custody, { after: record.lastEvent ?? 0, fetchImpl: options.fetchImpl });
16102
16120
  const earlier = readJson2(ledgerPath, { schemaVersion: "1.0.0", events: [], issues: [], importedReviews: {} });
16103
16121
  const events = [...earlier.events];
16104
- const ids = new Set(events.map((event) => event.id));
16122
+ const eventBytes = new Map(events.map((event) => [event.id, canonicalizeJson(event)]));
16123
+ const revisionBases = /* @__PURE__ */ new Map();
16124
+ for (const event of events) {
16125
+ const previous = revisionBases.get(event.revisionId);
16126
+ if (previous && previous !== event.reviewOf) throw new Error("Saved shared feedback binds one revision to conflicting design content.");
16127
+ revisionBases.set(event.revisionId, event.reviewOf);
16128
+ }
16105
16129
  const pageIssues = [...page.issues ?? []];
16106
16130
  for (const event of page.events ?? []) {
16107
- if (ids.has(event.id)) continue;
16131
+ if (eventBytes.has(event.id)) {
16132
+ 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." });
16133
+ continue;
16134
+ }
16108
16135
  try {
16136
+ const boundReviewOf = revisionBases.get(event.revisionId);
16137
+ if (boundReviewOf && boundReviewOf !== event.reviewOf) throw new Error("A shared revision identity is bound to conflicting design content.");
16109
16138
  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);
16139
+ const invalid3 = candidate.issues?.find((issue2) => (issue2.id ?? issue2.eventId) === event.id);
16111
16140
  if (invalid3) throw new Error(invalid3.reason);
16112
16141
  events.push(event);
16113
- ids.add(event.id);
16142
+ eventBytes.set(event.id, canonicalizeJson(event));
16143
+ revisionBases.set(event.revisionId, event.reviewOf);
16114
16144
  imported++;
16115
16145
  } catch (error) {
16116
16146
  pageIssues.push({ id: event.id, sequence: event.sequence, reason: error.message });
16117
16147
  }
16118
16148
  }
16119
- const revisions = new Map(events.map((event) => [event.revisionId, event.reviewOf]));
16149
+ const revisions = /* @__PURE__ */ new Map();
16150
+ for (const event of events) {
16151
+ const previous = revisions.get(event.revisionId);
16152
+ if (previous && previous !== event.reviewOf) throw new Error("Saved shared feedback binds one revision to conflicting design content.");
16153
+ revisions.set(event.revisionId, event.reviewOf);
16154
+ }
16120
16155
  const importedReviews = { ...earlier.importedReviews ?? {} }, directions = [], metadataByRevision = {};
16121
16156
  await withArtifactReviewLock(reviewPath, () => {
16122
16157
  const ledger = readArtifactReviewState(reviewPath, { allowMissing: true }) ?? createReviewLedger({ artifactId: reviewKey, currentReviewOf: currentDigest });
@@ -16145,7 +16180,7 @@ async function syncDesignShare(file, options = {}) {
16145
16180
  }
16146
16181
  writeArtifactReviewState(reviewPath, createReviewLedger({ artifactId: reviewKey, currentReviewOf: currentDigest, reviews: [...entries.values()] }));
16147
16182
  });
16148
- issues = [...new Map([...earlier.issues ?? [], ...pageIssues].map((issue) => [`${issue.id ?? issue.eventId}:${issue.sequence}`, issue])).values()];
16183
+ issues = [...new Map([...earlier.issues ?? [], ...pageIssues].map((issue2) => [`${issue2.id ?? issue2.eventId}:${issue2.sequence}`, issue2])).values()];
16149
16184
  atomicJson(ledgerPath, { schemaVersion: "1.0.0", workspaceId: record.custody.id, events, issues, directions, metadataByRevision, importedReviews });
16150
16185
  const next = page.cursor;
16151
16186
  hasMore = Boolean(page.hasMore) && next > record.lastEvent;
@@ -16159,11 +16194,883 @@ async function syncDesignShare(file, options = {}) {
16159
16194
  // packages/design/lib/design/handoff.mjs
16160
16195
  import { existsSync as existsSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "node:fs";
16161
16196
  import { dirname as dirname10, join as join9 } from "node:path";
16162
- var conflict2 = (message) => Object.assign(new Error(message), { statusCode: 409 });
16197
+
16198
+ // packages/design/lib/design/handoff-resolution.mjs
16199
+ var OUTCOMES = /* @__PURE__ */ new Set([
16200
+ "accepted",
16201
+ "open",
16202
+ "blocking",
16203
+ "deferred",
16204
+ "declined"
16205
+ ]);
16206
+ var CATEGORIES = /* @__PURE__ */ new Set([
16207
+ "question",
16208
+ "suggestion",
16209
+ "change-request",
16210
+ "blocker"
16211
+ ]);
16212
+ var DISPOSITIONS = /* @__PURE__ */ new Set(["accepted", "deferred", "rejected", "declined"]);
16213
+ var MAX_COMMENTS = 1e4;
16214
+ var MAX_ISSUES = 1e4;
16215
+ var digestPattern = /^[a-f0-9]{64}$/u;
16216
+ var revisionOf = (pin) => pin.revisionId ?? pin.reviewId;
16217
+ var keyOf = (pin) => `${revisionOf(pin)}:${pin.id}`;
16218
+ var compare = (left, right) => left.localeCompare(right, "en");
16219
+ function assertPlainData(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
16220
+ if (depth > 64)
16221
+ throw new TypeError(
16222
+ "Review resolution data exceeds the maximum nesting depth."
16223
+ );
16224
+ if (value === null || ["string", "boolean"].includes(typeof value)) return;
16225
+ if (typeof value === "number" && Number.isFinite(value)) return;
16226
+ if (typeof value !== "object" || seen.has(value))
16227
+ throw new TypeError("Review resolution data must be finite, acyclic JSON.");
16228
+ if (!Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
16229
+ throw new TypeError(
16230
+ "Review resolution data must contain only plain JSON objects."
16231
+ );
16232
+ seen.add(value);
16233
+ for (const [key, descriptor] of Object.entries(
16234
+ Object.getOwnPropertyDescriptors(value)
16235
+ )) {
16236
+ if (["__proto__", "prototype", "constructor"].includes(key) || !Object.hasOwn(descriptor, "value"))
16237
+ throw new TypeError(
16238
+ "Review resolution data contains a forbidden property."
16239
+ );
16240
+ assertPlainData(descriptor.value, depth + 1, seen);
16241
+ }
16242
+ seen.delete(value);
16243
+ }
16244
+ var clone3 = (value) => {
16245
+ assertPlainData(value);
16246
+ return JSON.parse(canonicalizeJson(value));
16247
+ };
16248
+ function issue(code, severity, message2, { pinId, revisionId, recoveryAction } = {}) {
16249
+ return {
16250
+ code,
16251
+ severity,
16252
+ message: message2,
16253
+ ...revisionId ? { revisionId } : {},
16254
+ ...pinId ? { pinId } : {},
16255
+ recoveryAction: recoveryAction ?? {
16256
+ id: "refresh-review-resolution",
16257
+ label: "Refresh review decisions"
16258
+ }
16259
+ };
16260
+ }
16261
+ function validatePin(pin) {
16262
+ if (!pin || typeof pin !== "object" || Array.isArray(pin))
16263
+ throw new TypeError("Every review comment must be an object.");
16264
+ if (typeof pin.id !== "string" || !pin.id || pin.id.length > 160)
16265
+ throw new TypeError("Every review comment requires a stable identity.");
16266
+ const revisionId = revisionOf(pin);
16267
+ if (typeof revisionId !== "string" || !revisionId || revisionId.length > 160)
16268
+ throw new TypeError(
16269
+ "Every review comment requires its original revision identity."
16270
+ );
16271
+ if (typeof pin.reviewOf !== "string" || !digestPattern.test(pin.reviewOf))
16272
+ throw new TypeError(
16273
+ "Every review comment requires its original review basis."
16274
+ );
16275
+ if (pin.screenId !== void 0 && (typeof pin.screenId !== "string" || !pin.screenId))
16276
+ throw new TypeError("Review screen references must be stable identities.");
16277
+ if (pin.elementId !== void 0 && (typeof pin.elementId !== "string" || !pin.elementId || !pin.screenId))
16278
+ throw new TypeError(
16279
+ "Review element references require a stable screen identity."
16280
+ );
16281
+ }
16282
+ function scopedMetadata(metadata2, pin, duplicatePinIds, diagnostics) {
16283
+ const revisionId = revisionOf(pin);
16284
+ const scoped = metadata2.byRevision?.[revisionId] ?? {};
16285
+ let category = scoped.categories?.[pin.id];
16286
+ let disposition = scoped.dispositions?.[pin.id];
16287
+ if (category === void 0 && Object.hasOwn(metadata2.categories ?? {}, pin.id)) {
16288
+ if (duplicatePinIds.has(pin.id))
16289
+ diagnostics.push(
16290
+ issue(
16291
+ "AMBIGUOUS_LEGACY_CATEGORY",
16292
+ "blocked",
16293
+ "A legacy category cannot be matched to one original revision.",
16294
+ { pinId: pin.id, revisionId }
16295
+ )
16296
+ );
16297
+ else category = metadata2.categories[pin.id];
16298
+ }
16299
+ if (disposition === void 0 && Object.hasOwn(metadata2.dispositions ?? {}, pin.id)) {
16300
+ if (duplicatePinIds.has(pin.id))
16301
+ diagnostics.push(
16302
+ issue(
16303
+ "AMBIGUOUS_LEGACY_DISPOSITION",
16304
+ "blocked",
16305
+ "A legacy owner decision cannot be matched to one original revision.",
16306
+ { pinId: pin.id, revisionId }
16307
+ )
16308
+ );
16309
+ else disposition = metadata2.dispositions[pin.id];
16310
+ }
16311
+ if (category !== void 0 && !CATEGORIES.has(category)) {
16312
+ diagnostics.push(
16313
+ issue(
16314
+ "UNKNOWN_CATEGORY",
16315
+ "blocked",
16316
+ "The comment category is not supported.",
16317
+ { pinId: pin.id, revisionId }
16318
+ )
16319
+ );
16320
+ category = void 0;
16321
+ }
16322
+ const dispositionValue = typeof disposition === "string" ? disposition : disposition?.disposition;
16323
+ if (dispositionValue !== void 0 && !DISPOSITIONS.has(dispositionValue)) {
16324
+ diagnostics.push(
16325
+ issue(
16326
+ "UNKNOWN_DISPOSITION",
16327
+ "blocked",
16328
+ "The owner decision is not supported.",
16329
+ { pinId: pin.id, revisionId }
16330
+ )
16331
+ );
16332
+ disposition = void 0;
16333
+ }
16334
+ return {
16335
+ category,
16336
+ disposition,
16337
+ dispositionValue: typeof disposition === "string" ? disposition : disposition?.disposition
16338
+ };
16339
+ }
16340
+ function unknownMetadata(metadata2, known, diagnostics) {
16341
+ for (const [revisionId, value] of Object.entries(
16342
+ metadata2.byRevision ?? {}
16343
+ ).sort(([left], [right]) => compare(left, right))) {
16344
+ for (const field of ["categories", "dispositions"]) {
16345
+ for (const pinId of Object.keys(value?.[field] ?? {}).sort(compare)) {
16346
+ if (!known.has(`${revisionId}:${pinId}`))
16347
+ diagnostics.push(
16348
+ issue(
16349
+ "UNKNOWN_COMMENT_METADATA",
16350
+ "blocked",
16351
+ "Review metadata targets a comment that is not present in the recorded review history.",
16352
+ { revisionId, pinId }
16353
+ )
16354
+ );
16355
+ }
16356
+ }
16357
+ }
16358
+ }
16359
+ function compileDesignHandoffResolution(input) {
16360
+ const source = clone3(input ?? {});
16361
+ const pins = Array.isArray(source.pins) ? source.pins : [];
16362
+ if (pins.length > MAX_COMMENTS)
16363
+ throw new RangeError("Review resolution exceeds the comment limit.");
16364
+ const metadata2 = source.metadata && typeof source.metadata === "object" && !Array.isArray(source.metadata) ? source.metadata : {};
16365
+ const diagnostics = [];
16366
+ const counts = /* @__PURE__ */ new Map();
16367
+ for (const pin of pins) {
16368
+ validatePin(pin);
16369
+ counts.set(pin.id, (counts.get(pin.id) ?? 0) + 1);
16370
+ }
16371
+ const duplicatePinIds = new Set(
16372
+ [...counts].filter(([, count]) => count > 1).map(([id4]) => id4)
16373
+ );
16374
+ const known = /* @__PURE__ */ new Set();
16375
+ for (const pin of pins) {
16376
+ const key = keyOf(pin);
16377
+ if (known.has(key))
16378
+ throw new TypeError(`Duplicate review comment identity: ${key}.`);
16379
+ known.add(key);
16380
+ }
16381
+ unknownMetadata(metadata2, known, diagnostics);
16382
+ const items = [...pins].sort((left, right) => compare(keyOf(left), keyOf(right))).map((pin) => {
16383
+ const revisionId = revisionOf(pin);
16384
+ const current = pin.stale !== true && (!source.currentReviewOf || pin.reviewOf === source.currentReviewOf);
16385
+ const resolved = scopedMetadata(
16386
+ metadata2,
16387
+ pin,
16388
+ duplicatePinIds,
16389
+ diagnostics
16390
+ );
16391
+ let outcome;
16392
+ if (resolved.dispositionValue === "accepted") outcome = "accepted";
16393
+ else if (resolved.dispositionValue === "deferred") outcome = "deferred";
16394
+ else if (["rejected", "declined"].includes(resolved.dispositionValue))
16395
+ outcome = "declined";
16396
+ else if (["blocker", "change-request"].includes(resolved.category))
16397
+ outcome = "blocking";
16398
+ else outcome = "open";
16399
+ if (!OUTCOMES.has(outcome))
16400
+ throw new Error("Review resolution produced an invalid outcome.");
16401
+ if (!current && outcome === "accepted")
16402
+ diagnostics.push(
16403
+ issue(
16404
+ "STALE_ACCEPTED_DECISION",
16405
+ "stale",
16406
+ "An accepted change belongs to an earlier design revision and must be reviewed again.",
16407
+ {
16408
+ pinId: pin.id,
16409
+ revisionId,
16410
+ recoveryAction: {
16411
+ id: "review-stale-decision",
16412
+ label: "Review this decision against the current design"
16413
+ }
16414
+ }
16415
+ )
16416
+ );
16417
+ if (current && outcome === "blocking")
16418
+ diagnostics.push(
16419
+ issue(
16420
+ "UNRESOLVED_BLOCKING_COMMENT",
16421
+ "blocked",
16422
+ "A blocking comment still needs an owner decision.",
16423
+ {
16424
+ pinId: pin.id,
16425
+ revisionId,
16426
+ recoveryAction: {
16427
+ id: "resolve-blocking-comment",
16428
+ label: "Record the owner decision"
16429
+ }
16430
+ }
16431
+ )
16432
+ );
16433
+ return {
16434
+ id: keyOf(pin),
16435
+ pinId: pin.id,
16436
+ reviewId: pin.reviewId,
16437
+ revisionId,
16438
+ reviewOf: pin.reviewOf,
16439
+ current,
16440
+ category: resolved.category ?? "question",
16441
+ outcome,
16442
+ implementationScope: current && outcome === "accepted",
16443
+ anchor: pin.elementId ? { screenId: pin.screenId, elementId: pin.elementId } : pin.screenId ? { screenId: pin.screenId } : pin.anchor?.planrId ? { planrId: pin.anchor.planrId } : { artifactId: pin.artifactId },
16444
+ source: {
16445
+ text: pin.comment,
16446
+ author: clone3(pin.author),
16447
+ status: pin.status,
16448
+ ...resolved.disposition && typeof resolved.disposition === "object" ? { decision: clone3(resolved.disposition) } : {}
16449
+ }
16450
+ };
16451
+ });
16452
+ if (source.historyComplete === false)
16453
+ diagnostics.push(
16454
+ issue(
16455
+ "INCOMPLETE_REVIEW_HISTORY",
16456
+ "blocked",
16457
+ "The complete review history is unavailable.",
16458
+ {
16459
+ recoveryAction: {
16460
+ id: "restore-review-history",
16461
+ label: "Restore the complete review history"
16462
+ }
16463
+ }
16464
+ )
16465
+ );
16466
+ if (source.synchronizationPending === true)
16467
+ diagnostics.push(
16468
+ issue(
16469
+ "SYNCHRONIZATION_PENDING",
16470
+ "blocked",
16471
+ "A review decision is still waiting to synchronize.",
16472
+ {
16473
+ recoveryAction: {
16474
+ id: "retry-review-sync",
16475
+ label: "Retry review synchronization"
16476
+ }
16477
+ }
16478
+ )
16479
+ );
16480
+ for (const value of (source.synchronizationIssues ?? []).slice(0, MAX_ISSUES))
16481
+ diagnostics.push(
16482
+ issue(
16483
+ "UNTRUSTED_HOSTED_FEEDBACK",
16484
+ "blocked",
16485
+ value?.reason || "Hosted feedback could not be validated.",
16486
+ {
16487
+ pinId: value?.pinId,
16488
+ revisionId: value?.revisionId,
16489
+ recoveryAction: {
16490
+ id: "inspect-review-sync",
16491
+ label: "Inspect the rejected hosted feedback"
16492
+ }
16493
+ }
16494
+ )
16495
+ );
16496
+ if ((source.synchronizationIssues ?? []).length > MAX_ISSUES)
16497
+ throw new RangeError(
16498
+ "Review resolution exceeds the synchronization issue limit."
16499
+ );
16500
+ diagnostics.sort(
16501
+ (left, right) => compare(
16502
+ `${left.code}:${left.revisionId ?? ""}:${left.pinId ?? ""}:${left.message}`,
16503
+ `${right.code}:${right.revisionId ?? ""}:${right.pinId ?? ""}:${right.message}`
16504
+ )
16505
+ );
16506
+ const severity = new Set(diagnostics.map((value) => value.severity));
16507
+ const status = severity.has("stale") ? "stale" : severity.has("blocked") ? "blocked" : items.some((item2) => item2.outcome === "open") ? "attention" : "ready";
16508
+ return {
16509
+ kind: "openplanr-design-handoff-resolution",
16510
+ schemaVersion: "1.0.0",
16511
+ currentReviewOf: source.currentReviewOf ?? null,
16512
+ status,
16513
+ complete: source.historyComplete !== false && !["blocked", "stale"].includes(status),
16514
+ items,
16515
+ implementationScope: items.filter((item2) => item2.implementationScope).map((item2) => item2.id),
16516
+ diagnostics
16517
+ };
16518
+ }
16519
+ function canApproveDesignHandoffResolution(value) {
16520
+ return Boolean(
16521
+ value?.complete && ["ready", "attention"].includes(value.status)
16522
+ );
16523
+ }
16524
+
16525
+ // packages/protocol/src/design-handoff-contracts.mjs
16526
+ var DESIGN_HANDOFF_PROTOCOL_VERSION = "1.11.0";
16527
+ var DESIGN_HANDOFF_CONTRACT_VERSION = "1.0.0";
16528
+ var DESIGN_HANDOFF_AUTHORITY = "prepare-plan";
16529
+ var DESIGN_HANDOFF_CHECK_IDS = Object.freeze([
16530
+ "current-revision",
16531
+ "selected-direction",
16532
+ "design-specification",
16533
+ "rendered-verification",
16534
+ "review-freshness",
16535
+ "review-dispositions",
16536
+ "unresolved-blockers",
16537
+ "approved-review-handoff"
16538
+ ]);
16539
+ var DESIGN_HANDOFF_SOURCE_KINDS = Object.freeze([
16540
+ "design-revision",
16541
+ "selected-direction",
16542
+ "design-specification",
16543
+ "rendered-verification",
16544
+ "review-context",
16545
+ "review-feedback",
16546
+ "review-metadata",
16547
+ "review-handoff",
16548
+ "screen",
16549
+ "frame",
16550
+ "component",
16551
+ "state",
16552
+ "flow",
16553
+ "token",
16554
+ "review-decision",
16555
+ "element-anchor"
16556
+ ]);
16557
+ var DESIGN_HANDOFF_REQUIREMENT_KINDS = Object.freeze([
16558
+ "behavior",
16559
+ "visual-state",
16560
+ "responsive",
16561
+ "accessibility",
16562
+ "content-data-assumption",
16563
+ "constraint",
16564
+ "verification-intent"
16565
+ ]);
16566
+ var DESIGN_HANDOFF_CONTRACT_FILES = Object.freeze({
16567
+ "design-handoff-readiness": "design-handoff-readiness.schema.json",
16568
+ "design-implementation-handoff": "design-implementation-handoff.schema.json",
16569
+ "design-planning-lineage": "design-planning-lineage.schema.json"
16570
+ });
16571
+ var text3 = { type: "string", minLength: 1, maxLength: 16384 };
16572
+ var title = { ...text3, maxLength: 240 };
16573
+ var id3 = { type: "string", minLength: 1, maxLength: 160, pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]*$" };
16574
+ var digest3 = { type: "string", pattern: "^sha256:[a-f0-9]{64}$" };
16575
+ var relativePath = { type: "string", minLength: 1, maxLength: 4096 };
16576
+ var timestamp = { type: "string", format: "date-time" };
16577
+ var list2 = (items, maxItems = 1e3) => ({ type: "array", items, maxItems });
16578
+ var closed2 = (properties, required = Object.keys(properties)) => ({
16579
+ type: "object",
16580
+ additionalProperties: false,
16581
+ properties,
16582
+ required
16583
+ });
16584
+ var contract = (name, body) => ({
16585
+ $schema: "https://json-schema.org/draft/2020-12/schema",
16586
+ $id: `https://openplanr.dev/schemas/v1.11.0/${name}.schema.json`,
16587
+ "x-openplanr-contract": { id: name, version: DESIGN_HANDOFF_PROTOCOL_VERSION },
16588
+ ...body
16589
+ });
16590
+ var action = closed2({ id: id3, label: title });
16591
+ var anchor = {
16592
+ oneOf: [
16593
+ closed2({ section: id3 }),
16594
+ closed2({ screenId: id3 }),
16595
+ closed2({ screenId: id3, elementId: id3 }),
16596
+ closed2({ reviewId: id3, pinId: id3 })
16597
+ ]
16598
+ };
16599
+ var evidenceReference = closed2({
16600
+ id: id3,
16601
+ kind: { enum: DESIGN_HANDOFF_SOURCE_KINDS },
16602
+ path: relativePath,
16603
+ revision: digest3,
16604
+ digest: digest3,
16605
+ anchor
16606
+ }, ["id", "kind", "path"]);
16607
+ var readinessCheck = closed2({
16608
+ id: { enum: DESIGN_HANDOFF_CHECK_IDS },
16609
+ status: { enum: ["pass", "attention", "blocked", "stale"] },
16610
+ message: title,
16611
+ evidenceRefs: list2(id3, 64),
16612
+ recoveryAction: action
16613
+ }, ["id", "status", "message", "evidenceRefs"]);
16614
+ var readinessRecord = closed2({
16615
+ kind: { const: "openplanr-design-handoff-readiness" },
16616
+ schemaVersion: { const: DESIGN_HANDOFF_CONTRACT_VERSION },
16617
+ scope: { const: "design-originated" },
16618
+ authority: { const: "none" },
16619
+ designId: id3,
16620
+ sourceRevision: { anyOf: [digest3, { type: "null" }] },
16621
+ selectedVariant: { anyOf: [id3, { type: "null" }] },
16622
+ status: { enum: ["ready", "attention", "blocked", "stale"] },
16623
+ continuation: closed2({ action: { const: DESIGN_HANDOFF_AUTHORITY }, available: { type: "boolean" } }),
16624
+ checks: list2(readinessCheck, DESIGN_HANDOFF_CHECK_IDS.length),
16625
+ evidence: list2(evidenceReference, 1e4),
16626
+ blockers: list2({ enum: DESIGN_HANDOFF_CHECK_IDS }, DESIGN_HANDOFF_CHECK_IDS.length),
16627
+ nextActions: list2(action, DESIGN_HANDOFF_CHECK_IDS.length)
16628
+ });
16629
+ var readinessAbsence = closed2({
16630
+ kind: { const: "openplanr-design-handoff-readiness-absence" },
16631
+ schemaVersion: { const: DESIGN_HANDOFF_CONTRACT_VERSION },
16632
+ status: { const: "absent" },
16633
+ reason: { enum: ["not-computed", "not-applicable", "unavailable"] },
16634
+ message: title,
16635
+ nextAction: action
16636
+ });
16637
+ var DESIGN_HANDOFF_READINESS_SCHEMA = contract("design-handoff-readiness", {
16638
+ oneOf: [readinessRecord, readinessAbsence]
16639
+ });
16640
+ var requirement = closed2({
16641
+ id: { type: "string", pattern: "^REQ-[0-9]{3,}$" },
16642
+ kind: { enum: DESIGN_HANDOFF_REQUIREMENT_KINDS },
16643
+ statement: text3,
16644
+ sourceRefs: { ...list2(id3, 256), minItems: 1 },
16645
+ verification: { ...list2(text3, 256), minItems: 1 }
16646
+ });
16647
+ var implementationHandoff = closed2({
16648
+ kind: { const: "openplanr-design-implementation-handoff" },
16649
+ schemaVersion: { const: DESIGN_HANDOFF_CONTRACT_VERSION },
16650
+ id: id3,
16651
+ version: { type: "integer", minimum: 1 },
16652
+ status: { enum: ["draft", "approved", "superseded", "revoked"] },
16653
+ authority: { const: DESIGN_HANDOFF_AUTHORITY },
16654
+ title,
16655
+ basis: closed2({
16656
+ designId: id3,
16657
+ sourceRevision: digest3,
16658
+ selectedVariant: id3,
16659
+ readiness: closed2({ status: { enum: ["ready", "attention", "blocked", "stale"] }, digest: digest3 }),
16660
+ reviewHandoff: closed2({ version: { type: "integer", minimum: 1 }, contentDigest: digest3 })
16661
+ }),
16662
+ sources: list2(evidenceReference, 1e4),
16663
+ requirements: { ...list2(requirement, 1e4), minItems: 1 },
16664
+ contentDigest: digest3,
16665
+ markdown: { type: "string", maxLength: 2097152 },
16666
+ approval: closed2({ actorId: id3, approvedAt: timestamp, contentDigest: digest3, authority: { const: DESIGN_HANDOFF_AUTHORITY } }),
16667
+ supersededBy: closed2({ id: id3, version: { type: "integer", minimum: 1 }, contentDigest: digest3 }),
16668
+ revocation: closed2({ actorId: id3, revokedAt: timestamp, reason: text3 })
16669
+ }, ["kind", "schemaVersion", "id", "version", "status", "authority", "title", "basis", "sources", "requirements", "contentDigest", "markdown"]);
16670
+ implementationHandoff.allOf = [
16671
+ {
16672
+ if: { properties: { status: { const: "approved" } }, required: ["status"] },
16673
+ then: { required: ["approval"], not: { anyOf: [{ required: ["supersededBy"] }, { required: ["revocation"] }] } }
16674
+ },
16675
+ {
16676
+ if: { properties: { status: { const: "superseded" } }, required: ["status"] },
16677
+ then: { required: ["approval", "supersededBy"], not: { required: ["revocation"] } }
16678
+ },
16679
+ {
16680
+ if: { properties: { status: { const: "revoked" } }, required: ["status"] },
16681
+ then: { required: ["approval", "revocation"], not: { required: ["supersededBy"] } }
16682
+ },
16683
+ {
16684
+ if: { properties: { status: { const: "draft" } }, required: ["status"] },
16685
+ then: { not: { anyOf: [{ required: ["approval"] }, { required: ["supersededBy"] }, { required: ["revocation"] }] } }
16686
+ }
16687
+ ];
16688
+ var DESIGN_IMPLEMENTATION_HANDOFF_SCHEMA = contract(
16689
+ "design-implementation-handoff",
16690
+ implementationHandoff
16691
+ );
16692
+ var lineageMapping = closed2({
16693
+ requirementId: { type: "string", pattern: "^REQ-[0-9]{3,}$" },
16694
+ acceptanceRefs: { ...list2(closed2({
16695
+ storyId: { type: "string", pattern: "^US-[0-9]{3,}$" },
16696
+ acceptanceId: { type: "string", pattern: "^AC-[0-9]{3,}$" }
16697
+ }), 256), minItems: 1 },
16698
+ taskIds: { ...list2({ type: "string", pattern: "^T-[0-9]{3,}$" }, 256), minItems: 1 }
16699
+ });
16700
+ var DESIGN_PLANNING_LINEAGE_SCHEMA = contract("design-planning-lineage", closed2({
16701
+ kind: { const: "openplanr-design-planning-lineage" },
16702
+ schemaVersion: { const: DESIGN_HANDOFF_CONTRACT_VERSION },
16703
+ handoff: closed2({ id: id3, version: { type: "integer", minimum: 1 }, contentDigest: digest3 }),
16704
+ specId: { type: "string", pattern: "^SPEC-[0-9]{3,}$" },
16705
+ mappings: { ...list2(lineageMapping, 1e4), minItems: 1 }
16706
+ }));
16707
+ var DESIGN_HANDOFF_SCHEMAS = deepFreeze({
16708
+ "design-handoff-readiness": DESIGN_HANDOFF_READINESS_SCHEMA,
16709
+ "design-implementation-handoff": DESIGN_IMPLEMENTATION_HANDOFF_SCHEMA,
16710
+ "design-planning-lineage": DESIGN_PLANNING_LINEAGE_SCHEMA
16711
+ });
16712
+ function deepFreeze(value) {
16713
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
16714
+ for (const nested of Object.values(value)) deepFreeze(nested);
16715
+ Object.freeze(value);
16716
+ }
16717
+ return value;
16718
+ }
16719
+ function assertPlainData2(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
16720
+ if (depth > 64) throw new TypeError("Design handoff data exceeds the maximum nesting depth.");
16721
+ if (value === null || ["string", "boolean"].includes(typeof value)) return;
16722
+ if (typeof value === "number" && Number.isFinite(value)) return;
16723
+ if (typeof value !== "object" || seen.has(value)) throw new TypeError("Design handoff data must be finite, acyclic JSON.");
16724
+ if (!Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value))) throw new TypeError("Design handoff data must contain only plain JSON objects.");
16725
+ seen.add(value);
16726
+ for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
16727
+ if (["__proto__", "prototype", "constructor"].includes(key) || !Object.hasOwn(descriptor, "value")) throw new TypeError("Design handoff data contains a forbidden property.");
16728
+ assertPlainData2(descriptor.value, depth + 1, seen);
16729
+ }
16730
+ seen.delete(value);
16731
+ }
16732
+ function distinct(items, select, label) {
16733
+ const values = items.map(select);
16734
+ if (new Set(values).size !== values.length) throw new TypeError(`Duplicate ${label}.`);
16735
+ }
16736
+ function isDesignHandoffRelativePath(value) {
16737
+ 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));
16738
+ }
16739
+ function assertDesignHandoffContract(value, schemaOrName) {
16740
+ const schema3 = typeof schemaOrName === "string" ? DESIGN_HANDOFF_SCHEMAS[schemaOrName] : schemaOrName;
16741
+ if (!schema3) throw new TypeError("Unknown design handoff contract.");
16742
+ assertPlainData2(value);
16743
+ canonicalizeJson(value);
16744
+ const errors = validateJson(value, schema3);
16745
+ 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("; ")}`);
16746
+ return value;
16747
+ }
16748
+ function assertEvidenceReferences(evidence2) {
16749
+ distinct(evidence2, (item2) => item2.id, "evidence reference identity");
16750
+ for (const item2 of evidence2) if (!isDesignHandoffRelativePath(item2.path)) throw new TypeError("Design handoff evidence requires a repository-relative logical path.");
16751
+ }
16752
+ function assertProductCopy(value) {
16753
+ if (/\b(?:hash|digest|checksum|sha[- ]?256|canonical(?:ize|ization)?)\b/iu.test(value)) throw new TypeError("Design readiness guidance must use product language.");
16754
+ }
16755
+ function assertDesignHandoffReadiness(value) {
16756
+ assertDesignHandoffContract(value, DESIGN_HANDOFF_READINESS_SCHEMA);
16757
+ if (value.kind.endsWith("-absence")) {
16758
+ assertProductCopy(`${value.message} ${value.nextAction.label}`);
16759
+ return value;
16760
+ }
16761
+ assertEvidenceReferences(value.evidence);
16762
+ distinct(value.checks, (item2) => item2.id, "readiness check identity");
16763
+ const expected = DESIGN_HANDOFF_CHECK_IDS.join("\n");
16764
+ if (value.checks.map((item2) => item2.id).join("\n") !== expected) throw new TypeError("Design readiness must contain every stable check in canonical order.");
16765
+ const evidenceIds = new Set(value.evidence.map((item2) => item2.id));
16766
+ for (const check of value.checks) {
16767
+ distinct(check.evidenceRefs, (item2) => item2, "readiness evidence reference");
16768
+ if (check.evidenceRefs.some((reference) => !evidenceIds.has(reference))) throw new TypeError("Design readiness references missing evidence.");
16769
+ assertProductCopy(`${check.message} ${check.recoveryAction?.label ?? ""}`);
16770
+ }
16771
+ const priority2 = { pass: 0, attention: 1, blocked: 2, stale: 3 };
16772
+ const worst = value.checks.reduce((current, check) => priority2[check.status] > priority2[current] ? check.status : current, "pass");
16773
+ const expectedStatus = worst === "pass" ? "ready" : worst;
16774
+ if (value.status !== expectedStatus) throw new TypeError("Design readiness summary does not match its checks.");
16775
+ const blockingIds = value.checks.filter((check) => ["blocked", "stale"].includes(check.status)).map((check) => check.id);
16776
+ if (value.blockers.join("\n") !== blockingIds.join("\n")) throw new TypeError("Design readiness blockers do not match its blocking checks.");
16777
+ const actionable = value.checks.filter((check) => check.status !== "pass");
16778
+ if (value.nextActions.length !== actionable.length || value.nextActions.some((actionValue, index) => {
16779
+ const recoveryAction = actionable[index].recoveryAction;
16780
+ return actionValue.id !== recoveryAction?.id || actionValue.label !== recoveryAction?.label;
16781
+ })) throw new TypeError("Design readiness next actions do not match its checks.");
16782
+ if (value.continuation.available !== ["ready", "attention"].includes(value.status)) throw new TypeError("Design readiness continuation availability does not match its status.");
16783
+ return value;
16784
+ }
16785
+ function designImplementationHandoffDigest(value) {
16786
+ const projection = {
16787
+ kind: value.kind,
16788
+ schemaVersion: value.schemaVersion,
16789
+ id: value.id,
16790
+ version: value.version,
16791
+ authority: value.authority,
16792
+ title: value.title,
16793
+ basis: value.basis,
16794
+ sources: value.sources,
16795
+ requirements: value.requirements,
16796
+ markdown: value.markdown
16797
+ };
16798
+ return `sha256:${sha256Hex2(canonicalizeJson(projection))}`;
16799
+ }
16800
+ function assertDesignImplementationHandoff(value) {
16801
+ assertDesignHandoffContract(value, DESIGN_IMPLEMENTATION_HANDOFF_SCHEMA);
16802
+ assertEvidenceReferences(value.sources);
16803
+ distinct(value.requirements, (item2) => item2.id, "implementation requirement identity");
16804
+ const sourceIds = new Set(value.sources.map((item2) => item2.id));
16805
+ for (const item2 of value.requirements) {
16806
+ distinct(item2.sourceRefs, (reference) => reference, "requirement source reference");
16807
+ if (item2.sourceRefs.some((reference) => !sourceIds.has(reference))) throw new TypeError("Implementation requirement references missing evidence.");
16808
+ }
16809
+ if (value.contentDigest !== designImplementationHandoffDigest(value)) throw new TypeError("Implementation handoff content does not match its recorded integrity value.");
16810
+ if (value.approval?.contentDigest !== void 0 && value.approval.contentDigest !== value.contentDigest) throw new TypeError("Implementation handoff approval does not match its content.");
16811
+ 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.");
16812
+ 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.");
16813
+ return value;
16814
+ }
16815
+
16816
+ // packages/design/lib/design/handoff-readiness.mjs
16817
+ var CHECKS = Object.freeze([
16818
+ "current-revision",
16819
+ "selected-direction",
16820
+ "design-specification",
16821
+ "rendered-verification",
16822
+ "review-freshness",
16823
+ "review-dispositions",
16824
+ "unresolved-blockers",
16825
+ "approved-review-handoff"
16826
+ ]);
16827
+ var action2 = Object.freeze({
16828
+ "current-revision": Object.freeze({ id: "render-design", label: "Render the current design" }),
16829
+ "selected-direction": Object.freeze({ id: "select-direction", label: "Choose one ready direction" }),
16830
+ "design-specification": Object.freeze({ id: "complete-design-specification", label: "Complete the design specification" }),
16831
+ "rendered-verification": Object.freeze({ id: "verify-rendered-design", label: "Verify the rendered design" }),
16832
+ "review-freshness": Object.freeze({ id: "refresh-review", label: "Refresh review against the current design" }),
16833
+ "review-dispositions": Object.freeze({ id: "resolve-review-decisions", label: "Record the remaining review decisions" }),
16834
+ "unresolved-blockers": Object.freeze({ id: "resolve-review-blockers", label: "Resolve the blocking feedback" }),
16835
+ "approved-review-handoff": Object.freeze({ id: "approve-review-handoff", label: "Prepare and approve the current review handoff" })
16836
+ });
16837
+ var message = Object.freeze({
16838
+ "current-revision": Object.freeze({
16839
+ pass: "The current design has a completed render.",
16840
+ blocked: "Render the design before preparing work for engineering."
16841
+ }),
16842
+ "selected-direction": Object.freeze({
16843
+ pass: "One ready design direction is selected.",
16844
+ blocked: "Choose one ready design direction before continuing.",
16845
+ stale: "The selected direction changed after the review handoff was prepared."
16846
+ }),
16847
+ "design-specification": Object.freeze({
16848
+ pass: "The design specification is complete for this revision.",
16849
+ blocked: "Complete the design specification before continuing.",
16850
+ stale: "The design specification belongs to an earlier revision."
16851
+ }),
16852
+ "rendered-verification": Object.freeze({
16853
+ pass: "The rendered design is verified for this revision.",
16854
+ blocked: "Complete rendered design verification before continuing.",
16855
+ stale: "Rendered verification belongs to an earlier revision."
16856
+ }),
16857
+ "review-freshness": Object.freeze({
16858
+ pass: "Review feedback is current for this revision.",
16859
+ blocked: "Review feedback contains ambiguous identities or anchors.",
16860
+ stale: "Review feedback belongs to an earlier revision."
16861
+ }),
16862
+ "review-dispositions": Object.freeze({
16863
+ pass: "Every current review comment has a recorded outcome.",
16864
+ attention: "Some non-blocking review comments still need an owner decision.",
16865
+ blocked: "Review decisions are incomplete or ambiguous."
16866
+ }),
16867
+ "unresolved-blockers": Object.freeze({
16868
+ pass: "No blocking feedback remains open.",
16869
+ blocked: "Blocking feedback must be resolved before continuing.",
16870
+ stale: "A prior decision must be reviewed against the current design."
16871
+ }),
16872
+ "approved-review-handoff": Object.freeze({
16873
+ pass: "The current review handoff is approved.",
16874
+ blocked: "Prepare and approve the review handoff before continuing.",
16875
+ stale: "The approved review handoff no longer matches the current design."
16876
+ })
16877
+ });
16878
+ var priority = Object.freeze({ pass: 0, attention: 1, blocked: 2, stale: 3 });
16879
+ var digestPattern2 = /^(?:sha256:)?[a-f0-9]{64}$/u;
16880
+ var normalizeDigest = (value, label) => {
16881
+ if (typeof value !== "string" || !digestPattern2.test(value)) throw new TypeError(`${label} must be a lowercase SHA-256 value.`);
16882
+ return value.startsWith("sha256:") ? value : `sha256:${value}`;
16883
+ };
16884
+ var optionalDigest = (value, label) => value === void 0 || value === null ? void 0 : normalizeDigest(value, label);
16885
+ var plainClone = (value) => value === void 0 ? void 0 : JSON.parse(canonicalizeJson(value));
16886
+ var logicalPath = (value, fallback, label) => {
16887
+ const result = value ?? fallback;
16888
+ if (!isDesignHandoffRelativePath(result) || /[?#%]/u.test(result)) throw new TypeError(`${label} must be a repository-relative logical path.`);
16889
+ return result;
16890
+ };
16891
+ var evidenceDigest = (value) => `sha256:${sha256Hex2(canonicalizeJson(value))}`;
16892
+ function ensureUnique(items, select, label) {
16893
+ const values = items.map(select);
16894
+ if (new Set(values).size !== values.length) throw new TypeError(`Duplicate ${label}.`);
16895
+ }
16896
+ function evidence(id4, kind, path, value, { revision, anchor: anchor2 } = {}) {
16897
+ return {
16898
+ id: id4,
16899
+ kind,
16900
+ path,
16901
+ ...revision ? { revision: normalizeDigest(revision, `${id4} revision`) } : {},
16902
+ digest: optionalDigest(value?.digest, `${id4} integrity`) ?? evidenceDigest(value),
16903
+ ...anchor2 ? { anchor: anchor2 } : {}
16904
+ };
16905
+ }
16906
+ function checked(id4, status, evidenceRefs = []) {
16907
+ return {
16908
+ id: id4,
16909
+ status,
16910
+ message: message[id4][status],
16911
+ evidenceRefs,
16912
+ ...status === "pass" ? {} : { recoveryAction: action2[id4] }
16913
+ };
16914
+ }
16915
+ function selectedDirection(document2, studioState) {
16916
+ const selected = studioState?.selectedVariant ?? document2?.selectedVariant ?? null;
16917
+ const variants = Array.isArray(document2?.variants) ? document2.variants : [];
16918
+ ensureUnique(variants, (item2) => item2?.id, "design direction identity");
16919
+ const matches = variants.filter((item2) => item2?.id === selected && item2?.status === "ready");
16920
+ return { selected, ready: matches.length === 1 };
16921
+ }
16922
+ function normalizedPins(review, revision) {
16923
+ const pins = Array.isArray(review?.pins) ? review.pins.map((pin) => ({ ...pin })) : [];
16924
+ ensureUnique(pins, (pin) => `${pin.revisionId ?? pin.reviewId ?? ""}:${pin.id ?? ""}`, "review comment identity");
16925
+ for (const pin of pins) {
16926
+ if (typeof pin.id !== "string" || !pin.id) throw new TypeError("Review comments require stable identities.");
16927
+ if (pin.elementId && !pin.screenId) throw new TypeError("An element review anchor must identify its screen.");
16928
+ if (pin.anchorCount !== void 0 && pin.anchorCount !== 1) throw new TypeError("Review comments must identify exactly one anchor.");
16929
+ }
16930
+ return pins.sort((left, right) => `${left.revisionId ?? left.reviewId}:${left.id}`.localeCompare(`${right.revisionId ?? right.reviewId}:${right.id}`)).map((pin) => ({
16931
+ ...pin,
16932
+ current: !pin.stale && (!revision || !pin.revisionId || normalizeDigest(pin.revisionId, "review revision") === revision)
16933
+ }));
16934
+ }
16935
+ function reviewState(input, revision) {
16936
+ if (!input) return { pins: [], stale: false, ambiguous: false };
16937
+ const pins = normalizedPins(input, revision);
16938
+ const recordedRevision = optionalDigest(input.revision, "review revision");
16939
+ return {
16940
+ pins,
16941
+ stale: input.current === false || recordedRevision !== void 0 && recordedRevision !== revision || pins.some((pin) => !pin.current),
16942
+ ambiguous: input.ambiguous === true
16943
+ };
16944
+ }
16945
+ function handoffState(handoff, designId, revision, selectedVariant) {
16946
+ if (!handoff) return "blocked";
16947
+ if (handoff.status !== "approved" || !handoff.approval || handoff.approval.contentHash !== handoff.contentHash) return "blocked";
16948
+ const basisRevision = optionalDigest(handoff.basis?.sourceRevision, "review handoff revision");
16949
+ if (handoff.current === false || handoff.basis?.designId !== designId || basisRevision !== revision || handoff.basis?.selectedVariant !== selectedVariant) return "stale";
16950
+ return "pass";
16951
+ }
16952
+ function compileDesignHandoffReadiness(input) {
16953
+ if (input === null || input === void 0) return designHandoffReadinessAbsence();
16954
+ const source = plainClone(input);
16955
+ const document2 = source.document;
16956
+ 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.");
16957
+ const revision = source.sourceRevision === null || source.sourceRevision === void 0 ? null : normalizeDigest(source.sourceRevision, "design revision");
16958
+ const direction = selectedDirection(document2, source.studioState);
16959
+ const evidenceItems = [];
16960
+ if (revision) evidenceItems.push(evidence("design-revision", "design-revision", logicalPath(source.documentPath, "design-document.json", "design document path"), document2, { revision }));
16961
+ const directionEvidence = direction.selected && revision ? evidence(
16962
+ "selected-direction",
16963
+ "selected-direction",
16964
+ logicalPath(source.studioStatePath, ".design/studio-state.json", "Studio state path"),
16965
+ { selectedVariant: direction.selected },
16966
+ { revision, anchor: { section: direction.selected } }
16967
+ ) : null;
16968
+ if (directionEvidence) evidenceItems.push(directionEvidence);
16969
+ const specification = source.specification;
16970
+ if (specification) evidenceItems.push(evidence(
16971
+ "design-specification",
16972
+ "design-specification",
16973
+ logicalPath(specification.path, "design-spec.md", "design specification path"),
16974
+ specification,
16975
+ { revision: specification.revision }
16976
+ ));
16977
+ const verification = source.verification;
16978
+ if (verification) evidenceItems.push(evidence(
16979
+ "rendered-verification",
16980
+ "rendered-verification",
16981
+ logicalPath(verification.path, ".design/verification/current.json", "verification path"),
16982
+ verification,
16983
+ { revision: verification.revision }
16984
+ ));
16985
+ const review = reviewState(source.review, revision);
16986
+ if (source.review) evidenceItems.push(evidence(
16987
+ "review-feedback",
16988
+ "review-feedback",
16989
+ logicalPath(source.review.path, ".design/review.json", "review feedback path"),
16990
+ source.review,
16991
+ { revision: source.review.revision }
16992
+ ));
16993
+ const handoff = source.reviewHandoff;
16994
+ if (handoff) evidenceItems.push(evidence(
16995
+ "review-handoff",
16996
+ "review-handoff",
16997
+ logicalPath(handoff.path, "review-handoff.json", "review handoff path"),
16998
+ handoff,
16999
+ { revision: handoff.basis?.sourceRevision }
17000
+ ));
17001
+ const checks = [];
17002
+ checks.push(checked("current-revision", revision ? "pass" : "blocked", revision ? ["design-revision"] : []));
17003
+ const handoffDirection = handoff?.basis?.selectedVariant;
17004
+ const directionStatus = !direction.ready ? "blocked" : handoffDirection && handoffDirection !== direction.selected ? "stale" : "pass";
17005
+ checks.push(checked("selected-direction", directionStatus, directionEvidence ? ["selected-direction"] : []));
17006
+ let specificationStatus = "blocked";
17007
+ if (specification?.complete === true) {
17008
+ const specificationRevision = optionalDigest(specification.revision, "design specification revision");
17009
+ specificationStatus = revision && specificationRevision && specificationRevision !== revision ? "stale" : "pass";
17010
+ }
17011
+ checks.push(checked("design-specification", specificationStatus, specification ? ["design-specification"] : []));
17012
+ let verificationStatus = "blocked";
17013
+ if (verification) {
17014
+ const verificationRevision = optionalDigest(verification.revision, "rendered verification revision");
17015
+ if (revision && verificationRevision && verificationRevision !== revision) verificationStatus = "stale";
17016
+ else if (verification.status === "verified") verificationStatus = "pass";
17017
+ }
17018
+ checks.push(checked("rendered-verification", verificationStatus, verification ? ["rendered-verification"] : []));
17019
+ const reviewRefs = source.review ? ["review-feedback"] : [];
17020
+ const freshnessStatus = review.ambiguous ? "blocked" : review.stale ? "stale" : "pass";
17021
+ checks.push(checked("review-freshness", freshnessStatus, reviewRefs));
17022
+ const undecided = review.pins.filter((pin) => pin.current && !["accepted", "deferred", "rejected"].includes(pin.disposition));
17023
+ const invalidDisposition = review.pins.some((pin) => pin.disposition && !["accepted", "deferred", "rejected"].includes(pin.disposition));
17024
+ const dispositionStatus = invalidDisposition || review.ambiguous ? "blocked" : undecided.length ? "attention" : "pass";
17025
+ checks.push(checked("review-dispositions", dispositionStatus, reviewRefs));
17026
+ const openBlockers = review.pins.filter((pin) => pin.current && ["blocker", "change-request"].includes(pin.category) && !["accepted", "deferred", "rejected"].includes(pin.disposition));
17027
+ const staleAccepted = review.pins.some((pin) => !pin.current && pin.disposition === "accepted");
17028
+ const blockerStatus = staleAccepted ? "stale" : openBlockers.length ? "blocked" : "pass";
17029
+ checks.push(checked("unresolved-blockers", blockerStatus, reviewRefs));
17030
+ const approvalStatus = handoffState(handoff, document2.id, revision, direction.selected);
17031
+ checks.push(checked("approved-review-handoff", approvalStatus, handoff ? ["review-handoff"] : []));
17032
+ if (checks.map((item2) => item2.id).join("\n") !== CHECKS.join("\n")) throw new TypeError("Readiness checks are not in canonical order.");
17033
+ const worst = checks.reduce((current, item2) => priority[item2.status] > priority[current] ? item2.status : current, "pass");
17034
+ const status = worst === "pass" ? "ready" : worst;
17035
+ const blockers = checks.filter((item2) => ["blocked", "stale"].includes(item2.status)).map((item2) => item2.id);
17036
+ const nextActions = checks.filter((item2) => item2.status !== "pass").map((item2) => item2.recoveryAction);
17037
+ return assertDesignHandoffReadiness({
17038
+ kind: "openplanr-design-handoff-readiness",
17039
+ schemaVersion: "1.0.0",
17040
+ scope: "design-originated",
17041
+ authority: "none",
17042
+ designId: document2.id,
17043
+ sourceRevision: revision,
17044
+ selectedVariant: direction.selected,
17045
+ status,
17046
+ continuation: { action: "prepare-plan", available: ["ready", "attention"].includes(status) },
17047
+ checks,
17048
+ evidence: evidenceItems.sort((left, right) => left.id.localeCompare(right.id)),
17049
+ blockers,
17050
+ nextActions
17051
+ });
17052
+ }
17053
+ function designHandoffReadinessAbsence(reason = "not-computed") {
17054
+ return assertDesignHandoffReadiness({
17055
+ kind: "openplanr-design-handoff-readiness-absence",
17056
+ schemaVersion: "1.0.0",
17057
+ status: "absent",
17058
+ reason,
17059
+ message: "No design handoff readiness has been prepared.",
17060
+ nextAction: { id: "inspect-design", label: "Open the design when you want to prepare a handoff" }
17061
+ });
17062
+ }
17063
+ function designHandoffReadinessDigest(value) {
17064
+ assertDesignHandoffReadiness(value);
17065
+ return `sha256:${sha256Hex2(canonicalizeJson(value))}`;
17066
+ }
17067
+
17068
+ // packages/design/lib/design/handoff.mjs
17069
+ var conflict2 = (message2) => Object.assign(new Error(message2), { statusCode: 409 });
16163
17070
  var sections = ["agreedChanges", "openQuestions", "deferred", "rejected"];
16164
17071
  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}`;
17072
+ var revisionOf2 = (pin) => pin.revisionId ?? pin.reviewId;
17073
+ var pinKey = (pin) => `${revisionOf2(pin)}:${pin.id}`;
16167
17074
  function metadata(current, feedback) {
16168
17075
  const local = readJson2(metadataPath(current), { version: 0, byRevision: {} });
16169
17076
  const byRevision = structuredClone(feedback.shared?.metadataByRevision ?? {});
@@ -16175,21 +17082,45 @@ function metadata(current, feedback) {
16175
17082
  for (const pin of feedback.pins) counts.set(pin.id, (counts.get(pin.id) ?? 0) + 1);
16176
17083
  const categories = {}, dispositions = {};
16177
17084
  for (const pin of feedback.pins) {
16178
- const value = byRevision[revisionOf(pin)];
17085
+ const value = byRevision[revisionOf2(pin)];
16179
17086
  if (counts.get(pin.id) === 1 && value) {
16180
17087
  if (Object.hasOwn(value.categories ?? {}, pin.id)) Object.defineProperty(categories, pin.id, { value: value.categories[pin.id], enumerable: true });
16181
17088
  if (Object.hasOwn(value.dispositions ?? {}, pin.id)) Object.defineProperty(dispositions, pin.id, { value: value.dispositions[pin.id], enumerable: true });
16182
17089
  }
17090
+ 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 });
17091
+ 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
17092
  }
16184
- return { version: local.version, categories, dispositions, byRevision };
17093
+ return {
17094
+ version: local.version,
17095
+ categories,
17096
+ dispositions,
17097
+ byRevision,
17098
+ legacy: {
17099
+ categories: structuredClone(local.categories ?? {}),
17100
+ dispositions: structuredClone(local.dispositions ?? {})
17101
+ }
17102
+ };
16185
17103
  }
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));
17104
+ function findPin(pins, id4, revision) {
17105
+ const candidates = pins.filter((pin) => pin.id === id4 && (!revision || revisionOf2(pin) === revision));
16189
17106
  if (candidates.length !== 1) throw new Error("The comment identity is missing or ambiguous. Include its original revisionId.");
16190
17107
  return candidates[0];
16191
17108
  }
16192
- function snapshot(file, env) {
17109
+ function resolutionFor(value, shareStatus) {
17110
+ return compileDesignHandoffResolution({
17111
+ currentReviewOf: value.basis.reviewOf,
17112
+ pins: value.feedback.pins,
17113
+ metadata: {
17114
+ byRevision: value.metadata.byRevision,
17115
+ categories: value.metadata.legacy.categories,
17116
+ dispositions: value.metadata.legacy.dispositions
17117
+ },
17118
+ historyComplete: !value.feedback.shared?.issues?.length,
17119
+ synchronizationPending: Boolean(shareStatus?.pendingReviewMetadata),
17120
+ synchronizationIssues: value.feedback.shared?.issues ?? []
17121
+ });
17122
+ }
17123
+ function snapshot(file, env, shareOptions = {}) {
16193
17124
  const current = currentDesign(file), feedback = readDesignFeedback(file, env), meta = metadata(current, feedback);
16194
17125
  const reviewContext = current.reviewContext ?? emptyReviewContext(current.document);
16195
17126
  const basis = {
@@ -16198,11 +17129,17 @@ function snapshot(file, env) {
16198
17129
  contextDigest: current.contextDigest ?? reviewDigest(reviewContext),
16199
17130
  reviewOf: digestArtifactEnvelope(current.envelope),
16200
17131
  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 ?? [] }),
17132
+ 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
17133
  verificationDigest: reviewDigest(current.verification),
16203
17134
  feedbackWatermark: Math.max(0, ...(feedback.shared?.events ?? []).map((event) => event.sequence ?? 0))
16204
17135
  };
16205
- return { current, feedback, metadata: meta, reviewContext, basis };
17136
+ let shareStatus = null;
17137
+ try {
17138
+ shareStatus = getDesignShareStatus(file, { env, ...shareOptions });
17139
+ } catch {
17140
+ }
17141
+ const value = { current, feedback, metadata: meta, reviewContext, basis };
17142
+ return { ...value, resolution: resolutionFor(value, shareStatus), shareStatus };
16206
17143
  }
16207
17144
  function readDesignExperience(file, { env = process.env } = {}) {
16208
17145
  const value = snapshot(file, env);
@@ -16217,8 +17154,8 @@ function readDesignExperience(file, { env = process.env } = {}) {
16217
17154
  loadingHistory: false
16218
17155
  };
16219
17156
  }
16220
- function readDesignHandoff(file, { env = process.env } = {}) {
16221
- const value = snapshot(file, env);
17157
+ function readDesignHandoff(file, { env = process.env, ...shareOptions } = {}) {
17158
+ const value = snapshot(file, env, shareOptions);
16222
17159
  const path = join9(dirname10(designSpecPath(value.current.root)), "review-handoff.json");
16223
17160
  const draft = readJson2(path, null);
16224
17161
  if (draft) {
@@ -16226,7 +17163,51 @@ function readDesignHandoff(file, { env = process.env } = {}) {
16226
17163
  const markdownPath = path.replace(/\.json$/u, ".md");
16227
17164
  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
17165
  }
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 };
17166
+ 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 };
17167
+ }
17168
+ function readDesignHandoffReadiness(file, { env = process.env, ...shareOptions } = {}) {
17169
+ const value = snapshot(file, env, shareOptions);
17170
+ const path = join9(dirname10(designSpecPath(value.current.root)), "review-handoff.json");
17171
+ const handoff = readJson2(path, null);
17172
+ if (handoff) assertDraft(handoff);
17173
+ const outcomes = new Map(value.resolution.items.map((item2) => [item2.id, item2]));
17174
+ const pins = value.feedback.pins.map((pin) => {
17175
+ const resolved = outcomes.get(pinKey(pin));
17176
+ const disposition = resolved?.outcome === "accepted" ? "accepted" : resolved?.outcome === "deferred" ? "deferred" : resolved?.outcome === "declined" ? "rejected" : void 0;
17177
+ return {
17178
+ id: pin.id,
17179
+ ...pin.screenId ? { screenId: pin.screenId } : {},
17180
+ ...pin.anchor?.planrId ? { elementId: pin.anchor.planrId } : {},
17181
+ category: resolved?.category,
17182
+ ...disposition ? { disposition } : {},
17183
+ stale: Boolean(pin.stale)
17184
+ };
17185
+ });
17186
+ const specificationPath = designSpecPath(value.current.root);
17187
+ const specification = existsSync8(specificationPath) ? { path: "design-spec.md", revision: value.current.revision, digest: `sha256:${hash(readFileSync13(specificationPath))}`, complete: true } : void 0;
17188
+ const studioState = readJson2(join9(value.current.root, ".design/studio-state.json"), { state: {} }).state;
17189
+ const readiness = compileDesignHandoffReadiness({
17190
+ document: value.current.document,
17191
+ documentPath: "design-document.json",
17192
+ sourceRevision: value.current.revision,
17193
+ studioState,
17194
+ studioStatePath: ".design/studio-state.json",
17195
+ specification,
17196
+ verification: { path: ".design/verification/current.json", ...value.current.verification },
17197
+ review: {
17198
+ path: ".design/review.json",
17199
+ revision: value.current.revision,
17200
+ current: pins.every((pin) => !pin.stale),
17201
+ pins
17202
+ },
17203
+ reviewHandoff: handoff ? {
17204
+ ...handoff,
17205
+ path: "review-handoff.json",
17206
+ digest: `sha256:${handoff.contentHash}`,
17207
+ current: reviewDigest(handoff.basis) === reviewDigest(value.basis)
17208
+ } : void 0
17209
+ });
17210
+ return { ok: true, readiness, digest: designHandoffReadinessDigest(readiness) };
16230
17211
  }
16231
17212
  function assertDraft(draft) {
16232
17213
  assertReviewExperience(draft, DESIGN_HANDOFF_SCHEMA);
@@ -16241,19 +17222,20 @@ function sourceItem(pin, shareUrl) {
16241
17222
  }
16242
17223
  function contentFromFeedback(value, shareUrl) {
16243
17224
  const content = { summary: "", agreedChanges: [], openQuestions: [], deferred: [], rejected: [] };
16244
- for (const pin of value.feedback.pins) {
16245
- const disposition = dispositionOf(value.metadata, pin);
17225
+ const pins = new Map(value.feedback.pins.map((pin) => [pinKey(pin), pin]));
17226
+ for (const resolved of value.resolution.items) {
17227
+ const pin = pins.get(resolved.id);
16246
17228
  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);
17229
+ if (resolved.outcome === "accepted") content.agreedChanges.push(item2);
17230
+ else if (resolved.outcome === "deferred") content.deferred.push(item2);
17231
+ else if (resolved.outcome === "declined") content.rejected.push(item2);
17232
+ else content.openQuestions.push(item2);
16251
17233
  }
16252
17234
  return content;
16253
17235
  }
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.", ""];
17236
+ var safeMd = (text4) => String(text4).replaceAll("[", "\\[").replaceAll("]", "\\]").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
17237
+ function renderMarkdown(draft, title2) {
17238
+ 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
17239
  for (const [key, label] of [["agreedChanges", "Agreed changes"], ["openQuestions", "Open questions"], ["deferred", "Deferred"], ["rejected", "Rejected"]]) {
16258
17240
  lines.push(`## ${label}`, "");
16259
17241
  for (const item2 of draft.content[key]) {
@@ -16275,19 +17257,35 @@ function enrichContent(content, value, shareUrl) {
16275
17257
  const pin = findPin(value.feedback.pins, item2.pinId, item2.revisionId ?? item2.reviewId);
16276
17258
  if (seen.has(pinKey(pin))) throw new Error("Handoff items must cite distinct recorded comments.");
16277
17259
  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.");
17260
+ const resolved = value.resolution.items.find((itemValue) => itemValue.id === pinKey(pin));
17261
+ const expected = key === "agreedChanges" ? ["accepted"] : key === "deferred" ? ["deferred"] : key === "rejected" ? ["declined"] : ["open", "blocking"];
17262
+ if (!resolved || !expected.includes(resolved.outcome)) throw new Error("Record the owner disposition before moving a comment into this handoff section.");
16280
17263
  const refinement = item2.refinement ?? (item2.text !== pin.comment ? item2.text : void 0);
16281
17264
  return { ...sourceItem(pin, shareUrl), ...refinement !== void 0 ? { refinement } : {} };
16282
17265
  });
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.");
17266
+ 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
17267
  return enriched;
16285
17268
  }
16286
- async function preserveReviewErrors(path, action) {
17269
+ function normalizedLocalMetadata(local, pins) {
17270
+ const byRevision = structuredClone(local.byRevision ?? {});
17271
+ const counts = /* @__PURE__ */ new Map();
17272
+ for (const pin of pins) counts.set(pin.id, (counts.get(pin.id) ?? 0) + 1);
17273
+ for (const field of ["categories", "dispositions"]) {
17274
+ for (const [pinId, item2] of Object.entries(local[field] ?? {})) {
17275
+ if (counts.get(pinId) !== 1) continue;
17276
+ const pin = pins.find((value) => value.id === pinId);
17277
+ const revision = revisionOf2(pin);
17278
+ const scoped = byRevision[revision] ?? { categories: {}, dispositions: {} };
17279
+ byRevision[revision] = { ...scoped, [field]: { ...scoped[field], [pinId]: item2 } };
17280
+ }
17281
+ }
17282
+ return { version: local.version ?? 0, byRevision };
17283
+ }
17284
+ async function preserveReviewErrors(path, action3) {
16287
17285
  let failure, result;
16288
17286
  await withArtifactReviewLock(path, async () => {
16289
17287
  try {
16290
- result = await action();
17288
+ result = await action3();
16291
17289
  } catch (error) {
16292
17290
  failure = error;
16293
17291
  }
@@ -16304,7 +17302,7 @@ async function updateDesignHandoff(file, input, { env = process.env, fetchImpl =
16304
17302
  const unlock = await acquireStartLock(join9(initial.root, ".design/handoff.lock"));
16305
17303
  try {
16306
17304
  await preserveReviewErrors(designReviewPath(file, env), async () => {
16307
- const value = snapshot(file, env);
17305
+ const value = snapshot(file, env, shareOptions);
16308
17306
  if (input.revision !== value.current.revision) throw conflict2("The design changed. Refresh the review before updating its handoff.");
16309
17307
  if (["category", "disposition"].includes(input.action)) {
16310
17308
  if (input.version !== value.metadata.version) throw conflict2("Review organization changed in another window. Reload before saving.");
@@ -16314,8 +17312,8 @@ async function updateDesignHandoff(file, input, { env = process.env, fetchImpl =
16314
17312
  if (!choices.includes(input[input.action])) throw new Error(`Invalid ${input.action}.`);
16315
17313
  if (typeof (input.reason ?? "") !== "string" || (input.reason ?? "").length > 16384) throw new Error("Disposition reason is too long.");
16316
17314
  const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
16317
- const local = readJson2(metadataPath(value.current), { version: 0, byRevision: {} });
16318
- const revision = revisionOf(pin);
17315
+ const local = normalizedLocalMetadata(readJson2(metadataPath(value.current), { version: 0, byRevision: {} }), value.feedback.pins);
17316
+ const revision = revisionOf2(pin);
16319
17317
  const scoped = local.byRevision?.[revision] ?? { categories: {}, dispositions: {} };
16320
17318
  const valueForRevision = { ...scoped };
16321
17319
  if (input.action === "category") valueForRevision.categories = { ...scoped.categories, [pin.id]: input.category };
@@ -16336,6 +17334,10 @@ async function updateDesignHandoff(file, input, { env = process.env, fetchImpl =
16336
17334
  }
16337
17335
  if (input.action === "approve") {
16338
17336
  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.");
17337
+ if (!canApproveDesignHandoffResolution(value.resolution)) {
17338
+ const diagnostic = value.resolution.diagnostics[0];
17339
+ throw conflict2(diagnostic?.message ?? "Resolve the blocking review decisions before approving this handoff.");
17340
+ }
16339
17341
  if (!previous.content.summary.trim()) throw new Error("Write or refine the handoff summary before approving it.");
16340
17342
  const approved = { ...previous, version: previous.version + 1, status: "approved", approval: { contentHash: previous.contentHash, at: (/* @__PURE__ */ new Date()).toISOString() } };
16341
17343
  approved.markdown = renderMarkdown(approved, value.current.document.title);
@@ -16350,11 +17352,11 @@ async function updateDesignHandoff(file, input, { env = process.env, fetchImpl =
16350
17352
  if (input.action === "update" && (!previous || reviewDigest(previous.basis) !== reviewDigest(value.basis))) throw conflict2("The review changed. Rebuild the draft before refining it.");
16351
17353
  const affectedScreens = [...new Set(sections.flatMap((key) => content[key].map((item2) => item2.screenId).filter(Boolean)))];
16352
17354
  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);
17355
+ for (const issue2 of value.current.verification.issues ?? []) if (issue2.message && verificationGaps.length < 256) verificationGaps.push(issue2.message);
16354
17356
  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;
17357
+ const title2 = value.current.document.title;
16356
17358
  const draft = {
16357
- title,
17359
+ title: title2,
16358
17360
  reviewNotes,
16359
17361
  kind: "openplanr-design-review-handoff",
16360
17362
  schemaVersion: "1.0.0",
@@ -16364,7 +17366,7 @@ async function updateDesignHandoff(file, input, { env = process.env, fetchImpl =
16364
17366
  content,
16365
17367
  affectedScreens,
16366
17368
  verificationGaps,
16367
- contentHash: reviewDigest({ title, reviewNotes, basis: value.basis, content, affectedScreens, verificationGaps }),
17369
+ contentHash: reviewDigest({ title: title2, reviewNotes, basis: value.basis, content, affectedScreens, verificationGaps }),
16368
17370
  markdown: ""
16369
17371
  };
16370
17372
  draft.markdown = renderMarkdown(draft, value.current.document.title);
@@ -16386,7 +17388,783 @@ async function updateDesignHandoff(file, input, { env = process.env, fetchImpl =
16386
17388
  synchronization = { pending: true, error: error.message };
16387
17389
  }
16388
17390
  }
16389
- return { ...readDesignHandoff(file, { env }), ...synchronization ? { synchronization } : {} };
17391
+ return { ...readDesignHandoff(file, { env, ...shareOptions }), ...synchronization ? { synchronization } : {} };
17392
+ }
17393
+
17394
+ // packages/design/lib/design/implementation-handoff.mjs
17395
+ import { createHash as createHash6, randomUUID as randomUUID3 } from "node:crypto";
17396
+ import {
17397
+ existsSync as existsSync9,
17398
+ mkdirSync as mkdirSync7,
17399
+ readFileSync as readFileSync14,
17400
+ realpathSync as realpathSync6,
17401
+ renameSync as renameSync6,
17402
+ rmSync as rmSync6,
17403
+ writeFileSync as writeFileSync7
17404
+ } from "node:fs";
17405
+ import { dirname as dirname11, join as join10, resolve as resolve6, sep } from "node:path";
17406
+
17407
+ // packages/design/lib/design/implementation-handoff-markdown.mjs
17408
+ var line = (value) => String(value).replaceAll("\r\n", "\n").replaceAll("\r", "\n").trim().replaceAll("\n", " ").replace(/([\\`*_[\]<>#|])/gu, "\\$1");
17409
+ var anchorLabel = (anchor2) => {
17410
+ if (!anchor2) return null;
17411
+ if (anchor2.section) return `section ${anchor2.section}`;
17412
+ if (anchor2.reviewId) return `review ${anchor2.reviewId}, comment ${anchor2.pinId}`;
17413
+ if (anchor2.elementId)
17414
+ return `screen ${anchor2.screenId}, element ${anchor2.elementId}`;
17415
+ return `screen ${anchor2.screenId}`;
17416
+ };
17417
+ function renderImplementationHandoffMarkdown(value) {
17418
+ const lines = [
17419
+ `# ${line(value.title)}`,
17420
+ "",
17421
+ `Design: ${line(value.basis.designId)}`,
17422
+ `Selected direction: ${line(value.basis.selectedVariant)}`,
17423
+ `Authority: Prepare Plan`,
17424
+ "",
17425
+ "## Source references",
17426
+ ""
17427
+ ];
17428
+ for (const source of value.sources) {
17429
+ const anchor2 = anchorLabel(source.anchor);
17430
+ lines.push(
17431
+ `- **${line(source.id)}** \u2014 ${line(source.kind)} \xB7 \`${line(source.path)}\`${anchor2 ? ` \xB7 ${line(anchor2)}` : ""}`
17432
+ );
17433
+ }
17434
+ if (!value.sources.length) lines.push("None recorded.");
17435
+ lines.push("", "## Implementation requirements", "");
17436
+ for (const requirement2 of value.requirements) {
17437
+ lines.push(
17438
+ `### ${line(requirement2.id)} \xB7 ${line(requirement2.kind)}`,
17439
+ "",
17440
+ line(requirement2.statement),
17441
+ "",
17442
+ `Sources: ${requirement2.sourceRefs.map((reference) => `\`${line(reference)}\``).join(", ")}`,
17443
+ "",
17444
+ "Verification:",
17445
+ ...requirement2.verification.map((expectation) => `- ${line(expectation)}`),
17446
+ ""
17447
+ );
17448
+ }
17449
+ lines.push(
17450
+ "This package prepares approved design context for Plan. Plan and Ship remain separate user invocations.",
17451
+ ""
17452
+ );
17453
+ return lines.join("\n");
17454
+ }
17455
+
17456
+ // packages/design/lib/design/implementation-handoff.mjs
17457
+ var MAX_PACKAGE_BYTES = 2 * 1024 * 1024;
17458
+ var MAX_SOURCE_BYTES = 16 * 1024 * 1024;
17459
+ var DIGEST = /^sha256:[a-f0-9]{64}$/u;
17460
+ var REQUIREMENT_KINDS = /* @__PURE__ */ new Set([
17461
+ "behavior",
17462
+ "visual-state",
17463
+ "responsive",
17464
+ "accessibility",
17465
+ "content-data-assumption",
17466
+ "constraint",
17467
+ "verification-intent"
17468
+ ]);
17469
+ var INPUT_FIELDS = /* @__PURE__ */ new Set(["id", "version", "title", "basis", "sources", "requirements"]);
17470
+ var REQUIREMENT_FIELDS = /* @__PURE__ */ new Set(["id", "kind", "statement", "sourceRefs", "verification"]);
17471
+ var normalizeText = (value, label) => {
17472
+ if (typeof value !== "string") throw new TypeError(`${label} must be text.`);
17473
+ const normalized = value.replaceAll("\r\n", "\n").replaceAll("\r", "\n").trim();
17474
+ if (!normalized) throw new TypeError(`${label} cannot be empty.`);
17475
+ return normalized;
17476
+ };
17477
+ var clone4 = (value) => JSON.parse(canonicalizeJson(value));
17478
+ var sha256 = (value) => `sha256:${createHash6("sha256").update(value).digest("hex")}`;
17479
+ var jsonBytes = (value) => `${JSON.stringify(value, null, 2)}
17480
+ `;
17481
+ var assertKnownKeys = (value, allowed, label) => {
17482
+ if (Object.keys(value).some((key) => !allowed.has(key)))
17483
+ throw new TypeError(`${label} contains unknown fields.`);
17484
+ };
17485
+ function atomicText(path, value) {
17486
+ mkdirSync7(dirname11(path), { recursive: true });
17487
+ const temporary = `${path}.${randomUUID3()}.tmp`;
17488
+ try {
17489
+ writeFileSync7(temporary, value, { flag: "wx", mode: 384 });
17490
+ renameSync6(temporary, path);
17491
+ } finally {
17492
+ rmSync6(temporary, { force: true });
17493
+ }
17494
+ }
17495
+ function assertPackageSize(value) {
17496
+ if (Buffer.byteLength(jsonBytes(value)) > MAX_PACKAGE_BYTES)
17497
+ throw new TypeError("The implementation handoff JSON exceeds 2 MB.");
17498
+ if (Buffer.byteLength(value.markdown) > MAX_PACKAGE_BYTES)
17499
+ throw new TypeError("The implementation handoff Markdown exceeds 2 MB.");
17500
+ }
17501
+ function normalizeSource(source) {
17502
+ if (!source || typeof source !== "object" || Array.isArray(source))
17503
+ throw new TypeError("Implementation sources must be objects.");
17504
+ if (!isDesignHandoffRelativePath(source.path) || /[?#%]/u.test(source.path))
17505
+ throw new TypeError("Implementation sources require repository-relative logical paths.");
17506
+ if (source.path.includes("`"))
17507
+ throw new TypeError("Implementation source paths cannot contain Markdown delimiters.");
17508
+ if (/(?:^|\/)[^/:\s]+:[^/@\s]+@/u.test(source.path))
17509
+ throw new TypeError("Implementation source paths cannot contain credentials.");
17510
+ if (!DIGEST.test(source.revision ?? "") || !DIGEST.test(source.digest ?? ""))
17511
+ throw new TypeError("Implementation sources require exact revision and integrity values.");
17512
+ return clone4(source);
17513
+ }
17514
+ function requirementFingerprint(requirement2) {
17515
+ return canonicalizeJson({
17516
+ kind: requirement2.kind,
17517
+ statement: normalizeText(requirement2.statement, "Requirement statement"),
17518
+ sourceRefs: requirement2.sourceRefs.map((value) => normalizeText(value, "Source reference")),
17519
+ verification: requirement2.verification.map(
17520
+ (value) => normalizeText(value, "Verification expectation")
17521
+ )
17522
+ });
17523
+ }
17524
+ function deriveImplementationRequirementId(requirement2) {
17525
+ const hexadecimal = createHash6("sha256").update(requirementFingerprint(requirement2)).digest("hex");
17526
+ const numeric = (BigInt(`0x${hexadecimal}`) % 1000000000000n).toString(10).padStart(12, "0");
17527
+ return `REQ-${numeric}`;
17528
+ }
17529
+ function normalizeRequirement(requirement2) {
17530
+ if (!requirement2 || typeof requirement2 !== "object" || Array.isArray(requirement2))
17531
+ throw new TypeError("Implementation requirements must be objects.");
17532
+ assertKnownKeys(
17533
+ requirement2,
17534
+ REQUIREMENT_FIELDS,
17535
+ "Implementation requirement"
17536
+ );
17537
+ if (!REQUIREMENT_KINDS.has(requirement2.kind))
17538
+ throw new TypeError("Unknown implementation requirement kind.");
17539
+ if (!Array.isArray(requirement2.sourceRefs) || !requirement2.sourceRefs.length)
17540
+ throw new TypeError("Implementation requirements need source references.");
17541
+ if (new Set(requirement2.sourceRefs).size !== requirement2.sourceRefs.length)
17542
+ throw new TypeError("Implementation requirement source references must be distinct.");
17543
+ if (!Array.isArray(requirement2.verification) || !requirement2.verification.length)
17544
+ throw new TypeError("Implementation requirements need observable verification expectations.");
17545
+ const normalized = {
17546
+ kind: requirement2.kind,
17547
+ statement: normalizeText(requirement2.statement, "Requirement statement"),
17548
+ sourceRefs: requirement2.sourceRefs.map((value) => normalizeText(value, "Source reference")),
17549
+ verification: requirement2.verification.map(
17550
+ (value) => normalizeText(value, "Verification expectation")
17551
+ )
17552
+ };
17553
+ const id4 = deriveImplementationRequirementId(normalized);
17554
+ if (requirement2.id !== void 0 && requirement2.id !== id4)
17555
+ throw new TypeError("The supplied requirement identity does not match its canonical content.");
17556
+ return { id: id4, ...normalized };
17557
+ }
17558
+ function composeImplementationHandoff(input) {
17559
+ if (!input || typeof input !== "object" || Array.isArray(input))
17560
+ throw new TypeError("Implementation handoff input must be an object.");
17561
+ assertKnownKeys(
17562
+ input,
17563
+ INPUT_FIELDS,
17564
+ "Implementation handoff input"
17565
+ );
17566
+ const sources = (input.sources ?? []).map(normalizeSource).sort(
17567
+ (left, right) => left.id.localeCompare(right.id)
17568
+ );
17569
+ if (new Set(sources.map((source) => source.id)).size !== sources.length)
17570
+ throw new TypeError("Duplicate implementation source identity.");
17571
+ const sourceIds = new Set(sources.map((source) => source.id));
17572
+ const requirements = (input.requirements ?? []).map(normalizeRequirement).sort((left, right) => left.id.localeCompare(right.id));
17573
+ if (!requirements.length)
17574
+ throw new TypeError("An implementation handoff needs at least one requirement.");
17575
+ if (new Set(requirements.map((item2) => item2.id)).size !== requirements.length)
17576
+ throw new TypeError("Duplicate or colliding implementation requirement identity.");
17577
+ for (const requirement2 of requirements) {
17578
+ if (requirement2.sourceRefs.some((reference) => !sourceIds.has(reference)))
17579
+ throw new TypeError("Implementation requirement references missing evidence.");
17580
+ }
17581
+ const base = {
17582
+ kind: "openplanr-design-implementation-handoff",
17583
+ schemaVersion: "1.0.0",
17584
+ id: normalizeText(input.id, "Implementation handoff identity"),
17585
+ version: input.version ?? 1,
17586
+ status: "draft",
17587
+ authority: "prepare-plan",
17588
+ title: normalizeText(input.title, "Implementation handoff title"),
17589
+ basis: clone4(input.basis),
17590
+ sources,
17591
+ requirements
17592
+ };
17593
+ const markdown = renderImplementationHandoffMarkdown(base);
17594
+ const value = { ...base, contentDigest: "sha256:" + "0".repeat(64), markdown };
17595
+ value.contentDigest = designImplementationHandoffDigest(value);
17596
+ assertDesignImplementationHandoff(value);
17597
+ assertPackageSize(value);
17598
+ return value;
17599
+ }
17600
+ function assertImplementationHandoffProjection(value) {
17601
+ assertDesignImplementationHandoff(value);
17602
+ 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"))
17603
+ throw new TypeError("Implementation handoff sources are not in canonical order.");
17604
+ 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"))
17605
+ throw new TypeError("Implementation handoff requirements are not in canonical order.");
17606
+ for (const requirement2 of value.requirements)
17607
+ if (requirement2.id !== deriveImplementationRequirementId(requirement2))
17608
+ throw new TypeError("Implementation requirement identity does not match its canonical content.");
17609
+ if (value.markdown !== renderImplementationHandoffMarkdown(value))
17610
+ throw new TypeError("Implementation handoff Markdown differs from its JSON projection.");
17611
+ assertPackageSize(value);
17612
+ return value;
17613
+ }
17614
+ var resolvedBytes = (resolved) => {
17615
+ const value = resolved?.bytes ?? resolved?.value ?? resolved;
17616
+ if (typeof value === "string" || Buffer.isBuffer(value)) return Buffer.from(value);
17617
+ if (value instanceof Uint8Array) return Buffer.from(value);
17618
+ throw new TypeError("The implementation source resolver must return bytes.");
17619
+ };
17620
+ function verifyImplementationHandoffSources(value, resolveSource) {
17621
+ assertImplementationHandoffProjection(value);
17622
+ if (typeof resolveSource !== "function")
17623
+ throw new TypeError("Source verification requires an explicit resolver.");
17624
+ for (const source of value.sources) {
17625
+ const resolved = resolveSource(source.path, clone4(source));
17626
+ const bytes = resolvedBytes(resolved);
17627
+ if (bytes.byteLength > MAX_SOURCE_BYTES)
17628
+ throw new TypeError(`Implementation source ${source.id} exceeds 16 MB.`);
17629
+ if (sha256(bytes) !== source.digest)
17630
+ throw new TypeError(`Implementation source ${source.id} no longer matches its reference.`);
17631
+ if (source.anchor) {
17632
+ if (!Array.isArray(resolved?.anchors))
17633
+ throw new TypeError(`Implementation source ${source.id} did not resolve its anchor.`);
17634
+ const expected = canonicalizeJson(source.anchor);
17635
+ if (resolved.anchors.filter((anchor2) => canonicalizeJson(anchor2) === expected).length !== 1)
17636
+ throw new TypeError(`Implementation source ${source.id} has an unresolved or ambiguous anchor.`);
17637
+ }
17638
+ }
17639
+ return value;
17640
+ }
17641
+ var regexEscape = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
17642
+ function discoverAnchor(bytes, anchor2) {
17643
+ const text4 = bytes.toString("utf8");
17644
+ const values = anchor2.elementId ? [anchor2.elementId] : anchor2.pinId ? [anchor2.reviewId, anchor2.pinId] : anchor2.section ? [anchor2.section] : [anchor2.screenId];
17645
+ const counts = values.map((value) => (text4.match(new RegExp(regexEscape(value), "gu")) ?? []).length);
17646
+ if (counts.every((count) => count === 1)) return [anchor2];
17647
+ if (counts.some((count) => count === 0)) return [];
17648
+ return [anchor2, anchor2];
17649
+ }
17650
+ function createRepositorySourceResolver(root) {
17651
+ const canonicalRoot = realpathSync6(resolve6(root));
17652
+ return (path, source) => {
17653
+ if (!isDesignHandoffRelativePath(path))
17654
+ throw new TypeError("Implementation source path is not repository-relative.");
17655
+ const candidate = realpathSync6(resolve6(canonicalRoot, path));
17656
+ if (candidate !== canonicalRoot && !candidate.startsWith(`${canonicalRoot}${sep}`))
17657
+ throw new TypeError("Implementation source resolves outside the repository root.");
17658
+ const bytes = readFileSync14(candidate);
17659
+ return source?.anchor ? { bytes, anchors: discoverAnchor(bytes, source.anchor) } : bytes;
17660
+ };
17661
+ }
17662
+ function implementationHandoffPaths(root) {
17663
+ const directory = join10(resolve6(root), "implementation-handoff");
17664
+ return Object.freeze({
17665
+ directory,
17666
+ draftJson: join10(directory, "draft.json"),
17667
+ draftMarkdown: join10(directory, "draft.md"),
17668
+ journal: join10(directory, "draft-publication.json"),
17669
+ current: join10(directory, "current.json"),
17670
+ history: join10(directory, "versions")
17671
+ });
17672
+ }
17673
+ function recoverImplementationHandoffDraft(root) {
17674
+ const paths = implementationHandoffPaths(root);
17675
+ if (!existsSync9(paths.journal)) return false;
17676
+ const journal = JSON.parse(readFileSync14(paths.journal, "utf8"));
17677
+ const value = assertImplementationHandoffProjection(journal.package);
17678
+ if (journal.markdown !== value.markdown)
17679
+ throw new TypeError("Implementation handoff recovery journal is inconsistent.");
17680
+ atomicText(paths.draftJson, jsonBytes(value));
17681
+ atomicText(paths.draftMarkdown, value.markdown);
17682
+ rmSync6(paths.journal, { force: true });
17683
+ return true;
17684
+ }
17685
+ function readImplementationHandoffDraft(root, { allowMissing = true } = {}) {
17686
+ const paths = implementationHandoffPaths(root);
17687
+ recoverImplementationHandoffDraft(root);
17688
+ if (!existsSync9(paths.draftJson)) {
17689
+ if (allowMissing) return null;
17690
+ throw new Error("No implementation handoff draft exists.");
17691
+ }
17692
+ const value = assertImplementationHandoffProjection(
17693
+ JSON.parse(readFileSync14(paths.draftJson, "utf8"))
17694
+ );
17695
+ if (!existsSync9(paths.draftMarkdown))
17696
+ throw new Error("Implementation handoff Markdown is missing.");
17697
+ if (readFileSync14(paths.draftMarkdown, "utf8") !== value.markdown)
17698
+ throw new Error("Implementation handoff JSON and Markdown projections differ.");
17699
+ return value;
17700
+ }
17701
+ function writeImplementationHandoffDraft(root, input, { resolveSource } = {}) {
17702
+ const value = input?.kind ? assertImplementationHandoffProjection(clone4(input)) : composeImplementationHandoff(input);
17703
+ if (value.status !== "draft")
17704
+ throw new TypeError("Only editable drafts can be written through the draft composer.");
17705
+ if (resolveSource) verifyImplementationHandoffSources(value, resolveSource);
17706
+ const paths = implementationHandoffPaths(root);
17707
+ atomicText(paths.journal, jsonBytes({ package: value, markdown: value.markdown }));
17708
+ recoverImplementationHandoffDraft(root);
17709
+ return value;
17710
+ }
17711
+ function exportImplementationHandoffPackage(value) {
17712
+ const checked2 = assertImplementationHandoffProjection(clone4(value));
17713
+ return Object.freeze({ json: jsonBytes(checked2), markdown: checked2.markdown });
17714
+ }
17715
+ function importImplementationHandoffPackage(input, { resolveSource } = {}) {
17716
+ if (!input || typeof input.json !== "string" || typeof input.markdown !== "string")
17717
+ throw new TypeError("Portable handoff import requires JSON and Markdown text.");
17718
+ if (Buffer.byteLength(input.json) > MAX_PACKAGE_BYTES || Buffer.byteLength(input.markdown) > MAX_PACKAGE_BYTES)
17719
+ throw new TypeError("Portable handoff import exceeds 2 MB.");
17720
+ const value = assertImplementationHandoffProjection(JSON.parse(input.json));
17721
+ if (input.markdown.replaceAll("\r\n", "\n").replaceAll("\r", "\n") !== value.markdown)
17722
+ throw new TypeError("Imported handoff Markdown does not match its JSON projection.");
17723
+ if (resolveSource) verifyImplementationHandoffSources(value, resolveSource);
17724
+ return value;
17725
+ }
17726
+
17727
+ // packages/design/lib/design/implementation-handoff-approval.mjs
17728
+ import { createHash as createHash7, randomUUID as randomUUID4 } from "node:crypto";
17729
+ import {
17730
+ existsSync as existsSync10,
17731
+ mkdirSync as mkdirSync8,
17732
+ readFileSync as readFileSync15,
17733
+ readdirSync as readdirSync4,
17734
+ renameSync as renameSync7,
17735
+ rmSync as rmSync7,
17736
+ writeFileSync as writeFileSync8
17737
+ } from "node:fs";
17738
+ import { dirname as dirname12, join as join11 } from "node:path";
17739
+ var DIGEST2 = /^sha256:[a-f0-9]{64}$/u;
17740
+ var ID = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/u;
17741
+ var APPROVE_CAPABILITY = "design:implementation-handoff:approve";
17742
+ var REVOKE_CAPABILITY = "design:implementation-handoff:revoke";
17743
+ var MAX_REASON_BYTES = 16 * 1024;
17744
+ var clone5 = (value) => JSON.parse(canonicalizeJson(value));
17745
+ var jsonBytes2 = (value) => `${JSON.stringify(value, null, 2)}
17746
+ `;
17747
+ var requestKey = (requestId) => createHash7("sha256").update(requestId).digest("hex");
17748
+ var packageKey = (value) => {
17749
+ const identity = createHash7("sha256").update(value.id).digest("hex").slice(0, 16);
17750
+ return `${identity}-v${value.version}-${value.contentDigest.slice(7, 23)}`;
17751
+ };
17752
+ function atomicText2(path, value) {
17753
+ mkdirSync8(dirname12(path), { recursive: true });
17754
+ const temporary = `${path}.${randomUUID4()}.tmp`;
17755
+ try {
17756
+ writeFileSync8(temporary, value, { flag: "wx", mode: 384 });
17757
+ renameSync7(temporary, path);
17758
+ } finally {
17759
+ rmSync7(temporary, { force: true });
17760
+ }
17761
+ }
17762
+ function immutableText(path, value) {
17763
+ mkdirSync8(dirname12(path), { recursive: true });
17764
+ try {
17765
+ writeFileSync8(path, value, { flag: "wx", mode: 384 });
17766
+ } catch (error) {
17767
+ if (error.code !== "EEXIST") throw error;
17768
+ if (readFileSync15(path, "utf8") !== value)
17769
+ throw lifecycleConflict("Immutable implementation handoff history conflicts with this operation.");
17770
+ }
17771
+ }
17772
+ function lifecycleConflict(message2) {
17773
+ return Object.assign(new Error(message2), { code: "E_IMPLEMENTATION_HANDOFF_CONFLICT", statusCode: 409 });
17774
+ }
17775
+ function lifecycleForbidden(message2) {
17776
+ return Object.assign(new Error(message2), { code: "E_IMPLEMENTATION_HANDOFF_FORBIDDEN", statusCode: 403 });
17777
+ }
17778
+ function normalizeId(value, label) {
17779
+ if (typeof value !== "string" || value.length > 160 || !ID.test(value))
17780
+ throw new TypeError(`${label} is invalid.`);
17781
+ return value;
17782
+ }
17783
+ function normalizeRequestId(value) {
17784
+ return normalizeId(value, "Implementation handoff request identity");
17785
+ }
17786
+ function timestamp2(clock) {
17787
+ const candidate = typeof clock === "function" ? clock() : /* @__PURE__ */ new Date();
17788
+ const value = candidate instanceof Date ? candidate : new Date(candidate);
17789
+ if (!Number.isFinite(value.getTime())) throw new TypeError("The approval clock returned an invalid timestamp.");
17790
+ return value.toISOString();
17791
+ }
17792
+ function authorizeActor(actor, capability, at) {
17793
+ if (!actor || typeof actor !== "object" || Array.isArray(actor))
17794
+ throw lifecycleForbidden("Implementation handoff approval requires an owner identity.");
17795
+ const actorId = normalizeId(actor.id, "Implementation handoff actor identity");
17796
+ if (!["owner", "maintainer"].includes(actor.role))
17797
+ throw lifecycleForbidden("Only an owner or maintainer can change implementation handoff approval.");
17798
+ if (!Array.isArray(actor.capabilities) || !actor.capabilities.includes(capability))
17799
+ throw lifecycleForbidden("The actor lacks the required implementation handoff capability.");
17800
+ if (actor.sessionExpiresAt === void 0) {
17801
+ if (actorId !== "local-owner")
17802
+ throw lifecycleForbidden("Hosted implementation handoff approval requires a bounded session.");
17803
+ } else {
17804
+ const expiry = new Date(actor.sessionExpiresAt);
17805
+ if (!Number.isFinite(expiry.getTime()) || expiry.getTime() <= new Date(at).getTime())
17806
+ throw lifecycleForbidden("The implementation handoff approval session has expired.");
17807
+ }
17808
+ return Object.freeze({ actorId, role: actor.role, capability });
17809
+ }
17810
+ function implementationHandoffApprovalPaths(root) {
17811
+ const base = implementationHandoffPaths(root);
17812
+ return Object.freeze({
17813
+ ...base,
17814
+ events: join11(base.directory, "events"),
17815
+ journal: join11(base.directory, "lifecycle-publication.json")
17816
+ });
17817
+ }
17818
+ function archivePaths(root, value) {
17819
+ const directory = join11(implementationHandoffPaths(root).history, packageKey(value));
17820
+ return { directory, json: join11(directory, "handoff.json"), markdown: join11(directory, "handoff.md") };
17821
+ }
17822
+ function eventPath(root, requestId) {
17823
+ return join11(implementationHandoffApprovalPaths(root).events, `${requestKey(requestId)}.json`);
17824
+ }
17825
+ function readJson3(path, fallback = void 0) {
17826
+ try {
17827
+ return JSON.parse(readFileSync15(path, "utf8"));
17828
+ } catch (error) {
17829
+ if (error.code === "ENOENT" && fallback !== void 0) return fallback;
17830
+ throw error;
17831
+ }
17832
+ }
17833
+ function readExistingRequest(root, signature2) {
17834
+ const existing = readJson3(eventPath(root, signature2.requestId), null);
17835
+ if (!existing) return null;
17836
+ if (canonicalizeJson(existing.signature) !== canonicalizeJson(signature2))
17837
+ throw lifecycleConflict("This implementation handoff request identity was already used for different input.");
17838
+ return existing;
17839
+ }
17840
+ function pointerFor(value, status, eventId, extra = {}) {
17841
+ return {
17842
+ kind: "openplanr-design-implementation-handoff-current",
17843
+ schemaVersion: "1.0.0",
17844
+ id: value.id,
17845
+ version: value.version,
17846
+ contentDigest: value.contentDigest,
17847
+ status,
17848
+ authority: "prepare-plan",
17849
+ eventId,
17850
+ ...extra
17851
+ };
17852
+ }
17853
+ function writeLifecycleJournal(root, journal) {
17854
+ const paths = implementationHandoffApprovalPaths(root);
17855
+ if (existsSync10(paths.journal)) recoverImplementationHandoffApproval(root);
17856
+ atomicText2(paths.journal, jsonBytes2(journal));
17857
+ return recoverImplementationHandoffApproval(root);
17858
+ }
17859
+ function recoverImplementationHandoffApproval(root) {
17860
+ const paths = implementationHandoffApprovalPaths(root);
17861
+ if (!existsSync10(paths.journal)) return false;
17862
+ const journal = readJson3(paths.journal);
17863
+ if (journal.kind !== "openplanr-design-implementation-handoff-lifecycle-publication" || journal.schemaVersion !== "1.0.0")
17864
+ throw new TypeError("The implementation handoff lifecycle journal is invalid.");
17865
+ if (journal.archive) {
17866
+ const value = assertImplementationHandoffProjection(journal.archive);
17867
+ if (value.status !== "approved") throw new TypeError("Only approved packages belong in immutable history.");
17868
+ const archive = archivePaths(root, value);
17869
+ immutableText(archive.json, jsonBytes2(value));
17870
+ immutableText(archive.markdown, value.markdown);
17871
+ }
17872
+ immutableText(eventPath(root, journal.event.requestId), jsonBytes2(journal.event));
17873
+ atomicText2(paths.current, jsonBytes2(journal.pointer));
17874
+ rmSync7(paths.journal, { force: true });
17875
+ return true;
17876
+ }
17877
+ function readImplementationHandoffVersion(root, identity) {
17878
+ recoverImplementationHandoffApproval(root);
17879
+ const matches = listImplementationHandoffHistory(root).filter((value) => value.id === identity.id && value.version === identity.version && (identity.contentDigest === void 0 || value.contentDigest === identity.contentDigest));
17880
+ if (matches.length !== 1)
17881
+ throw lifecycleConflict(matches.length ? "Implementation handoff version identity is ambiguous." : "Implementation handoff version was not found.");
17882
+ return matches[0];
17883
+ }
17884
+ function listImplementationHandoffHistory(root) {
17885
+ recoverImplementationHandoffApproval(root);
17886
+ const directory = implementationHandoffPaths(root).history;
17887
+ if (!existsSync10(directory)) return [];
17888
+ 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));
17889
+ }
17890
+ function readImplementationHandoffLifecycle(root) {
17891
+ recoverImplementationHandoffApproval(root);
17892
+ const paths = implementationHandoffApprovalPaths(root);
17893
+ const current = readJson3(paths.current, null);
17894
+ 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)) : [];
17895
+ return Object.freeze({ current, history: listImplementationHandoffHistory(root), events });
17896
+ }
17897
+ function previewImplementationHandoffApproval(root) {
17898
+ const draft = readImplementationHandoffDraft(root);
17899
+ const lifecycle = readImplementationHandoffLifecycle(root);
17900
+ if (!draft) return Object.freeze({ available: false, summary: null, approvalRequest: null });
17901
+ const alreadyCurrent = lifecycle.current?.status === "approved" && lifecycle.current.id === draft.id && lifecycle.current.version === draft.version && lifecycle.current.contentDigest === draft.contentDigest;
17902
+ return Object.freeze({
17903
+ available: draft.basis.readiness.status === "ready" && !alreadyCurrent,
17904
+ summary: {
17905
+ title: draft.title,
17906
+ packageVersion: draft.version,
17907
+ selectedVariant: draft.basis.selectedVariant,
17908
+ requirementCount: draft.requirements.length,
17909
+ unresolvedNonblockingItems: 0,
17910
+ effect: "Prepare Plan",
17911
+ description: "Approve this reviewed design context for a later, separate Plan invocation."
17912
+ },
17913
+ approvalRequest: {
17914
+ expectedVersion: draft.version,
17915
+ expectedContentDigest: draft.contentDigest
17916
+ }
17917
+ });
17918
+ }
17919
+ function assertExpectedDraft(draft, request2, currentBasis) {
17920
+ if (!Number.isInteger(request2.expectedVersion) || request2.expectedVersion < 1 || !DIGEST2.test(request2.expectedContentDigest ?? ""))
17921
+ throw new TypeError("Approval requires the expected draft version and content identity.");
17922
+ if (draft.version !== request2.expectedVersion || draft.contentDigest !== request2.expectedContentDigest)
17923
+ throw lifecycleConflict("The implementation package changed after it was loaded. Refresh before approving.");
17924
+ if (currentBasis && canonicalizeJson(draft.basis) !== canonicalizeJson(currentBasis))
17925
+ throw lifecycleConflict("The design basis changed after this implementation package was composed.");
17926
+ if (draft.basis.readiness.status !== "ready")
17927
+ throw lifecycleConflict("Only a ready implementation package can be approved.");
17928
+ }
17929
+ function assertUniqueVersion(root, value) {
17930
+ const existing = listImplementationHandoffHistory(root).find((item2) => item2.id === value.id && item2.version === value.version);
17931
+ if (existing && existing.contentDigest !== value.contentDigest)
17932
+ throw lifecycleConflict("This implementation handoff version already identifies different content.");
17933
+ return existing;
17934
+ }
17935
+ function approveImplementationHandoff(root, request2, options = {}) {
17936
+ const requestId = normalizeRequestId(request2?.requestId);
17937
+ const at = timestamp2(options.clock);
17938
+ const actor = authorizeActor(options.actor, APPROVE_CAPABILITY, at);
17939
+ const signature2 = {
17940
+ operation: "approve",
17941
+ requestId,
17942
+ expectedVersion: request2.expectedVersion,
17943
+ expectedContentDigest: request2.expectedContentDigest,
17944
+ actor
17945
+ };
17946
+ const repeated = readExistingRequest(root, signature2);
17947
+ if (repeated) {
17948
+ return {
17949
+ package: readImplementationHandoffVersion(root, repeated.package),
17950
+ current: readImplementationHandoffLifecycle(root).current,
17951
+ event: repeated,
17952
+ repeated: true
17953
+ };
17954
+ }
17955
+ const draft = readImplementationHandoffDraft(root, { allowMissing: false });
17956
+ assertExpectedDraft(draft, request2, options.currentBasis);
17957
+ if (options.resolveSource) verifyImplementationHandoffSources(draft, options.resolveSource);
17958
+ const approved = assertImplementationHandoffProjection({
17959
+ ...clone5(draft),
17960
+ status: "approved",
17961
+ approval: {
17962
+ actorId: actor.actorId,
17963
+ approvedAt: at,
17964
+ contentDigest: draft.contentDigest,
17965
+ authority: "prepare-plan"
17966
+ }
17967
+ });
17968
+ const existing = assertUniqueVersion(root, approved);
17969
+ if (existing) {
17970
+ const lifecycle = readImplementationHandoffLifecycle(root);
17971
+ if (lifecycle.current?.status === "approved" && lifecycle.current.id === approved.id && lifecycle.current.version === approved.version && lifecycle.current.contentDigest === approved.contentDigest)
17972
+ return { package: existing, current: lifecycle.current, event: lifecycle.events.find((item2) => item2.type === "approved" && item2.package.contentDigest === approved.contentDigest) ?? null, repeated: true };
17973
+ throw lifecycleConflict("This immutable implementation handoff version already exists outside the current approval.");
17974
+ }
17975
+ const eventId = `handoff-approved-${requestKey(requestId).slice(0, 24)}`;
17976
+ const event = {
17977
+ kind: "openplanr-design-implementation-handoff-event",
17978
+ schemaVersion: "1.0.0",
17979
+ eventId,
17980
+ type: "approved",
17981
+ requestId,
17982
+ signature: signature2,
17983
+ package: { id: approved.id, version: approved.version, contentDigest: approved.contentDigest },
17984
+ actor,
17985
+ at,
17986
+ authority: "prepare-plan"
17987
+ };
17988
+ const pointer = pointerFor(approved, "approved", eventId, { updatedAt: at });
17989
+ writeLifecycleJournal(root, {
17990
+ kind: "openplanr-design-implementation-handoff-lifecycle-publication",
17991
+ schemaVersion: "1.0.0",
17992
+ archive: approved,
17993
+ event,
17994
+ pointer
17995
+ });
17996
+ return { package: approved, current: pointer, event, repeated: false };
17997
+ }
17998
+ function supersedeImplementationHandoff(root, replacement, request2, options = {}) {
17999
+ const checked2 = assertImplementationHandoffProjection(clone5(replacement));
18000
+ if (checked2.status !== "draft") throw new TypeError("A superseding package must still be a draft.");
18001
+ const requestId = normalizeRequestId(request2?.requestId);
18002
+ const at = timestamp2(options.clock);
18003
+ const actor = authorizeActor(options.actor, APPROVE_CAPABILITY, at);
18004
+ const lifecycle = readImplementationHandoffLifecycle(root);
18005
+ const currentIdentity = lifecycle.current && {
18006
+ id: lifecycle.current.id,
18007
+ version: lifecycle.current.version,
18008
+ contentDigest: lifecycle.current.contentDigest
18009
+ };
18010
+ const signature2 = {
18011
+ operation: "supersede",
18012
+ requestId,
18013
+ current: currentIdentity,
18014
+ replacement: { id: checked2.id, version: checked2.version, contentDigest: checked2.contentDigest },
18015
+ actor
18016
+ };
18017
+ const repeated = readExistingRequest(root, signature2);
18018
+ if (repeated) return { current: readImplementationHandoffLifecycle(root).current, event: repeated, repeated: true };
18019
+ if (!lifecycle.current || lifecycle.current.status !== "approved") return null;
18020
+ if (lifecycle.current.id === checked2.id && lifecycle.current.version === checked2.version && lifecycle.current.contentDigest === checked2.contentDigest) return null;
18021
+ if (checked2.version <= lifecycle.current.version)
18022
+ throw lifecycleConflict("A regenerated implementation package must use a newer version.");
18023
+ const eventId = `handoff-superseded-${requestKey(requestId).slice(0, 24)}`;
18024
+ const event = {
18025
+ kind: "openplanr-design-implementation-handoff-event",
18026
+ schemaVersion: "1.0.0",
18027
+ eventId,
18028
+ type: "superseded",
18029
+ requestId,
18030
+ signature: signature2,
18031
+ package: signature2.current,
18032
+ supersededBy: signature2.replacement,
18033
+ actor,
18034
+ at,
18035
+ authority: "prepare-plan"
18036
+ };
18037
+ const prior = readImplementationHandoffVersion(root, signature2.current);
18038
+ const pointer = pointerFor(prior, "superseded", eventId, { supersededBy: signature2.replacement, updatedAt: at });
18039
+ writeLifecycleJournal(root, {
18040
+ kind: "openplanr-design-implementation-handoff-lifecycle-publication",
18041
+ schemaVersion: "1.0.0",
18042
+ event,
18043
+ pointer
18044
+ });
18045
+ return { current: pointer, event, repeated: false };
18046
+ }
18047
+ function regenerateImplementationHandoffDraft(root, input, request2, options = {}) {
18048
+ const requestId = normalizeRequestId(request2?.requestId);
18049
+ const existingEvent = readJson3(eventPath(root, requestId), null);
18050
+ if (existingEvent) {
18051
+ const at = timestamp2(options.clock);
18052
+ const actor = authorizeActor(options.actor, APPROVE_CAPABILITY, at);
18053
+ if (existingEvent.type !== "superseded" || existingEvent.requestId !== requestId || canonicalizeJson(existingEvent.actor) !== canonicalizeJson(actor))
18054
+ throw lifecycleConflict("This implementation handoff request identity was already used for a different operation.");
18055
+ const candidate = composeImplementationHandoff({ ...input, version: existingEvent.supersededBy.version });
18056
+ if (options.resolveSource) verifyImplementationHandoffSources(candidate, options.resolveSource);
18057
+ if (candidate.id !== existingEvent.supersededBy.id || candidate.contentDigest !== existingEvent.supersededBy.contentDigest)
18058
+ throw lifecycleConflict("This regeneration request identity was already used for different package content.");
18059
+ const draft2 = readImplementationHandoffDraft(root, { allowMissing: false });
18060
+ if (draft2.id !== candidate.id || draft2.version !== candidate.version || draft2.contentDigest !== candidate.contentDigest)
18061
+ throw lifecycleConflict("The regenerated draft no longer matches this completed request.");
18062
+ return {
18063
+ draft: draft2,
18064
+ supersession: { current: readImplementationHandoffLifecycle(root).current, event: existingEvent, repeated: true }
18065
+ };
18066
+ }
18067
+ const lifecycle = readImplementationHandoffLifecycle(root);
18068
+ const currentDraft = readImplementationHandoffDraft(root);
18069
+ const maximum = Math.max(0, currentDraft?.version ?? 0, ...lifecycle.history.map((item2) => item2.version));
18070
+ const draft = writeImplementationHandoffDraft(root, { ...input, version: maximum + 1 }, { resolveSource: options.resolveSource });
18071
+ const supersession = supersedeImplementationHandoff(root, draft, { requestId }, options);
18072
+ return { draft, supersession };
18073
+ }
18074
+ function revokeImplementationHandoff(root, request2, options = {}) {
18075
+ const requestId = normalizeRequestId(request2.requestId);
18076
+ const reason = typeof request2.reason === "string" ? request2.reason.trim() : "";
18077
+ if (!reason || Buffer.byteLength(reason) > MAX_REASON_BYTES)
18078
+ throw new TypeError("Revocation requires a concise reason.");
18079
+ const at = timestamp2(options.clock);
18080
+ const actor = authorizeActor(options.actor, REVOKE_CAPABILITY, at);
18081
+ const signature2 = {
18082
+ operation: "revoke",
18083
+ requestId,
18084
+ expectedVersion: request2.expectedVersion,
18085
+ expectedContentDigest: request2.expectedContentDigest,
18086
+ reason,
18087
+ actor
18088
+ };
18089
+ const repeated = readExistingRequest(root, signature2);
18090
+ if (repeated) return { current: readImplementationHandoffLifecycle(root).current, event: repeated, repeated: true };
18091
+ const lifecycle = readImplementationHandoffLifecycle(root);
18092
+ if (!lifecycle.current || lifecycle.current.status !== "approved")
18093
+ throw lifecycleConflict("There is no current approved implementation package to revoke.");
18094
+ if (request2?.expectedVersion !== lifecycle.current.version || request2?.expectedContentDigest !== lifecycle.current.contentDigest)
18095
+ throw lifecycleConflict("The current implementation package changed before revocation.");
18096
+ const approved = readImplementationHandoffVersion(root, lifecycle.current);
18097
+ const eventId = `handoff-revoked-${requestKey(requestId).slice(0, 24)}`;
18098
+ const event = {
18099
+ kind: "openplanr-design-implementation-handoff-event",
18100
+ schemaVersion: "1.0.0",
18101
+ eventId,
18102
+ type: "revoked",
18103
+ requestId,
18104
+ signature: signature2,
18105
+ package: { id: approved.id, version: approved.version, contentDigest: approved.contentDigest },
18106
+ actor,
18107
+ at,
18108
+ reason,
18109
+ authority: "prepare-plan"
18110
+ };
18111
+ const pointer = pointerFor(approved, "revoked", eventId, {
18112
+ revocation: { actorId: actor.actorId, revokedAt: at, reason },
18113
+ updatedAt: at
18114
+ });
18115
+ writeLifecycleJournal(root, {
18116
+ kind: "openplanr-design-implementation-handoff-lifecycle-publication",
18117
+ schemaVersion: "1.0.0",
18118
+ event,
18119
+ pointer
18120
+ });
18121
+ return { current: pointer, event, repeated: false };
18122
+ }
18123
+ function compareImplementationHandoffVersions(root, leftIdentity, rightIdentity) {
18124
+ const resolveValue = (identity) => identity === "draft" ? readImplementationHandoffDraft(root, { allowMissing: false }) : readImplementationHandoffVersion(root, identity);
18125
+ const left = resolveValue(leftIdentity);
18126
+ const right = resolveValue(rightIdentity);
18127
+ const sourceIds = (value) => new Set(value.sources.map((item2) => item2.id));
18128
+ const requirementIds = (value) => new Set(value.requirements.map((item2) => item2.id));
18129
+ const difference = (before, after) => ({
18130
+ added: [...after].filter((id4) => !before.has(id4)).sort(),
18131
+ removed: [...before].filter((id4) => !after.has(id4)).sort()
18132
+ });
18133
+ return Object.freeze({
18134
+ left: { id: left.id, version: left.version, contentDigest: left.contentDigest },
18135
+ right: { id: right.id, version: right.version, contentDigest: right.contentDigest },
18136
+ changed: left.contentDigest !== right.contentDigest,
18137
+ basisChanged: canonicalizeJson(left.basis) !== canonicalizeJson(right.basis),
18138
+ titleChanged: left.title !== right.title,
18139
+ sources: difference(sourceIds(left), sourceIds(right)),
18140
+ requirements: difference(requirementIds(left), requirementIds(right))
18141
+ });
18142
+ }
18143
+ var IMPLEMENTATION_HANDOFF_APPROVE_CAPABILITY = APPROVE_CAPABILITY;
18144
+ var IMPLEMENTATION_HANDOFF_REVOKE_CAPABILITY = REVOKE_CAPABILITY;
18145
+
18146
+ // packages/design/lib/design/design-plan-handoff.mjs
18147
+ var clone6 = (value) => JSON.parse(canonicalizeJson(value));
18148
+ function prepareDesignPlanHandoff(handoff, { subject } = {}) {
18149
+ assertDesignImplementationHandoff(handoff);
18150
+ if (handoff.status !== "approved") throw new TypeError("Continue to Plan requires an approved implementation handoff.");
18151
+ const target = String(subject ?? handoff.basis.designId).trim();
18152
+ if (!target || /[\r\n]/u.test(target)) throw new TypeError("Plan subject must be one non-empty line.");
18153
+ return Object.freeze({
18154
+ kind: "openplanr-design-plan-handoff",
18155
+ schemaVersion: "1.0.0",
18156
+ authority: "prepare-plan",
18157
+ handoff: clone6({ id: handoff.id, version: handoff.version, contentDigest: handoff.contentDigest }),
18158
+ subject: target,
18159
+ invocations: Object.freeze({
18160
+ claudeCode: `/planr:plan ${target}`,
18161
+ codex: `$planr:plan ${target}`,
18162
+ chatgpt: `$planr:plan ${target}`,
18163
+ cursor: `$planr:plan ${target}`,
18164
+ fallback: `$planr:plan ${target}`
18165
+ }),
18166
+ effects: Object.freeze({ planningFilesWritten: false, agentDispatched: false, shipStarted: false, gitChanged: false })
18167
+ });
16390
18168
  }
16391
18169
 
16392
18170
  // packages/design/lib/design/review.mjs
@@ -16405,7 +18183,7 @@ function readDesignFeedback(file, env = process.env) {
16405
18183
  const ledger = readArtifactReviewState(designReviewPath(file, env), {
16406
18184
  allowMissing: true
16407
18185
  });
16408
- const digest3 = digestArtifactEnvelope(current.envelope);
18186
+ const digest4 = digestArtifactEnvelope(current.envelope);
16409
18187
  const pins = (ledger?.reviews ?? []).flatMap(
16410
18188
  (entry) => entry.review.pins.map((pin) => {
16411
18189
  const target = current.entries.find(
@@ -16418,7 +18196,7 @@ function readDesignFeedback(file, env = process.env) {
16418
18196
  reviewId: entry.review.reviewId,
16419
18197
  reviewOf: entry.review.reviewOf,
16420
18198
  ...entry.review.reviewId.startsWith("shared-") ? { revisionId: entry.review.reviewId.slice(7) } : {},
16421
- stale: entry.stale || entry.review.reviewOf !== digest3 || !target || Boolean(anchorMissing),
18199
+ stale: entry.stale || entry.review.reviewOf !== digest4 || !target || Boolean(anchorMissing),
16422
18200
  ...target ? { screenId: target.screenId, variantId: target.variantId } : {}
16423
18201
  };
16424
18202
  })
@@ -16427,12 +18205,12 @@ function readDesignFeedback(file, env = process.env) {
16427
18205
  revision: current.revision,
16428
18206
  reviewPath: designReviewPath(file, env),
16429
18207
  pins,
16430
- state: readJson2(join10(current.root, ".design/studio-state.json"), {
18208
+ state: readJson2(join12(current.root, ".design/studio-state.json"), {
16431
18209
  state: {},
16432
18210
  stateVersion: 0
16433
18211
  }).state,
16434
18212
  ledger,
16435
- shared: readJson2(join10(current.root, ".design/shared-feedback.json"), null)
18213
+ shared: readJson2(join12(current.root, ".design/shared-feedback.json"), null)
16436
18214
  };
16437
18215
  }
16438
18216
  function exportDesignReview(file, { scope = "all", env = process.env } = {}) {
@@ -16542,26 +18320,99 @@ function validateState(value, current) {
16542
18320
  for (const viewport of Object.values(value.viewports))
16543
18321
  validateViewport(viewport);
16544
18322
  }
16545
- for (const [id3, rating] of Object.entries(value.ratings ?? {}))
16546
- if (!variants.has(id3) || !Number.isInteger(rating) || rating < 1 || rating > 5)
18323
+ for (const [id4, rating] of Object.entries(value.ratings ?? {}))
18324
+ if (!variants.has(id4) || !Number.isInteger(rating) || rating < 1 || rating > 5)
16547
18325
  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)
18326
+ for (const [id4, point] of Object.entries(value.positions ?? {}))
18327
+ 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
18328
  throw new Error("Invalid artboard arrangement.");
16551
18329
  return structuredClone(value);
16552
18330
  }
16553
18331
  function projectRoot(root) {
16554
18332
  let candidate = root;
16555
18333
  while (true) {
16556
- if (existsSync9(join10(candidate, ".planr")) || existsSync9(join10(candidate, ".git")))
18334
+ if (existsSync11(join12(candidate, ".planr")) || existsSync11(join12(candidate, ".git")))
16557
18335
  return candidate;
16558
- const parent = dirname11(candidate);
18336
+ const parent = dirname13(candidate);
16559
18337
  if (parent === candidate) return root;
16560
18338
  candidate = parent;
16561
18339
  }
16562
18340
  }
18341
+ function currentImplementationBasis(file, env) {
18342
+ const handoff = readDesignHandoff(file, { env });
18343
+ const readiness = readDesignHandoffReadiness(file, { env });
18344
+ if (!handoff.draft || handoff.draft.status !== "approved" || !handoff.current)
18345
+ throw Object.assign(
18346
+ new Error("Approve the current review handoff before composing the implementation package."),
18347
+ { statusCode: 409 }
18348
+ );
18349
+ if (readiness.readiness.status !== "ready")
18350
+ throw Object.assign(
18351
+ new Error("Resolve the remaining design readiness checks before composing the implementation package."),
18352
+ { statusCode: 409 }
18353
+ );
18354
+ return {
18355
+ designId: handoff.basis.designId,
18356
+ sourceRevision: `sha256:${handoff.basis.sourceRevision}`,
18357
+ selectedVariant: handoff.basis.selectedVariant,
18358
+ readiness: {
18359
+ status: readiness.readiness.status,
18360
+ digest: readiness.digest
18361
+ },
18362
+ reviewHandoff: {
18363
+ version: handoff.draft.version,
18364
+ contentDigest: `sha256:${handoff.draft.contentHash}`
18365
+ }
18366
+ };
18367
+ }
18368
+ function proposeImplementationPackage(file, env) {
18369
+ const current = currentDesign(file);
18370
+ const basis = currentImplementationBasis(file, env);
18371
+ const repository = projectRoot(current.root);
18372
+ const paths = [.../* @__PURE__ */ new Set([
18373
+ designSpecPath(current.root),
18374
+ ...(current.sourceFiles ?? []).map((path) => resolve7(current.root, path))
18375
+ ])].filter((path) => existsSync11(path));
18376
+ const sources = paths.map((path, index) => {
18377
+ const logicalPath2 = relative4(repository, path).replaceAll("\\", "/");
18378
+ const extension = logicalPath2.split(".").pop()?.toLowerCase();
18379
+ return {
18380
+ id: `SRC-${String(index + 1).padStart(3, "0")}`,
18381
+ kind: path === designSpecPath(current.root) ? "design-specification" : extension === "html" ? "screen" : ["css", "json"].includes(extension) ? "token" : "component",
18382
+ path: logicalPath2,
18383
+ revision: basis.sourceRevision,
18384
+ digest: `sha256:${hash(readFileSync16(path))}`
18385
+ };
18386
+ });
18387
+ const sourceByPath = new Map(sources.map((source) => [source.path, source.id]));
18388
+ const sourceId = (path) => sourceByPath.get(relative4(repository, resolve7(current.root, path)).replaceAll("\\", "/"));
18389
+ const selected = current.document.variants.find((variant) => variant.id === current.document.selectedVariant);
18390
+ const requirements = current.document.screens.map((screen) => {
18391
+ const authored = selected?.sources?.[screen.id] ?? screen.source;
18392
+ const refs = [authored?.html, ...authored?.styles ?? [], ...authored?.scripts ?? []].map(sourceId).filter(Boolean);
18393
+ return {
18394
+ kind: "behavior",
18395
+ statement: `${screen.title}: ${screen.description || "Implement the approved screen behavior and states."}`,
18396
+ sourceRefs: refs.length ? refs : [sources[0].id],
18397
+ verification: current.document.frames.map((frame) => `${screen.title} matches the approved ${frame.label} frame at ${frame.width} \xD7 ${frame.height}.`)
18398
+ };
18399
+ });
18400
+ const allRefs = sources.map((source) => source.id);
18401
+ requirements.push({
18402
+ kind: "accessibility",
18403
+ statement: "Preserve the approved interaction semantics, keyboard path, focus behavior and readable status communication.",
18404
+ sourceRefs: allRefs,
18405
+ verification: ["Keyboard-only use, visible focus, screen-reader labels and status announcements pass on every implemented screen."]
18406
+ });
18407
+ return {
18408
+ id: `${current.document.id}-implementation`,
18409
+ title: `${current.document.title} implementation package`,
18410
+ sources,
18411
+ requirements
18412
+ };
18413
+ }
16563
18414
  async function persistDesignTaste(current, state) {
16564
- const path = join10(
18415
+ const path = join12(
16565
18416
  projectRoot(current.root),
16566
18417
  ".planr/design-system/taste.json"
16567
18418
  );
@@ -16574,7 +18425,7 @@ async function persistDesignTaste(current, state) {
16574
18425
  );
16575
18426
  const ids = (values) => [
16576
18427
  ...new Set(
16577
- (Array.isArray(values) ? values : []).filter((id3) => validIds.has(id3))
18428
+ (Array.isArray(values) ? values : []).filter((id4) => validIds.has(id4))
16578
18429
  )
16579
18430
  ];
16580
18431
  const explicitSelected = ids(
@@ -16583,7 +18434,7 @@ async function persistDesignTaste(current, state) {
16583
18434
  const selected = explicitSelected.includes(state.selectedVariant) ? [state.selectedVariant] : explicitSelected;
16584
18435
  const rejected = ids(
16585
18436
  state.preferences?.rejected ?? previous.rejected
16586
- ).filter((id3) => !selected.includes(id3));
18437
+ ).filter((id4) => !selected.includes(id4));
16587
18438
  atomicJson(path, {
16588
18439
  ...taste,
16589
18440
  designs: {
@@ -16610,7 +18461,7 @@ async function saveDesignState(file, { state, revision, stateVersion }) {
16610
18461
  new Error("The design changed. Reload before saving feedback."),
16611
18462
  { statusCode: 409 }
16612
18463
  );
16613
- const path = join10(current.root, ".design/studio-state.json");
18464
+ const path = join12(current.root, ".design/studio-state.json");
16614
18465
  const release = await acquireStartLock(`${path}.lock`);
16615
18466
  try {
16616
18467
  current = currentDesign(file);
@@ -16650,9 +18501,12 @@ function respond(res, status, value) {
16650
18501
  var readBody = async (req) => JSON.parse(
16651
18502
  await readRequestBody(req, { maxBytes: 128 * 1024, encoding: "utf8" })
16652
18503
  );
18504
+ var readImplementationBody = async (req) => JSON.parse(
18505
+ await readRequestBody(req, { maxBytes: 5 * 1024 * 1024, encoding: "utf8" })
18506
+ );
16653
18507
  async function startDesignReview(file, options = {}) {
16654
18508
  const { root } = currentDesign(file);
16655
- const release = await acquireStartLock(join10(root, ".design/start.lock"));
18509
+ const release = await acquireStartLock(join12(root, ".design/start.lock"));
16656
18510
  try {
16657
18511
  return await startDesignReviewUnlocked(file, options);
16658
18512
  } finally {
@@ -16665,11 +18519,12 @@ async function startDesignReviewUnlocked(file, {
16665
18519
  noOpen = true,
16666
18520
  view,
16667
18521
  openUrl,
16668
- fetchImpl = fetch
18522
+ fetchImpl = fetch,
18523
+ clock = () => /* @__PURE__ */ new Date()
16669
18524
  } = {}) {
16670
18525
  let current = currentDesign(file);
16671
18526
  if (view !== void 0) {
16672
- const saved = readJson2(join10(current.root, ".design/studio-state.json"), {
18527
+ const saved = readJson2(join12(current.root, ".design/studio-state.json"), {
16673
18528
  state: {},
16674
18529
  stateVersion: 0
16675
18530
  });
@@ -16679,7 +18534,7 @@ async function startDesignReviewUnlocked(file, {
16679
18534
  state: { ...saved.state, view }
16680
18535
  });
16681
18536
  }
16682
- const stateFile = join10(current.root, ".design/server.json");
18537
+ const stateFile = join12(current.root, ".design/server.json");
16683
18538
  const old = readJson2(stateFile, null);
16684
18539
  if (old?.version === VERSION && old.url && /^http:\/\/127\.0\.0\.1:\d+\/r\//u.test(old.url)) {
16685
18540
  try {
@@ -16710,15 +18565,15 @@ async function startDesignReviewUnlocked(file, {
16710
18565
  current = currentDesign(file);
16711
18566
  if (session.designRevision === current.revision) return;
16712
18567
  await session.writeQueue;
16713
- const digest3 = digestArtifactEnvelope(current.envelope);
18568
+ const digest4 = digestArtifactEnvelope(current.envelope);
16714
18569
  await withArtifactReviewLock(session.reviewPath, () => {
16715
18570
  const ledger = readArtifactReviewState(session.reviewPath, { allowMissing: true }) ?? session.reviewState;
16716
18571
  session.reviewState = createReviewLedger({
16717
18572
  artifactId: ledger.artifactId,
16718
- currentReviewOf: digest3,
18573
+ currentReviewOf: digest4,
16719
18574
  reviews: ledger.reviews.map((entry) => ({
16720
18575
  review: entry.review,
16721
- stale: entry.stale || entry.review.reviewOf !== digest3
18576
+ stale: entry.stale || entry.review.reviewOf !== digest4
16722
18577
  }))
16723
18578
  });
16724
18579
  writeArtifactReviewState(session.reviewPath, session.reviewState);
@@ -16727,7 +18582,7 @@ async function startDesignReviewUnlocked(file, {
16727
18582
  session.designRevision = current.revision;
16728
18583
  },
16729
18584
  renderDocument({ model, base }) {
16730
- const state = readJson2(join10(current.root, ".design/studio-state.json"), {
18585
+ const state = readJson2(join12(current.root, ".design/studio-state.json"), {
16731
18586
  state: {}
16732
18587
  }).state;
16733
18588
  const stalePins = readDesignFeedback(file, env).pins.filter(
@@ -16736,7 +18591,7 @@ async function startDesignReviewUnlocked(file, {
16736
18591
  return renderDesignStudio(
16737
18592
  { ...current, envelope: model.envelope, state, stalePins },
16738
18593
  { 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>`);
18594
+ ).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
18595
  },
16741
18596
  renderRuntime({ options, base }) {
16742
18597
  const settings = {
@@ -16745,11 +18600,13 @@ async function startDesignReviewUnlocked(file, {
16745
18600
  readyUrl: `${base}api/design-ready`,
16746
18601
  shareUrl: `${base}api/design-share`,
16747
18602
  experienceUrl: `${base}api/design-experience`,
18603
+ readinessUrl: `${base}api/design-handoff-readiness`,
16748
18604
  handoffUrl: `${base}api/design-handoff`,
18605
+ implementationHandoffUrl: `${base}api/design-implementation-handoff`,
16749
18606
  revisionsUrl: `${base}api/design-revisions`,
16750
18607
  reviewExportUrl: `${base}api/design-feedback-export`
16751
18608
  };
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()}};
18609
+ 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
18610
  ${renderArtifactParentRuntime({ ...options, adapterRuntimeUrl: `${base}api/design-share-runtime` })}`;
16754
18611
  },
16755
18612
  async handleSessionRequest({ req, res, segments }) {
@@ -16757,17 +18614,104 @@ ${renderArtifactParentRuntime({ ...options, adapterRuntimeUrl: `${base}api/desig
16757
18614
  return false;
16758
18615
  try {
16759
18616
  const route = segments[4];
18617
+ const localImplementationActor = {
18618
+ id: "local-owner",
18619
+ role: "owner",
18620
+ capabilities: [
18621
+ IMPLEMENTATION_HANDOFF_APPROVE_CAPABILITY,
18622
+ IMPLEMENTATION_HANDOFF_REVOKE_CAPABILITY
18623
+ ]
18624
+ };
16760
18625
  if (route === "design-experience" && req.method === "GET") {
16761
18626
  respond(res, 200, readDesignExperience(file, { env }));
18627
+ } else if (route === "design-handoff-readiness" && req.method === "GET") {
18628
+ respond(res, 200, readDesignHandoffReadiness(file, { env }));
16762
18629
  } else if (route === "design-handoff" && req.method === "GET") {
16763
18630
  respond(res, 200, readDesignHandoff(file, { env }));
18631
+ } else if (route === "design-implementation-handoff" && req.method === "GET") {
18632
+ const design = currentDesign(file);
18633
+ const unlock = await acquireStartLock(join12(design.root, ".design/render.lock"));
18634
+ try {
18635
+ const root = dirname13(designSpecPath(design.root));
18636
+ let proposal = null;
18637
+ try {
18638
+ proposal = proposeImplementationPackage(file, env);
18639
+ } catch {
18640
+ }
18641
+ respond(res, 200, { ok: true, draft: readImplementationHandoffDraft(root), ...readImplementationHandoffLifecycle(root), approvalPreview: previewImplementationHandoffApproval(root), proposal });
18642
+ } finally {
18643
+ unlock();
18644
+ }
16764
18645
  } else if (route === "design-revisions" && req.method === "GET") {
16765
18646
  respond(res, 200, listDesignRevisions(file));
16766
- } else if (["design-handoff", "design-revisions", "design-feedback-export"].includes(route) && req.method === "POST") {
18647
+ } else if (["design-handoff", "design-implementation-handoff", "design-revisions", "design-feedback-export"].includes(route) && req.method === "POST") {
16767
18648
  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);
18649
+ const input = route === "design-implementation-handoff" ? await readImplementationBody(req) : await readBody(req);
16769
18650
  if (route === "design-handoff") respond(res, 200, await updateDesignHandoff(file, input, { env, fetchImpl }));
16770
- else if (route === "design-feedback-export") {
18651
+ else if (route === "design-implementation-handoff") {
18652
+ 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.");
18653
+ const initial = currentDesign(file);
18654
+ const unlock = await acquireStartLock(join12(initial.root, ".design/render.lock"));
18655
+ try {
18656
+ const design = currentDesign(file);
18657
+ const root = dirname13(designSpecPath(design.root));
18658
+ const resolver = createRepositorySourceResolver(projectRoot(design.root));
18659
+ const approvalOptions = {
18660
+ actor: localImplementationActor,
18661
+ clock,
18662
+ resolveSource: resolver,
18663
+ ...["draft", "regenerate", "import", "approve"].includes(input.action) ? { currentBasis: currentImplementationBasis(file, env) } : {}
18664
+ };
18665
+ if (input.action === "draft") {
18666
+ if (!input.package || input.package.kind)
18667
+ throw new Error("Draft composition requires editable package fields, not a lifecycle record.");
18668
+ if (readImplementationHandoffLifecycle(root).history.length)
18669
+ throw Object.assign(new Error("Use regenerate to create a new version after approval."), { statusCode: 409 });
18670
+ const draft = writeImplementationHandoffDraft(root, {
18671
+ ...input.package,
18672
+ version: 1,
18673
+ basis: approvalOptions.currentBasis
18674
+ }, { resolveSource: resolver });
18675
+ respond(res, 200, { ok: true, draft });
18676
+ } else if (input.action === "regenerate") {
18677
+ if (!input.package || input.package.kind)
18678
+ throw new Error("Regeneration requires editable package fields, not a lifecycle record.");
18679
+ const value = regenerateImplementationHandoffDraft(root, {
18680
+ ...input.package,
18681
+ basis: approvalOptions.currentBasis
18682
+ }, { requestId: input.requestId }, approvalOptions);
18683
+ respond(res, 200, { ok: true, ...value });
18684
+ } else if (input.action === "import") {
18685
+ const draft = importImplementationHandoffPackage(input.package, { resolveSource: resolver });
18686
+ if (reviewDigest(draft.basis) !== reviewDigest(approvalOptions.currentBasis))
18687
+ throw Object.assign(new Error("The imported implementation package belongs to a different or earlier design basis."), { statusCode: 409 });
18688
+ const maximumVersion = Math.max(0, ...readImplementationHandoffLifecycle(root).history.map((item2) => item2.version));
18689
+ if (draft.version <= maximumVersion)
18690
+ throw Object.assign(new Error("Imported implementation packages cannot replace immutable version history."), { statusCode: 409 });
18691
+ writeImplementationHandoffDraft(root, draft, { resolveSource: resolver });
18692
+ respond(res, 200, { ok: true, draft });
18693
+ } else if (input.action === "approve") {
18694
+ const value = approveImplementationHandoff(root, input, approvalOptions);
18695
+ respond(res, 200, { ok: true, ...value });
18696
+ } else if (input.action === "revoke") {
18697
+ const value = revokeImplementationHandoff(root, input, approvalOptions);
18698
+ respond(res, 200, { ok: true, ...value });
18699
+ } else if (input.action === "compare") {
18700
+ respond(res, 200, { ok: true, comparison: compareImplementationHandoffVersions(root, input.left, input.right) });
18701
+ } else if (input.action === "continue-to-plan") {
18702
+ const lifecycle = readImplementationHandoffLifecycle(root);
18703
+ if (!lifecycle.current || lifecycle.current.status !== "approved")
18704
+ throw Object.assign(new Error("Continue to Plan requires a current approved implementation package."), { statusCode: 409 });
18705
+ const approved = readImplementationHandoffVersion(root, lifecycle.current);
18706
+ respond(res, 200, { ok: true, handoff: prepareDesignPlanHandoff(approved, { subject: input.subject }) });
18707
+ } else {
18708
+ const draft = readImplementationHandoffDraft(root, { allowMissing: false });
18709
+ respond(res, 200, { ok: true, package: exportImplementationHandoffPackage(draft) });
18710
+ }
18711
+ } finally {
18712
+ unlock();
18713
+ }
18714
+ } else if (route === "design-feedback-export") {
16771
18715
  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
18716
  await syncDesignShare(file, { env, fetchImpl });
16773
18717
  respond(res, 200, exportDesignReview(file, { scope: input.scope ?? "all", env }));
@@ -16778,25 +18722,25 @@ ${renderArtifactParentRuntime({ ...options, adapterRuntimeUrl: `${base}api/desig
16778
18722
  }
16779
18723
  } else if (route === "design-share-runtime" && req.method === "GET") {
16780
18724
  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"));
18725
+ res.end(readFileSync16(new URL("../../templates/studio/share.js", new URL("./runtime/packages/design/lib/design/review.mjs", import.meta.url).href), "utf8"));
16782
18726
  } else if (route === "design-share" && req.method === "GET") {
16783
18727
  respond(res, 200, getDesignShareStatus(file, { env }));
16784
18728
  } else if (route === "design-share" && req.method === "POST") {
16785
18729
  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
18730
  const origin = req.headers.origin;
16787
18731
  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);
18732
+ const { action: action3 } = await readBody(req);
16789
18733
  const options = { env, fetchImpl };
16790
18734
  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);
18735
+ if (action3 === "create") result = await shareDesign(file, options);
18736
+ else if (action3 === "publish") result = await publishDesignShare(file, options);
18737
+ else if (action3 === "sync") result = await syncDesignShare(file, options);
18738
+ else if (action3 === "recovery") result = await exportDesignShareRecovery(file, { ...options, output: join12(env.HOME ?? process.env.HOME, "Downloads", `openplanr-design-recovery-${Date.now()}.json`) });
18739
+ else result = await manageDesignShare(file, action3, options);
16796
18740
  respond(res, 200, result);
16797
18741
  } else if (route === "design-status" && req.method === "GET") {
16798
18742
  const ready = readJson2(
16799
- join10(current.root, ".design/browser-ready.json"),
18743
+ join12(current.root, ".design/browser-ready.json"),
16800
18744
  null
16801
18745
  );
16802
18746
  respond(res, 200, {
@@ -16808,7 +18752,7 @@ ${renderArtifactParentRuntime({ ...options, adapterRuntimeUrl: `${base}api/desig
16808
18752
  });
16809
18753
  } else if (route === "design-state" && req.method === "GET") {
16810
18754
  respond(res, 200, {
16811
- ...readJson2(join10(current.root, ".design/studio-state.json"), {
18755
+ ...readJson2(join12(current.root, ".design/studio-state.json"), {
16812
18756
  state: {},
16813
18757
  stateVersion: 0
16814
18758
  }),
@@ -16824,7 +18768,7 @@ ${renderArtifactParentRuntime({ ...options, adapterRuntimeUrl: `${base}api/desig
16824
18768
  throw new Error(
16825
18769
  "Browser readiness does not cover every expected design artboard."
16826
18770
  );
16827
- atomicJson(join10(current.root, ".design/browser-ready.json"), {
18771
+ atomicJson(join12(current.root, ".design/browser-ready.json"), {
16828
18772
  status: "ready",
16829
18773
  revision: current.revision,
16830
18774
  checkedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -16832,7 +18776,7 @@ ${renderArtifactParentRuntime({ ...options, adapterRuntimeUrl: `${base}api/desig
16832
18776
  respond(res, 200, { ok: true });
16833
18777
  } else if (route === "design-export" && req.method === "GET") {
16834
18778
  const state = readJson2(
16835
- join10(current.root, ".design/studio-state.json"),
18779
+ join12(current.root, ".design/studio-state.json"),
16836
18780
  { state: {} }
16837
18781
  ).state;
16838
18782
  res.writeHead(200, {
@@ -16962,10 +18906,10 @@ async function resolveDesignPins(file, { pinIds, summary, env = process.env }) {
16962
18906
  createReviewLedger({ ...ledger, reviews: revisions })
16963
18907
  );
16964
18908
  const history = readJson2(
16965
- join10(current.root, ".design/review-history.json"),
18909
+ join12(current.root, ".design/review-history.json"),
16966
18910
  []
16967
18911
  );
16968
- atomicJson(join10(current.root, ".design/review-history.json"), [
18912
+ atomicJson(join12(current.root, ".design/review-history.json"), [
16969
18913
  ...history,
16970
18914
  {
16971
18915
  revision: current.revision,
@@ -16981,7 +18925,7 @@ async function resolveDesignPins(file, { pinIds, summary, env = process.env }) {
16981
18925
  // packages/design/lib/design/browser-audit.mjs
16982
18926
  function auditRenderedScreen() {
16983
18927
  const issues = [];
16984
- const add = (rule, severity, message) => issues.push({ rule, severity, message });
18928
+ const add = (rule, severity, message2) => issues.push({ rule, severity, message: message2 });
16985
18929
  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
18930
  const viewport = { width: window.innerWidth, height: window.innerHeight };
16987
18931
  const screenId = document.body?.getAttribute("data-planr-screen") ?? null;
@@ -17049,7 +18993,7 @@ function auditRenderedScreen() {
17049
18993
  }
17050
18994
  if (node.tabIndex >= 0 && !node.disabled && node.matches('button,a[href],input:not([type="hidden"]),select,textarea,[tabindex],[contenteditable="true"]')) {
17051
18995
  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());
18996
+ 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
18997
  if (!name) add("unnamed-control", "error", `${label(node)} has no accessible name.`);
17054
18998
  }
17055
18999
  }
@@ -17088,7 +19032,7 @@ function auditRenderedScreen() {
17088
19032
  async function auditDesignPage(page, { revision, entries = [], screenshotPaths = [], scenarios = [] } = {}) {
17089
19033
  const issues = [], checkedArtifacts = [], screens = [];
17090
19034
  const expected = new Map(entries.map((entry) => [entry.artifactId, entry]));
17091
- const add = (artifactId, rule, severity, message) => issues.push({ artifactId, rule, severity, message });
19035
+ const add = (artifactId, rule, severity, message2) => issues.push({ artifactId, rule, severity, message: message2 });
17092
19036
  if (!entries.length || expected.size !== entries.length) add("studio", "invalid-audit-scope", "error", "Audit scope must contain every expected artifact exactly once.");
17093
19037
  if (!page || typeof page.frames !== "function") {
17094
19038
  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 +19066,7 @@ async function auditDesignPage(page, { revision, entries = [], screenshotPaths =
17122
19066
  }
17123
19067
  checkedArtifacts.push(artifactId);
17124
19068
  screens.push({ artifactId, ...result });
17125
- for (const issue of result.issues) add(artifactId, issue.rule, issue.severity, issue.message);
19069
+ for (const issue2 of result.issues) add(artifactId, issue2.rule, issue2.severity, issue2.message);
17126
19070
  } catch (error) {
17127
19071
  if (artifactId && expected.has(artifactId)) add(artifactId, "frame-load-error", "error", `The rendered frame could not be inspected: ${error.message}`);
17128
19072
  } finally {
@@ -17175,7 +19119,7 @@ function verifyDesignDocument(file, report) {
17175
19119
  const images = (Array.isArray(report.screenshots) ? report.screenshots : []).filter((item2) => {
17176
19120
  const path = typeof item2 === "string" ? item2 : item2?.path;
17177
19121
  try {
17178
- return typeof path === "string" && isPng(readFileSync15(resolve6(path)));
19122
+ return typeof path === "string" && isPng(readFileSync17(resolve8(path)));
17179
19123
  } catch {
17180
19124
  return false;
17181
19125
  }
@@ -17184,7 +19128,7 @@ function verifyDesignDocument(file, report) {
17184
19128
  const issues = [...Array.isArray(report.issues) ? report.issues : [], ...screenEvidence.flatMap((item2) => Array.isArray(item2?.issues) ? item2.issues : [])];
17185
19129
  const status = issues.some((item2) => item2?.severity === "error") || report.status === "failed" ? "failed" : coverage && images.length > 0 && journeys && report.status !== "unverified" ? "verified" : "unverified";
17186
19130
  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);
19131
+ atomicJson(join13(current.root, ".design/verification", `${current.revision}.json`), saved);
17188
19132
  return saved;
17189
19133
  }
17190
19134
  async function designUtility(argv, { stdout = (value) => process.stdout.write(`${JSON.stringify(value)}
@@ -17204,15 +19148,15 @@ async function designUtility(argv, { stdout = (value) => process.stdout.write(`$
17204
19148
  flags[key] = ["json", "no-open"].includes(key) ? true : args[++i];
17205
19149
  }
17206
19150
  if (flags.view && !["canvas", "prototype", "walkthrough"].includes(flags.view)) throw new Error("View must be canvas, prototype, or walkthrough.");
17207
- const file = resolve6(input);
19151
+ const file = resolve8(input);
17208
19152
  let result;
17209
19153
  if (command === "handoff") {
17210
- const action = flags.action ?? "inspect";
17211
- if (action === "inspect") result = readDesignHandoff(file);
19154
+ const action3 = flags.action ?? "inspect";
19155
+ if (action3 === "inspect") result = readDesignHandoff(file);
17212
19156
  else {
17213
19157
  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) });
19158
+ const input2 = flags.input ? readJson2(resolve8(flags.input)) : {};
19159
+ result = await updateDesignHandoff(file, { ...input2, action: action3, revision: input2.revision ?? snapshot2.revision, version: input2.version ?? (snapshot2.draft?.version ?? 0) });
17216
19160
  }
17217
19161
  }
17218
19162
  if (command === "share") result = await shareDesign(file);
@@ -17244,39 +19188,39 @@ async function designUtility(argv, { stdout = (value) => process.stdout.write(`$
17244
19188
  }
17245
19189
  if (command === "export") {
17246
19190
  const current = currentDesign(file), view = flags.view ?? current.document.defaultView;
17247
- const state = readJson2(join11(current.root, ".design/studio-state.json"), { state: {} }).state;
19191
+ const state = readJson2(join13(current.root, ".design/studio-state.json"), { state: {} }).state;
17248
19192
  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" });
19193
+ const output = resolve8(flags.output ?? join13(current.root, `${view}-export.html`));
19194
+ if (existsSync12(output)) throw new Error(`Export already exists: ${output}. Choose a new --output path.`);
19195
+ mkdirSync9(dirname14(output), { recursive: true });
19196
+ writeFileSync9(output, standaloneDesignHtml({ ...current, state }, view), { flag: "wx" });
17253
19197
  result = { ok: true, output, view, revision: current.revision };
17254
19198
  }
17255
19199
  if (command === "verify") {
17256
19200
  if (!flags.report) throw new Error("verify requires --report <browser-report.json>.");
17257
- result = verifyDesignDocument(file, readJson2(resolve6(flags.report)));
19201
+ result = verifyDesignDocument(file, readJson2(resolve8(flags.report)));
17258
19202
  }
17259
19203
  if (command === "feedback") {
17260
19204
  await syncDesignShare(file, { env, fetchImpl });
17261
- const action = flags.action ?? "inspect";
17262
- if (action === "inspect") result = readDesignFeedback(file, env);
17263
- else if (action === "export") {
19205
+ const action3 = flags.action ?? "inspect";
19206
+ if (action3 === "inspect") result = readDesignFeedback(file, env);
19207
+ else if (action3 === "export") {
17264
19208
  const format = flags.format ?? "json", scope = flags.scope ?? "all";
17265
19209
  if (!["json", "markdown"].includes(format)) throw new Error("Feedback export format must be json or markdown.");
17266
19210
  if (!["current", "all"].includes(scope)) throw new Error("Feedback export scope must be current or all.");
17267
19211
  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.");
19212
+ const output = resolve8(flags.output);
19213
+ if (existsSync12(output)) throw new Error("Feedback export already exists. Choose a new --output path.");
17270
19214
  const snapshot2 = exportDesignReview(file, { scope, env });
17271
- mkdirSync7(dirname12(output), { recursive: true });
17272
- writeFileSync7(output, serializeDesignReviewExport(snapshot2, format), { flag: "wx", mode: 384 });
19215
+ mkdirSync9(dirname14(output), { recursive: true });
19216
+ writeFileSync9(output, serializeDesignReviewExport(snapshot2, format), { flag: "wx", mode: 384 });
17273
19217
  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 });
19218
+ } else if (action3 === "resolve") result = await resolveDesignPins(file, { pinIds: flags.pins?.split(",").filter(Boolean), summary: flags.summary, env });
19219
+ else if (action3 === "select") {
19220
+ const current = currentDesign(file), saved = readJson2(join13(current.root, ".design/studio-state.json"), { state: {}, stateVersion: 0 });
17277
19221
  const variant = flags.variant ?? saved.state.selectedVariant;
17278
19222
  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);
19223
+ const selected = [variant], rejected = (saved.state.preferences?.rejected ?? []).filter((id4) => id4 !== variant);
17280
19224
  result = await saveDesignState(file, { ...saved, revision: current.revision, state: { ...saved.state, selectedVariant: variant, preferences: { selected, rejected } } });
17281
19225
  result = { ok: true, selectedVariant: variant, tastePath: result.tastePath, revision: current.revision };
17282
19226
  } else throw new Error("Feedback action must be inspect, export, select, or resolve.");
@@ -17284,7 +19228,7 @@ async function designUtility(argv, { stdout = (value) => process.stdout.write(`$
17284
19228
  stdout(result);
17285
19229
  return result;
17286
19230
  }
17287
- var main = process.argv[1] && fileURLToPath3(import.meta.url) === realpathSync6(process.argv[1]);
19231
+ var main = process.argv[1] && fileURLToPath3(import.meta.url) === realpathSync7(process.argv[1]);
17288
19232
  if (main) designUtility(process.argv.slice(2), { openUrl: async (url) => {
17289
19233
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
17290
19234
  const child = spawn(command, [url], { stdio: "ignore" });