openplanr 3.4.0 → 3.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 +28 -0
- package/dist/cli/commands/design.d.ts +3 -0
- package/dist/cli/commands/design.d.ts.map +1 -0
- package/dist/cli/commands/design.js +137 -0
- package/dist/cli/commands/design.js.map +1 -0
- package/dist/cli/commands/groups/operations.d.ts.map +1 -1
- package/dist/cli/commands/groups/operations.js +2 -0
- package/dist/cli/commands/groups/operations.js.map +1 -1
- package/dist/dashboard/.vite/manifest.json +1 -1
- package/dist/dashboard/assets/{index-CvQAlBy3.js → index-B-l_941W.js} +2 -2
- package/dist/dashboard/assets/{index-CvQAlBy3.js.map → index-B-l_941W.js.map} +1 -1
- package/dist/dashboard/dashboard-manifest.json +13 -13
- package/dist/dashboard/index.html +1 -1
- package/dist/services/design-artifact-service.d.ts +22 -0
- package/dist/services/design-artifact-service.d.ts.map +1 -1
- package/dist/services/design-artifact-service.js +76 -16
- package/dist/services/design-artifact-service.js.map +1 -1
- package/dist/services/runtime-manager/runtime-package.js +1 -1
- package/dist/services/runtime-manager/runtime-package.js.map +1 -1
- package/docs/CLI.md +29 -0
- 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 +24 -340
- package/lib/host-packages/claude/openplanr/README.md +8 -8
- package/lib/host-packages/claude/openplanr/skills/artifact/references/design-sharing.md +2 -2
- package/lib/host-packages/claude/openplanr/skills/design/SKILL.md +23 -12
- package/lib/host-packages/claude/openplanr/skills/design/references/design-system.md +2 -2
- package/lib/host-packages/claude/openplanr/skills/design/references/generation.md +3 -3
- package/lib/host-packages/claude/openplanr/skills/design/references/hosted-canvas.md +6 -3
- package/lib/host-packages/claude/openplanr/skills/design/references/review.md +6 -6
- package/lib/host-packages/claude/openplanr/skills/design/references/team-review.md +3 -4
- package/lib/host-packages/claude/openplanr/skills/design/references/utilities.md +78 -38
- package/lib/host-packages/claude/openplanr/skills/design-loop/SKILL.md +15 -7
- package/lib/host-packages/claude/openplanr/skills/design-loop/references/design-system.md +2 -2
- package/lib/host-packages/claude/openplanr/skills/design-loop/references/generation.md +3 -3
- package/lib/host-packages/claude/openplanr/skills/design-loop/references/review.md +6 -6
- package/lib/host-packages/claude/openplanr/skills/design-loop/references/team-review.md +3 -4
- package/lib/host-packages/claude/openplanr/skills/design-loop/references/utilities.md +78 -38
- package/lib/host-packages/claude/openplanr/skills/design-review/SKILL.md +25 -5
- package/lib/host-packages/claude/openplanr/skills/design-review/references/design-system.md +2 -2
- package/lib/host-packages/claude/openplanr/skills/design-review/references/generation.md +3 -3
- package/lib/host-packages/claude/openplanr/skills/design-review/references/review.md +6 -6
- package/lib/host-packages/claude/openplanr/skills/design-review/references/team-review.md +3 -4
- package/lib/host-packages/claude/openplanr/skills/design-review/references/utilities.md +78 -38
- package/lib/host-packages/claude/openplanr/skills/plan/SKILL.md +7 -3
- package/lib/host-packages/cursor/openplanr/.openplanr-content.json +23 -339
- package/lib/host-packages/cursor/openplanr/manifest.json +1 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-artifact/references/design-sharing.md +2 -2
- package/lib/host-packages/cursor/openplanr/rules/planr-design/references/design-system.md +2 -2
- package/lib/host-packages/cursor/openplanr/rules/planr-design/references/generation.md +3 -3
- package/lib/host-packages/cursor/openplanr/rules/planr-design/references/hosted-canvas.md +6 -3
- package/lib/host-packages/cursor/openplanr/rules/planr-design/references/review.md +6 -6
- package/lib/host-packages/cursor/openplanr/rules/planr-design/references/team-review.md +3 -4
- package/lib/host-packages/cursor/openplanr/rules/planr-design/references/utilities.md +78 -38
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/references/design-system.md +2 -2
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/references/generation.md +3 -3
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/references/review.md +6 -6
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/references/team-review.md +3 -4
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/references/utilities.md +78 -38
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop.mdc +15 -7
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/references/design-system.md +2 -2
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/references/generation.md +3 -3
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/references/review.md +6 -6
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/references/team-review.md +3 -4
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/references/utilities.md +78 -38
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review.mdc +25 -5
- package/lib/host-packages/cursor/openplanr/rules/planr-design.mdc +23 -12
- package/lib/host-packages/cursor/openplanr/rules/planr-plan.mdc +7 -3
- 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 +27 -343
- package/lib/host-packages/openai/openplanr/skills/artifact/references/design-sharing.md +2 -2
- package/lib/host-packages/openai/openplanr/skills/design/SKILL.md +23 -12
- package/lib/host-packages/openai/openplanr/skills/design/openplanr.skill.json +0 -11
- package/lib/host-packages/openai/openplanr/skills/design/references/design-system.md +2 -2
- package/lib/host-packages/openai/openplanr/skills/design/references/generation.md +3 -3
- package/lib/host-packages/openai/openplanr/skills/design/references/hosted-canvas.md +6 -3
- package/lib/host-packages/openai/openplanr/skills/design/references/review.md +6 -6
- package/lib/host-packages/openai/openplanr/skills/design/references/team-review.md +3 -4
- package/lib/host-packages/openai/openplanr/skills/design/references/utilities.md +78 -38
- package/lib/host-packages/openai/openplanr/skills/design-loop/SKILL.md +15 -7
- package/lib/host-packages/openai/openplanr/skills/design-loop/openplanr.skill.json +0 -11
- package/lib/host-packages/openai/openplanr/skills/design-loop/references/design-system.md +2 -2
- package/lib/host-packages/openai/openplanr/skills/design-loop/references/generation.md +3 -3
- package/lib/host-packages/openai/openplanr/skills/design-loop/references/review.md +6 -6
- package/lib/host-packages/openai/openplanr/skills/design-loop/references/team-review.md +3 -4
- package/lib/host-packages/openai/openplanr/skills/design-loop/references/utilities.md +78 -38
- package/lib/host-packages/openai/openplanr/skills/design-review/SKILL.md +25 -5
- package/lib/host-packages/openai/openplanr/skills/design-review/openplanr.skill.json +0 -11
- package/lib/host-packages/openai/openplanr/skills/design-review/references/design-system.md +2 -2
- package/lib/host-packages/openai/openplanr/skills/design-review/references/generation.md +3 -3
- package/lib/host-packages/openai/openplanr/skills/design-review/references/review.md +6 -6
- package/lib/host-packages/openai/openplanr/skills/design-review/references/team-review.md +3 -4
- package/lib/host-packages/openai/openplanr/skills/design-review/references/utilities.md +78 -38
- package/lib/host-packages/openai/openplanr/skills/plan/SKILL.md +7 -3
- package/lib/host-packages/openai/openplanr/skills/plan/openplanr.skill.json +0 -11
- package/package.json +2 -2
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-artifact-shell.mjs +0 -1772
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-document.mjs +0 -2937
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-entities.mjs +0 -439
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-handoff.mjs +0 -2370
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-loopback-server.mjs +0 -359
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-parse5-parser.mjs +0 -3970
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-parse5-tokenizer.mjs +0 -3501
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-review-state-store.mjs +0 -45
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-review.mjs +0 -3376
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-runtime-home.mjs +0 -38
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-sandbox-guards.mjs +0 -1102
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-share-cli-bridge.mjs +0 -321
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-shared-artifact-support-dependencies-design-support-protocol-contracts-d707e49b.mjs +0 -1652
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-shared-artifact-support-protocol-contracts-9f6918b0.mjs +0 -1037
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-shared-design-support-0bac378d.mjs +0 -137
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design.mjs +0 -1500
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/notices/THIRD_PARTY_NOTICES.txt +0 -2023
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/lib/artifact/ui/studio-shell.css +0 -2
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/01-module-runtime.js +0 -18
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/02-artifact-review-ui.js +0 -814
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/03-protocol-contracts.js +0 -10
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/04-artifact-review-ui.js +0 -2568
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources.json +0 -60
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/package.json +0 -65
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/enhancements.css +0 -388
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/handoff-center.css +0 -81
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/share.css +0 -1
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/share.js +0 -139
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.css +0 -1159
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/01-module-runtime.js +0 -30
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/02-react.js +0 -900
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/03-react-dom-client-fiber-foundations.js +0 -2261
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/04-react-dom-client-reconciliation-hooks.js +0 -1595
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/05-react-dom-client-render-phases.js +0 -1467
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/06-react-dom-client-commit-effects.js +0 -1860
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/07-react-dom-client-work-loop-scheduling.js +0 -914
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/08-react-dom-client-dom-events-properties-hydration.js +0 -2578
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/09-react-dom-client-public-roots.js +0 -582
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/10-react.js +0 -17
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/11-artifact-review-ui.js +0 -12
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/12-protocol-contracts.js +0 -285
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/13-design-review-experience.js +0 -2773
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/14-studio-shell.js +0 -134
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/15-canvas-engine.js +0 -2207
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/16-design-review-experience.js +0 -16
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/17-studio-shell.js +0 -8
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/18-interface-primitives.js +0 -3883
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/19-interface-primitives.js +0 -966
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/20-studio-shell.js +0 -520
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/21-design-studio.js +0 -1597
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources.json +0 -311
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/package.json +0 -163
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/registries/artifact-theme.json +0 -64
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.0.0/design-manifest.schema.json +0 -90
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.1.0/artifact-envelope.schema.json +0 -142
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.1.0/artifact-review.schema.json +0 -97
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.10.0/design-review-bundle.schema.json +0 -584
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.10.0/design-review-context.schema.json +0 -191
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.10.0/design-review-handoff.schema.json +0 -407
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.10.0/design-review-metadata-payload.schema.json +0 -106
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +0 -398
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +0 -518
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +0 -106
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-review-metadata-payload.schema.json +0 -107
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.14.0/artifact-theme.schema.json +0 -316
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.16.0/artifact-envelope.schema.json +0 -408
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.16.0/design-review-bundle.schema.json +0 -590
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.22.0/design-document.schema.json +0 -506
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.22.0/design-review-bundle.schema.json +0 -822
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.9.0/design-review-bundle.schema.json +0 -346
- package/lib/host-packages/claude/openplanr/runtime/plan/scripts/design-protocol-contracts.mjs +0 -1232
- package/lib/host-packages/claude/openplanr/runtime/plan/scripts/design.mjs +0 -2611
- package/lib/host-packages/claude/openplanr/runtime/plan/scripts/runtime/notices/THIRD_PARTY_NOTICES.txt +0 -21
- package/lib/host-packages/claude/openplanr/runtime/plan/scripts/runtime/packages/protocol/package.json +0 -163
- package/lib/host-packages/claude/openplanr/runtime/plan/scripts/runtime/packages/protocol/schemas/v1.1.0/artifact-review.schema.json +0 -97
- package/lib/host-packages/claude/openplanr/skills/design/scripts/design.mjs +0 -12
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design.mjs +0 -12
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design.mjs +0 -12
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/design.mjs +0 -12
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design.mjs +0 -12
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design.mjs +0 -12
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design.mjs +0 -12
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design.mjs +0 -12
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-artifact-shell.mjs +0 -1772
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-document.mjs +0 -2937
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-entities.mjs +0 -439
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-handoff.mjs +0 -2370
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-loopback-server.mjs +0 -359
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-parse5-parser.mjs +0 -3970
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-parse5-tokenizer.mjs +0 -3501
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-review-state-store.mjs +0 -45
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-review.mjs +0 -3376
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-runtime-home.mjs +0 -38
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-sandbox-guards.mjs +0 -1102
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-share-cli-bridge.mjs +0 -321
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-shared-artifact-support-dependencies-design-support-protocol-contracts-d707e49b.mjs +0 -1652
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-shared-artifact-support-protocol-contracts-9f6918b0.mjs +0 -1037
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-shared-design-support-0bac378d.mjs +0 -137
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design.mjs +0 -1500
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/notices/THIRD_PARTY_NOTICES.txt +0 -2023
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/lib/artifact/ui/studio-shell.css +0 -2
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/01-module-runtime.js +0 -18
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/02-artifact-review-ui.js +0 -814
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/03-protocol-contracts.js +0 -10
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/04-artifact-review-ui.js +0 -2568
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources.json +0 -60
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/package.json +0 -65
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/enhancements.css +0 -388
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/handoff-center.css +0 -81
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/share.css +0 -1
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/share.js +0 -139
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.css +0 -1159
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/01-module-runtime.js +0 -30
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/02-react.js +0 -900
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/03-react-dom-client-fiber-foundations.js +0 -2261
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/04-react-dom-client-reconciliation-hooks.js +0 -1595
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/05-react-dom-client-render-phases.js +0 -1467
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/06-react-dom-client-commit-effects.js +0 -1860
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/07-react-dom-client-work-loop-scheduling.js +0 -914
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/08-react-dom-client-dom-events-properties-hydration.js +0 -2578
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/09-react-dom-client-public-roots.js +0 -582
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/10-react.js +0 -17
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/11-artifact-review-ui.js +0 -12
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/12-protocol-contracts.js +0 -285
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/13-design-review-experience.js +0 -2773
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/14-studio-shell.js +0 -134
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/15-canvas-engine.js +0 -2207
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/16-design-review-experience.js +0 -16
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/17-studio-shell.js +0 -8
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/18-interface-primitives.js +0 -3883
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/19-interface-primitives.js +0 -966
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/20-studio-shell.js +0 -520
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/21-design-studio.js +0 -1597
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources.json +0 -311
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/package.json +0 -163
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/registries/artifact-theme.json +0 -64
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.0.0/design-manifest.schema.json +0 -90
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.1.0/artifact-envelope.schema.json +0 -142
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.1.0/artifact-review.schema.json +0 -97
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.10.0/design-review-bundle.schema.json +0 -584
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.10.0/design-review-context.schema.json +0 -191
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.10.0/design-review-handoff.schema.json +0 -407
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.10.0/design-review-metadata-payload.schema.json +0 -106
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +0 -398
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +0 -518
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +0 -106
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-review-metadata-payload.schema.json +0 -107
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.14.0/artifact-theme.schema.json +0 -316
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.16.0/artifact-envelope.schema.json +0 -408
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.16.0/design-review-bundle.schema.json +0 -590
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.22.0/design-document.schema.json +0 -506
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.22.0/design-review-bundle.schema.json +0 -822
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.9.0/design-review-bundle.schema.json +0 -346
- package/lib/host-packages/cursor/openplanr/runtime/plan/scripts/design-protocol-contracts.mjs +0 -1232
- package/lib/host-packages/cursor/openplanr/runtime/plan/scripts/design.mjs +0 -2611
- package/lib/host-packages/cursor/openplanr/runtime/plan/scripts/runtime/notices/THIRD_PARTY_NOTICES.txt +0 -21
- package/lib/host-packages/cursor/openplanr/runtime/plan/scripts/runtime/packages/protocol/package.json +0 -163
- package/lib/host-packages/cursor/openplanr/runtime/plan/scripts/runtime/packages/protocol/schemas/v1.1.0/artifact-review.schema.json +0 -97
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-artifact-shell.mjs +0 -1772
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-document.mjs +0 -2937
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-entities.mjs +0 -439
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-handoff.mjs +0 -2370
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-loopback-server.mjs +0 -359
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-parse5-parser.mjs +0 -3970
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-parse5-tokenizer.mjs +0 -3501
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-review-state-store.mjs +0 -45
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-review.mjs +0 -3376
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-runtime-home.mjs +0 -38
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-sandbox-guards.mjs +0 -1102
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-share-cli-bridge.mjs +0 -321
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-shared-artifact-support-dependencies-design-support-protocol-contracts-d707e49b.mjs +0 -1652
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-shared-artifact-support-protocol-contracts-9f6918b0.mjs +0 -1037
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-shared-design-support-0bac378d.mjs +0 -137
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design.mjs +0 -1500
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/notices/THIRD_PARTY_NOTICES.txt +0 -2023
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/lib/artifact/ui/studio-shell.css +0 -2
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/01-module-runtime.js +0 -18
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/02-artifact-review-ui.js +0 -814
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/03-protocol-contracts.js +0 -10
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/04-artifact-review-ui.js +0 -2568
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources.json +0 -60
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/package.json +0 -65
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/enhancements.css +0 -388
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/handoff-center.css +0 -81
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/share.css +0 -1
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/share.js +0 -139
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.css +0 -1159
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/01-module-runtime.js +0 -30
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/02-react.js +0 -900
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/03-react-dom-client-fiber-foundations.js +0 -2261
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/04-react-dom-client-reconciliation-hooks.js +0 -1595
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/05-react-dom-client-render-phases.js +0 -1467
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/06-react-dom-client-commit-effects.js +0 -1860
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/07-react-dom-client-work-loop-scheduling.js +0 -914
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/08-react-dom-client-dom-events-properties-hydration.js +0 -2578
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/09-react-dom-client-public-roots.js +0 -582
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/10-react.js +0 -17
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/11-artifact-review-ui.js +0 -12
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/12-protocol-contracts.js +0 -285
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/13-design-review-experience.js +0 -2773
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/14-studio-shell.js +0 -134
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/15-canvas-engine.js +0 -2207
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/16-design-review-experience.js +0 -16
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/17-studio-shell.js +0 -8
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/18-interface-primitives.js +0 -3883
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/19-interface-primitives.js +0 -966
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/20-studio-shell.js +0 -520
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/21-design-studio.js +0 -1597
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources.json +0 -311
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/package.json +0 -163
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/registries/artifact-theme.json +0 -64
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.0.0/design-manifest.schema.json +0 -90
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.1.0/artifact-envelope.schema.json +0 -142
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.1.0/artifact-review.schema.json +0 -97
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.10.0/design-review-bundle.schema.json +0 -584
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.10.0/design-review-context.schema.json +0 -191
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.10.0/design-review-handoff.schema.json +0 -407
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.10.0/design-review-metadata-payload.schema.json +0 -106
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-handoff-readiness.schema.json +0 -398
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-implementation-handoff.schema.json +0 -518
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-planning-lineage.schema.json +0 -106
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.11.0/design-review-metadata-payload.schema.json +0 -107
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.14.0/artifact-theme.schema.json +0 -316
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.16.0/artifact-envelope.schema.json +0 -408
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.16.0/design-review-bundle.schema.json +0 -590
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.22.0/design-document.schema.json +0 -506
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.22.0/design-review-bundle.schema.json +0 -822
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/schemas/v1.9.0/design-review-bundle.schema.json +0 -346
- package/lib/host-packages/openai/openplanr/runtime/plan/scripts/design-protocol-contracts.mjs +0 -1232
- package/lib/host-packages/openai/openplanr/runtime/plan/scripts/design.mjs +0 -2611
- package/lib/host-packages/openai/openplanr/runtime/plan/scripts/runtime/notices/THIRD_PARTY_NOTICES.txt +0 -21
- package/lib/host-packages/openai/openplanr/runtime/plan/scripts/runtime/packages/protocol/package.json +0 -163
- package/lib/host-packages/openai/openplanr/runtime/plan/scripts/runtime/packages/protocol/schemas/v1.1.0/artifact-review.schema.json +0 -97
- package/lib/host-packages/openai/openplanr/skills/design/scripts/design.mjs +0 -12
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design.mjs +0 -12
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design.mjs +0 -12
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/design.mjs +0 -12
|
@@ -1,2370 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DESIGN_HANDOFF_CONTENT_SCHEMA,
|
|
3
|
-
DESIGN_HANDOFF_SCHEMA,
|
|
4
|
-
assertPlainData,
|
|
5
|
-
assertReviewExperience,
|
|
6
|
-
canonicalizeJson,
|
|
7
|
-
deepFreeze,
|
|
8
|
-
emptyReviewContext,
|
|
9
|
-
reviewDigest,
|
|
10
|
-
sha256Hex
|
|
11
|
-
} from "./design-shared-artifact-support-dependencies-design-support-protocol-contracts-d707e49b.mjs";
|
|
12
|
-
import {
|
|
13
|
-
ARTIFACT_ERROR_CODES,
|
|
14
|
-
PipelineError,
|
|
15
|
-
canonicalSerialize,
|
|
16
|
-
digestArtifactEnvelope,
|
|
17
|
-
validateArtifactEnvelope,
|
|
18
|
-
validateArtifactReview,
|
|
19
|
-
validateJson
|
|
20
|
-
} from "./design-shared-artifact-support-protocol-contracts-9f6918b0.mjs";
|
|
21
|
-
import {
|
|
22
|
-
assertDesignShareAvailable,
|
|
23
|
-
getDesignShareStatus,
|
|
24
|
-
publishDesignReviewMetadata
|
|
25
|
-
} from "./design-share-cli-bridge.mjs";
|
|
26
|
-
import {
|
|
27
|
-
atomicJson,
|
|
28
|
-
currentDesign,
|
|
29
|
-
designSpecPath,
|
|
30
|
-
hash,
|
|
31
|
-
readJson
|
|
32
|
-
} from "./design-shared-design-support-0bac378d.mjs";
|
|
33
|
-
import {
|
|
34
|
-
planrHome
|
|
35
|
-
} from "./design-runtime-home.mjs";
|
|
36
|
-
import {
|
|
37
|
-
acquireStartLock
|
|
38
|
-
} from "./design-loopback-server.mjs";
|
|
39
|
-
|
|
40
|
-
// packages/design/lib/design/feedback-reader.mjs
|
|
41
|
-
import { join as join2 } from "node:path";
|
|
42
|
-
|
|
43
|
-
// packages/artifact/lib/artifact/import.mjs
|
|
44
|
-
import {
|
|
45
|
-
existsSync as existsSync2,
|
|
46
|
-
lstatSync as lstatSync2,
|
|
47
|
-
mkdirSync as mkdirSync2,
|
|
48
|
-
readFileSync as readFileSync2,
|
|
49
|
-
realpathSync,
|
|
50
|
-
renameSync as renameSync2,
|
|
51
|
-
rmSync as rmSync2,
|
|
52
|
-
statSync as statSync2,
|
|
53
|
-
writeFileSync as writeFileSync2
|
|
54
|
-
} from "node:fs";
|
|
55
|
-
import { homedir } from "node:os";
|
|
56
|
-
import { dirname as dirname2, isAbsolute, join, relative, resolve } from "node:path";
|
|
57
|
-
|
|
58
|
-
// packages/protocol/src/names.mjs
|
|
59
|
-
var PLANNING_FOLDER = ".planr";
|
|
60
|
-
var CLI_COMMAND = "openplanr";
|
|
61
|
-
|
|
62
|
-
// packages/artifact/lib/artifact/internal/feedback.mjs
|
|
63
|
-
var FEEDBACK_FILE = "feedback.json";
|
|
64
|
-
|
|
65
|
-
// packages/artifact/lib/artifact/merge.mjs
|
|
66
|
-
var ARTIFACT_REVIEW_STATE_VERSION = "1.0.0";
|
|
67
|
-
var ARTIFACT_REVIEW_STATE_KIND = "artifact-review-state";
|
|
68
|
-
var SHA256_RE = /^[a-f0-9]{64}$/;
|
|
69
|
-
var ARTIFACT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
70
|
-
function conflict(message2, details = void 0) {
|
|
71
|
-
throw new PipelineError(
|
|
72
|
-
ARTIFACT_ERROR_CODES.MERGE_CONFLICT,
|
|
73
|
-
message2,
|
|
74
|
-
"Keep both reviews under different stable IDs or resolve the conflicting item explicitly.",
|
|
75
|
-
details
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
function assertUniqueReviewItemIds(review) {
|
|
79
|
-
const pinIds = /* @__PURE__ */ new Set();
|
|
80
|
-
for (const pin of review.pins ?? []) {
|
|
81
|
-
if (pinIds.has(pin.id)) {
|
|
82
|
-
throw new PipelineError(
|
|
83
|
-
ARTIFACT_ERROR_CODES.REVIEW_INVALID,
|
|
84
|
-
"Artifact review contains a duplicate pin ID."
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
pinIds.add(pin.id);
|
|
88
|
-
const replyIds = /* @__PURE__ */ new Set();
|
|
89
|
-
for (const reply of pin.replies ?? []) {
|
|
90
|
-
if (replyIds.has(reply.id)) {
|
|
91
|
-
throw new PipelineError(
|
|
92
|
-
ARTIFACT_ERROR_CODES.REVIEW_INVALID,
|
|
93
|
-
"Artifact review contains a duplicate reply ID in one thread."
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
replyIds.add(reply.id);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return review;
|
|
100
|
-
}
|
|
101
|
-
function clone(value) {
|
|
102
|
-
return structuredClone(value);
|
|
103
|
-
}
|
|
104
|
-
function same(a, b) {
|
|
105
|
-
return canonicalSerialize(a) === canonicalSerialize(b);
|
|
106
|
-
}
|
|
107
|
-
function timeValue(value) {
|
|
108
|
-
if (typeof value !== "string") return Number.NEGATIVE_INFINITY;
|
|
109
|
-
const parsed = Date.parse(value);
|
|
110
|
-
return Number.isFinite(parsed) ? parsed : Number.NEGATIVE_INFINITY;
|
|
111
|
-
}
|
|
112
|
-
function compareStable(a, b, idKey) {
|
|
113
|
-
const aTime = a.createdAt ?? a.updatedAt ?? "";
|
|
114
|
-
const bTime = b.createdAt ?? b.updatedAt ?? "";
|
|
115
|
-
return aTime.localeCompare(bTime) || String(a[idKey]).localeCompare(String(b[idKey]));
|
|
116
|
-
}
|
|
117
|
-
function replyMap(replies) {
|
|
118
|
-
const map = /* @__PURE__ */ new Map();
|
|
119
|
-
for (const reply of replies ?? []) {
|
|
120
|
-
const previous = map.get(reply.id);
|
|
121
|
-
if (previous && !same(previous, reply)) {
|
|
122
|
-
conflict("A reply ID refers to different immutable reply content.", { entity: "reply" });
|
|
123
|
-
}
|
|
124
|
-
map.set(reply.id, clone(reply));
|
|
125
|
-
}
|
|
126
|
-
return map;
|
|
127
|
-
}
|
|
128
|
-
function mergeReplies(stored = [], incoming = []) {
|
|
129
|
-
const merged = replyMap(stored);
|
|
130
|
-
for (const reply of incoming) {
|
|
131
|
-
const previous = merged.get(reply.id);
|
|
132
|
-
if (previous && !same(previous, reply)) {
|
|
133
|
-
conflict("A reply ID refers to different immutable reply content.", { entity: "reply" });
|
|
134
|
-
}
|
|
135
|
-
if (!previous) merged.set(reply.id, clone(reply));
|
|
136
|
-
}
|
|
137
|
-
return [...merged.values()].sort((a, b) => compareStable(a, b, "id"));
|
|
138
|
-
}
|
|
139
|
-
var immutablePin = (pin) => ({
|
|
140
|
-
id: pin.id,
|
|
141
|
-
author: pin.author,
|
|
142
|
-
artifactId: pin.artifactId,
|
|
143
|
-
...pin.variant === void 0 ? {} : { variant: pin.variant },
|
|
144
|
-
region: pin.region,
|
|
145
|
-
viewport: pin.viewport,
|
|
146
|
-
...pin.anchor === void 0 ? {} : { anchor: pin.anchor },
|
|
147
|
-
createdAt: pin.createdAt
|
|
148
|
-
});
|
|
149
|
-
var mutablePin = (pin) => ({
|
|
150
|
-
intent: pin.intent,
|
|
151
|
-
status: pin.status,
|
|
152
|
-
comment: pin.comment
|
|
153
|
-
});
|
|
154
|
-
function mergePin(stored, incoming) {
|
|
155
|
-
if (!same(immutablePin(stored), immutablePin(incoming))) {
|
|
156
|
-
conflict("A pin ID refers to different immutable geometry, author, or artifact content.", {
|
|
157
|
-
entity: "pin"
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
const replies = mergeReplies(stored.replies, incoming.replies);
|
|
161
|
-
const storedTime = timeValue(stored.updatedAt);
|
|
162
|
-
const incomingTime = timeValue(incoming.updatedAt);
|
|
163
|
-
let selected = stored;
|
|
164
|
-
if (incomingTime > storedTime) selected = incoming;
|
|
165
|
-
if (incomingTime === storedTime && !same(mutablePin(stored), mutablePin(incoming))) {
|
|
166
|
-
conflict("A pin has divergent mutable content at the same update time.", { entity: "pin" });
|
|
167
|
-
}
|
|
168
|
-
return { ...clone(selected), replies };
|
|
169
|
-
}
|
|
170
|
-
function mergePins(stored = [], incoming = []) {
|
|
171
|
-
const merged = /* @__PURE__ */ new Map();
|
|
172
|
-
for (const pin of stored) merged.set(pin.id, clone(pin));
|
|
173
|
-
for (const pin of incoming) {
|
|
174
|
-
const previous = merged.get(pin.id);
|
|
175
|
-
merged.set(pin.id, previous ? mergePin(previous, pin) : clone(pin));
|
|
176
|
-
}
|
|
177
|
-
return [...merged.values()].sort((a, b) => compareStable(a, b, "id"));
|
|
178
|
-
}
|
|
179
|
-
var immutableReview = (review) => ({
|
|
180
|
-
schemaVersion: review.schemaVersion,
|
|
181
|
-
reviewId: review.reviewId,
|
|
182
|
-
reviewOf: review.reviewOf,
|
|
183
|
-
...review.createdAt === void 0 ? {} : { createdAt: review.createdAt }
|
|
184
|
-
});
|
|
185
|
-
var mutableReview = (review) => ({ decision: review.decision, overall: review.overall });
|
|
186
|
-
function mergeArtifactReviews(stored, incoming) {
|
|
187
|
-
validateArtifactReview(stored);
|
|
188
|
-
validateArtifactReview(incoming);
|
|
189
|
-
assertUniqueReviewItemIds(stored);
|
|
190
|
-
assertUniqueReviewItemIds(incoming);
|
|
191
|
-
if (stored.reviewId !== incoming.reviewId) {
|
|
192
|
-
conflict("Only reviews with the same review ID can be revision-merged.", {
|
|
193
|
-
entity: "review"
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
if (same(stored, incoming)) return stored;
|
|
197
|
-
if (!same(immutableReview(stored), immutableReview(incoming))) {
|
|
198
|
-
conflict("A review ID refers to a different digest or creation identity.", {
|
|
199
|
-
entity: "review"
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
const storedTime = timeValue(stored.updatedAt);
|
|
203
|
-
const incomingTime = timeValue(incoming.updatedAt);
|
|
204
|
-
let selected = stored;
|
|
205
|
-
if (incomingTime > storedTime) selected = incoming;
|
|
206
|
-
if (incomingTime === storedTime && !same(mutableReview(stored), mutableReview(incoming))) {
|
|
207
|
-
conflict("A review has divergent verdict or overall feedback at the same update time.", {
|
|
208
|
-
entity: "review"
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
const merged = {
|
|
212
|
-
...clone(selected),
|
|
213
|
-
pins: mergePins(stored.pins, incoming.pins)
|
|
214
|
-
};
|
|
215
|
-
validateArtifactReview(merged);
|
|
216
|
-
return merged;
|
|
217
|
-
}
|
|
218
|
-
function validateReviewLedger(ledger) {
|
|
219
|
-
if (!ledger || typeof ledger !== "object" || Array.isArray(ledger) || ledger.schemaVersion !== ARTIFACT_REVIEW_STATE_VERSION || ledger.kind !== ARTIFACT_REVIEW_STATE_KIND || !ARTIFACT_ID_RE.test(ledger.artifactId ?? "") || !SHA256_RE.test(ledger.currentReviewOf ?? "") || !Array.isArray(ledger.reviews)) {
|
|
220
|
-
throw new PipelineError(
|
|
221
|
-
ARTIFACT_ERROR_CODES.REVIEW_INVALID,
|
|
222
|
-
"Artifact review state is not a valid versioned review ledger."
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
const ids = /* @__PURE__ */ new Set();
|
|
226
|
-
for (const entry of ledger.reviews) {
|
|
227
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry) || typeof entry.stale !== "boolean" || !entry.review) {
|
|
228
|
-
throw new PipelineError(
|
|
229
|
-
ARTIFACT_ERROR_CODES.REVIEW_INVALID,
|
|
230
|
-
"Artifact review ledger entry is invalid."
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
validateArtifactReview(entry.review);
|
|
234
|
-
assertUniqueReviewItemIds(entry.review);
|
|
235
|
-
if (ids.has(entry.review.reviewId)) {
|
|
236
|
-
throw new PipelineError(
|
|
237
|
-
ARTIFACT_ERROR_CODES.REVIEW_INVALID,
|
|
238
|
-
"Artifact review ledger IDs must be unique."
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
ids.add(entry.review.reviewId);
|
|
242
|
-
}
|
|
243
|
-
return ledger;
|
|
244
|
-
}
|
|
245
|
-
function createReviewLedger({ artifactId, currentReviewOf, reviews = [] } = {}) {
|
|
246
|
-
const ledger = {
|
|
247
|
-
schemaVersion: ARTIFACT_REVIEW_STATE_VERSION,
|
|
248
|
-
kind: ARTIFACT_REVIEW_STATE_KIND,
|
|
249
|
-
artifactId,
|
|
250
|
-
currentReviewOf,
|
|
251
|
-
reviews: reviews.map((entry) => ({ review: clone(entry.review), stale: Boolean(entry.stale) }))
|
|
252
|
-
};
|
|
253
|
-
ledger.reviews.sort((a, b) => compareStable(a.review, b.review, "reviewId"));
|
|
254
|
-
return validateReviewLedger(ledger);
|
|
255
|
-
}
|
|
256
|
-
function mergeReviewLedger(ledger, incoming, { stale = false } = {}) {
|
|
257
|
-
validateReviewLedger(ledger);
|
|
258
|
-
const reviews = Array.isArray(incoming) ? incoming : [incoming];
|
|
259
|
-
const byId = new Map(
|
|
260
|
-
ledger.reviews.map((entry) => [
|
|
261
|
-
entry.review.reviewId,
|
|
262
|
-
{
|
|
263
|
-
review: clone(entry.review),
|
|
264
|
-
stale: entry.stale
|
|
265
|
-
}
|
|
266
|
-
])
|
|
267
|
-
);
|
|
268
|
-
for (const review of reviews) {
|
|
269
|
-
validateArtifactReview(review);
|
|
270
|
-
const previous = byId.get(review.reviewId);
|
|
271
|
-
if (previous) {
|
|
272
|
-
byId.set(review.reviewId, {
|
|
273
|
-
review: mergeArtifactReviews(previous.review, review),
|
|
274
|
-
stale: previous.stale || Boolean(stale)
|
|
275
|
-
});
|
|
276
|
-
} else {
|
|
277
|
-
byId.set(review.reviewId, { review: clone(review), stale: Boolean(stale) });
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
const merged = createReviewLedger({
|
|
281
|
-
artifactId: ledger.artifactId,
|
|
282
|
-
currentReviewOf: ledger.currentReviewOf,
|
|
283
|
-
reviews: [...byId.values()]
|
|
284
|
-
});
|
|
285
|
-
return same(ledger, merged) ? ledger : merged;
|
|
286
|
-
}
|
|
287
|
-
function effectiveReviewDecision(value) {
|
|
288
|
-
const entries = Array.isArray(value) ? value : value?.reviews ?? [];
|
|
289
|
-
const decisions = entries.map((entry) => entry.review?.decision ?? entry.decision);
|
|
290
|
-
if (decisions.includes("changes_requested")) return "changes_requested";
|
|
291
|
-
if (decisions.includes("pending")) return "pending";
|
|
292
|
-
return decisions.length > 0 && decisions.every((decision) => decision === "approved") ? "approved" : "pending";
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// packages/artifact/lib/artifact/review.mjs
|
|
296
|
-
import { randomBytes, randomUUID } from "node:crypto";
|
|
297
|
-
import {
|
|
298
|
-
existsSync,
|
|
299
|
-
lstatSync,
|
|
300
|
-
mkdirSync,
|
|
301
|
-
readFileSync,
|
|
302
|
-
renameSync,
|
|
303
|
-
rmSync,
|
|
304
|
-
statSync,
|
|
305
|
-
writeFileSync
|
|
306
|
-
} from "node:fs";
|
|
307
|
-
import { dirname } from "node:path";
|
|
308
|
-
var ARTIFACT_REVIEW_MAX_STATE_BYTES = 5 * 1024 * 1024;
|
|
309
|
-
var reviewPathQueues = /* @__PURE__ */ new Map();
|
|
310
|
-
function finalEntry(path, fs = { lstatSync }) {
|
|
311
|
-
try {
|
|
312
|
-
return fs.lstatSync(path);
|
|
313
|
-
} catch (error) {
|
|
314
|
-
if (error?.code === "ENOENT") return null;
|
|
315
|
-
throw error;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
function clone2(value) {
|
|
319
|
-
return structuredClone(value);
|
|
320
|
-
}
|
|
321
|
-
function canonicalObject(value) {
|
|
322
|
-
if (Array.isArray(value)) return value.map(canonicalObject);
|
|
323
|
-
if (!value || typeof value !== "object") return value;
|
|
324
|
-
return Object.fromEntries(
|
|
325
|
-
Object.keys(value).sort().map((key) => [key, canonicalObject(value[key])])
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
function stableItemSort(a, b) {
|
|
329
|
-
return String(a.createdAt ?? a.updatedAt ?? "").localeCompare(
|
|
330
|
-
String(b.createdAt ?? b.updatedAt ?? "")
|
|
331
|
-
) || String(a.id ?? a.reviewId).localeCompare(String(b.id ?? b.reviewId));
|
|
332
|
-
}
|
|
333
|
-
function normalizeArtifactReview(review) {
|
|
334
|
-
let normalized;
|
|
335
|
-
try {
|
|
336
|
-
normalized = clone2(review);
|
|
337
|
-
} catch {
|
|
338
|
-
throw new PipelineError(
|
|
339
|
-
ARTIFACT_ERROR_CODES.REVIEW_INVALID,
|
|
340
|
-
"Artifact review is not cloneable."
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
try {
|
|
344
|
-
validateArtifactReview(normalized);
|
|
345
|
-
assertUniqueReviewItemIds(normalized);
|
|
346
|
-
} catch (error) {
|
|
347
|
-
if (error instanceof PipelineError) {
|
|
348
|
-
throw new PipelineError(
|
|
349
|
-
ARTIFACT_ERROR_CODES.REVIEW_INVALID,
|
|
350
|
-
"Artifact review does not satisfy the Protocol v1.1 review contract.",
|
|
351
|
-
"",
|
|
352
|
-
{ issues: error.details?.issues ?? [] }
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
throw error;
|
|
356
|
-
}
|
|
357
|
-
normalized.pins = normalized.pins.map((pin) => ({
|
|
358
|
-
...pin,
|
|
359
|
-
replies: [...pin.replies].sort(stableItemSort)
|
|
360
|
-
})).sort(stableItemSort);
|
|
361
|
-
validateArtifactReview(normalized);
|
|
362
|
-
return normalized;
|
|
363
|
-
}
|
|
364
|
-
function serializeReviewState(value) {
|
|
365
|
-
const normalized = value?.kind === "artifact-review-state" ? validateReviewLedger(clone2(value)) : normalizeArtifactReview(value);
|
|
366
|
-
return `${JSON.stringify(canonicalObject(normalized), null, 2)}
|
|
367
|
-
`;
|
|
368
|
-
}
|
|
369
|
-
function readArtifactReviewState(path, { allowMissing = false } = {}) {
|
|
370
|
-
const entry = finalEntry(path);
|
|
371
|
-
if (!entry) {
|
|
372
|
-
if (allowMissing) return null;
|
|
373
|
-
throw new PipelineError(
|
|
374
|
-
ARTIFACT_ERROR_CODES.REVIEW_IMPORT,
|
|
375
|
-
"Artifact review state does not exist."
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
try {
|
|
379
|
-
if (entry.isSymbolicLink() || !entry.isFile()) {
|
|
380
|
-
throw new PipelineError(
|
|
381
|
-
ARTIFACT_ERROR_CODES.SYMLINK_ESCAPE,
|
|
382
|
-
"Artifact review state destination is not a regular file."
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
if (statSync(path).size > ARTIFACT_REVIEW_MAX_STATE_BYTES) {
|
|
386
|
-
throw new PipelineError(
|
|
387
|
-
ARTIFACT_ERROR_CODES.REQUEST_LIMIT,
|
|
388
|
-
`Artifact review state exceeds ${ARTIFACT_REVIEW_MAX_STATE_BYTES} bytes.`
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
return validateReviewLedger(JSON.parse(readFileSync(path, "utf8")));
|
|
392
|
-
} catch (error) {
|
|
393
|
-
if (error instanceof PipelineError) throw error;
|
|
394
|
-
throw new PipelineError(
|
|
395
|
-
ARTIFACT_ERROR_CODES.REVIEW_INVALID,
|
|
396
|
-
"Artifact review state is malformed."
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
function writeArtifactReviewState(path, ledger, { fileSystem = {}, suffix = `${process.pid}.${randomBytes(8).toString("hex")}` } = {}) {
|
|
401
|
-
validateReviewLedger(ledger);
|
|
402
|
-
const fs = { existsSync, lstatSync, mkdirSync, writeFileSync, renameSync, rmSync, ...fileSystem };
|
|
403
|
-
const temporary = `${path}.${suffix}.tmp`;
|
|
404
|
-
const serialized = serializeReviewState(ledger);
|
|
405
|
-
if (Buffer.byteLength(serialized, "utf8") > ARTIFACT_REVIEW_MAX_STATE_BYTES) {
|
|
406
|
-
throw new PipelineError(
|
|
407
|
-
ARTIFACT_ERROR_CODES.REQUEST_LIMIT,
|
|
408
|
-
`Artifact review state exceeds ${ARTIFACT_REVIEW_MAX_STATE_BYTES} bytes.`
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
try {
|
|
412
|
-
const initial = finalEntry(path, fs);
|
|
413
|
-
if (initial) {
|
|
414
|
-
const target = initial;
|
|
415
|
-
if (target.isSymbolicLink() || !target.isFile()) throw new Error("unsafe final target");
|
|
416
|
-
}
|
|
417
|
-
fs.mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
418
|
-
fs.writeFileSync(temporary, serialized, { mode: 384, flag: "wx" });
|
|
419
|
-
const final = finalEntry(path, fs);
|
|
420
|
-
if (final) {
|
|
421
|
-
const target = final;
|
|
422
|
-
if (target.isSymbolicLink() || !target.isFile()) throw new Error("unsafe final target");
|
|
423
|
-
}
|
|
424
|
-
fs.renameSync(temporary, path);
|
|
425
|
-
} catch {
|
|
426
|
-
try {
|
|
427
|
-
fs.rmSync(temporary, { force: true });
|
|
428
|
-
} catch {
|
|
429
|
-
}
|
|
430
|
-
throw new PipelineError(
|
|
431
|
-
ARTIFACT_ERROR_CODES.REVIEW_WRITE,
|
|
432
|
-
"Artifact review state could not be written atomically.",
|
|
433
|
-
"Check destination permissions and retry; the previous review state was left unchanged."
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
return ledger;
|
|
437
|
-
}
|
|
438
|
-
function withArtifactReviewLock(path, action3) {
|
|
439
|
-
const previous = reviewPathQueues.get(path) ?? Promise.resolve();
|
|
440
|
-
const run = async () => {
|
|
441
|
-
let release;
|
|
442
|
-
try {
|
|
443
|
-
release = await acquireStartLock(`${path}.lock`, { timeout: 15e3, stale: 3e4 });
|
|
444
|
-
return await action3();
|
|
445
|
-
} catch (error) {
|
|
446
|
-
if (error instanceof PipelineError) throw error;
|
|
447
|
-
throw new PipelineError(
|
|
448
|
-
ARTIFACT_ERROR_CODES.REVIEW_WRITE,
|
|
449
|
-
"Artifact review state is busy or its cross-process lock is unavailable.",
|
|
450
|
-
"Wait for the active review write to finish, then retry."
|
|
451
|
-
);
|
|
452
|
-
} finally {
|
|
453
|
-
release?.();
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
const operation = previous.then(run, run);
|
|
457
|
-
const tail = operation.then(
|
|
458
|
-
() => void 0,
|
|
459
|
-
() => void 0
|
|
460
|
-
);
|
|
461
|
-
reviewPathQueues.set(path, tail);
|
|
462
|
-
tail.finally(() => {
|
|
463
|
-
if (reviewPathQueues.get(path) === tail) reviewPathQueues.delete(path);
|
|
464
|
-
});
|
|
465
|
-
return operation;
|
|
466
|
-
}
|
|
467
|
-
function normalizedExportInput(value) {
|
|
468
|
-
if (value?.kind === "artifact-review-state") return validateReviewLedger(clone2(value));
|
|
469
|
-
if (value?.review && value?.artifacts) return normalizeArtifactReview(value.review);
|
|
470
|
-
return normalizeArtifactReview(value);
|
|
471
|
-
}
|
|
472
|
-
function markdownText(value) {
|
|
473
|
-
return String(value ?? "").replace(/\r\n?/g, "\n").trim();
|
|
474
|
-
}
|
|
475
|
-
function renderPinMarkdown(pin, number) {
|
|
476
|
-
const region = `${pin.region.x}, ${pin.region.y}, ${pin.region.w}, ${pin.region.h}`;
|
|
477
|
-
const lines = [
|
|
478
|
-
`### ${number}. ${pin.intent.toUpperCase()} \xB7 ${pin.status}`,
|
|
479
|
-
"",
|
|
480
|
-
`- Pin: \`${pin.id}\``,
|
|
481
|
-
`- Artifact: \`${pin.artifactId}\`${pin.variant ? ` \xB7 variant \`${pin.variant}\`` : ""}`,
|
|
482
|
-
`- Author: ${pin.author.name}`,
|
|
483
|
-
`- Region: \`${region}\` at ${pin.viewport.width}\xD7${pin.viewport.height}`,
|
|
484
|
-
...pin.anchor ? [
|
|
485
|
-
`- Anchor: \`${pin.anchor.planrId}\`${pin.anchor.screen ? ` \xB7 screen \`${pin.anchor.screen}\`` : ""}`
|
|
486
|
-
] : [],
|
|
487
|
-
`- Updated: ${pin.updatedAt}`,
|
|
488
|
-
"",
|
|
489
|
-
markdownText(pin.comment)
|
|
490
|
-
];
|
|
491
|
-
if (pin.replies.length > 0) {
|
|
492
|
-
lines.push("", "#### Thread", "");
|
|
493
|
-
for (const reply of pin.replies) {
|
|
494
|
-
lines.push(`- **${reply.author.name}** (${reply.createdAt}): ${markdownText(reply.comment)}`);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
return lines.join("\n");
|
|
498
|
-
}
|
|
499
|
-
function reviewMarkdown(review, headingLevel = 2, stale = false) {
|
|
500
|
-
const mark = "#".repeat(headingLevel);
|
|
501
|
-
const lines = [
|
|
502
|
-
`${mark} Review ${review.reviewId}${stale ? " \xB7 STALE" : ""}`,
|
|
503
|
-
"",
|
|
504
|
-
`- Digest: \`${review.reviewOf}\``,
|
|
505
|
-
`- Decision: **${review.decision}**`,
|
|
506
|
-
`- Pins: ${review.pins.length}`,
|
|
507
|
-
...review.createdAt ? [`- Created: ${review.createdAt}`] : [],
|
|
508
|
-
...review.updatedAt ? [`- Updated: ${review.updatedAt}`] : [],
|
|
509
|
-
"",
|
|
510
|
-
`${mark}# Overall feedback`,
|
|
511
|
-
"",
|
|
512
|
-
markdownText(review.overall) || "_No overall feedback._"
|
|
513
|
-
];
|
|
514
|
-
if (review.pins.length > 0) {
|
|
515
|
-
lines.push("", `${mark}# Pins`, "");
|
|
516
|
-
review.pins.forEach((pin, index) => lines.push(renderPinMarkdown(pin, index + 1), ""));
|
|
517
|
-
if (lines.at(-1) === "") lines.pop();
|
|
518
|
-
}
|
|
519
|
-
return lines.join("\n");
|
|
520
|
-
}
|
|
521
|
-
function exportArtifactReview(value, { format = "json" } = {}) {
|
|
522
|
-
let normalized;
|
|
523
|
-
try {
|
|
524
|
-
normalized = normalizedExportInput(value);
|
|
525
|
-
} catch (error) {
|
|
526
|
-
if (error instanceof PipelineError) throw error;
|
|
527
|
-
throw new PipelineError(
|
|
528
|
-
ARTIFACT_ERROR_CODES.REVIEW_EXPORT,
|
|
529
|
-
"Artifact review cannot be exported."
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
if (format === "json") return serializeReviewState(normalized);
|
|
533
|
-
if (format !== "markdown") {
|
|
534
|
-
throw new PipelineError(
|
|
535
|
-
ARTIFACT_ERROR_CODES.REVIEW_EXPORT,
|
|
536
|
-
"Artifact review export format must be json or markdown."
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
|
-
if (normalized.kind !== "artifact-review-state") {
|
|
540
|
-
return `# OpenPlanr Artifact Review
|
|
541
|
-
|
|
542
|
-
${reviewMarkdown(normalized)}
|
|
543
|
-
`;
|
|
544
|
-
}
|
|
545
|
-
const lines = [
|
|
546
|
-
"# OpenPlanr Artifact Reviews",
|
|
547
|
-
"",
|
|
548
|
-
`- Artifact: \`${normalized.artifactId}\``,
|
|
549
|
-
`- Current digest: \`${normalized.currentReviewOf}\``,
|
|
550
|
-
`- Effective decision: **${effectiveReviewDecision(normalized)}**`,
|
|
551
|
-
`- Reviews: ${normalized.reviews.length}`
|
|
552
|
-
];
|
|
553
|
-
for (const entry of normalized.reviews) {
|
|
554
|
-
lines.push("", reviewMarkdown(entry.review, 2, entry.stale));
|
|
555
|
-
}
|
|
556
|
-
return `${lines.join("\n")}
|
|
557
|
-
`;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// packages/artifact/lib/artifact/import.mjs
|
|
561
|
-
var ARTIFACT_ID_RE2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
562
|
-
function pathError(code, message2) {
|
|
563
|
-
throw new PipelineError(
|
|
564
|
-
code,
|
|
565
|
-
message2,
|
|
566
|
-
"Choose a real, non-symlinked project or user review destination."
|
|
567
|
-
);
|
|
568
|
-
}
|
|
569
|
-
function pathEntry(path, fs = { lstatSync: lstatSync2 }) {
|
|
570
|
-
try {
|
|
571
|
-
return fs.lstatSync(path);
|
|
572
|
-
} catch (error) {
|
|
573
|
-
if (error?.code === "ENOENT") return null;
|
|
574
|
-
throw error;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
function inside(base, candidate) {
|
|
578
|
-
const rel = relative(base, candidate);
|
|
579
|
-
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
580
|
-
}
|
|
581
|
-
function parseablePlanrConfig(root) {
|
|
582
|
-
const path = join(root, PLANNING_FOLDER, "config.json");
|
|
583
|
-
if (!existsSync2(path)) return false;
|
|
584
|
-
try {
|
|
585
|
-
if (lstatSync2(path).isSymbolicLink()) return false;
|
|
586
|
-
const value = JSON.parse(readFileSync2(path, "utf8"));
|
|
587
|
-
return value && typeof value === "object" && !Array.isArray(value) && (typeof value.projectName === "string" && value.projectName.trim() !== "" || value.idPrefix && typeof value.idPrefix === "object" && !Array.isArray(value.idPrefix) && Object.keys(value.idPrefix).length > 0);
|
|
588
|
-
} catch {
|
|
589
|
-
return false;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
function hasGitMarker(root) {
|
|
593
|
-
const marker = join(root, ".git");
|
|
594
|
-
if (!existsSync2(marker)) return false;
|
|
595
|
-
try {
|
|
596
|
-
const stat = lstatSync2(marker);
|
|
597
|
-
if (stat.isSymbolicLink()) return false;
|
|
598
|
-
if (stat.isDirectory()) {
|
|
599
|
-
const head = join(marker, "HEAD");
|
|
600
|
-
return existsSync2(head) && lstatSync2(head).isFile();
|
|
601
|
-
}
|
|
602
|
-
if (!stat.isFile() || stat.size > 4096) return false;
|
|
603
|
-
const match = /^gitdir:\s*(.+?)\s*$/u.exec(readFileSync2(marker, "utf8"));
|
|
604
|
-
if (!match) return false;
|
|
605
|
-
const gitDir = resolve(root, match[1]);
|
|
606
|
-
return existsSync2(gitDir) && statSync2(gitDir).isDirectory() && existsSync2(join(gitDir, "HEAD")) && lstatSync2(join(gitDir, "HEAD")).isFile();
|
|
607
|
-
} catch {
|
|
608
|
-
return false;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
function findArtifactProjectRoot(start = process.cwd(), { env = process.env } = {}) {
|
|
612
|
-
let current;
|
|
613
|
-
try {
|
|
614
|
-
current = realpathSync(resolve(start));
|
|
615
|
-
} catch {
|
|
616
|
-
pathError(
|
|
617
|
-
ARTIFACT_ERROR_CODES.REVIEW_IMPORT,
|
|
618
|
-
"Artifact review working directory does not exist."
|
|
619
|
-
);
|
|
620
|
-
}
|
|
621
|
-
const homeCandidate = resolve(env.HOME ?? homedir());
|
|
622
|
-
let home = homeCandidate;
|
|
623
|
-
try {
|
|
624
|
-
home = realpathSync(homeCandidate);
|
|
625
|
-
} catch {
|
|
626
|
-
}
|
|
627
|
-
while (true) {
|
|
628
|
-
if (current !== home && (parseablePlanrConfig(current) || hasGitMarker(current)))
|
|
629
|
-
return current;
|
|
630
|
-
const parent = dirname2(current);
|
|
631
|
-
if (parent === current) return null;
|
|
632
|
-
current = parent;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
function assertSafeDestination(base, relativeParts) {
|
|
636
|
-
const absoluteBase = resolve(base);
|
|
637
|
-
if (existsSync2(absoluteBase) && lstatSync2(absoluteBase).isSymbolicLink()) {
|
|
638
|
-
pathError(
|
|
639
|
-
ARTIFACT_ERROR_CODES.SYMLINK_ESCAPE,
|
|
640
|
-
"Artifact review destination base is a symlink."
|
|
641
|
-
);
|
|
642
|
-
}
|
|
643
|
-
let realBase = absoluteBase;
|
|
644
|
-
if (existsSync2(absoluteBase)) realBase = realpathSync(absoluteBase);
|
|
645
|
-
let current = absoluteBase;
|
|
646
|
-
for (const part of relativeParts) {
|
|
647
|
-
if (!part || part === "." || part === ".." || part.includes("/") || part.includes("\\") || part.includes("\0")) {
|
|
648
|
-
pathError(
|
|
649
|
-
ARTIFACT_ERROR_CODES.PATH_TRAVERSAL,
|
|
650
|
-
"Artifact review destination contains an unsafe segment."
|
|
651
|
-
);
|
|
652
|
-
}
|
|
653
|
-
current = join(current, part);
|
|
654
|
-
const entry = pathEntry(current);
|
|
655
|
-
if (!entry) continue;
|
|
656
|
-
if (entry.isSymbolicLink()) {
|
|
657
|
-
pathError(
|
|
658
|
-
ARTIFACT_ERROR_CODES.SYMLINK_ESCAPE,
|
|
659
|
-
"Artifact review destination contains a symlink."
|
|
660
|
-
);
|
|
661
|
-
}
|
|
662
|
-
if (!inside(realBase, realpathSync(current))) {
|
|
663
|
-
pathError(
|
|
664
|
-
ARTIFACT_ERROR_CODES.PATH_TRAVERSAL,
|
|
665
|
-
"Artifact review destination escapes its storage root."
|
|
666
|
-
);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
if (!inside(absoluteBase, current)) {
|
|
670
|
-
pathError(
|
|
671
|
-
ARTIFACT_ERROR_CODES.PATH_TRAVERSAL,
|
|
672
|
-
"Artifact review destination escapes its storage root."
|
|
673
|
-
);
|
|
674
|
-
}
|
|
675
|
-
return current;
|
|
676
|
-
}
|
|
677
|
-
function resolveArtifactReviewDestination({
|
|
678
|
-
cwd = process.cwd(),
|
|
679
|
-
env = process.env,
|
|
680
|
-
artifactId,
|
|
681
|
-
designDir
|
|
682
|
-
} = {}) {
|
|
683
|
-
if (!ARTIFACT_ID_RE2.test(artifactId ?? "")) {
|
|
684
|
-
pathError(
|
|
685
|
-
ARTIFACT_ERROR_CODES.PATH_TRAVERSAL,
|
|
686
|
-
"Artifact review ID is unsafe for local storage."
|
|
687
|
-
);
|
|
688
|
-
}
|
|
689
|
-
if (designDir !== void 0) {
|
|
690
|
-
const lexical = resolve(designDir);
|
|
691
|
-
if (!existsSync2(lexical) || !statSync2(lexical).isDirectory()) {
|
|
692
|
-
pathError(ARTIFACT_ERROR_CODES.REVIEW_IMPORT, "Design review destination does not exist.");
|
|
693
|
-
}
|
|
694
|
-
if (lstatSync2(lexical).isSymbolicLink()) {
|
|
695
|
-
pathError(
|
|
696
|
-
ARTIFACT_ERROR_CODES.SYMLINK_ESCAPE,
|
|
697
|
-
"Design review destination must be a real directory."
|
|
698
|
-
);
|
|
699
|
-
}
|
|
700
|
-
const requested = realpathSync(lexical);
|
|
701
|
-
return Object.freeze({
|
|
702
|
-
kind: "design",
|
|
703
|
-
artifactId,
|
|
704
|
-
directory: requested,
|
|
705
|
-
path: join(requested, FEEDBACK_FILE),
|
|
706
|
-
reviewStatePath: join(requested, "artifact-review-state.json")
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
const projectRoot = findArtifactProjectRoot(cwd, { env });
|
|
710
|
-
if (projectRoot) {
|
|
711
|
-
const directory2 = assertSafeDestination(projectRoot, [
|
|
712
|
-
PLANNING_FOLDER,
|
|
713
|
-
"artifacts",
|
|
714
|
-
artifactId
|
|
715
|
-
]);
|
|
716
|
-
return Object.freeze({
|
|
717
|
-
kind: "project",
|
|
718
|
-
artifactId,
|
|
719
|
-
root: projectRoot,
|
|
720
|
-
directory: directory2,
|
|
721
|
-
path: join(directory2, "review-state.json")
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
const root = resolve(planrHome(env));
|
|
725
|
-
const directory = assertSafeDestination(root, ["artifacts", artifactId]);
|
|
726
|
-
return Object.freeze({
|
|
727
|
-
kind: "user",
|
|
728
|
-
artifactId,
|
|
729
|
-
root,
|
|
730
|
-
directory,
|
|
731
|
-
path: join(directory, "review-state.json")
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
function reviewsFromDecoded(value) {
|
|
735
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
736
|
-
throw new PipelineError(
|
|
737
|
-
ARTIFACT_ERROR_CODES.REVIEW_IMPORT,
|
|
738
|
-
"Decoded review payload is not an object."
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
if (value.kind === "artifact-review-state") {
|
|
742
|
-
validateReviewLedger(value);
|
|
743
|
-
return value.reviews.map((entry) => ({ review: entry.review, stale: entry.stale }));
|
|
744
|
-
}
|
|
745
|
-
if (value.artifacts && value.viewer) {
|
|
746
|
-
validateArtifactEnvelope(value);
|
|
747
|
-
if (!value.review) {
|
|
748
|
-
throw new PipelineError(
|
|
749
|
-
ARTIFACT_ERROR_CODES.REVIEW_IMPORT,
|
|
750
|
-
"Decoded artifact envelope contains no review."
|
|
751
|
-
);
|
|
752
|
-
}
|
|
753
|
-
return [{ review: value.review, stale: false }];
|
|
754
|
-
}
|
|
755
|
-
if (value.review && !value.reviewId) return reviewsFromDecoded(value.review);
|
|
756
|
-
validateArtifactReview(value);
|
|
757
|
-
return [{ review: value, stale: false }];
|
|
758
|
-
}
|
|
759
|
-
async function decodeArtifactReviewSources(sources, { decodeSource, withMetadata = false } = {}) {
|
|
760
|
-
const list2 = Array.isArray(sources) ? sources : [sources];
|
|
761
|
-
if (list2.length === 0) {
|
|
762
|
-
throw new PipelineError(
|
|
763
|
-
ARTIFACT_ERROR_CODES.REVIEW_IMPORT,
|
|
764
|
-
"At least one artifact review source is required."
|
|
765
|
-
);
|
|
766
|
-
}
|
|
767
|
-
const entries = [];
|
|
768
|
-
for (let index = 0; index < list2.length; index += 1) {
|
|
769
|
-
const source = list2[index];
|
|
770
|
-
let decoded = source;
|
|
771
|
-
if (typeof source === "string") {
|
|
772
|
-
if (typeof decodeSource !== "function") {
|
|
773
|
-
throw new PipelineError(
|
|
774
|
-
ARTIFACT_ERROR_CODES.REVIEW_DECODER_REQUIRED,
|
|
775
|
-
"String and URL review sources require an injected async decoder.",
|
|
776
|
-
"Provide decodeSource(source, { index }); the review lifecycle has no transport dependency.",
|
|
777
|
-
{ sourceIndex: index }
|
|
778
|
-
);
|
|
779
|
-
}
|
|
780
|
-
try {
|
|
781
|
-
decoded = await decodeSource(source, { index });
|
|
782
|
-
} catch {
|
|
783
|
-
throw new PipelineError(
|
|
784
|
-
ARTIFACT_ERROR_CODES.REVIEW_IMPORT,
|
|
785
|
-
"Artifact review source could not be decoded.",
|
|
786
|
-
"Verify the immutable review link and retry.",
|
|
787
|
-
{ sourceIndex: index }
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
try {
|
|
792
|
-
entries.push(
|
|
793
|
-
...reviewsFromDecoded(decoded).map((entry) => ({
|
|
794
|
-
review: structuredClone(entry.review),
|
|
795
|
-
stale: Boolean(entry.stale)
|
|
796
|
-
}))
|
|
797
|
-
);
|
|
798
|
-
} catch (error) {
|
|
799
|
-
if (error instanceof PipelineError && error.code === ARTIFACT_ERROR_CODES.REVIEW_IMPORT) {
|
|
800
|
-
error.details = { sourceIndex: index };
|
|
801
|
-
throw error;
|
|
802
|
-
}
|
|
803
|
-
throw new PipelineError(
|
|
804
|
-
ARTIFACT_ERROR_CODES.REVIEW_IMPORT,
|
|
805
|
-
"Decoded artifact review does not satisfy the Protocol v1.1 contract.",
|
|
806
|
-
"",
|
|
807
|
-
{ sourceIndex: index }
|
|
808
|
-
);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
return withMetadata ? entries : entries.map((entry) => entry.review);
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// packages/design/lib/design/feedback-reader.mjs
|
|
815
|
-
var designReviewKey = (document) => `design-${hash(document.id).slice(0, 24)}`;
|
|
816
|
-
function designReviewPath(file, env = process.env, options = {}) {
|
|
817
|
-
const { root, document } = currentDesign(file, options);
|
|
818
|
-
return resolveArtifactReviewDestination({
|
|
819
|
-
cwd: root,
|
|
820
|
-
env,
|
|
821
|
-
artifactId: designReviewKey(document)
|
|
822
|
-
}).path;
|
|
823
|
-
}
|
|
824
|
-
function readDesignFeedback(file, env = process.env, options = {}) {
|
|
825
|
-
const current = currentDesign(file, options);
|
|
826
|
-
const ledger = readArtifactReviewState(designReviewPath(file, env, options), {
|
|
827
|
-
allowMissing: true
|
|
828
|
-
});
|
|
829
|
-
const digest2 = digestArtifactEnvelope(current.envelope);
|
|
830
|
-
const pins = (ledger?.reviews ?? []).flatMap(
|
|
831
|
-
(entry) => entry.review.pins.map((pin) => {
|
|
832
|
-
const target = current.entries.find((item) => item.artifactId === pin.artifactId);
|
|
833
|
-
const screen = target && current.document.screens.find((item) => item.id === target.screenId);
|
|
834
|
-
const anchorMissing = pin.anchor?.planrId && screen?.anchors?.length && !screen.anchors.includes(pin.anchor.planrId) && pin.anchor.planrId !== screen.id;
|
|
835
|
-
return {
|
|
836
|
-
...pin,
|
|
837
|
-
reviewId: entry.review.reviewId,
|
|
838
|
-
reviewOf: entry.review.reviewOf,
|
|
839
|
-
...entry.review.reviewId.startsWith("shared-") ? { revisionId: entry.review.reviewId.slice(7) } : {},
|
|
840
|
-
stale: entry.stale || entry.review.reviewOf !== digest2 || !target || Boolean(anchorMissing),
|
|
841
|
-
...target ? { screenId: target.screenId, variantId: target.variantId } : {}
|
|
842
|
-
};
|
|
843
|
-
})
|
|
844
|
-
);
|
|
845
|
-
return {
|
|
846
|
-
revision: current.revision,
|
|
847
|
-
reviewPath: designReviewPath(file, env, options),
|
|
848
|
-
pins,
|
|
849
|
-
state: readJson(join2(current.root, ".design/studio-state.json"), {
|
|
850
|
-
state: {},
|
|
851
|
-
stateVersion: 0
|
|
852
|
-
}).state,
|
|
853
|
-
ledger,
|
|
854
|
-
shared: readJson(join2(current.root, ".design/shared-feedback.json"), null)
|
|
855
|
-
};
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// packages/design/lib/design/handoff-reader.mjs
|
|
859
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
860
|
-
import { dirname as dirname3, join as join3 } from "node:path";
|
|
861
|
-
|
|
862
|
-
// packages/design/lib/design/handoff-format.mjs
|
|
863
|
-
var safeMd = (text2) => String(text2).replaceAll("[", "\\[").replaceAll("]", "\\]").replaceAll("<", "<").replaceAll(">", ">");
|
|
864
|
-
function renderMarkdown(draft, title2) {
|
|
865
|
-
const lines = [
|
|
866
|
-
`# ${safeMd(title2)} \u2014 review handoff`,
|
|
867
|
-
"",
|
|
868
|
-
`Status: ${draft.status}. Source revision: ${draft.basis.sourceRevision}.`,
|
|
869
|
-
"",
|
|
870
|
-
draft.content.summary || "Owner summary has not been written.",
|
|
871
|
-
""
|
|
872
|
-
];
|
|
873
|
-
for (const [key, label] of [
|
|
874
|
-
["agreedChanges", "Agreed changes"],
|
|
875
|
-
["openQuestions", "Open questions"],
|
|
876
|
-
["deferred", "Deferred"],
|
|
877
|
-
["rejected", "Rejected"]
|
|
878
|
-
]) {
|
|
879
|
-
lines.push(`## ${label}`, "");
|
|
880
|
-
for (const item of draft.content[key]) {
|
|
881
|
-
lines.push(
|
|
882
|
-
`- ${safeMd(item.refinement ?? item.text)} \u2014 ${safeMd(item.author ?? "Reviewer")}${item.stale ? " \xB7 original revision" : ""} [source comment](${item.source})`
|
|
883
|
-
);
|
|
884
|
-
if (item.refinement) lines.push(` Original comment: ${safeMd(item.text)}`);
|
|
885
|
-
}
|
|
886
|
-
if (!draft.content[key].length) lines.push("None recorded.");
|
|
887
|
-
lines.push("");
|
|
888
|
-
}
|
|
889
|
-
lines.push(
|
|
890
|
-
"## Overall review notes",
|
|
891
|
-
"",
|
|
892
|
-
...draft.reviewNotes.map((note) => `- ${safeMd(note.text)} (${safeMd(note.reviewId)})`),
|
|
893
|
-
""
|
|
894
|
-
);
|
|
895
|
-
lines.push(
|
|
896
|
-
"## Verification gaps",
|
|
897
|
-
"",
|
|
898
|
-
...draft.verificationGaps.length ? draft.verificationGaps.map((value) => `- ${safeMd(value)}`) : ["None recorded."],
|
|
899
|
-
"",
|
|
900
|
-
"Plan and Ship remain separate user invocations.",
|
|
901
|
-
""
|
|
902
|
-
);
|
|
903
|
-
return lines.join("\n");
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
// packages/protocol/src/design-handoff-contracts.mjs
|
|
907
|
-
var DESIGN_HANDOFF_PROTOCOL_VERSION = "1.11.0";
|
|
908
|
-
var DESIGN_HANDOFF_CONTRACT_VERSION = "1.0.0";
|
|
909
|
-
var DESIGN_HANDOFF_AUTHORITY = "prepare-plan";
|
|
910
|
-
var DESIGN_HANDOFF_CHECK_IDS = Object.freeze([
|
|
911
|
-
"current-revision",
|
|
912
|
-
"selected-direction",
|
|
913
|
-
"design-specification",
|
|
914
|
-
"rendered-verification",
|
|
915
|
-
"review-freshness",
|
|
916
|
-
"review-dispositions",
|
|
917
|
-
"unresolved-blockers",
|
|
918
|
-
"approved-review-handoff"
|
|
919
|
-
]);
|
|
920
|
-
var DESIGN_HANDOFF_SOURCE_KINDS = Object.freeze([
|
|
921
|
-
"design-revision",
|
|
922
|
-
"selected-direction",
|
|
923
|
-
"design-specification",
|
|
924
|
-
"rendered-verification",
|
|
925
|
-
"review-context",
|
|
926
|
-
"review-feedback",
|
|
927
|
-
"review-metadata",
|
|
928
|
-
"review-handoff",
|
|
929
|
-
"screen",
|
|
930
|
-
"frame",
|
|
931
|
-
"component",
|
|
932
|
-
"state",
|
|
933
|
-
"flow",
|
|
934
|
-
"token",
|
|
935
|
-
"review-decision",
|
|
936
|
-
"element-anchor"
|
|
937
|
-
]);
|
|
938
|
-
var DESIGN_HANDOFF_REQUIREMENT_KINDS = Object.freeze([
|
|
939
|
-
"behavior",
|
|
940
|
-
"visual-state",
|
|
941
|
-
"responsive",
|
|
942
|
-
"accessibility",
|
|
943
|
-
"content-data-assumption",
|
|
944
|
-
"constraint",
|
|
945
|
-
"verification-intent"
|
|
946
|
-
]);
|
|
947
|
-
var DESIGN_HANDOFF_CONTRACT_FILES = Object.freeze({
|
|
948
|
-
"design-handoff-readiness": "design-handoff-readiness.schema.json",
|
|
949
|
-
"design-implementation-handoff": "design-implementation-handoff.schema.json",
|
|
950
|
-
"design-planning-lineage": "design-planning-lineage.schema.json"
|
|
951
|
-
});
|
|
952
|
-
var text = { type: "string", minLength: 1, maxLength: 16384 };
|
|
953
|
-
var title = { ...text, maxLength: 240 };
|
|
954
|
-
var id = {
|
|
955
|
-
type: "string",
|
|
956
|
-
minLength: 1,
|
|
957
|
-
maxLength: 160,
|
|
958
|
-
pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]*$"
|
|
959
|
-
};
|
|
960
|
-
var digest = { type: "string", pattern: "^sha256:[a-f0-9]{64}$" };
|
|
961
|
-
var relativePath = { type: "string", minLength: 1, maxLength: 4096 };
|
|
962
|
-
var timestamp = { type: "string", format: "date-time" };
|
|
963
|
-
var list = (items, maxItems = 1e3) => ({ type: "array", items, maxItems });
|
|
964
|
-
var closed = (properties, required = Object.keys(properties)) => ({
|
|
965
|
-
type: "object",
|
|
966
|
-
additionalProperties: false,
|
|
967
|
-
properties,
|
|
968
|
-
required
|
|
969
|
-
});
|
|
970
|
-
var contract = (name, body) => ({
|
|
971
|
-
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
972
|
-
$id: `https://openplanr.dev/schemas/v1.11.0/${name}.schema.json`,
|
|
973
|
-
"x-openplanr-contract": { id: name, version: DESIGN_HANDOFF_PROTOCOL_VERSION },
|
|
974
|
-
...body
|
|
975
|
-
});
|
|
976
|
-
var action = closed({ id, label: title });
|
|
977
|
-
var anchor = {
|
|
978
|
-
oneOf: [
|
|
979
|
-
closed({ section: id }),
|
|
980
|
-
closed({ screenId: id }),
|
|
981
|
-
closed({ screenId: id, elementId: id }),
|
|
982
|
-
closed({ reviewId: id, pinId: id })
|
|
983
|
-
]
|
|
984
|
-
};
|
|
985
|
-
var evidenceReference = closed(
|
|
986
|
-
{
|
|
987
|
-
id,
|
|
988
|
-
kind: { enum: DESIGN_HANDOFF_SOURCE_KINDS },
|
|
989
|
-
path: relativePath,
|
|
990
|
-
revision: digest,
|
|
991
|
-
digest,
|
|
992
|
-
anchor
|
|
993
|
-
},
|
|
994
|
-
["id", "kind", "path"]
|
|
995
|
-
);
|
|
996
|
-
var readinessCheck = closed(
|
|
997
|
-
{
|
|
998
|
-
id: { enum: DESIGN_HANDOFF_CHECK_IDS },
|
|
999
|
-
status: { enum: ["pass", "attention", "blocked", "stale"] },
|
|
1000
|
-
message: title,
|
|
1001
|
-
evidenceRefs: list(id, 64),
|
|
1002
|
-
recoveryAction: action
|
|
1003
|
-
},
|
|
1004
|
-
["id", "status", "message", "evidenceRefs"]
|
|
1005
|
-
);
|
|
1006
|
-
var readinessRecord = closed({
|
|
1007
|
-
kind: { const: "openplanr-design-handoff-readiness" },
|
|
1008
|
-
schemaVersion: { const: DESIGN_HANDOFF_CONTRACT_VERSION },
|
|
1009
|
-
scope: { const: "design-originated" },
|
|
1010
|
-
authority: { const: "none" },
|
|
1011
|
-
designId: id,
|
|
1012
|
-
sourceRevision: { anyOf: [digest, { type: "null" }] },
|
|
1013
|
-
selectedVariant: { anyOf: [id, { type: "null" }] },
|
|
1014
|
-
status: { enum: ["ready", "attention", "blocked", "stale"] },
|
|
1015
|
-
continuation: closed({
|
|
1016
|
-
action: { const: DESIGN_HANDOFF_AUTHORITY },
|
|
1017
|
-
available: { type: "boolean" }
|
|
1018
|
-
}),
|
|
1019
|
-
checks: list(readinessCheck, DESIGN_HANDOFF_CHECK_IDS.length),
|
|
1020
|
-
evidence: list(evidenceReference, 1e4),
|
|
1021
|
-
blockers: list({ enum: DESIGN_HANDOFF_CHECK_IDS }, DESIGN_HANDOFF_CHECK_IDS.length),
|
|
1022
|
-
nextActions: list(action, DESIGN_HANDOFF_CHECK_IDS.length)
|
|
1023
|
-
});
|
|
1024
|
-
var readinessAbsence = closed({
|
|
1025
|
-
kind: { const: "openplanr-design-handoff-readiness-absence" },
|
|
1026
|
-
schemaVersion: { const: DESIGN_HANDOFF_CONTRACT_VERSION },
|
|
1027
|
-
status: { const: "absent" },
|
|
1028
|
-
reason: { enum: ["not-computed", "not-applicable", "unavailable"] },
|
|
1029
|
-
message: title,
|
|
1030
|
-
nextAction: action
|
|
1031
|
-
});
|
|
1032
|
-
var DESIGN_HANDOFF_READINESS_SCHEMA = contract("design-handoff-readiness", {
|
|
1033
|
-
oneOf: [readinessRecord, readinessAbsence]
|
|
1034
|
-
});
|
|
1035
|
-
var requirement = closed({
|
|
1036
|
-
id: { type: "string", pattern: "^REQ-[0-9]{3,}$" },
|
|
1037
|
-
kind: { enum: DESIGN_HANDOFF_REQUIREMENT_KINDS },
|
|
1038
|
-
statement: text,
|
|
1039
|
-
sourceRefs: { ...list(id, 256), minItems: 1 },
|
|
1040
|
-
verification: { ...list(text, 256), minItems: 1 }
|
|
1041
|
-
});
|
|
1042
|
-
var implementationHandoff = closed(
|
|
1043
|
-
{
|
|
1044
|
-
kind: { const: "openplanr-design-implementation-handoff" },
|
|
1045
|
-
schemaVersion: { const: DESIGN_HANDOFF_CONTRACT_VERSION },
|
|
1046
|
-
id,
|
|
1047
|
-
version: { type: "integer", minimum: 1 },
|
|
1048
|
-
status: { enum: ["draft", "approved", "superseded", "revoked"] },
|
|
1049
|
-
authority: { const: DESIGN_HANDOFF_AUTHORITY },
|
|
1050
|
-
title,
|
|
1051
|
-
basis: closed({
|
|
1052
|
-
designId: id,
|
|
1053
|
-
sourceRevision: digest,
|
|
1054
|
-
selectedVariant: id,
|
|
1055
|
-
readiness: closed({ status: { enum: ["ready", "attention", "blocked", "stale"] }, digest }),
|
|
1056
|
-
reviewHandoff: closed({ version: { type: "integer", minimum: 1 }, contentDigest: digest })
|
|
1057
|
-
}),
|
|
1058
|
-
sources: list(evidenceReference, 1e4),
|
|
1059
|
-
requirements: { ...list(requirement, 1e4), minItems: 1 },
|
|
1060
|
-
contentDigest: digest,
|
|
1061
|
-
markdown: { type: "string", maxLength: 2097152 },
|
|
1062
|
-
approval: closed({
|
|
1063
|
-
actorId: id,
|
|
1064
|
-
approvedAt: timestamp,
|
|
1065
|
-
contentDigest: digest,
|
|
1066
|
-
authority: { const: DESIGN_HANDOFF_AUTHORITY }
|
|
1067
|
-
}),
|
|
1068
|
-
supersededBy: closed({ id, version: { type: "integer", minimum: 1 }, contentDigest: digest }),
|
|
1069
|
-
revocation: closed({ actorId: id, revokedAt: timestamp, reason: text })
|
|
1070
|
-
},
|
|
1071
|
-
[
|
|
1072
|
-
"kind",
|
|
1073
|
-
"schemaVersion",
|
|
1074
|
-
"id",
|
|
1075
|
-
"version",
|
|
1076
|
-
"status",
|
|
1077
|
-
"authority",
|
|
1078
|
-
"title",
|
|
1079
|
-
"basis",
|
|
1080
|
-
"sources",
|
|
1081
|
-
"requirements",
|
|
1082
|
-
"contentDigest",
|
|
1083
|
-
"markdown"
|
|
1084
|
-
]
|
|
1085
|
-
);
|
|
1086
|
-
implementationHandoff.allOf = [
|
|
1087
|
-
{
|
|
1088
|
-
if: { properties: { status: { const: "approved" } }, required: ["status"] },
|
|
1089
|
-
then: {
|
|
1090
|
-
required: ["approval"],
|
|
1091
|
-
not: { anyOf: [{ required: ["supersededBy"] }, { required: ["revocation"] }] }
|
|
1092
|
-
}
|
|
1093
|
-
},
|
|
1094
|
-
{
|
|
1095
|
-
if: { properties: { status: { const: "superseded" } }, required: ["status"] },
|
|
1096
|
-
then: { required: ["approval", "supersededBy"], not: { required: ["revocation"] } }
|
|
1097
|
-
},
|
|
1098
|
-
{
|
|
1099
|
-
if: { properties: { status: { const: "revoked" } }, required: ["status"] },
|
|
1100
|
-
then: { required: ["approval", "revocation"], not: { required: ["supersededBy"] } }
|
|
1101
|
-
},
|
|
1102
|
-
{
|
|
1103
|
-
if: { properties: { status: { const: "draft" } }, required: ["status"] },
|
|
1104
|
-
then: {
|
|
1105
|
-
not: {
|
|
1106
|
-
anyOf: [
|
|
1107
|
-
{ required: ["approval"] },
|
|
1108
|
-
{ required: ["supersededBy"] },
|
|
1109
|
-
{ required: ["revocation"] }
|
|
1110
|
-
]
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
];
|
|
1115
|
-
var DESIGN_IMPLEMENTATION_HANDOFF_SCHEMA = contract(
|
|
1116
|
-
"design-implementation-handoff",
|
|
1117
|
-
implementationHandoff
|
|
1118
|
-
);
|
|
1119
|
-
var lineageMapping = closed({
|
|
1120
|
-
requirementId: { type: "string", pattern: "^REQ-[0-9]{3,}$" },
|
|
1121
|
-
acceptanceRefs: {
|
|
1122
|
-
...list(
|
|
1123
|
-
closed({
|
|
1124
|
-
storyId: { type: "string", pattern: "^US-[0-9]{3,}$" },
|
|
1125
|
-
acceptanceId: { type: "string", pattern: "^AC-[0-9]{3,}$" }
|
|
1126
|
-
}),
|
|
1127
|
-
256
|
|
1128
|
-
),
|
|
1129
|
-
minItems: 1
|
|
1130
|
-
},
|
|
1131
|
-
taskIds: { ...list({ type: "string", pattern: "^T-[0-9]{3,}$" }, 256), minItems: 1 }
|
|
1132
|
-
});
|
|
1133
|
-
var DESIGN_PLANNING_LINEAGE_SCHEMA = contract(
|
|
1134
|
-
"design-planning-lineage",
|
|
1135
|
-
closed({
|
|
1136
|
-
kind: { const: "openplanr-design-planning-lineage" },
|
|
1137
|
-
schemaVersion: { const: DESIGN_HANDOFF_CONTRACT_VERSION },
|
|
1138
|
-
handoff: closed({ id, version: { type: "integer", minimum: 1 }, contentDigest: digest }),
|
|
1139
|
-
specId: { type: "string", pattern: "^SPEC-[0-9]{3,}$" },
|
|
1140
|
-
mappings: { ...list(lineageMapping, 1e4), minItems: 1 }
|
|
1141
|
-
})
|
|
1142
|
-
);
|
|
1143
|
-
var DESIGN_HANDOFF_SCHEMAS = deepFreeze({
|
|
1144
|
-
"design-handoff-readiness": DESIGN_HANDOFF_READINESS_SCHEMA,
|
|
1145
|
-
"design-implementation-handoff": DESIGN_IMPLEMENTATION_HANDOFF_SCHEMA,
|
|
1146
|
-
"design-planning-lineage": DESIGN_PLANNING_LINEAGE_SCHEMA
|
|
1147
|
-
});
|
|
1148
|
-
function distinct(items, select, label) {
|
|
1149
|
-
const values = items.map(select);
|
|
1150
|
-
if (new Set(values).size !== values.length) throw new TypeError(`Duplicate ${label}.`);
|
|
1151
|
-
}
|
|
1152
|
-
function isDesignHandoffRelativePath(value) {
|
|
1153
|
-
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(
|
|
1154
|
-
(part) => part && part !== "." && part !== ".." && !["__proto__", "prototype", "constructor"].includes(part)
|
|
1155
|
-
);
|
|
1156
|
-
}
|
|
1157
|
-
function assertDesignHandoffContract(value, schemaOrName) {
|
|
1158
|
-
const schema = typeof schemaOrName === "string" ? DESIGN_HANDOFF_SCHEMAS[schemaOrName] : schemaOrName;
|
|
1159
|
-
if (!schema) throw new TypeError("Unknown design handoff contract.");
|
|
1160
|
-
assertPlainData(value, "Design handoff data");
|
|
1161
|
-
canonicalizeJson(value);
|
|
1162
|
-
const errors = validateJson(value, schema);
|
|
1163
|
-
if (errors.length)
|
|
1164
|
-
throw new TypeError(
|
|
1165
|
-
`Invalid ${schema["x-openplanr-contract"]?.id ?? "design handoff data"}: ${errors.slice(0, 5).map((error) => `${error.path} (${error.rule})`).join("; ")}`
|
|
1166
|
-
);
|
|
1167
|
-
return value;
|
|
1168
|
-
}
|
|
1169
|
-
function assertEvidenceReferences(evidence2) {
|
|
1170
|
-
distinct(evidence2, (item) => item.id, "evidence reference identity");
|
|
1171
|
-
for (const item of evidence2)
|
|
1172
|
-
if (!isDesignHandoffRelativePath(item.path))
|
|
1173
|
-
throw new TypeError("Design handoff evidence requires a repository-relative logical path.");
|
|
1174
|
-
}
|
|
1175
|
-
function assertProductCopy(value) {
|
|
1176
|
-
if (/\b(?:hash|digest|checksum|sha[- ]?256|canonical(?:ize|ization)?)\b/iu.test(value))
|
|
1177
|
-
throw new TypeError("Design readiness guidance must use product language.");
|
|
1178
|
-
}
|
|
1179
|
-
function assertDesignHandoffReadiness(value) {
|
|
1180
|
-
assertDesignHandoffContract(value, DESIGN_HANDOFF_READINESS_SCHEMA);
|
|
1181
|
-
if (value.kind.endsWith("-absence")) {
|
|
1182
|
-
assertProductCopy(`${value.message} ${value.nextAction.label}`);
|
|
1183
|
-
return value;
|
|
1184
|
-
}
|
|
1185
|
-
assertEvidenceReferences(value.evidence);
|
|
1186
|
-
distinct(value.checks, (item) => item.id, "readiness check identity");
|
|
1187
|
-
const expected = DESIGN_HANDOFF_CHECK_IDS.join("\n");
|
|
1188
|
-
if (value.checks.map((item) => item.id).join("\n") !== expected)
|
|
1189
|
-
throw new TypeError("Design readiness must contain every stable check in canonical order.");
|
|
1190
|
-
const evidenceIds = new Set(value.evidence.map((item) => item.id));
|
|
1191
|
-
for (const check of value.checks) {
|
|
1192
|
-
distinct(check.evidenceRefs, (item) => item, "readiness evidence reference");
|
|
1193
|
-
if (check.evidenceRefs.some((reference) => !evidenceIds.has(reference)))
|
|
1194
|
-
throw new TypeError("Design readiness references missing evidence.");
|
|
1195
|
-
assertProductCopy(`${check.message} ${check.recoveryAction?.label ?? ""}`);
|
|
1196
|
-
}
|
|
1197
|
-
const priority2 = { pass: 0, attention: 1, blocked: 2, stale: 3 };
|
|
1198
|
-
const worst = value.checks.reduce(
|
|
1199
|
-
(current, check) => priority2[check.status] > priority2[current] ? check.status : current,
|
|
1200
|
-
"pass"
|
|
1201
|
-
);
|
|
1202
|
-
const expectedStatus = worst === "pass" ? "ready" : worst;
|
|
1203
|
-
if (value.status !== expectedStatus)
|
|
1204
|
-
throw new TypeError("Design readiness summary does not match its checks.");
|
|
1205
|
-
const blockingIds = value.checks.filter((check) => ["blocked", "stale"].includes(check.status)).map((check) => check.id);
|
|
1206
|
-
if (value.blockers.join("\n") !== blockingIds.join("\n"))
|
|
1207
|
-
throw new TypeError("Design readiness blockers do not match its blocking checks.");
|
|
1208
|
-
const actionable = value.checks.filter((check) => check.status !== "pass");
|
|
1209
|
-
if (value.nextActions.length !== actionable.length || value.nextActions.some((actionValue, index) => {
|
|
1210
|
-
const recoveryAction = actionable[index].recoveryAction;
|
|
1211
|
-
return actionValue.id !== recoveryAction?.id || actionValue.label !== recoveryAction?.label;
|
|
1212
|
-
}))
|
|
1213
|
-
throw new TypeError("Design readiness next actions do not match its checks.");
|
|
1214
|
-
if (value.continuation.available !== ["ready", "attention"].includes(value.status))
|
|
1215
|
-
throw new TypeError("Design readiness continuation availability does not match its status.");
|
|
1216
|
-
return value;
|
|
1217
|
-
}
|
|
1218
|
-
function designImplementationHandoffDigest(value) {
|
|
1219
|
-
const projection = {
|
|
1220
|
-
kind: value.kind,
|
|
1221
|
-
schemaVersion: value.schemaVersion,
|
|
1222
|
-
id: value.id,
|
|
1223
|
-
version: value.version,
|
|
1224
|
-
authority: value.authority,
|
|
1225
|
-
title: value.title,
|
|
1226
|
-
basis: value.basis,
|
|
1227
|
-
sources: value.sources,
|
|
1228
|
-
requirements: value.requirements,
|
|
1229
|
-
markdown: value.markdown
|
|
1230
|
-
};
|
|
1231
|
-
return `sha256:${sha256Hex(canonicalizeJson(projection))}`;
|
|
1232
|
-
}
|
|
1233
|
-
function assertDesignImplementationHandoff(value) {
|
|
1234
|
-
assertDesignHandoffContract(value, DESIGN_IMPLEMENTATION_HANDOFF_SCHEMA);
|
|
1235
|
-
assertEvidenceReferences(value.sources);
|
|
1236
|
-
distinct(value.requirements, (item) => item.id, "implementation requirement identity");
|
|
1237
|
-
const sourceIds = new Set(value.sources.map((item) => item.id));
|
|
1238
|
-
for (const item of value.requirements) {
|
|
1239
|
-
distinct(item.sourceRefs, (reference) => reference, "requirement source reference");
|
|
1240
|
-
if (item.sourceRefs.some((reference) => !sourceIds.has(reference)))
|
|
1241
|
-
throw new TypeError("Implementation requirement references missing evidence.");
|
|
1242
|
-
}
|
|
1243
|
-
if (value.contentDigest !== designImplementationHandoffDigest(value))
|
|
1244
|
-
throw new TypeError(
|
|
1245
|
-
"Implementation handoff content does not match its recorded integrity value."
|
|
1246
|
-
);
|
|
1247
|
-
if (value.approval?.contentDigest !== void 0 && value.approval.contentDigest !== value.contentDigest)
|
|
1248
|
-
throw new TypeError("Implementation handoff approval does not match its content.");
|
|
1249
|
-
if (value.status !== "draft" && value.basis.readiness.status !== "ready")
|
|
1250
|
-
throw new TypeError(
|
|
1251
|
-
"Only a ready implementation handoff can be approved or retained as approved history."
|
|
1252
|
-
);
|
|
1253
|
-
if (value.supersededBy && value.supersededBy.id === value.id && value.supersededBy.version <= value.version)
|
|
1254
|
-
throw new TypeError("A superseding handoff must identify a newer package version.");
|
|
1255
|
-
return value;
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
// packages/design/lib/design/handoff-readiness.mjs
|
|
1259
|
-
var CHECKS = Object.freeze([
|
|
1260
|
-
"current-revision",
|
|
1261
|
-
"selected-direction",
|
|
1262
|
-
"design-specification",
|
|
1263
|
-
"rendered-verification",
|
|
1264
|
-
"review-freshness",
|
|
1265
|
-
"review-dispositions",
|
|
1266
|
-
"unresolved-blockers",
|
|
1267
|
-
"approved-review-handoff"
|
|
1268
|
-
]);
|
|
1269
|
-
var action2 = Object.freeze({
|
|
1270
|
-
"current-revision": Object.freeze({ id: "render-design", label: "Render the current design" }),
|
|
1271
|
-
"selected-direction": Object.freeze({
|
|
1272
|
-
id: "select-direction",
|
|
1273
|
-
label: "Choose one ready direction"
|
|
1274
|
-
}),
|
|
1275
|
-
"design-specification": Object.freeze({
|
|
1276
|
-
id: "complete-design-specification",
|
|
1277
|
-
label: "Complete the design specification"
|
|
1278
|
-
}),
|
|
1279
|
-
"rendered-verification": Object.freeze({
|
|
1280
|
-
id: "verify-rendered-design",
|
|
1281
|
-
label: "Verify the rendered design"
|
|
1282
|
-
}),
|
|
1283
|
-
"review-freshness": Object.freeze({
|
|
1284
|
-
id: "refresh-review",
|
|
1285
|
-
label: "Refresh review against the current design"
|
|
1286
|
-
}),
|
|
1287
|
-
"review-dispositions": Object.freeze({
|
|
1288
|
-
id: "resolve-review-decisions",
|
|
1289
|
-
label: "Record the remaining review decisions"
|
|
1290
|
-
}),
|
|
1291
|
-
"unresolved-blockers": Object.freeze({
|
|
1292
|
-
id: "resolve-review-blockers",
|
|
1293
|
-
label: "Resolve the blocking feedback"
|
|
1294
|
-
}),
|
|
1295
|
-
"approved-review-handoff": Object.freeze({
|
|
1296
|
-
id: "approve-review-handoff",
|
|
1297
|
-
label: "Prepare and approve the current review handoff"
|
|
1298
|
-
})
|
|
1299
|
-
});
|
|
1300
|
-
var message = Object.freeze({
|
|
1301
|
-
"current-revision": Object.freeze({
|
|
1302
|
-
pass: "The current design has a completed render.",
|
|
1303
|
-
blocked: "Render the design before preparing work for engineering."
|
|
1304
|
-
}),
|
|
1305
|
-
"selected-direction": Object.freeze({
|
|
1306
|
-
pass: "One ready design direction is selected.",
|
|
1307
|
-
blocked: "Choose one ready design direction before continuing.",
|
|
1308
|
-
stale: "The selected direction changed after the review handoff was prepared."
|
|
1309
|
-
}),
|
|
1310
|
-
"design-specification": Object.freeze({
|
|
1311
|
-
pass: "The design specification is complete for this revision.",
|
|
1312
|
-
blocked: "Complete the design specification before continuing.",
|
|
1313
|
-
stale: "The design specification belongs to an earlier revision."
|
|
1314
|
-
}),
|
|
1315
|
-
"rendered-verification": Object.freeze({
|
|
1316
|
-
pass: "The rendered design is verified for this revision.",
|
|
1317
|
-
blocked: "Complete rendered design verification before continuing.",
|
|
1318
|
-
stale: "Rendered verification belongs to an earlier revision."
|
|
1319
|
-
}),
|
|
1320
|
-
"review-freshness": Object.freeze({
|
|
1321
|
-
pass: "Review feedback is current for this revision.",
|
|
1322
|
-
blocked: "Review feedback contains ambiguous identities or anchors.",
|
|
1323
|
-
stale: "Review feedback belongs to an earlier revision."
|
|
1324
|
-
}),
|
|
1325
|
-
"review-dispositions": Object.freeze({
|
|
1326
|
-
pass: "Every current review comment has a recorded outcome.",
|
|
1327
|
-
attention: "Some non-blocking review comments still need an owner decision.",
|
|
1328
|
-
blocked: "Review decisions are incomplete or ambiguous."
|
|
1329
|
-
}),
|
|
1330
|
-
"unresolved-blockers": Object.freeze({
|
|
1331
|
-
pass: "No blocking feedback remains open.",
|
|
1332
|
-
blocked: "Blocking feedback must be resolved before continuing.",
|
|
1333
|
-
stale: "A prior decision must be reviewed against the current design."
|
|
1334
|
-
}),
|
|
1335
|
-
"approved-review-handoff": Object.freeze({
|
|
1336
|
-
pass: "The current review handoff is approved.",
|
|
1337
|
-
blocked: "Prepare and approve the review handoff before continuing.",
|
|
1338
|
-
stale: "The approved review handoff no longer matches the current design."
|
|
1339
|
-
})
|
|
1340
|
-
});
|
|
1341
|
-
var priority = Object.freeze({ pass: 0, attention: 1, blocked: 2, stale: 3 });
|
|
1342
|
-
var digestPattern = /^(?:sha256:)?[a-f0-9]{64}$/u;
|
|
1343
|
-
var normalizeDigest = (value, label) => {
|
|
1344
|
-
if (typeof value !== "string" || !digestPattern.test(value))
|
|
1345
|
-
throw new TypeError(`${label} must be a lowercase SHA-256 value.`);
|
|
1346
|
-
return value.startsWith("sha256:") ? value : `sha256:${value}`;
|
|
1347
|
-
};
|
|
1348
|
-
var optionalDigest = (value, label) => value === void 0 || value === null ? void 0 : normalizeDigest(value, label);
|
|
1349
|
-
var plainClone = (value) => value === void 0 ? void 0 : JSON.parse(canonicalizeJson(value));
|
|
1350
|
-
var logicalPath = (value, fallback, label) => {
|
|
1351
|
-
const result = value ?? fallback;
|
|
1352
|
-
if (!isDesignHandoffRelativePath(result) || /[?#%]/u.test(result))
|
|
1353
|
-
throw new TypeError(`${label} must be a repository-relative logical path.`);
|
|
1354
|
-
return result;
|
|
1355
|
-
};
|
|
1356
|
-
var evidenceDigest = (value) => `sha256:${sha256Hex(canonicalizeJson(value))}`;
|
|
1357
|
-
function ensureUnique(items, select, label) {
|
|
1358
|
-
const values = items.map(select);
|
|
1359
|
-
if (new Set(values).size !== values.length) throw new TypeError(`Duplicate ${label}.`);
|
|
1360
|
-
}
|
|
1361
|
-
function evidence(id2, kind, path, value, { revision, anchor: anchor2 } = {}) {
|
|
1362
|
-
return {
|
|
1363
|
-
id: id2,
|
|
1364
|
-
kind,
|
|
1365
|
-
path,
|
|
1366
|
-
...revision ? { revision: normalizeDigest(revision, `${id2} revision`) } : {},
|
|
1367
|
-
digest: optionalDigest(value?.digest, `${id2} integrity`) ?? evidenceDigest(value),
|
|
1368
|
-
...anchor2 ? { anchor: anchor2 } : {}
|
|
1369
|
-
};
|
|
1370
|
-
}
|
|
1371
|
-
function checked(id2, status, evidenceRefs = []) {
|
|
1372
|
-
return {
|
|
1373
|
-
id: id2,
|
|
1374
|
-
status,
|
|
1375
|
-
message: message[id2][status],
|
|
1376
|
-
evidenceRefs,
|
|
1377
|
-
...status === "pass" ? {} : { recoveryAction: action2[id2] }
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
function selectedDirection(document, studioState) {
|
|
1381
|
-
const selected = studioState?.selectedVariant ?? document?.selectedVariant ?? null;
|
|
1382
|
-
const variants = Array.isArray(document?.variants) ? document.variants : [];
|
|
1383
|
-
ensureUnique(variants, (item) => item?.id, "design direction identity");
|
|
1384
|
-
const matches = variants.filter((item) => item?.id === selected && item?.status === "ready");
|
|
1385
|
-
return { selected, ready: matches.length === 1 };
|
|
1386
|
-
}
|
|
1387
|
-
function normalizedPins(review, revision) {
|
|
1388
|
-
const pins = Array.isArray(review?.pins) ? review.pins.map((pin) => ({ ...pin })) : [];
|
|
1389
|
-
ensureUnique(
|
|
1390
|
-
pins,
|
|
1391
|
-
(pin) => `${pin.revisionId ?? pin.reviewId ?? ""}:${pin.id ?? ""}`,
|
|
1392
|
-
"review comment identity"
|
|
1393
|
-
);
|
|
1394
|
-
for (const pin of pins) {
|
|
1395
|
-
if (typeof pin.id !== "string" || !pin.id)
|
|
1396
|
-
throw new TypeError("Review comments require stable identities.");
|
|
1397
|
-
if (pin.elementId && !pin.screenId)
|
|
1398
|
-
throw new TypeError("An element review anchor must identify its screen.");
|
|
1399
|
-
if (pin.anchorCount !== void 0 && pin.anchorCount !== 1)
|
|
1400
|
-
throw new TypeError("Review comments must identify exactly one anchor.");
|
|
1401
|
-
}
|
|
1402
|
-
return pins.sort(
|
|
1403
|
-
(left, right) => `${left.revisionId ?? left.reviewId}:${left.id}`.localeCompare(
|
|
1404
|
-
`${right.revisionId ?? right.reviewId}:${right.id}`
|
|
1405
|
-
)
|
|
1406
|
-
).map((pin) => ({
|
|
1407
|
-
...pin,
|
|
1408
|
-
current: !pin.stale && (!revision || !pin.revisionId || normalizeDigest(pin.revisionId, "review revision") === revision)
|
|
1409
|
-
}));
|
|
1410
|
-
}
|
|
1411
|
-
function reviewState(input, revision) {
|
|
1412
|
-
if (!input) return { pins: [], stale: false, ambiguous: false };
|
|
1413
|
-
const pins = normalizedPins(input, revision);
|
|
1414
|
-
const recordedRevision = optionalDigest(input.revision, "review revision");
|
|
1415
|
-
return {
|
|
1416
|
-
pins,
|
|
1417
|
-
stale: input.current === false || recordedRevision !== void 0 && recordedRevision !== revision || pins.some((pin) => !pin.current),
|
|
1418
|
-
ambiguous: input.ambiguous === true
|
|
1419
|
-
};
|
|
1420
|
-
}
|
|
1421
|
-
function handoffState(handoff, designId, revision, selectedVariant) {
|
|
1422
|
-
if (!handoff) return "blocked";
|
|
1423
|
-
if (handoff.status !== "approved" || !handoff.approval || handoff.approval.contentHash !== handoff.contentHash)
|
|
1424
|
-
return "blocked";
|
|
1425
|
-
const basisRevision = optionalDigest(handoff.basis?.sourceRevision, "review handoff revision");
|
|
1426
|
-
if (handoff.current === false || handoff.basis?.designId !== designId || basisRevision !== revision || handoff.basis?.selectedVariant !== selectedVariant)
|
|
1427
|
-
return "stale";
|
|
1428
|
-
return "pass";
|
|
1429
|
-
}
|
|
1430
|
-
function compileDesignHandoffReadiness(input) {
|
|
1431
|
-
if (input === null || input === void 0) return designHandoffReadinessAbsence();
|
|
1432
|
-
const source = plainClone(input);
|
|
1433
|
-
const document = source.document;
|
|
1434
|
-
if (!document || document.kind !== "openplanr-design-document" || !["1.0.0", "1.1.0"].includes(document.schemaVersion) || typeof document.id !== "string" || !document.id)
|
|
1435
|
-
throw new TypeError("Readiness requires one supported design document.");
|
|
1436
|
-
const revision = source.sourceRevision === null || source.sourceRevision === void 0 ? null : normalizeDigest(source.sourceRevision, "design revision");
|
|
1437
|
-
const direction = selectedDirection(document, source.studioState);
|
|
1438
|
-
const evidenceItems = [];
|
|
1439
|
-
if (revision)
|
|
1440
|
-
evidenceItems.push(
|
|
1441
|
-
evidence(
|
|
1442
|
-
"design-revision",
|
|
1443
|
-
"design-revision",
|
|
1444
|
-
logicalPath(source.documentPath, "design-document.json", "design document path"),
|
|
1445
|
-
document,
|
|
1446
|
-
{ revision }
|
|
1447
|
-
)
|
|
1448
|
-
);
|
|
1449
|
-
const directionEvidence = direction.selected && revision ? evidence(
|
|
1450
|
-
"selected-direction",
|
|
1451
|
-
"selected-direction",
|
|
1452
|
-
logicalPath(source.studioStatePath, ".design/studio-state.json", "Studio state path"),
|
|
1453
|
-
{ selectedVariant: direction.selected },
|
|
1454
|
-
{ revision, anchor: { section: direction.selected } }
|
|
1455
|
-
) : null;
|
|
1456
|
-
if (directionEvidence) evidenceItems.push(directionEvidence);
|
|
1457
|
-
const specification = source.specification;
|
|
1458
|
-
if (specification)
|
|
1459
|
-
evidenceItems.push(
|
|
1460
|
-
evidence(
|
|
1461
|
-
"design-specification",
|
|
1462
|
-
"design-specification",
|
|
1463
|
-
logicalPath(specification.path, "design-spec.md", "design specification path"),
|
|
1464
|
-
specification,
|
|
1465
|
-
{ revision: specification.revision }
|
|
1466
|
-
)
|
|
1467
|
-
);
|
|
1468
|
-
const verification = source.verification;
|
|
1469
|
-
if (verification)
|
|
1470
|
-
evidenceItems.push(
|
|
1471
|
-
evidence(
|
|
1472
|
-
"rendered-verification",
|
|
1473
|
-
"rendered-verification",
|
|
1474
|
-
logicalPath(verification.path, ".design/verification/current.json", "verification path"),
|
|
1475
|
-
verification,
|
|
1476
|
-
{ revision: verification.revision }
|
|
1477
|
-
)
|
|
1478
|
-
);
|
|
1479
|
-
const review = reviewState(source.review, revision);
|
|
1480
|
-
if (source.review)
|
|
1481
|
-
evidenceItems.push(
|
|
1482
|
-
evidence(
|
|
1483
|
-
"review-feedback",
|
|
1484
|
-
"review-feedback",
|
|
1485
|
-
logicalPath(source.review.path, ".design/review.json", "review feedback path"),
|
|
1486
|
-
source.review,
|
|
1487
|
-
{ revision: source.review.revision }
|
|
1488
|
-
)
|
|
1489
|
-
);
|
|
1490
|
-
const handoff = source.reviewHandoff;
|
|
1491
|
-
if (handoff)
|
|
1492
|
-
evidenceItems.push(
|
|
1493
|
-
evidence(
|
|
1494
|
-
"review-handoff",
|
|
1495
|
-
"review-handoff",
|
|
1496
|
-
logicalPath(handoff.path, "review-handoff.json", "review handoff path"),
|
|
1497
|
-
handoff,
|
|
1498
|
-
{ revision: handoff.basis?.sourceRevision }
|
|
1499
|
-
)
|
|
1500
|
-
);
|
|
1501
|
-
const checks = [];
|
|
1502
|
-
checks.push(
|
|
1503
|
-
checked("current-revision", revision ? "pass" : "blocked", revision ? ["design-revision"] : [])
|
|
1504
|
-
);
|
|
1505
|
-
const handoffDirection = handoff?.basis?.selectedVariant;
|
|
1506
|
-
const directionStatus = !direction.ready ? "blocked" : handoffDirection && handoffDirection !== direction.selected ? "stale" : "pass";
|
|
1507
|
-
checks.push(
|
|
1508
|
-
checked("selected-direction", directionStatus, directionEvidence ? ["selected-direction"] : [])
|
|
1509
|
-
);
|
|
1510
|
-
let specificationStatus = "blocked";
|
|
1511
|
-
if (specification?.complete === true) {
|
|
1512
|
-
const specificationRevision = optionalDigest(
|
|
1513
|
-
specification.revision,
|
|
1514
|
-
"design specification revision"
|
|
1515
|
-
);
|
|
1516
|
-
specificationStatus = revision && specificationRevision && specificationRevision !== revision ? "stale" : "pass";
|
|
1517
|
-
}
|
|
1518
|
-
checks.push(
|
|
1519
|
-
checked(
|
|
1520
|
-
"design-specification",
|
|
1521
|
-
specificationStatus,
|
|
1522
|
-
specification ? ["design-specification"] : []
|
|
1523
|
-
)
|
|
1524
|
-
);
|
|
1525
|
-
let verificationStatus = "blocked";
|
|
1526
|
-
if (verification) {
|
|
1527
|
-
const verificationRevision = optionalDigest(
|
|
1528
|
-
verification.revision,
|
|
1529
|
-
"rendered verification revision"
|
|
1530
|
-
);
|
|
1531
|
-
if (revision && verificationRevision && verificationRevision !== revision)
|
|
1532
|
-
verificationStatus = "stale";
|
|
1533
|
-
else if (verification.status === "verified") verificationStatus = "pass";
|
|
1534
|
-
}
|
|
1535
|
-
checks.push(
|
|
1536
|
-
checked(
|
|
1537
|
-
"rendered-verification",
|
|
1538
|
-
verificationStatus,
|
|
1539
|
-
verification ? ["rendered-verification"] : []
|
|
1540
|
-
)
|
|
1541
|
-
);
|
|
1542
|
-
const reviewRefs = source.review ? ["review-feedback"] : [];
|
|
1543
|
-
const freshnessStatus = review.ambiguous ? "blocked" : review.stale ? "stale" : "pass";
|
|
1544
|
-
checks.push(checked("review-freshness", freshnessStatus, reviewRefs));
|
|
1545
|
-
const undecided = review.pins.filter(
|
|
1546
|
-
(pin) => pin.current && !["accepted", "deferred", "rejected"].includes(pin.disposition)
|
|
1547
|
-
);
|
|
1548
|
-
const invalidDisposition = review.pins.some(
|
|
1549
|
-
(pin) => pin.disposition && !["accepted", "deferred", "rejected"].includes(pin.disposition)
|
|
1550
|
-
);
|
|
1551
|
-
const dispositionStatus = invalidDisposition || review.ambiguous ? "blocked" : undecided.length ? "attention" : "pass";
|
|
1552
|
-
checks.push(checked("review-dispositions", dispositionStatus, reviewRefs));
|
|
1553
|
-
const openBlockers = review.pins.filter(
|
|
1554
|
-
(pin) => pin.current && ["blocker", "change-request"].includes(pin.category) && !["accepted", "deferred", "rejected"].includes(pin.disposition)
|
|
1555
|
-
);
|
|
1556
|
-
const staleAccepted = review.pins.some((pin) => !pin.current && pin.disposition === "accepted");
|
|
1557
|
-
const blockerStatus = staleAccepted ? "stale" : openBlockers.length ? "blocked" : "pass";
|
|
1558
|
-
checks.push(checked("unresolved-blockers", blockerStatus, reviewRefs));
|
|
1559
|
-
const approvalStatus = handoffState(handoff, document.id, revision, direction.selected);
|
|
1560
|
-
checks.push(
|
|
1561
|
-
checked("approved-review-handoff", approvalStatus, handoff ? ["review-handoff"] : [])
|
|
1562
|
-
);
|
|
1563
|
-
if (checks.map((item) => item.id).join("\n") !== CHECKS.join("\n"))
|
|
1564
|
-
throw new TypeError("Readiness checks are not in canonical order.");
|
|
1565
|
-
const worst = checks.reduce(
|
|
1566
|
-
(current, item) => priority[item.status] > priority[current] ? item.status : current,
|
|
1567
|
-
"pass"
|
|
1568
|
-
);
|
|
1569
|
-
const status = worst === "pass" ? "ready" : worst;
|
|
1570
|
-
const blockers = checks.filter((item) => ["blocked", "stale"].includes(item.status)).map((item) => item.id);
|
|
1571
|
-
const nextActions = checks.filter((item) => item.status !== "pass").map((item) => item.recoveryAction);
|
|
1572
|
-
return assertDesignHandoffReadiness({
|
|
1573
|
-
kind: "openplanr-design-handoff-readiness",
|
|
1574
|
-
schemaVersion: "1.0.0",
|
|
1575
|
-
scope: "design-originated",
|
|
1576
|
-
authority: "none",
|
|
1577
|
-
designId: document.id,
|
|
1578
|
-
sourceRevision: revision,
|
|
1579
|
-
selectedVariant: direction.selected,
|
|
1580
|
-
status,
|
|
1581
|
-
continuation: { action: "prepare-plan", available: ["ready", "attention"].includes(status) },
|
|
1582
|
-
checks,
|
|
1583
|
-
evidence: evidenceItems.sort((left, right) => left.id.localeCompare(right.id)),
|
|
1584
|
-
blockers,
|
|
1585
|
-
nextActions
|
|
1586
|
-
});
|
|
1587
|
-
}
|
|
1588
|
-
function designHandoffReadinessAbsence(reason = "not-computed") {
|
|
1589
|
-
return assertDesignHandoffReadiness({
|
|
1590
|
-
kind: "openplanr-design-handoff-readiness-absence",
|
|
1591
|
-
schemaVersion: "1.0.0",
|
|
1592
|
-
status: "absent",
|
|
1593
|
-
reason,
|
|
1594
|
-
message: "No design handoff readiness has been prepared.",
|
|
1595
|
-
nextAction: {
|
|
1596
|
-
id: "inspect-design",
|
|
1597
|
-
label: "Open the design when you want to prepare a handoff"
|
|
1598
|
-
}
|
|
1599
|
-
});
|
|
1600
|
-
}
|
|
1601
|
-
function designHandoffReadinessDigest(value) {
|
|
1602
|
-
assertDesignHandoffReadiness(value);
|
|
1603
|
-
return `sha256:${sha256Hex(canonicalizeJson(value))}`;
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
// packages/design/lib/design/handoff-resolution.mjs
|
|
1607
|
-
var OUTCOMES = /* @__PURE__ */ new Set(["accepted", "open", "blocking", "deferred", "declined"]);
|
|
1608
|
-
var CATEGORIES = /* @__PURE__ */ new Set(["question", "suggestion", "change-request", "blocker"]);
|
|
1609
|
-
var DISPOSITIONS = /* @__PURE__ */ new Set(["accepted", "deferred", "rejected", "declined"]);
|
|
1610
|
-
var MAX_COMMENTS = 1e4;
|
|
1611
|
-
var MAX_ISSUES = 1e4;
|
|
1612
|
-
var digestPattern2 = /^[a-f0-9]{64}$/u;
|
|
1613
|
-
var revisionOf = (pin) => pin.revisionId ?? pin.reviewId;
|
|
1614
|
-
var keyOf = (pin) => `${revisionOf(pin)}:${pin.id}`;
|
|
1615
|
-
var compare = (left, right) => left.localeCompare(right, "en");
|
|
1616
|
-
var clone3 = (value) => {
|
|
1617
|
-
assertPlainData(value, "Review resolution data");
|
|
1618
|
-
return JSON.parse(canonicalizeJson(value));
|
|
1619
|
-
};
|
|
1620
|
-
function issue(code, severity, message2, { pinId, revisionId, recoveryAction } = {}) {
|
|
1621
|
-
return {
|
|
1622
|
-
code,
|
|
1623
|
-
severity,
|
|
1624
|
-
message: message2,
|
|
1625
|
-
...revisionId ? { revisionId } : {},
|
|
1626
|
-
...pinId ? { pinId } : {},
|
|
1627
|
-
recoveryAction: recoveryAction ?? {
|
|
1628
|
-
id: "refresh-review-resolution",
|
|
1629
|
-
label: "Refresh review decisions"
|
|
1630
|
-
}
|
|
1631
|
-
};
|
|
1632
|
-
}
|
|
1633
|
-
function validatePin(pin) {
|
|
1634
|
-
if (!pin || typeof pin !== "object" || Array.isArray(pin))
|
|
1635
|
-
throw new TypeError("Every review comment must be an object.");
|
|
1636
|
-
if (typeof pin.id !== "string" || !pin.id || pin.id.length > 160)
|
|
1637
|
-
throw new TypeError("Every review comment requires a stable identity.");
|
|
1638
|
-
const revisionId = revisionOf(pin);
|
|
1639
|
-
if (typeof revisionId !== "string" || !revisionId || revisionId.length > 160)
|
|
1640
|
-
throw new TypeError("Every review comment requires its original revision identity.");
|
|
1641
|
-
if (typeof pin.reviewOf !== "string" || !digestPattern2.test(pin.reviewOf))
|
|
1642
|
-
throw new TypeError("Every review comment requires its original review basis.");
|
|
1643
|
-
if (pin.screenId !== void 0 && (typeof pin.screenId !== "string" || !pin.screenId))
|
|
1644
|
-
throw new TypeError("Review screen references must be stable identities.");
|
|
1645
|
-
if (pin.elementId !== void 0 && (typeof pin.elementId !== "string" || !pin.elementId || !pin.screenId))
|
|
1646
|
-
throw new TypeError("Review element references require a stable screen identity.");
|
|
1647
|
-
}
|
|
1648
|
-
function scopedMetadata(metadata2, pin, duplicatePinIds, diagnostics) {
|
|
1649
|
-
const revisionId = revisionOf(pin);
|
|
1650
|
-
const scoped = metadata2.byRevision?.[revisionId] ?? {};
|
|
1651
|
-
let category = scoped.categories?.[pin.id];
|
|
1652
|
-
let disposition = scoped.dispositions?.[pin.id];
|
|
1653
|
-
if (category === void 0 && Object.hasOwn(metadata2.categories ?? {}, pin.id)) {
|
|
1654
|
-
if (duplicatePinIds.has(pin.id))
|
|
1655
|
-
diagnostics.push(
|
|
1656
|
-
issue(
|
|
1657
|
-
"AMBIGUOUS_LEGACY_CATEGORY",
|
|
1658
|
-
"blocked",
|
|
1659
|
-
"A legacy category cannot be matched to one original revision.",
|
|
1660
|
-
{ pinId: pin.id, revisionId }
|
|
1661
|
-
)
|
|
1662
|
-
);
|
|
1663
|
-
else category = metadata2.categories[pin.id];
|
|
1664
|
-
}
|
|
1665
|
-
if (disposition === void 0 && Object.hasOwn(metadata2.dispositions ?? {}, pin.id)) {
|
|
1666
|
-
if (duplicatePinIds.has(pin.id))
|
|
1667
|
-
diagnostics.push(
|
|
1668
|
-
issue(
|
|
1669
|
-
"AMBIGUOUS_LEGACY_DISPOSITION",
|
|
1670
|
-
"blocked",
|
|
1671
|
-
"A legacy owner decision cannot be matched to one original revision.",
|
|
1672
|
-
{ pinId: pin.id, revisionId }
|
|
1673
|
-
)
|
|
1674
|
-
);
|
|
1675
|
-
else disposition = metadata2.dispositions[pin.id];
|
|
1676
|
-
}
|
|
1677
|
-
if (category !== void 0 && !CATEGORIES.has(category)) {
|
|
1678
|
-
diagnostics.push(
|
|
1679
|
-
issue("UNKNOWN_CATEGORY", "blocked", "The comment category is not supported.", {
|
|
1680
|
-
pinId: pin.id,
|
|
1681
|
-
revisionId
|
|
1682
|
-
})
|
|
1683
|
-
);
|
|
1684
|
-
category = void 0;
|
|
1685
|
-
}
|
|
1686
|
-
const dispositionValue = typeof disposition === "string" ? disposition : disposition?.disposition;
|
|
1687
|
-
if (dispositionValue !== void 0 && !DISPOSITIONS.has(dispositionValue)) {
|
|
1688
|
-
diagnostics.push(
|
|
1689
|
-
issue("UNKNOWN_DISPOSITION", "blocked", "The owner decision is not supported.", {
|
|
1690
|
-
pinId: pin.id,
|
|
1691
|
-
revisionId
|
|
1692
|
-
})
|
|
1693
|
-
);
|
|
1694
|
-
disposition = void 0;
|
|
1695
|
-
}
|
|
1696
|
-
return {
|
|
1697
|
-
category,
|
|
1698
|
-
disposition,
|
|
1699
|
-
dispositionValue: typeof disposition === "string" ? disposition : disposition?.disposition
|
|
1700
|
-
};
|
|
1701
|
-
}
|
|
1702
|
-
function unknownMetadata(metadata2, known, diagnostics) {
|
|
1703
|
-
for (const [revisionId, value] of Object.entries(metadata2.byRevision ?? {}).sort(
|
|
1704
|
-
([left], [right]) => compare(left, right)
|
|
1705
|
-
)) {
|
|
1706
|
-
for (const field of ["categories", "dispositions"]) {
|
|
1707
|
-
for (const pinId of Object.keys(value?.[field] ?? {}).sort(compare)) {
|
|
1708
|
-
if (!known.has(`${revisionId}:${pinId}`))
|
|
1709
|
-
diagnostics.push(
|
|
1710
|
-
issue(
|
|
1711
|
-
"UNKNOWN_COMMENT_METADATA",
|
|
1712
|
-
"blocked",
|
|
1713
|
-
"Review metadata targets a comment that is not present in the recorded review history.",
|
|
1714
|
-
{ revisionId, pinId }
|
|
1715
|
-
)
|
|
1716
|
-
);
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1721
|
-
function compileDesignHandoffResolution(input) {
|
|
1722
|
-
const source = clone3(input ?? {});
|
|
1723
|
-
const pins = Array.isArray(source.pins) ? source.pins : [];
|
|
1724
|
-
if (pins.length > MAX_COMMENTS)
|
|
1725
|
-
throw new RangeError("Review resolution exceeds the comment limit.");
|
|
1726
|
-
const metadata2 = source.metadata && typeof source.metadata === "object" && !Array.isArray(source.metadata) ? source.metadata : {};
|
|
1727
|
-
const diagnostics = [];
|
|
1728
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1729
|
-
for (const pin of pins) {
|
|
1730
|
-
validatePin(pin);
|
|
1731
|
-
counts.set(pin.id, (counts.get(pin.id) ?? 0) + 1);
|
|
1732
|
-
}
|
|
1733
|
-
const duplicatePinIds = new Set([...counts].filter(([, count]) => count > 1).map(([id2]) => id2));
|
|
1734
|
-
const known = /* @__PURE__ */ new Set();
|
|
1735
|
-
for (const pin of pins) {
|
|
1736
|
-
const key = keyOf(pin);
|
|
1737
|
-
if (known.has(key)) throw new TypeError(`Duplicate review comment identity: ${key}.`);
|
|
1738
|
-
known.add(key);
|
|
1739
|
-
}
|
|
1740
|
-
unknownMetadata(metadata2, known, diagnostics);
|
|
1741
|
-
const items = [...pins].sort((left, right) => compare(keyOf(left), keyOf(right))).map((pin) => {
|
|
1742
|
-
const revisionId = revisionOf(pin);
|
|
1743
|
-
const current = pin.stale !== true && (!source.currentReviewOf || pin.reviewOf === source.currentReviewOf);
|
|
1744
|
-
const resolved = scopedMetadata(metadata2, pin, duplicatePinIds, diagnostics);
|
|
1745
|
-
let outcome;
|
|
1746
|
-
if (resolved.dispositionValue === "accepted") outcome = "accepted";
|
|
1747
|
-
else if (resolved.dispositionValue === "deferred") outcome = "deferred";
|
|
1748
|
-
else if (["rejected", "declined"].includes(resolved.dispositionValue)) outcome = "declined";
|
|
1749
|
-
else if (["blocker", "change-request"].includes(resolved.category)) outcome = "blocking";
|
|
1750
|
-
else outcome = "open";
|
|
1751
|
-
if (!OUTCOMES.has(outcome)) throw new Error("Review resolution produced an invalid outcome.");
|
|
1752
|
-
if (!current && outcome === "accepted")
|
|
1753
|
-
diagnostics.push(
|
|
1754
|
-
issue(
|
|
1755
|
-
"STALE_ACCEPTED_DECISION",
|
|
1756
|
-
"stale",
|
|
1757
|
-
"An accepted change belongs to an earlier design revision and must be reviewed again.",
|
|
1758
|
-
{
|
|
1759
|
-
pinId: pin.id,
|
|
1760
|
-
revisionId,
|
|
1761
|
-
recoveryAction: {
|
|
1762
|
-
id: "review-stale-decision",
|
|
1763
|
-
label: "Review this decision against the current design"
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
)
|
|
1767
|
-
);
|
|
1768
|
-
if (current && outcome === "blocking")
|
|
1769
|
-
diagnostics.push(
|
|
1770
|
-
issue(
|
|
1771
|
-
"UNRESOLVED_BLOCKING_COMMENT",
|
|
1772
|
-
"blocked",
|
|
1773
|
-
"A blocking comment still needs an owner decision.",
|
|
1774
|
-
{
|
|
1775
|
-
pinId: pin.id,
|
|
1776
|
-
revisionId,
|
|
1777
|
-
recoveryAction: {
|
|
1778
|
-
id: "resolve-blocking-comment",
|
|
1779
|
-
label: "Record the owner decision"
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
)
|
|
1783
|
-
);
|
|
1784
|
-
return {
|
|
1785
|
-
id: keyOf(pin),
|
|
1786
|
-
pinId: pin.id,
|
|
1787
|
-
reviewId: pin.reviewId,
|
|
1788
|
-
revisionId,
|
|
1789
|
-
reviewOf: pin.reviewOf,
|
|
1790
|
-
current,
|
|
1791
|
-
category: resolved.category ?? "question",
|
|
1792
|
-
outcome,
|
|
1793
|
-
implementationScope: current && outcome === "accepted",
|
|
1794
|
-
anchor: pin.elementId ? { screenId: pin.screenId, elementId: pin.elementId } : pin.screenId ? { screenId: pin.screenId } : pin.anchor?.planrId ? { planrId: pin.anchor.planrId } : { artifactId: pin.artifactId },
|
|
1795
|
-
source: {
|
|
1796
|
-
text: pin.comment,
|
|
1797
|
-
author: clone3(pin.author),
|
|
1798
|
-
status: pin.status,
|
|
1799
|
-
...resolved.disposition && typeof resolved.disposition === "object" ? { decision: clone3(resolved.disposition) } : {}
|
|
1800
|
-
}
|
|
1801
|
-
};
|
|
1802
|
-
});
|
|
1803
|
-
if (source.historyComplete === false)
|
|
1804
|
-
diagnostics.push(
|
|
1805
|
-
issue("INCOMPLETE_REVIEW_HISTORY", "blocked", "The complete review history is unavailable.", {
|
|
1806
|
-
recoveryAction: {
|
|
1807
|
-
id: "restore-review-history",
|
|
1808
|
-
label: "Restore the complete review history"
|
|
1809
|
-
}
|
|
1810
|
-
})
|
|
1811
|
-
);
|
|
1812
|
-
if (source.synchronizationPending === true)
|
|
1813
|
-
diagnostics.push(
|
|
1814
|
-
issue(
|
|
1815
|
-
"SYNCHRONIZATION_PENDING",
|
|
1816
|
-
"blocked",
|
|
1817
|
-
"A review decision is still waiting to synchronize.",
|
|
1818
|
-
{
|
|
1819
|
-
recoveryAction: {
|
|
1820
|
-
id: "retry-review-sync",
|
|
1821
|
-
label: "Retry review synchronization"
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
)
|
|
1825
|
-
);
|
|
1826
|
-
for (const value of (source.synchronizationIssues ?? []).slice(0, MAX_ISSUES))
|
|
1827
|
-
diagnostics.push(
|
|
1828
|
-
issue(
|
|
1829
|
-
"UNTRUSTED_HOSTED_FEEDBACK",
|
|
1830
|
-
"blocked",
|
|
1831
|
-
value?.reason || "Hosted feedback could not be validated.",
|
|
1832
|
-
{
|
|
1833
|
-
pinId: value?.pinId,
|
|
1834
|
-
revisionId: value?.revisionId,
|
|
1835
|
-
recoveryAction: {
|
|
1836
|
-
id: "inspect-review-sync",
|
|
1837
|
-
label: "Inspect the rejected hosted feedback"
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
)
|
|
1841
|
-
);
|
|
1842
|
-
if ((source.synchronizationIssues ?? []).length > MAX_ISSUES)
|
|
1843
|
-
throw new RangeError("Review resolution exceeds the synchronization issue limit.");
|
|
1844
|
-
diagnostics.sort(
|
|
1845
|
-
(left, right) => compare(
|
|
1846
|
-
`${left.code}:${left.revisionId ?? ""}:${left.pinId ?? ""}:${left.message}`,
|
|
1847
|
-
`${right.code}:${right.revisionId ?? ""}:${right.pinId ?? ""}:${right.message}`
|
|
1848
|
-
)
|
|
1849
|
-
);
|
|
1850
|
-
const severity = new Set(diagnostics.map((value) => value.severity));
|
|
1851
|
-
const status = severity.has("stale") ? "stale" : severity.has("blocked") ? "blocked" : items.some((item) => item.outcome === "open") ? "attention" : "ready";
|
|
1852
|
-
return {
|
|
1853
|
-
kind: "openplanr-design-handoff-resolution",
|
|
1854
|
-
schemaVersion: "1.0.0",
|
|
1855
|
-
currentReviewOf: source.currentReviewOf ?? null,
|
|
1856
|
-
status,
|
|
1857
|
-
complete: source.historyComplete !== false && !["blocked", "stale"].includes(status),
|
|
1858
|
-
items,
|
|
1859
|
-
implementationScope: items.filter((item) => item.implementationScope).map((item) => item.id),
|
|
1860
|
-
diagnostics
|
|
1861
|
-
};
|
|
1862
|
-
}
|
|
1863
|
-
function canApproveDesignHandoffResolution(value) {
|
|
1864
|
-
return Boolean(value?.complete && ["ready", "attention"].includes(value.status));
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
// packages/design/lib/design/handoff-reader.mjs
|
|
1868
|
-
var sections = ["agreedChanges", "openQuestions", "deferred", "rejected"];
|
|
1869
|
-
var metadataPath = (current) => join3(current.root, ".design/review-metadata.json");
|
|
1870
|
-
var revisionOf2 = (pin) => pin.revisionId ?? pin.reviewId;
|
|
1871
|
-
var pinKey = (pin) => `${revisionOf2(pin)}:${pin.id}`;
|
|
1872
|
-
function metadata(current, feedback) {
|
|
1873
|
-
const local = readJson(metadataPath(current), { version: 0, byRevision: {} });
|
|
1874
|
-
const byRevision = structuredClone(feedback.shared?.metadataByRevision ?? {});
|
|
1875
|
-
for (const [revision, value] of Object.entries(local.byRevision ?? {})) {
|
|
1876
|
-
const remote = byRevision[revision] ?? {};
|
|
1877
|
-
byRevision[revision] = {
|
|
1878
|
-
categories: { ...remote.categories, ...value.categories },
|
|
1879
|
-
dispositions: { ...remote.dispositions, ...value.dispositions }
|
|
1880
|
-
};
|
|
1881
|
-
}
|
|
1882
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1883
|
-
for (const pin of feedback.pins) counts.set(pin.id, (counts.get(pin.id) ?? 0) + 1);
|
|
1884
|
-
const categories = {}, dispositions = {};
|
|
1885
|
-
for (const pin of feedback.pins) {
|
|
1886
|
-
const value = byRevision[revisionOf2(pin)];
|
|
1887
|
-
if (counts.get(pin.id) === 1 && value) {
|
|
1888
|
-
if (Object.hasOwn(value.categories ?? {}, pin.id))
|
|
1889
|
-
Object.defineProperty(categories, pin.id, {
|
|
1890
|
-
value: value.categories[pin.id],
|
|
1891
|
-
enumerable: true
|
|
1892
|
-
});
|
|
1893
|
-
if (Object.hasOwn(value.dispositions ?? {}, pin.id))
|
|
1894
|
-
Object.defineProperty(dispositions, pin.id, {
|
|
1895
|
-
value: value.dispositions[pin.id],
|
|
1896
|
-
enumerable: true
|
|
1897
|
-
});
|
|
1898
|
-
}
|
|
1899
|
-
if (counts.get(pin.id) === 1 && !Object.hasOwn(categories, pin.id) && Object.hasOwn(local.categories ?? {}, pin.id))
|
|
1900
|
-
Object.defineProperty(categories, pin.id, {
|
|
1901
|
-
value: local.categories[pin.id],
|
|
1902
|
-
enumerable: true
|
|
1903
|
-
});
|
|
1904
|
-
if (counts.get(pin.id) === 1 && !Object.hasOwn(dispositions, pin.id) && Object.hasOwn(local.dispositions ?? {}, pin.id))
|
|
1905
|
-
Object.defineProperty(dispositions, pin.id, {
|
|
1906
|
-
value: local.dispositions[pin.id],
|
|
1907
|
-
enumerable: true
|
|
1908
|
-
});
|
|
1909
|
-
}
|
|
1910
|
-
return {
|
|
1911
|
-
version: local.version,
|
|
1912
|
-
categories,
|
|
1913
|
-
dispositions,
|
|
1914
|
-
byRevision,
|
|
1915
|
-
legacy: {
|
|
1916
|
-
categories: structuredClone(local.categories ?? {}),
|
|
1917
|
-
dispositions: structuredClone(local.dispositions ?? {})
|
|
1918
|
-
}
|
|
1919
|
-
};
|
|
1920
|
-
}
|
|
1921
|
-
function findPin(pins, id2, revision) {
|
|
1922
|
-
const candidates = pins.filter(
|
|
1923
|
-
(pin) => pin.id === id2 && (!revision || revisionOf2(pin) === revision)
|
|
1924
|
-
);
|
|
1925
|
-
if (candidates.length !== 1)
|
|
1926
|
-
throw new Error(
|
|
1927
|
-
"The comment identity is missing or ambiguous. Include its original revisionId."
|
|
1928
|
-
);
|
|
1929
|
-
return candidates[0];
|
|
1930
|
-
}
|
|
1931
|
-
function resolutionFor(value, shareStatus) {
|
|
1932
|
-
return compileDesignHandoffResolution({
|
|
1933
|
-
currentReviewOf: value.basis.reviewOf,
|
|
1934
|
-
pins: value.feedback.pins,
|
|
1935
|
-
metadata: {
|
|
1936
|
-
byRevision: value.metadata.byRevision,
|
|
1937
|
-
categories: value.metadata.legacy.categories,
|
|
1938
|
-
dispositions: value.metadata.legacy.dispositions
|
|
1939
|
-
},
|
|
1940
|
-
historyComplete: !value.feedback.shared?.issues?.length,
|
|
1941
|
-
synchronizationPending: Boolean(shareStatus?.pendingReviewMetadata),
|
|
1942
|
-
synchronizationIssues: value.feedback.shared?.issues ?? []
|
|
1943
|
-
});
|
|
1944
|
-
}
|
|
1945
|
-
function snapshot(file, env, shareOptions = {}) {
|
|
1946
|
-
const current = currentDesign(file, shareOptions), feedback = readDesignFeedback(file, env, shareOptions), meta = metadata(current, feedback);
|
|
1947
|
-
const reviewContext = current.reviewContext ?? emptyReviewContext(current.document);
|
|
1948
|
-
const basis = {
|
|
1949
|
-
designId: current.document.id,
|
|
1950
|
-
sourceRevision: current.revision,
|
|
1951
|
-
contextDigest: current.contextDigest ?? reviewDigest(reviewContext),
|
|
1952
|
-
reviewOf: digestArtifactEnvelope(current.envelope),
|
|
1953
|
-
selectedVariant: feedback.state.selectedVariant ?? current.document.selectedVariant,
|
|
1954
|
-
feedbackDigest: reviewDigest({
|
|
1955
|
-
pins: feedback.pins,
|
|
1956
|
-
metadata: {
|
|
1957
|
-
byRevision: meta.byRevision,
|
|
1958
|
-
categories: meta.categories,
|
|
1959
|
-
dispositions: meta.dispositions
|
|
1960
|
-
},
|
|
1961
|
-
overall: (feedback.ledger?.reviews ?? []).map((entry) => ({
|
|
1962
|
-
reviewId: entry.review.reviewId,
|
|
1963
|
-
reviewOf: entry.review.reviewOf,
|
|
1964
|
-
overall: entry.review.overall
|
|
1965
|
-
})),
|
|
1966
|
-
directions: feedback.shared?.directions ?? []
|
|
1967
|
-
}),
|
|
1968
|
-
verificationDigest: reviewDigest(current.verification),
|
|
1969
|
-
feedbackWatermark: Math.max(
|
|
1970
|
-
0,
|
|
1971
|
-
...(feedback.shared?.events ?? []).map((event) => event.sequence ?? 0)
|
|
1972
|
-
)
|
|
1973
|
-
};
|
|
1974
|
-
let shareStatus = null;
|
|
1975
|
-
try {
|
|
1976
|
-
shareStatus = getDesignShareStatus(file, { env, ...shareOptions });
|
|
1977
|
-
} catch (error) {
|
|
1978
|
-
if (error.code === "E_DESIGN_PUBLICATION_PENDING") throw error;
|
|
1979
|
-
}
|
|
1980
|
-
const value = { current, feedback, metadata: meta, reviewContext, basis };
|
|
1981
|
-
return { ...value, resolution: resolutionFor(value, shareStatus), shareStatus };
|
|
1982
|
-
}
|
|
1983
|
-
function readDesignExperience(file, { env = process.env } = {}) {
|
|
1984
|
-
const value = snapshot(file, env);
|
|
1985
|
-
return {
|
|
1986
|
-
ok: true,
|
|
1987
|
-
capabilities: { owner: true, revisions: true, handoff: true },
|
|
1988
|
-
revision: value.current.revision,
|
|
1989
|
-
reviewContext: value.reviewContext,
|
|
1990
|
-
contextDigest: value.basis.contextDigest,
|
|
1991
|
-
fingerprints: value.current.fingerprints ?? [],
|
|
1992
|
-
metadata: value.metadata,
|
|
1993
|
-
loadingHistory: false
|
|
1994
|
-
};
|
|
1995
|
-
}
|
|
1996
|
-
function readDesignHandoff(file, { env = process.env, ...shareOptions } = {}) {
|
|
1997
|
-
const value = snapshot(file, env, shareOptions);
|
|
1998
|
-
const path = join3(dirname3(designSpecPath(value.current.root)), "review-handoff.json");
|
|
1999
|
-
const draft = readJson(path, null);
|
|
2000
|
-
if (draft) {
|
|
2001
|
-
assertDraft(draft);
|
|
2002
|
-
const markdownPath = path.replace(/\.json$/u, ".md");
|
|
2003
|
-
if (!existsSync3(markdownPath) || readFileSync3(markdownPath, "utf8") !== draft.markdown)
|
|
2004
|
-
throw new Error(
|
|
2005
|
-
"The handoff Markdown differs from its approved JSON. Rebuild the handoff projection before using it in Plan."
|
|
2006
|
-
);
|
|
2007
|
-
}
|
|
2008
|
-
return {
|
|
2009
|
-
ok: true,
|
|
2010
|
-
path,
|
|
2011
|
-
revision: value.current.revision,
|
|
2012
|
-
draft,
|
|
2013
|
-
current: Boolean(draft && reviewDigest(draft.basis) === reviewDigest(value.basis)),
|
|
2014
|
-
metadata: value.metadata,
|
|
2015
|
-
feedback: { pins: value.feedback.pins },
|
|
2016
|
-
basis: value.basis,
|
|
2017
|
-
resolution: value.resolution
|
|
2018
|
-
};
|
|
2019
|
-
}
|
|
2020
|
-
function readDesignHandoffReadiness(file, { env = process.env, ...shareOptions } = {}) {
|
|
2021
|
-
const value = snapshot(file, env, shareOptions);
|
|
2022
|
-
const path = join3(dirname3(designSpecPath(value.current.root)), "review-handoff.json");
|
|
2023
|
-
const handoff = readJson(path, null);
|
|
2024
|
-
if (handoff) assertDraft(handoff);
|
|
2025
|
-
const outcomes = new Map(value.resolution.items.map((item) => [item.id, item]));
|
|
2026
|
-
const pins = value.feedback.pins.map((pin) => {
|
|
2027
|
-
const resolved = outcomes.get(pinKey(pin));
|
|
2028
|
-
const disposition = resolved?.outcome === "accepted" ? "accepted" : resolved?.outcome === "deferred" ? "deferred" : resolved?.outcome === "declined" ? "rejected" : void 0;
|
|
2029
|
-
return {
|
|
2030
|
-
id: pin.id,
|
|
2031
|
-
...pin.screenId ? { screenId: pin.screenId } : {},
|
|
2032
|
-
...pin.anchor?.planrId ? { elementId: pin.anchor.planrId } : {},
|
|
2033
|
-
category: resolved?.category,
|
|
2034
|
-
...disposition ? { disposition } : {},
|
|
2035
|
-
stale: Boolean(pin.stale)
|
|
2036
|
-
};
|
|
2037
|
-
});
|
|
2038
|
-
const specificationPath = designSpecPath(value.current.root);
|
|
2039
|
-
const specification = existsSync3(specificationPath) ? {
|
|
2040
|
-
path: "design-spec.md",
|
|
2041
|
-
revision: value.current.revision,
|
|
2042
|
-
digest: `sha256:${hash(readFileSync3(specificationPath))}`,
|
|
2043
|
-
complete: true
|
|
2044
|
-
} : void 0;
|
|
2045
|
-
const studioState = readJson(join3(value.current.root, ".design/studio-state.json"), {
|
|
2046
|
-
state: {}
|
|
2047
|
-
}).state;
|
|
2048
|
-
const readiness = compileDesignHandoffReadiness({
|
|
2049
|
-
document: value.current.document,
|
|
2050
|
-
documentPath: "design-document.json",
|
|
2051
|
-
sourceRevision: value.current.revision,
|
|
2052
|
-
studioState,
|
|
2053
|
-
studioStatePath: ".design/studio-state.json",
|
|
2054
|
-
specification,
|
|
2055
|
-
verification: { path: ".design/verification/current.json", ...value.current.verification },
|
|
2056
|
-
review: {
|
|
2057
|
-
path: ".design/review.json",
|
|
2058
|
-
revision: value.current.revision,
|
|
2059
|
-
current: pins.every((pin) => !pin.stale),
|
|
2060
|
-
pins
|
|
2061
|
-
},
|
|
2062
|
-
reviewHandoff: handoff ? {
|
|
2063
|
-
...handoff,
|
|
2064
|
-
path: "review-handoff.json",
|
|
2065
|
-
digest: `sha256:${handoff.contentHash}`,
|
|
2066
|
-
current: reviewDigest(handoff.basis) === reviewDigest(value.basis)
|
|
2067
|
-
} : void 0
|
|
2068
|
-
});
|
|
2069
|
-
return { ok: true, readiness, digest: designHandoffReadinessDigest(readiness) };
|
|
2070
|
-
}
|
|
2071
|
-
function assertDraft(draft) {
|
|
2072
|
-
assertReviewExperience(draft, DESIGN_HANDOFF_SCHEMA);
|
|
2073
|
-
const expected = reviewDigest({
|
|
2074
|
-
title: draft.title,
|
|
2075
|
-
reviewNotes: draft.reviewNotes,
|
|
2076
|
-
basis: draft.basis,
|
|
2077
|
-
content: draft.content,
|
|
2078
|
-
affectedScreens: draft.affectedScreens ?? [],
|
|
2079
|
-
verificationGaps: draft.verificationGaps ?? []
|
|
2080
|
-
});
|
|
2081
|
-
if (draft.contentHash !== expected || draft.status === "approved" && draft.approval?.contentHash !== expected)
|
|
2082
|
-
throw new Error(
|
|
2083
|
-
"The handoff content does not match its approval digest. Refine it through the handoff utility."
|
|
2084
|
-
);
|
|
2085
|
-
if (draft.markdown !== renderMarkdown(draft, draft.title))
|
|
2086
|
-
throw new Error("The handoff Markdown does not match its approved content.");
|
|
2087
|
-
return draft;
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
// packages/design/lib/design/handoff.mjs
|
|
2091
|
-
import { existsSync as existsSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2092
|
-
import { dirname as dirname4, join as join4 } from "node:path";
|
|
2093
|
-
var conflict2 = (message2) => Object.assign(new Error(message2), { statusCode: 409 });
|
|
2094
|
-
function sourceItem(pin, shareUrl) {
|
|
2095
|
-
const source = pin.revisionId ? `${shareUrl ?? ""}#revision=${encodeURIComponent(pin.revisionId)}&pin=${encodeURIComponent(pin.id)}` : `#review=${encodeURIComponent(pin.reviewId)}&pin=${encodeURIComponent(pin.id)}`;
|
|
2096
|
-
return {
|
|
2097
|
-
pinId: pin.id,
|
|
2098
|
-
...pin.screenId ? { screenId: pin.screenId } : {},
|
|
2099
|
-
reviewId: pin.reviewId,
|
|
2100
|
-
...pin.revisionId ? { revisionId: pin.revisionId } : {},
|
|
2101
|
-
text: pin.comment,
|
|
2102
|
-
author: pin.author.name,
|
|
2103
|
-
reviewOf: pin.reviewOf,
|
|
2104
|
-
stale: Boolean(pin.stale),
|
|
2105
|
-
source
|
|
2106
|
-
};
|
|
2107
|
-
}
|
|
2108
|
-
function contentFromFeedback(value, shareUrl) {
|
|
2109
|
-
const content = { summary: "", agreedChanges: [], openQuestions: [], deferred: [], rejected: [] };
|
|
2110
|
-
const pins = new Map(value.feedback.pins.map((pin) => [pinKey(pin), pin]));
|
|
2111
|
-
for (const resolved of value.resolution.items) {
|
|
2112
|
-
const pin = pins.get(resolved.id);
|
|
2113
|
-
const item = sourceItem(pin, shareUrl);
|
|
2114
|
-
if (resolved.outcome === "accepted") content.agreedChanges.push(item);
|
|
2115
|
-
else if (resolved.outcome === "deferred") content.deferred.push(item);
|
|
2116
|
-
else if (resolved.outcome === "declined") content.rejected.push(item);
|
|
2117
|
-
else content.openQuestions.push(item);
|
|
2118
|
-
}
|
|
2119
|
-
return content;
|
|
2120
|
-
}
|
|
2121
|
-
function enrichContent(content, value, shareUrl) {
|
|
2122
|
-
assertReviewExperience(content, DESIGN_HANDOFF_CONTENT_SCHEMA);
|
|
2123
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2124
|
-
const enriched = { summary: content.summary };
|
|
2125
|
-
for (const key of sections)
|
|
2126
|
-
enriched[key] = content[key].map((item) => {
|
|
2127
|
-
const pin = findPin(value.feedback.pins, item.pinId, item.revisionId ?? item.reviewId);
|
|
2128
|
-
if (seen.has(pinKey(pin)))
|
|
2129
|
-
throw new Error("Handoff items must cite distinct recorded comments.");
|
|
2130
|
-
seen.add(pinKey(pin));
|
|
2131
|
-
const resolved = value.resolution.items.find((itemValue) => itemValue.id === pinKey(pin));
|
|
2132
|
-
const expected = key === "agreedChanges" ? ["accepted"] : key === "deferred" ? ["deferred"] : key === "rejected" ? ["declined"] : ["open", "blocking"];
|
|
2133
|
-
if (!resolved || !expected.includes(resolved.outcome))
|
|
2134
|
-
throw new Error(
|
|
2135
|
-
"Record the owner disposition before moving a comment into this handoff section."
|
|
2136
|
-
);
|
|
2137
|
-
const refinement = item.refinement ?? (item.text !== pin.comment ? item.text : void 0);
|
|
2138
|
-
return { ...sourceItem(pin, shareUrl), ...refinement !== void 0 ? { refinement } : {} };
|
|
2139
|
-
});
|
|
2140
|
-
for (const pin of value.feedback.pins)
|
|
2141
|
-
if (!seen.has(pinKey(pin)))
|
|
2142
|
-
throw new Error(
|
|
2143
|
-
`Keep every recorded review comment in the handoff; ${pinKey(pin)} is missing.`
|
|
2144
|
-
);
|
|
2145
|
-
return enriched;
|
|
2146
|
-
}
|
|
2147
|
-
function normalizedLocalMetadata(local, pins) {
|
|
2148
|
-
const byRevision = structuredClone(local.byRevision ?? {});
|
|
2149
|
-
const counts = /* @__PURE__ */ new Map();
|
|
2150
|
-
for (const pin of pins) counts.set(pin.id, (counts.get(pin.id) ?? 0) + 1);
|
|
2151
|
-
for (const field of ["categories", "dispositions"]) {
|
|
2152
|
-
for (const [pinId, item] of Object.entries(local[field] ?? {})) {
|
|
2153
|
-
if (counts.get(pinId) !== 1) continue;
|
|
2154
|
-
const pin = pins.find((value) => value.id === pinId);
|
|
2155
|
-
const revision = revisionOf2(pin);
|
|
2156
|
-
const scoped = byRevision[revision] ?? { categories: {}, dispositions: {} };
|
|
2157
|
-
byRevision[revision] = { ...scoped, [field]: { ...scoped[field], [pinId]: item } };
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
return { version: local.version ?? 0, byRevision };
|
|
2161
|
-
}
|
|
2162
|
-
async function preserveReviewErrors(path, action3) {
|
|
2163
|
-
let failure, result;
|
|
2164
|
-
await withArtifactReviewLock(path, async () => {
|
|
2165
|
-
try {
|
|
2166
|
-
result = await action3();
|
|
2167
|
-
} catch (error) {
|
|
2168
|
-
failure = error;
|
|
2169
|
-
}
|
|
2170
|
-
});
|
|
2171
|
-
if (failure) throw failure;
|
|
2172
|
-
return result;
|
|
2173
|
-
}
|
|
2174
|
-
async function updateDesignHandoff(file, input, { env = process.env, ...shareOptions } = {}) {
|
|
2175
|
-
if (!input || !["draft", "update", "approve", "category", "disposition"].includes(input.action))
|
|
2176
|
-
throw new Error("Unknown design handoff action.");
|
|
2177
|
-
const initial = currentDesign(file);
|
|
2178
|
-
const unlockRender = await acquireStartLock(join4(initial.root, ".design/render.lock"));
|
|
2179
|
-
let outgoing;
|
|
2180
|
-
try {
|
|
2181
|
-
const unlock = await acquireStartLock(join4(initial.root, ".design/handoff.lock"));
|
|
2182
|
-
try {
|
|
2183
|
-
await preserveReviewErrors(designReviewPath(file, env), async () => {
|
|
2184
|
-
const value = snapshot(file, env, shareOptions);
|
|
2185
|
-
if (input.revision !== value.current.revision)
|
|
2186
|
-
throw conflict2("The design changed. Refresh the review before updating its handoff.");
|
|
2187
|
-
if (["category", "disposition"].includes(input.action)) {
|
|
2188
|
-
if (input.version !== value.metadata.version)
|
|
2189
|
-
throw conflict2("Review organization changed in another window. Reload before saving.");
|
|
2190
|
-
const pin = findPin(value.feedback.pins, input.pinId, input.revisionId ?? input.reviewId);
|
|
2191
|
-
if (!pin) throw new Error("The comment is no longer available.");
|
|
2192
|
-
const choices = input.action === "category" ? ["question", "suggestion", "change-request", "blocker"] : ["accepted", "deferred", "rejected"];
|
|
2193
|
-
if (!choices.includes(input[input.action])) throw new Error(`Invalid ${input.action}.`);
|
|
2194
|
-
if (typeof (input.reason ?? "") !== "string" || (input.reason ?? "").length > 16384)
|
|
2195
|
-
throw new Error("Disposition reason is too long.");
|
|
2196
|
-
if (pin.revisionId) assertDesignShareAvailable({ env, ...shareOptions });
|
|
2197
|
-
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2198
|
-
const local = normalizedLocalMetadata(
|
|
2199
|
-
readJson(metadataPath(value.current), { version: 0, byRevision: {} }),
|
|
2200
|
-
value.feedback.pins
|
|
2201
|
-
);
|
|
2202
|
-
const revision = revisionOf2(pin);
|
|
2203
|
-
const scoped = local.byRevision?.[revision] ?? { categories: {}, dispositions: {} };
|
|
2204
|
-
const valueForRevision = { ...scoped };
|
|
2205
|
-
if (input.action === "category")
|
|
2206
|
-
valueForRevision.categories = { ...scoped.categories, [pin.id]: input.category };
|
|
2207
|
-
else
|
|
2208
|
-
valueForRevision.dispositions = {
|
|
2209
|
-
...scoped.dispositions,
|
|
2210
|
-
[pin.id]: {
|
|
2211
|
-
disposition: input.disposition,
|
|
2212
|
-
reason: input.reason ?? "",
|
|
2213
|
-
updatedAt,
|
|
2214
|
-
author: "Design owner"
|
|
2215
|
-
}
|
|
2216
|
-
};
|
|
2217
|
-
const next = {
|
|
2218
|
-
version: value.metadata.version + 1,
|
|
2219
|
-
byRevision: { ...local.byRevision, [revision]: valueForRevision }
|
|
2220
|
-
};
|
|
2221
|
-
atomicJson(metadataPath(value.current), next);
|
|
2222
|
-
if (pin.revisionId)
|
|
2223
|
-
outgoing = {
|
|
2224
|
-
revisionId: pin.revisionId,
|
|
2225
|
-
payload: {
|
|
2226
|
-
schemaVersion: input.category === "change-request" ? "1.1.0" : "1.0.0",
|
|
2227
|
-
kind: input.action,
|
|
2228
|
-
author: "Design owner",
|
|
2229
|
-
reviewOf: pin.reviewOf,
|
|
2230
|
-
pinId: pin.id,
|
|
2231
|
-
[input.action]: input[input.action],
|
|
2232
|
-
...input.action === "disposition" ? { reason: input.reason ?? "" } : {},
|
|
2233
|
-
updatedAt
|
|
2234
|
-
}
|
|
2235
|
-
};
|
|
2236
|
-
return;
|
|
2237
|
-
}
|
|
2238
|
-
const path = join4(dirname4(designSpecPath(value.current.root)), "review-handoff.json");
|
|
2239
|
-
const previous = readJson(path, null);
|
|
2240
|
-
if (previous) assertDraft(previous);
|
|
2241
|
-
if (input.version !== (previous?.version ?? 0))
|
|
2242
|
-
throw conflict2("The handoff changed in another window. Reload before saving.");
|
|
2243
|
-
let shareUrl;
|
|
2244
|
-
try {
|
|
2245
|
-
shareUrl = getDesignShareStatus(file, { env, ...shareOptions }).url;
|
|
2246
|
-
} catch {
|
|
2247
|
-
}
|
|
2248
|
-
if (input.action === "approve") {
|
|
2249
|
-
if (!previous || input.contentHash !== previous.contentHash || reviewDigest(previous.basis) !== reviewDigest(value.basis))
|
|
2250
|
-
throw conflict2(
|
|
2251
|
-
"The handoff is out of date. Rebuild and review the current draft before approving."
|
|
2252
|
-
);
|
|
2253
|
-
if (!canApproveDesignHandoffResolution(value.resolution)) {
|
|
2254
|
-
const diagnostic = value.resolution.diagnostics[0];
|
|
2255
|
-
throw conflict2(
|
|
2256
|
-
diagnostic?.message ?? "Resolve the blocking review decisions before approving this handoff."
|
|
2257
|
-
);
|
|
2258
|
-
}
|
|
2259
|
-
if (!previous.content.summary.trim())
|
|
2260
|
-
throw new Error("Write or refine the handoff summary before approving it.");
|
|
2261
|
-
const approved = {
|
|
2262
|
-
...previous,
|
|
2263
|
-
version: previous.version + 1,
|
|
2264
|
-
status: "approved",
|
|
2265
|
-
approval: { contentHash: previous.contentHash, at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
2266
|
-
};
|
|
2267
|
-
approved.markdown = renderMarkdown(approved, value.current.document.title);
|
|
2268
|
-
assertReviewExperience(approved, DESIGN_HANDOFF_SCHEMA);
|
|
2269
|
-
const archive = join4(
|
|
2270
|
-
value.current.root,
|
|
2271
|
-
".design/handoff-approvals",
|
|
2272
|
-
`${approved.contentHash}.json`
|
|
2273
|
-
);
|
|
2274
|
-
if (!existsSync4(archive)) atomicJson(archive, approved);
|
|
2275
|
-
atomicJson(path, approved);
|
|
2276
|
-
writeFileSync3(path.replace(/\.json$/u, ".md"), approved.markdown);
|
|
2277
|
-
return;
|
|
2278
|
-
}
|
|
2279
|
-
const content = input.action === "draft" ? contentFromFeedback(value, shareUrl) : enrichContent(input.content, value, shareUrl);
|
|
2280
|
-
if (input.action === "update" && (!previous || reviewDigest(previous.basis) !== reviewDigest(value.basis)))
|
|
2281
|
-
throw conflict2("The review changed. Rebuild the draft before refining it.");
|
|
2282
|
-
const affectedScreens = [
|
|
2283
|
-
...new Set(
|
|
2284
|
-
sections.flatMap((key) => content[key].map((item) => item.screenId).filter(Boolean))
|
|
2285
|
-
)
|
|
2286
|
-
];
|
|
2287
|
-
const verificationGaps = value.current.verification.status === "verified" ? [] : [`Rendered design verification: ${value.current.verification.status}.`];
|
|
2288
|
-
for (const issue2 of value.current.verification.issues ?? [])
|
|
2289
|
-
if (issue2.message && verificationGaps.length < 256) verificationGaps.push(issue2.message);
|
|
2290
|
-
const reviewNotes = (value.feedback.ledger?.reviews ?? []).filter((entry) => entry.review.overall?.trim()).map((entry) => ({ reviewId: entry.review.reviewId, text: entry.review.overall }));
|
|
2291
|
-
const title2 = value.current.document.title;
|
|
2292
|
-
const draft = {
|
|
2293
|
-
title: title2,
|
|
2294
|
-
reviewNotes,
|
|
2295
|
-
kind: "openplanr-design-review-handoff",
|
|
2296
|
-
schemaVersion: "1.0.0",
|
|
2297
|
-
version: (previous?.version ?? 0) + 1,
|
|
2298
|
-
status: "draft",
|
|
2299
|
-
basis: value.basis,
|
|
2300
|
-
content,
|
|
2301
|
-
affectedScreens,
|
|
2302
|
-
verificationGaps,
|
|
2303
|
-
contentHash: reviewDigest({
|
|
2304
|
-
title: title2,
|
|
2305
|
-
reviewNotes,
|
|
2306
|
-
basis: value.basis,
|
|
2307
|
-
content,
|
|
2308
|
-
affectedScreens,
|
|
2309
|
-
verificationGaps
|
|
2310
|
-
}),
|
|
2311
|
-
markdown: ""
|
|
2312
|
-
};
|
|
2313
|
-
draft.markdown = renderMarkdown(draft, value.current.document.title);
|
|
2314
|
-
assertReviewExperience(draft, DESIGN_HANDOFF_SCHEMA);
|
|
2315
|
-
atomicJson(path, draft);
|
|
2316
|
-
writeFileSync3(path.replace(/\.json$/u, ".md"), draft.markdown);
|
|
2317
|
-
});
|
|
2318
|
-
} finally {
|
|
2319
|
-
unlock();
|
|
2320
|
-
}
|
|
2321
|
-
} finally {
|
|
2322
|
-
unlockRender();
|
|
2323
|
-
}
|
|
2324
|
-
let synchronization;
|
|
2325
|
-
if (outgoing) {
|
|
2326
|
-
try {
|
|
2327
|
-
synchronization = await publishDesignReviewMetadata(file, outgoing.payload, {
|
|
2328
|
-
revisionId: outgoing.revisionId,
|
|
2329
|
-
env,
|
|
2330
|
-
...shareOptions
|
|
2331
|
-
});
|
|
2332
|
-
} catch (error) {
|
|
2333
|
-
synchronization = {
|
|
2334
|
-
pending: false,
|
|
2335
|
-
error: error.message,
|
|
2336
|
-
...error.code ? { code: error.code } : {}
|
|
2337
|
-
};
|
|
2338
|
-
}
|
|
2339
|
-
}
|
|
2340
|
-
return {
|
|
2341
|
-
...readDesignHandoff(file, { env, ...shareOptions }),
|
|
2342
|
-
...synchronization ? { synchronization } : {}
|
|
2343
|
-
};
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
export {
|
|
2347
|
-
PLANNING_FOLDER,
|
|
2348
|
-
CLI_COMMAND,
|
|
2349
|
-
createReviewLedger,
|
|
2350
|
-
mergeReviewLedger,
|
|
2351
|
-
effectiveReviewDecision,
|
|
2352
|
-
ARTIFACT_REVIEW_MAX_STATE_BYTES,
|
|
2353
|
-
normalizeArtifactReview,
|
|
2354
|
-
readArtifactReviewState,
|
|
2355
|
-
writeArtifactReviewState,
|
|
2356
|
-
withArtifactReviewLock,
|
|
2357
|
-
exportArtifactReview,
|
|
2358
|
-
resolveArtifactReviewDestination,
|
|
2359
|
-
decodeArtifactReviewSources,
|
|
2360
|
-
designReviewKey,
|
|
2361
|
-
designReviewPath,
|
|
2362
|
-
readDesignFeedback,
|
|
2363
|
-
isDesignHandoffRelativePath,
|
|
2364
|
-
designImplementationHandoffDigest,
|
|
2365
|
-
assertDesignImplementationHandoff,
|
|
2366
|
-
readDesignExperience,
|
|
2367
|
-
readDesignHandoff,
|
|
2368
|
-
readDesignHandoffReadiness,
|
|
2369
|
-
updateDesignHandoff
|
|
2370
|
-
};
|