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