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.
- package/CHANGELOG.md +19 -0
- package/dist/dashboard/.vite/manifest.json +1 -1
- package/dist/dashboard/assets/{index-B74ddyXA.js → index-BaARCc1f.js} +2 -2
- package/dist/dashboard/assets/{index-B74ddyXA.js.map → index-BaARCc1f.js.map} +1 -1
- package/dist/dashboard/dashboard-manifest.json +8 -8
- package/dist/dashboard/index.html +1 -1
- package/lib/host-packages/adapter-registry.json +5 -5
- package/lib/host-packages/capability-map.json +1 -1
- package/lib/host-packages/claude/.claude-plugin/marketplace.json +2 -2
- package/lib/host-packages/claude/openplanr/.claude-plugin/plugin.json +1 -1
- package/lib/host-packages/claude/openplanr/.openplanr-content.json +114 -26
- package/lib/host-packages/claude/openplanr/skills/design/SKILL.md +3 -0
- package/lib/host-packages/claude/openplanr/skills/design/openplanr.skill.json +56 -1
- package/lib/host-packages/claude/openplanr/skills/design/references/handoff.md +35 -0
- package/lib/host-packages/claude/openplanr/skills/design/scripts/design.mjs +2285 -341
- package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
- package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
- package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
- package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
- package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
- package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
- package/lib/host-packages/claude/openplanr/skills/design-loop/openplanr.skill.json +55 -0
- package/lib/host-packages/claude/openplanr/skills/design-loop/references/handoff.md +35 -0
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design.mjs +2285 -341
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
- package/lib/host-packages/claude/openplanr/skills/design-review/SKILL.md +3 -0
- package/lib/host-packages/claude/openplanr/skills/design-review/openplanr.skill.json +56 -1
- package/lib/host-packages/claude/openplanr/skills/design-review/references/handoff.md +35 -0
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design.mjs +2285 -341
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
- package/lib/host-packages/claude/openplanr/skills/plan/SKILL.md +12 -0
- package/lib/host-packages/claude/openplanr/skills/plan/openplanr.skill.json +67 -1
- package/lib/host-packages/claude/openplanr/skills/plan/references/design-handoff.md +33 -0
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/design.mjs +2285 -341
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
- package/lib/host-packages/claude/openplanr/skills/ship/SKILL.md +9 -0
- package/lib/host-packages/claude/openplanr/skills/ship/openplanr.skill.json +12 -1
- package/lib/host-packages/claude/openplanr/skills/ship/references/design-lineage.md +26 -0
- package/lib/host-packages/cursor/openplanr/.openplanr-content.json +109 -21
- package/lib/host-packages/cursor/openplanr/manifest.json +1 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-design/references/handoff.md +35 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design.mjs +2285 -341
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/references/handoff.md +35 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design.mjs +2285 -341
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/protocol/package.json +5 -1
- 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
- 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
- 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
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/references/handoff.md +35 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design.mjs +2285 -341
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/protocol/package.json +5 -1
- 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
- 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
- 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
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review.mdc +3 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design.mdc +3 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/references/design-handoff.md +33 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design.mjs +2285 -341
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan.mdc +12 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-ship/references/design-lineage.md +26 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-ship.mdc +9 -0
- package/lib/host-packages/openai/.claude-plugin/marketplace.json +2 -2
- package/lib/host-packages/openai/openplanr/.codex-plugin/plugin.json +1 -1
- package/lib/host-packages/openai/openplanr/.openplanr-content.json +114 -26
- package/lib/host-packages/openai/openplanr/skills/design/SKILL.md +3 -0
- package/lib/host-packages/openai/openplanr/skills/design/openplanr.skill.json +56 -1
- package/lib/host-packages/openai/openplanr/skills/design/references/handoff.md +35 -0
- package/lib/host-packages/openai/openplanr/skills/design/scripts/design.mjs +2285 -341
- package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
- package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
- package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
- package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
- package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
- package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
- package/lib/host-packages/openai/openplanr/skills/design-loop/openplanr.skill.json +55 -0
- package/lib/host-packages/openai/openplanr/skills/design-loop/references/handoff.md +35 -0
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design.mjs +2285 -341
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
- package/lib/host-packages/openai/openplanr/skills/design-review/SKILL.md +3 -0
- package/lib/host-packages/openai/openplanr/skills/design-review/openplanr.skill.json +56 -1
- package/lib/host-packages/openai/openplanr/skills/design-review/references/handoff.md +35 -0
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design.mjs +2285 -341
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
- package/lib/host-packages/openai/openplanr/skills/plan/SKILL.md +12 -0
- package/lib/host-packages/openai/openplanr/skills/plan/openplanr.skill.json +67 -1
- package/lib/host-packages/openai/openplanr/skills/plan/references/design-handoff.md +33 -0
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/design.mjs +2285 -341
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +398 -0
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +518 -0
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +106 -0
- package/lib/host-packages/openai/openplanr/skills/ship/SKILL.md +9 -0
- package/lib/host-packages/openai/openplanr/skills/ship/openplanr.skill.json +12 -1
- package/lib/host-packages/openai/openplanr/skills/ship/references/design-lineage.md +26 -0
- 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
|
|
6
|
-
import { dirname as
|
|
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
|
|
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 (
|
|
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
|
|
205
|
-
return typeof
|
|
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:
|
|
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,
|
|
223
|
+
function normalizeArtifactBridgeToolResult(requestType, message2, viewport) {
|
|
224
224
|
const base = ["channel", "schemaVersion", "type", "nonce", "artifactId", "requestId"];
|
|
225
|
-
const exact = (keys) =>
|
|
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) &&
|
|
228
|
-
if (!exact([...base, "inspection"]) ||
|
|
229
|
-
const value = normalizeArtifactInspection(
|
|
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"]) &&
|
|
234
|
-
if (!exact([...base, "dataUrl", "width", "height", "label"]) ||
|
|
235
|
-
const value = normalizeArtifactThumbnail({ dataUrl:
|
|
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 = (
|
|
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(
|
|
299
|
-
if (!
|
|
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") ===
|
|
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
|
|
350
|
-
setAttribute.call(
|
|
351
|
-
const markup = serialize3.call(new Serializer(),
|
|
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((
|
|
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
|
-
|
|
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:
|
|
591
|
-
endLine:
|
|
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,
|
|
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 +=
|
|
4825
|
+
prevNode.value += text4;
|
|
4826
4826
|
return;
|
|
4827
4827
|
}
|
|
4828
4828
|
}
|
|
4829
|
-
defaultTreeAdapter.appendChild(parentNode, defaultTreeAdapter.createTextNode(
|
|
4829
|
+
defaultTreeAdapter.appendChild(parentNode, defaultTreeAdapter.createTextNode(text4));
|
|
4830
4830
|
},
|
|
4831
|
-
insertTextBefore(parentNode,
|
|
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 +=
|
|
4834
|
+
prevNode.value += text4;
|
|
4835
4835
|
} else {
|
|
4836
|
-
defaultTreeAdapter.insertBefore(parentNode, defaultTreeAdapter.createTextNode(
|
|
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,
|
|
5803
|
+
_isSpecialElement(element2, id4) {
|
|
5804
5804
|
const ns = this.treeAdapter.getNamespaceURI(element2);
|
|
5805
|
-
return SPECIAL_ELEMENTS[ns].has(
|
|
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,
|
|
8538
|
-
super(
|
|
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,
|
|
8603
|
-
return new PipelineError(code,
|
|
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
|
|
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=${
|
|
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(
|
|
9590
|
-
throw new PipelineError(ARTIFACT_ERROR_CODES.ENVELOPE_INVALID,
|
|
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
|
|
9646
|
-
const
|
|
9647
|
-
if (typeof
|
|
9648
|
-
throw new PipelineError(ARTIFACT_ERROR_CODES.ENVELOPE_INVALID, `Invalid artifact id: ${String(
|
|
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
|
|
9651
|
-
throw new PipelineError(ARTIFACT_ERROR_CODES.ENVELOPE_INVALID, `Artifact ${
|
|
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 ${
|
|
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 ${
|
|
9658
|
+
invalid(`Artifact ${id4} exceeds ${MAX_ARTIFACT_HTML_BYTES} UTF-8 bytes.`);
|
|
9659
9659
|
}
|
|
9660
|
-
const
|
|
9661
|
-
if (artifact.sha256 !== void 0 && artifact.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 ${
|
|
9664
|
+
`Artifact ${id4} digest does not match its canonical bundled HTML.`,
|
|
9665
9665
|
"",
|
|
9666
|
-
{ expected:
|
|
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 ${
|
|
9671
|
+
throw new PipelineError(ARTIFACT_ERROR_CODES.ENVELOPE_INVALID, `Artifact ${id4} has an invalid color scheme.`);
|
|
9672
9672
|
}
|
|
9673
9673
|
return {
|
|
9674
|
-
id:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
9863
|
-
const error = new Error(
|
|
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
|
|
10129
|
-
const file =
|
|
10128
|
+
const checked2 = readSource?.(path, from);
|
|
10129
|
+
const file = checked2?.file ?? resolveLocalDocumentFile(root, path, from);
|
|
10130
10130
|
if (!files.has(file)) {
|
|
10131
|
-
const value =
|
|
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:
|
|
10468
|
+
const screenIds = new Set(value.screens.map(({ id: id4 }) => id4));
|
|
10469
10469
|
const orderedIds = new Set(value.screenOrder);
|
|
10470
|
-
for (const
|
|
10471
|
-
if (!screenIds.has(
|
|
10470
|
+
for (const id4 of value.screenOrder) {
|
|
10471
|
+
if (!screenIds.has(id4)) errors.push(`$.screenOrder: unknown screen '${id4}'`);
|
|
10472
10472
|
}
|
|
10473
|
-
for (const
|
|
10474
|
-
if (!orderedIds.has(
|
|
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
|
|
10478
|
-
if (!screenIds.has(
|
|
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
|
|
10483
|
-
if (!screenIds.has(
|
|
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:
|
|
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,
|
|
10792
|
-
const errors = validateJson(value,
|
|
10793
|
-
if (errors.length) throw new TypeError(`Invalid ${
|
|
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,
|
|
10885
|
-
const errors = validateJson(value,
|
|
10886
|
-
if (errors.length) throw new TypeError(`Invalid ${
|
|
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((
|
|
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,
|
|
11456
|
-
super(
|
|
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(
|
|
11522
|
-
if (
|
|
11523
|
-
if (
|
|
11524
|
-
if (
|
|
11525
|
-
if (
|
|
11526
|
-
if (
|
|
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
|
|
11600
|
-
assertSchema(theme,
|
|
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
|
|
11775
|
+
const id4 = plainText(artifact.id, `artifact-${index + 1}`) || `artifact-${index + 1}`;
|
|
11776
11776
|
return Object.freeze({
|
|
11777
|
-
id:
|
|
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
|
|
11942
|
-
return `<aside class="planr-review-rail" id="planr-review-rail" aria-label="Review comments"${
|
|
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:
|
|
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
|
|
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(
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
13211
|
-
({ id:
|
|
13212
|
-
) || !document2.frames.some(({ id:
|
|
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
|
|
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
|
|
13302
|
-
const
|
|
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
|
|
13311
|
-
const order = (a, b) =>
|
|
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
|
|
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:
|
|
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
|
|
13337
|
-
return
|
|
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 =
|
|
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
|
|
13346
|
-
return
|
|
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, ...
|
|
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 =
|
|
13365
|
-
const screen =
|
|
13366
|
-
const direction =
|
|
13367
|
-
const frame =
|
|
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 =
|
|
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 =
|
|
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:
|
|
13395
|
-
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:
|
|
13399
|
-
replies: [...replies].sort(order).map((reply) => ({ id: field(reply.id), author: identity(reply.author), createdAt:
|
|
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]) =>
|
|
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 }] :
|
|
13418
|
-
const overallNotes = reviews.filter((entry) => entry.review?.overall).map(({ review }) => ({ reviewId: field(review.reviewId), reviewOf:
|
|
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
|
-
...
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
13641
|
-
const path =
|
|
13642
|
-
const document2 = assertDesignDocument(
|
|
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
|
|
13735
|
-
if (!bundled.html.includes(`data-planr-id="${
|
|
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 ${
|
|
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
|
-
(
|
|
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
|
|
13997
|
-
import { dirname as
|
|
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(
|
|
14024
|
+
function conflict(message2, details = void 0) {
|
|
14023
14025
|
throw new PipelineError(
|
|
14024
14026
|
ARTIFACT_ERROR_CODES.MERGE_CONFLICT,
|
|
14025
|
-
|
|
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,
|
|
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
|
|
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,
|
|
14484
|
-
throw new PipelineError(code,
|
|
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,
|
|
14683
|
-
return new PipelineError(code,
|
|
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
|
|
14774
|
+
const title2 = value.title ?? envelope.artifacts[0]?.title ?? "Artifact review";
|
|
14773
14775
|
const theme = value.theme ?? "auto";
|
|
14774
|
-
if (typeof
|
|
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:
|
|
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
|
|
14982
|
-
const
|
|
14983
|
+
const id4 = mintCapabilityToken({ bytes: SESSION_ID_BYTES });
|
|
14984
|
+
const reviewState2 = await initializeSessionReview(registration, env);
|
|
14983
14985
|
const session2 = {
|
|
14984
14986
|
...registration,
|
|
14985
|
-
id:
|
|
14987
|
+
id: id4,
|
|
14986
14988
|
capability: mintCapabilityToken({ bytes: SESSION_TOKEN_BYTES }),
|
|
14987
14989
|
bridgeNonce: createArtifactBridgeNonce(),
|
|
14988
14990
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14989
|
-
reviewState:
|
|
14990
|
-
reviewPath:
|
|
14991
|
+
reviewState: reviewState2.ledger,
|
|
14992
|
+
reviewPath: reviewState2.path,
|
|
14991
14993
|
writeQueue: Promise.resolve()
|
|
14992
14994
|
};
|
|
14993
|
-
sessions.set(
|
|
14995
|
+
sessions.set(id4, session2);
|
|
14994
14996
|
sendJson(res, 201, {
|
|
14995
14997
|
ok: true,
|
|
14996
|
-
sessionId:
|
|
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
|
|
15086
|
+
const reviewState2 = await queueSessionReviewWrite(session, review);
|
|
15085
15087
|
sendJson(res, 200, {
|
|
15086
15088
|
ok: true,
|
|
15087
15089
|
reviewId: review.reviewId,
|
|
15088
|
-
effectiveDecision: effectiveReviewDecision(
|
|
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,
|
|
15276
|
-
if (!tokenPattern.test(token) || decodeWorkspaceBytes(token).length !== 32 || !idPattern.test(
|
|
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(
|
|
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,
|
|
15281
|
-
return encodeWorkspaceBytes(await tokenMaterial(token,
|
|
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 = (
|
|
15322
|
-
var revisionContext = (
|
|
15323
|
-
var eventContext = (
|
|
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,
|
|
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 (
|
|
15424
|
-
else if (
|
|
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(
|
|
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
|
-
|
|
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 <=
|
|
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,
|
|
15530
|
-
super(
|
|
15541
|
+
constructor(code, message2) {
|
|
15542
|
+
super(message2);
|
|
15531
15543
|
this.name = "ArtifactReviewStateError";
|
|
15532
15544
|
this.code = code;
|
|
15533
15545
|
}
|
|
15534
15546
|
};
|
|
15535
|
-
function invalid2(
|
|
15536
|
-
throw new ArtifactReviewStateError("E_ARTIFACT_REVIEW_INVALID",
|
|
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
|
|
15568
|
-
if (typeof
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
15622
|
-
if (
|
|
15623
|
-
if (typeof
|
|
15624
|
-
const planrId = boundedString(
|
|
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(
|
|
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:
|
|
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
|
|
15707
|
+
const anchor2 = normalizeAnchor(pin.anchor);
|
|
15696
15708
|
if (variant !== void 0) normalized.variant = variant;
|
|
15697
|
-
if (
|
|
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:
|
|
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.
|
|
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 [
|
|
15778
|
-
if (Number.isInteger(rating) && rating >= 1 && rating <= 5) ratings[
|
|
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 [
|
|
15781
|
-
if (typeof note === "string" && note.length <= 8192) remix[
|
|
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,
|
|
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
|
|
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,
|
|
16004
|
-
if (!["rotate", "pause", "resume", "revoke", "delete", "access"].includes(
|
|
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 (
|
|
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 (
|
|
16012
|
-
if (record.custody.pendingMutation && record.custody.pendingMutation.action !==
|
|
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,
|
|
16033
|
+
await prepareWorkspaceMutation(record.custody, action3);
|
|
16016
16034
|
save(record);
|
|
16017
16035
|
}
|
|
16018
16036
|
await commitMutation(record, save, options);
|
|
16019
|
-
if (
|
|
16020
|
-
if (
|
|
16021
|
-
if (
|
|
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
|
|
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 (
|
|
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((
|
|
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
|
-
|
|
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(
|
|
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((
|
|
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
|
-
|
|
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
|
|
16166
|
-
var pinKey = (pin) => `${
|
|
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[
|
|
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 {
|
|
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
|
-
|
|
16187
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
16245
|
-
|
|
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 (
|
|
16248
|
-
else if (
|
|
16249
|
-
else if (
|
|
16250
|
-
else
|
|
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 = (
|
|
16255
|
-
function renderMarkdown(draft,
|
|
16256
|
-
const lines = [`# ${safeMd(
|
|
17236
|
+
var safeMd = (text4) => String(text4).replaceAll("[", "\\[").replaceAll("]", "\\]").replaceAll("<", "<").replaceAll(">", ">");
|
|
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
|
|
16279
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 !==
|
|
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(
|
|
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(
|
|
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 [
|
|
16546
|
-
if (!variants.has(
|
|
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 [
|
|
16549
|
-
if (!current.entries.some((entry) => entry.artifactId ===
|
|
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 (
|
|
18334
|
+
if (existsSync11(join12(candidate, ".planr")) || existsSync11(join12(candidate, ".git")))
|
|
16557
18335
|
return candidate;
|
|
16558
|
-
const parent =
|
|
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 =
|
|
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((
|
|
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((
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
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:
|
|
18573
|
+
currentReviewOf: digest4,
|
|
16719
18574
|
reviews: ledger.reviews.map((entry) => ({
|
|
16720
18575
|
review: entry.review,
|
|
16721
|
-
stale: entry.stale || entry.review.reviewOf !==
|
|
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(
|
|
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>${
|
|
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-
|
|
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(
|
|
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 (
|
|
16792
|
-
else if (
|
|
16793
|
-
else if (
|
|
16794
|
-
else if (
|
|
16795
|
-
else result = await manageDesignShare(file,
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
18909
|
+
join12(current.root, ".design/review-history.json"),
|
|
16966
18910
|
[]
|
|
16967
18911
|
);
|
|
16968
|
-
atomicJson(
|
|
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,
|
|
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((
|
|
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,
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
19151
|
+
const file = resolve8(input);
|
|
17208
19152
|
let result;
|
|
17209
19153
|
if (command === "handoff") {
|
|
17210
|
-
const
|
|
17211
|
-
if (
|
|
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(
|
|
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(
|
|
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 =
|
|
17250
|
-
if (
|
|
17251
|
-
|
|
17252
|
-
|
|
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(
|
|
19201
|
+
result = verifyDesignDocument(file, readJson2(resolve8(flags.report)));
|
|
17258
19202
|
}
|
|
17259
19203
|
if (command === "feedback") {
|
|
17260
19204
|
await syncDesignShare(file, { env, fetchImpl });
|
|
17261
|
-
const
|
|
17262
|
-
if (
|
|
17263
|
-
else if (
|
|
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 =
|
|
17269
|
-
if (
|
|
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
|
-
|
|
17272
|
-
|
|
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 (
|
|
17275
|
-
else if (
|
|
17276
|
-
const current = currentDesign(file), saved = readJson2(
|
|
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((
|
|
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) ===
|
|
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" });
|