@rig-c/create 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +163 -2
- package/docs/CHECKS.md +1665 -0
- package/docs/COLLECTIONS.md +337 -0
- package/docs/CRAFT.md +277 -0
- package/docs/INTERVIEW.md +886 -0
- package/docs/LESSONS.md +172 -0
- package/docs/PROJECT.md +2657 -0
- package/docs/SOURCE.md +225 -0
- package/package.json +44 -4
- package/src/agent-bounce.ts +27 -0
- package/src/agent-guide.ts +237 -0
- package/src/amend.ts +193 -0
- package/src/applied-review.ts +104 -0
- package/src/ask-coverage.ts +69 -0
- package/src/assemble-answers.ts +53 -0
- package/src/cg-gates.ts +211 -0
- package/src/check-reads.ts +106 -0
- package/src/check.ts +1059 -0
- package/src/checkpoint-evidence-writer.ts +125 -0
- package/src/checkpoint-evidence.ts +152 -0
- package/src/choice-writers.ts +271 -0
- package/src/choices.ts +378 -0
- package/src/cli.ts +371 -0
- package/src/collection-answer.ts +136 -0
- package/src/collection-binding-reader.ts +496 -0
- package/src/collection-binding.ts +478 -0
- package/src/collection.ts +348 -0
- package/src/config-inputs.ts +155 -0
- package/src/config-lost.ts +135 -0
- package/src/correction-writers.ts +207 -0
- package/src/corrections.ts +685 -0
- package/src/diff-classes.ts +219 -0
- package/src/diff-map.ts +248 -0
- package/src/direction-citation.ts +13 -0
- package/src/direction-writers.ts +119 -0
- package/src/direction.ts +266 -0
- package/src/drafts.ts +363 -0
- package/src/feedback.ts +744 -0
- package/src/figure-mask.ts +143 -0
- package/src/fix-ledger.ts +355 -0
- package/src/init.ts +265 -0
- package/src/inputs.ts +160 -0
- package/src/intake.ts +158 -0
- package/src/interview.ts +53 -0
- package/src/invariants.ts +132 -0
- package/src/job-acts.ts +80 -0
- package/src/job-advance.ts +65 -0
- package/src/job-close.ts +226 -0
- package/src/job-host.ts +529 -0
- package/src/job-outbox.ts +383 -0
- package/src/job-publication.ts +701 -0
- package/src/job-runtime.ts +99 -0
- package/src/job-writer.ts +123 -0
- package/src/meshes-bust.ts +63 -0
- package/src/next.ts +1506 -0
- package/src/overrides.ts +42 -0
- package/src/package-upgrades.ts +32 -0
- package/src/packages-upgrade-check.ts +6 -0
- package/src/packages-upgrade.ts +168 -0
- package/src/parts-act.ts +96 -0
- package/src/parts-check.ts +242 -0
- package/src/parts-review.ts +196 -0
- package/src/parts-sections.ts +98 -0
- package/src/parts-verdict-record.ts +265 -0
- package/src/parts-verdict.ts +33 -0
- package/src/pause.ts +118 -0
- package/src/pins-check.ts +619 -0
- package/src/pins.ts +17 -0
- package/src/plan-bar-amendments.ts +54 -0
- package/src/plan-history.ts +147 -0
- package/src/plan-writers.ts +202 -0
- package/src/plate-receipt.ts +110 -0
- package/src/pre-interview.ts +302 -0
- package/src/prepared-inputs.ts +113 -0
- package/src/progress-writers.ts +181 -0
- package/src/provenance.ts +301 -0
- package/src/record-direction-target.ts +26 -0
- package/src/record-input.ts +18 -0
- package/src/refusal-baseline.ts +55 -0
- package/src/refusal.ts +30 -0
- package/src/reopen-lapses.ts +80 -0
- package/src/reopen.ts +144 -0
- package/src/republish.ts +93 -0
- package/src/restart.ts +141 -0
- package/src/review-act-runtime.ts +16 -0
- package/src/review-act.ts +592 -0
- package/src/review-package.ts +524 -0
- package/src/review-read.ts +238 -0
- package/src/review.ts +432 -0
- package/src/rig-version-date.ts +22 -0
- package/src/rig-version.ts +1047 -0
- package/src/run-record.ts +232 -0
- package/src/scene-authority.ts +410 -0
- package/src/scene-writers.ts +144 -0
- package/src/scene.ts +284 -0
- package/src/setup-painting.ts +46 -0
- package/src/signable.ts +65 -0
- package/src/signoff-answers.ts +76 -0
- package/src/signoff-checkpoint-act.ts +95 -0
- package/src/signoff-target.ts +127 -0
- package/src/signoff.ts +331 -0
- package/src/split-captions.ts +154 -0
- package/src/stage-progress.ts +379 -0
- package/src/stages.ts +148 -0
- package/src/step-hash.ts +19 -0
- package/src/structure-snapshot.ts +109 -0
- package/src/structure.ts +166 -0
- package/src/templates.ts +16 -0
- package/src/waypoints.ts +163 -0
- package/templates/agent-guide.md +108 -0
- package/templates/collection-agent-guide.md +72 -0
- package/templates/collection.json +252 -0
- package/templates/example-chain/AGENTS.md +134 -0
- package/templates/example-chain/CLAUDE.md +3 -0
- package/templates/example-chain/README.md +13 -0
- package/templates/example-chain/chain.json +2869 -0
- package/templates/example-chain/checkpoints/0a-interview-options/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/0a-interview-options/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/0a-interview-options/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/0a-interview-options/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/0a-interview-options/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/0a-interview-options/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/0a-interview-options/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/0a-interview-options/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/0a-interview-options/package.json +9 -0
- package/templates/example-chain/checkpoints/0a-interview-options/project.json +295 -0
- package/templates/example-chain/checkpoints/0a-interview-options.json +127 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/package.json +9 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/project.json +685 -0
- package/templates/example-chain/checkpoints/0b-plan-pending.json +81 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/parts-check.json +127 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/package.json +9 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/project.json +955 -0
- package/templates/example-chain/checkpoints/1-parts-verdict.json +204 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/diff-components.json +34 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/bones.json +190 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/parts-check.json +127 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/parts-review.json +88 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/structure.json +30 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/package.json +9 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/project.json +1198 -0
- package/templates/example-chain/checkpoints/2-rigging-answers.json +254 -0
- package/templates/example-chain/checkpoints/3-rig-review/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/3-rig-review/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/diff-components.json +34 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/bones.json +190 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/constraints.json +272 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/meshes.json +190 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/motion.json +272 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/parts-check.json +127 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/parts-review.json +88 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/setup.png +0 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/structure.json +30 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/v1/config.json +88 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/v1/interview-requirements.json +8 -0
- package/templates/example-chain/checkpoints/3-rig-review/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/3-rig-review/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/3-rig-review/package.json +9 -0
- package/templates/example-chain/checkpoints/3-rig-review/project.json +1909 -0
- package/templates/example-chain/checkpoints/3-rig-review.json +424 -0
- package/templates/example-chain/checkpoints/4-direction-post/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/4-direction-post/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/diff-components.json +34 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/bones.json +190 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/camera_depth.json +102 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/constraints.json +272 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/meshes.json +190 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/motion.json +272 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/post_composition.json +122 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/post_timing.json +102 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/parts-check.json +127 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/parts-review.json +88 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/setup.png +0 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/structure.json +30 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/v1/config.json +88 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/v1/interview-requirements.json +8 -0
- package/templates/example-chain/checkpoints/4-direction-post/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/4-direction-post/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/4-direction-post/package.json +9 -0
- package/templates/example-chain/checkpoints/4-direction-post/project.json +2302 -0
- package/templates/example-chain/checkpoints/4-direction-post.json +456 -0
- package/templates/example-chain/evidence/.gitkeep +0 -0
- package/templates/example-chain/evidence/approval.txt +1 -0
- package/templates/example-chain/evidence/captions.json +101 -0
- package/templates/example-chain/evidence/diff-components.json +34 -0
- package/templates/example-chain/evidence/direction/v2.json +74 -0
- package/templates/example-chain/evidence/direction-input.json +74 -0
- package/templates/example-chain/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/evidence/interview/bones.json +190 -0
- package/templates/example-chain/evidence/interview/camera_depth.json +102 -0
- package/templates/example-chain/evidence/interview/constraints.json +272 -0
- package/templates/example-chain/evidence/interview/meshes.json +190 -0
- package/templates/example-chain/evidence/interview/motion.json +272 -0
- package/templates/example-chain/evidence/interview/post_composition.json +122 -0
- package/templates/example-chain/evidence/interview/post_timing.json +102 -0
- package/templates/example-chain/evidence/interview/reference.json +59 -0
- package/templates/example-chain/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/evidence/parts-check.json +127 -0
- package/templates/example-chain/evidence/parts-review.json +88 -0
- package/templates/example-chain/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
- package/templates/example-chain/evidence/scene/s1.json +65 -0
- package/templates/example-chain/evidence/scene-citations.json +9 -0
- package/templates/example-chain/evidence/setup.png +0 -0
- package/templates/example-chain/evidence/split-config.json +1 -0
- package/templates/example-chain/evidence/structure.json +30 -0
- package/templates/example-chain/evidence/v1/config.json +88 -0
- package/templates/example-chain/evidence/v1/interview-requirements.json +8 -0
- package/templates/example-chain/inputs/background.png +0 -0
- package/templates/example-chain/inputs/painting.png +0 -0
- package/templates/example-chain/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/package.json +9 -0
- package/templates/example-chain/project.json +2593 -0
- package/templates/example-diff/diff-components.json +71 -0
- package/templates/example-diff/recomposite_rig.png +0 -0
- package/templates/example-direction/approval.txt +1 -0
- package/templates/example-direction/derived/review/v1/out/check/build/rectangle.png +0 -0
- package/templates/example-direction/derived/review/v1/out/check/build/run.json +40 -0
- package/templates/example-direction/derived/review/v1/out/check/build/skeleton.atlas +8 -0
- package/templates/example-direction/derived/review/v1/out/check/build/skeleton.json +73 -0
- package/templates/example-direction/derived/review/v1/out/check/build/skeleton.model.json +195 -0
- package/templates/example-direction/evidence/direction/v2.json +69 -0
- package/templates/example-direction/evidence/direction/v3.json +59 -0
- package/templates/example-direction/evidence/images/rectangle.png +0 -0
- package/templates/example-direction/evidence/input.json +69 -0
- package/templates/example-direction/evidence/motion.json +33 -0
- package/templates/example-direction/evidence/patch.json +23 -0
- package/templates/example-direction/evidence/rig-config.json +55 -0
- package/templates/example-direction/evidence/still.png +0 -0
- package/templates/example-direction/project.json +372 -0
- package/templates/example-direction/reasoning.txt +1 -0
- package/templates/example-direction/words.txt +1 -0
- package/templates/example-draft/README.md +28 -0
- package/templates/example-draft/config.json +71 -0
- package/templates/example-draft/derived/drafts/lantern/config.json +71 -0
- package/templates/example-draft/derived/drafts/lantern/input/painting_in_rig.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/background.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/eye_l.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/eye_r.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/face.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/lantern.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/torso.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts.json +188 -0
- package/templates/example-draft/derived/drafts/lantern/input/recomposite_rig.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/interview-requirements.json +8 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/run.json +40 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/background.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/eye_l.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/eye_r.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/face.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/lantern.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/torso.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/mesh_report.json +1 -0
- package/templates/example-draft/derived/drafts/lantern/out/motion.json +137 -0
- package/templates/example-draft/derived/drafts/lantern/out/rig.json +102 -0
- package/templates/example-draft/derived/drafts/lantern/out/run.json +52 -0
- package/templates/example-draft/derived/parts/rig/painting_in_rig.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/background.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/eye_l.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/eye_r.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/face.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/lantern.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/torso.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts.json +188 -0
- package/templates/example-draft/derived/parts/rig/recomposite_rig.png +0 -0
- package/templates/example-draft/derived/review/v1/config.json +71 -0
- package/templates/example-draft/derived/review/v1/interview-requirements.json +8 -0
- package/templates/example-draft/derived/review/v1/out/check/build/run.json +40 -0
- package/templates/example-draft/derived/review/v1/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/derived/review/v1/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/derived/review/v1/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/derived/review/v1/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/background.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/eye_l.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/eye_r.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/face.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/lantern.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/torso.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts.json +188 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/background.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/eye_l.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/eye_r.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/face.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/lantern.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/torso.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/mesh_report.json +1 -0
- package/templates/example-draft/derived/review/v1/out/rig/motion.json +137 -0
- package/templates/example-draft/derived/review/v1/out/rig/rig.json +102 -0
- package/templates/example-draft/derived/review/v1/out/rig/run.json +52 -0
- package/templates/example-draft/derived/review/v2/config.json +71 -0
- package/templates/example-draft/derived/review/v2/interview-requirements.json +8 -0
- package/templates/example-draft/derived/review/v2/out/check/build/run.json +40 -0
- package/templates/example-draft/derived/review/v2/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/derived/review/v2/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/derived/review/v2/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/derived/review/v2/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/background.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/eye_l.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/eye_r.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/face.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/lantern.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/torso.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts.json +188 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/background.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/eye_l.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/eye_r.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/face.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/lantern.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/torso.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/mesh_report.json +1 -0
- package/templates/example-draft/derived/review/v2/out/rig/motion.json +137 -0
- package/templates/example-draft/derived/review/v2/out/rig/rig.json +102 -0
- package/templates/example-draft/derived/review/v2/out/rig/run.json +52 -0
- package/templates/example-draft/evidence/diff-components.json +71 -0
- package/templates/example-draft/evidence/direction/v4.json +64 -0
- package/templates/example-draft/evidence/direction-input-v4.json +64 -0
- package/templates/example-draft/evidence/fix-ledger/lantern.json +17 -0
- package/templates/example-draft/evidence/parts-check.json +127 -0
- package/templates/example-draft/evidence/parts-review.json +88 -0
- package/templates/example-draft/evidence/review-reads/4ed0507381809746443021554e012032be5b5f8417da0f2e525e9b6df16e9f05.json +59 -0
- package/templates/example-draft/evidence/scene/s1.json +65 -0
- package/templates/example-draft/evidence/scene-background.png +0 -0
- package/templates/example-draft/evidence/scene-citations.json +9 -0
- package/templates/example-draft/evidence/structure-decisions.json +9 -0
- package/templates/example-draft/evidence/v1/interview-config.json +71 -0
- package/templates/example-draft/evidence/v1/interview-requirements.json +8 -0
- package/templates/example-draft/evidence/v1/review-config.json +71 -0
- package/templates/example-draft/evidence/v2/interview-config.json +71 -0
- package/templates/example-draft/evidence/v2/interview-requirements.json +8 -0
- package/templates/example-draft/evidence/v2/review-config.json +71 -0
- package/templates/example-draft/input/painting_in_rig.png +0 -0
- package/templates/example-draft/input/parts/background.png +0 -0
- package/templates/example-draft/input/parts/eye_l.png +0 -0
- package/templates/example-draft/input/parts/eye_r.png +0 -0
- package/templates/example-draft/input/parts/face.png +0 -0
- package/templates/example-draft/input/parts/lantern.png +0 -0
- package/templates/example-draft/input/parts/torso.png +0 -0
- package/templates/example-draft/input/parts.json +188 -0
- package/templates/example-draft/input/recomposite_rig.png +0 -0
- package/templates/example-draft/inputs/painting.png +0 -0
- package/templates/example-draft/interview-requirements.json +8 -0
- package/templates/example-draft/out/check/build/run.json +40 -0
- package/templates/example-draft/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/out/images/background.png +0 -0
- package/templates/example-draft/out/images/eye_l.png +0 -0
- package/templates/example-draft/out/images/eye_r.png +0 -0
- package/templates/example-draft/out/images/face.png +0 -0
- package/templates/example-draft/out/images/lantern.png +0 -0
- package/templates/example-draft/out/images/torso.png +0 -0
- package/templates/example-draft/out/mesh_report.json +1 -0
- package/templates/example-draft/out/motion.json +137 -0
- package/templates/example-draft/out/rig.json +102 -0
- package/templates/example-draft/out/run.json +52 -0
- package/templates/example-draft/outputs/v1/out/check/build/run.json +40 -0
- package/templates/example-draft/outputs/v1/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/outputs/v1/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/outputs/v1/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/outputs/v1/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/outputs/v1/out/painting_in_rig.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/background.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/eye_l.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/eye_r.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/face.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/lantern.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/torso.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts.json +188 -0
- package/templates/example-draft/outputs/v1/out/rig/images/background.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/eye_l.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/eye_r.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/face.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/lantern.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/torso.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/mesh_report.json +1 -0
- package/templates/example-draft/outputs/v1/out/rig/motion.json +137 -0
- package/templates/example-draft/outputs/v1/out/rig/rig.json +102 -0
- package/templates/example-draft/outputs/v1/out/rig/run.json +52 -0
- package/templates/example-draft/outputs/v2/out/check/build/run.json +40 -0
- package/templates/example-draft/outputs/v2/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/outputs/v2/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/outputs/v2/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/outputs/v2/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/outputs/v2/out/painting_in_rig.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/background.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/eye_l.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/eye_r.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/face.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/lantern.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/torso.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts.json +188 -0
- package/templates/example-draft/outputs/v2/out/rig/images/background.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/eye_l.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/eye_r.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/face.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/lantern.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/torso.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/mesh_report.json +1 -0
- package/templates/example-draft/outputs/v2/out/rig/motion.json +137 -0
- package/templates/example-draft/outputs/v2/out/rig/rig.json +102 -0
- package/templates/example-draft/outputs/v2/out/rig/run.json +52 -0
- package/templates/example-interview-choice.json +1068 -0
- package/templates/example-interview-options/constraint.json +376 -0
- package/templates/example-interview-options/example-assemble.json +376 -0
- package/templates/example-interview-options/example-bones.json +190 -0
- package/templates/example-interview-options/example-camera-depth.json +99 -0
- package/templates/example-interview-options/example-constraints.json +272 -0
- package/templates/example-interview-options/example-meshes.json +190 -0
- package/templates/example-interview-options/example-motion.json +272 -0
- package/templates/example-interview-options/example-picks.json +15 -0
- package/templates/example-interview-options/example-post-composition.json +110 -0
- package/templates/example-interview-options/example-post-timing.json +105 -0
- package/templates/example-interview-options/example-reference.json +63 -0
- package/templates/example-interview-options/example-rig-type.json +63 -0
- package/templates/example-interview-options/example-seethrough.json +190 -0
- package/templates/example-interview-options/fullscreen.json +63 -0
- package/templates/example-interview-options/other-explicit.json +63 -0
- package/templates/example-interview-options/other-pending.json +63 -0
- package/templates/example-interview-options/other-picks.json +16 -0
- package/templates/example-interview.md +213 -0
- package/templates/example-invariants/invariants.json +1339 -0
- package/templates/example-prepared/prepared.svg +1 -0
- package/templates/example-prepared/source.svg +1 -0
- package/templates/example-project.json +4259 -0
- package/templates/example-remesh/AGENTS.md +134 -0
- package/templates/example-remesh/CLAUDE.md +3 -0
- package/templates/example-remesh/evidence/.gitkeep +0 -0
- package/templates/example-remesh/evidence/approval.txt +1 -0
- package/templates/example-remesh/evidence/captions.json +101 -0
- package/templates/example-remesh/evidence/diff-components.json +34 -0
- package/templates/example-remesh/evidence/direction/v4.json +74 -0
- package/templates/example-remesh/evidence/direction/v5.json +74 -0
- package/templates/example-remesh/evidence/direction-input-v4.json +74 -0
- package/templates/example-remesh/evidence/direction-input-v5.json +74 -0
- package/templates/example-remesh/evidence/interview/assemble.json +309 -0
- package/templates/example-remesh/evidence/interview/bones.json +190 -0
- package/templates/example-remesh/evidence/interview/camera_depth.json +103 -0
- package/templates/example-remesh/evidence/interview/constraints.json +272 -0
- package/templates/example-remesh/evidence/interview/meshes.json +190 -0
- package/templates/example-remesh/evidence/interview/motion.json +272 -0
- package/templates/example-remesh/evidence/interview/post_composition.json +123 -0
- package/templates/example-remesh/evidence/interview/post_timing.json +103 -0
- package/templates/example-remesh/evidence/interview/reference.json +59 -0
- package/templates/example-remesh/evidence/interview/rig_type.json +59 -0
- package/templates/example-remesh/evidence/interview/seethrough.json +186 -0
- package/templates/example-remesh/evidence/parts-check.json +127 -0
- package/templates/example-remesh/evidence/parts-review.json +88 -0
- package/templates/example-remesh/evidence/reopen.txt +1 -0
- package/templates/example-remesh/evidence/review-reads/6a17db694db96f1b6049a61a017e0c8e66757b982b8851e0187e561cfcf9af68.json +59 -0
- package/templates/example-remesh/evidence/scene/s1.json +69 -0
- package/templates/example-remesh/evidence/scene-citations.json +9 -0
- package/templates/example-remesh/evidence/setup.png +0 -0
- package/templates/example-remesh/evidence/split-config.json +1 -0
- package/templates/example-remesh/evidence/structure.json +30 -0
- package/templates/example-remesh/evidence/v1/registration/at-source.json +44 -0
- package/templates/example-remesh/evidence/v1/registration/citations.json +68 -0
- package/templates/example-remesh/evidence/v1/registration/compile-report.json +368 -0
- package/templates/example-remesh/evidence/v1/registration/config-source.json +88 -0
- package/templates/example-remesh/evidence/v1/registration/config.json +88 -0
- package/templates/example-remesh/evidence/v1/registration/interview-requirements.json +8 -0
- package/templates/example-remesh/evidence/v1/registration/package-lock.json +11 -0
- package/templates/example-remesh/evidence/v1/registration/path-adapter.json +1 -0
- package/templates/example-remesh/evidence/v1/registration/rigc-run-1.json +44 -0
- package/templates/example-remesh/evidence/v2/registration/at-source.json +44 -0
- package/templates/example-remesh/evidence/v2/registration/citations.json +68 -0
- package/templates/example-remesh/evidence/v2/registration/compile-report.json +368 -0
- package/templates/example-remesh/evidence/v2/registration/config-source.json +88 -0
- package/templates/example-remesh/evidence/v2/registration/config.json +88 -0
- package/templates/example-remesh/evidence/v2/registration/interview-requirements.json +8 -0
- package/templates/example-remesh/evidence/v2/registration/package-lock.json +11 -0
- package/templates/example-remesh/evidence/v2/registration/path-adapter.json +1 -0
- package/templates/example-remesh/evidence/v2/registration/rigc-run-1.json +44 -0
- package/templates/example-remesh/evidence/v3/registration/at-source.json +44 -0
- package/templates/example-remesh/evidence/v3/registration/citations.json +68 -0
- package/templates/example-remesh/evidence/v3/registration/compile-report.json +368 -0
- package/templates/example-remesh/evidence/v3/registration/config-source.json +88 -0
- package/templates/example-remesh/evidence/v3/registration/config.json +88 -0
- package/templates/example-remesh/evidence/v3/registration/interview-requirements.json +8 -0
- package/templates/example-remesh/evidence/v3/registration/package-lock.json +11 -0
- package/templates/example-remesh/evidence/v3/registration/path-adapter.json +1 -0
- package/templates/example-remesh/evidence/v3/registration/rigc-run-1.json +44 -0
- package/templates/example-remesh/inputs/background.png +0 -0
- package/templates/example-remesh/inputs/painting.png +0 -0
- package/templates/example-remesh/inputs/references/.gitkeep +0 -0
- package/templates/example-remesh/package.json +9 -0
- package/templates/example-remesh/project.json +3916 -0
- package/templates/example-reopened/project.json +4568 -0
- package/templates/example-review/config.json +1 -0
- package/templates/example-review/out/check/build/skeleton.model.json +1 -0
- package/templates/example-review/out/parts/piece.svg +1 -0
- package/templates/example-review/out/parts.json +1 -0
- package/templates/example-review/out/rig/mesh_report.json +1 -0
- package/templates/example-review/out/rig/motion.json +1 -0
- package/templates/example-review/out/rig/rig.json +1 -0
- package/templates/example-split/captions.json +357 -0
- package/templates/example-split/config.json +215 -0
- package/templates/legacy-example-project.json +1071 -0
- package/templates/project.json +199 -0
- package/templates/shared/conventions.json +23 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
import { checkInputs } from './inputs.ts';
|
|
2
|
+
import { pngDimensions } from './scene.ts';
|
|
3
|
+
import { checkpointRequired } from './plan-history.ts';
|
|
4
|
+
import { renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from './check-reads.ts';
|
|
6
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
7
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
8
|
+
import { partsFileReferences } from '@rig-c/parts/src/parts.ts';
|
|
9
|
+
import partsPackage from '@rig-c/parts/package.json';
|
|
10
|
+
import { JOB_ACT_SCHEMA, JOB_API_SCHEMA, type JobPublicationReader, type JobPublishedContext, type JobSelector, type JobRecordSignoffStage } from '@rig-c/review/job-host';
|
|
11
|
+
import { stepHash } from './step-hash.ts';
|
|
12
|
+
import { checkpointEvidenceContext, type CheckpointEvidence } from './checkpoint-evidence.ts';
|
|
13
|
+
import { locateParts, PARTS_PACKAGE } from './parts-sections.ts';
|
|
14
|
+
import { asArray, checkProject, collectItems, isObject, loadCollection, postNotPassed, type Finding } from './check.ts';
|
|
15
|
+
import { checkDiffMap } from './diff-map.ts';
|
|
16
|
+
import {
|
|
17
|
+
COLLECTION_BINDING_SPEC,
|
|
18
|
+
isLiveMemberRoot,
|
|
19
|
+
isValidCollectionBindingPath,
|
|
20
|
+
projectUsesCollection,
|
|
21
|
+
verifyCollectionBinding,
|
|
22
|
+
withCandidateProject,
|
|
23
|
+
} from './collection-binding-reader.ts';
|
|
24
|
+
import { optionContextFiles, optionFile, validateContext } from './choices.ts';
|
|
25
|
+
import { hasStageOrder, interviewStageOfGuide, stageOfStep, stepCoveredByApproval } from './stages.ts';
|
|
26
|
+
import { selectAnswerHashes } from './signoff-answers.ts';
|
|
27
|
+
import { assertInterviewRecordSignable, packageSignSubjects, stageSignable } from './signable.ts';
|
|
28
|
+
import { collectReopens, liveSignoff } from './reopen.ts';
|
|
29
|
+
import { acceptedReviewVerdict } from './review.ts';
|
|
30
|
+
import { requireRecordDirectionTarget } from './record-direction-target.ts';
|
|
31
|
+
import { latestPartsApproval, riggingContext } from './structure.ts';
|
|
32
|
+
import { jobHash, pendingPublications, isRecordResult, type JobIdentity } from './job-outbox.ts';
|
|
33
|
+
import { Refusal, refuse } from './refusal.ts';
|
|
34
|
+
import type { Json, JsonObject } from './templates.ts';
|
|
35
|
+
|
|
36
|
+
export const JOB_READER_ID = 'rig-create/publication-reader/2';
|
|
37
|
+
export const JOB_BINDING_ID = 'rig-create/writer-binding/1';
|
|
38
|
+
export const JOB_ENUMERATOR_ID = 'rig-create/parts-file-references/1';
|
|
39
|
+
export const partsEnumerator = { version: partsPackage.version, enumerate: partsFileReferences };
|
|
40
|
+
|
|
41
|
+
/** The parts the check loads: its name and version, and that version's integrity from the lockfile beside its install. */
|
|
42
|
+
export interface RecordPartsIdentity { readonly name: string; readonly version: string; readonly integrity: string }
|
|
43
|
+
|
|
44
|
+
export interface RecordPublicationRequest {
|
|
45
|
+
readonly root: string;
|
|
46
|
+
readonly record: { readonly path: 'project.json'; readonly sha256: string };
|
|
47
|
+
readonly result: { readonly kind: 'option-set'; readonly questionId: string; readonly setId: string } | { readonly kind: 'plan' };
|
|
48
|
+
readonly phase: 'inventory' | 'retained';
|
|
49
|
+
/** Retained phase only: the `parts` identity the inventory phase returned. */
|
|
50
|
+
readonly parts?: RecordPartsIdentity;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type RecordContextBase = {
|
|
54
|
+
readonly label: string;
|
|
55
|
+
readonly parts: RecordPartsIdentity;
|
|
56
|
+
readonly inputs: JobPublishedContext['files'];
|
|
57
|
+
readonly files: JobPublishedContext['files'];
|
|
58
|
+
};
|
|
59
|
+
type RecordSignoffStage = JobRecordSignoffStage;
|
|
60
|
+
type RecordPlanSubject = { readonly kind: 'checkpoint'; readonly checkpointId: string }
|
|
61
|
+
| { readonly kind: 'signoff'; readonly stage: RecordSignoffStage };
|
|
62
|
+
export type RecordOptionContext = RecordContextBase & {
|
|
63
|
+
readonly plan: null;
|
|
64
|
+
readonly subjects: readonly [{ readonly kind: 'interview-picks'; readonly questionId: string; readonly setId: string }];
|
|
65
|
+
};
|
|
66
|
+
export type RecordPlanContext = RecordContextBase & {
|
|
67
|
+
readonly checkpointEvidence?: readonly CheckpointEvidence[];
|
|
68
|
+
readonly plan: NonNullable<JobPublishedContext['plan']>;
|
|
69
|
+
readonly subjects: readonly RecordPlanSubject[];
|
|
70
|
+
};
|
|
71
|
+
export type RecordPublishedContext = RecordOptionContext | RecordPlanContext;
|
|
72
|
+
|
|
73
|
+
export class RecordClosureIncomplete extends Refusal {
|
|
74
|
+
constructor(readonly details: readonly { code: string; path: string }[]) {
|
|
75
|
+
super('JOB_RECORD_CLOSURE_INCOMPLETE', 'project.json: the retained check refuses while its sealed files match, so the retained root lacks or changed a file of the inventory closure; restore the complete inventory closure before finalization');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The lockfile entries for parts beside `base/node_modules`: bun.lock and package-lock.json, each one that exists. */
|
|
80
|
+
function lockedParts(base: string): { file: string; version: unknown; integrity: unknown }[] {
|
|
81
|
+
const entries: { file: string; version: unknown; integrity: unknown }[] = [];
|
|
82
|
+
const bun = join(base, 'bun.lock');
|
|
83
|
+
if (existsSync(bun)) {
|
|
84
|
+
let entry: unknown;
|
|
85
|
+
try { entry = (Bun.JSONC.parse(readFileSync(bun, 'utf8')) as { packages?: Record<string, unknown> }).packages?.[PARTS_PACKAGE]; } catch { entry = undefined; }
|
|
86
|
+
const spec = Array.isArray(entry) && typeof entry[0] === 'string' && entry[0].startsWith(`${PARTS_PACKAGE}@`) ? entry[0].slice(PARTS_PACKAGE.length + 1) : undefined;
|
|
87
|
+
entries.push({ file: 'bun.lock', version: spec, integrity: Array.isArray(entry) ? entry.at(-1) : undefined });
|
|
88
|
+
}
|
|
89
|
+
const npm = join(base, 'package-lock.json');
|
|
90
|
+
if (existsSync(npm)) {
|
|
91
|
+
let entry: unknown;
|
|
92
|
+
try { entry = (JSON.parse(readFileSync(npm, 'utf8')) as { packages?: Record<string, unknown> }).packages?.[`node_modules/${PARTS_PACKAGE}`]; } catch { entry = undefined; }
|
|
93
|
+
entries.push({ file: 'package-lock.json', version: isObject(entry as Json) ? (entry as JsonObject).version : undefined, integrity: isObject(entry as Json) ? (entry as JsonObject).integrity : undefined });
|
|
94
|
+
}
|
|
95
|
+
return entries;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The identity of the parts `checkProject(dir)` loads (`locateParts`): the installed version and the
|
|
100
|
+
* integrity every lockfile beside that install records for it. Never the version alone.
|
|
101
|
+
*/
|
|
102
|
+
export function loadedPartsIdentity(dir: string, who: 'project' | 'host'): RecordPartsIdentity {
|
|
103
|
+
const missing = (why: string): never => refuse('JOB_RECORD_PARTS_LOCK_MISSING', `project.json: the ${who}'s ${PARTS_PACKAGE} ${why}; install it from a lockfile that records its integrity and publish again`);
|
|
104
|
+
const installed = locateParts(dir);
|
|
105
|
+
if (installed === null) return missing('is not installed');
|
|
106
|
+
let version: unknown;
|
|
107
|
+
try { version = (JSON.parse(readFileSync(join(installed, 'package.json'), 'utf8')) as { version?: unknown }).version; } catch { version = undefined; }
|
|
108
|
+
if (typeof version !== 'string') return missing(`at ${installed} has no readable version`);
|
|
109
|
+
const base = dirname(dirname(dirname(installed)));
|
|
110
|
+
const entries = lockedParts(base);
|
|
111
|
+
if (!entries.length) return missing(`${version} has no lockfile beside its install`);
|
|
112
|
+
const integrities = new Set<string>();
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
if (entry.version !== version || typeof entry.integrity !== 'string' || !/^sha(256|384|512)-/.test(entry.integrity)) {
|
|
115
|
+
return missing(`${version} has no matching ${entry.file} entry with an integrity`);
|
|
116
|
+
}
|
|
117
|
+
integrities.add(entry.integrity);
|
|
118
|
+
}
|
|
119
|
+
if (integrities.size !== 1) return missing(`${version} has different integrities in bun.lock and package-lock.json`);
|
|
120
|
+
return { name: PARTS_PACKAGE, version, integrity: [...integrities][0]! };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Reuse check's eligibility and the exact retained evidence of its subject. No writes or locks. */
|
|
124
|
+
export function readRecordPublished(input: RecordPublicationRequest & { result: Extract<RecordPublicationRequest['result'], { kind: 'option-set' }> }, now?: number): RecordOptionContext;
|
|
125
|
+
export function readRecordPublished(input: RecordPublicationRequest & { result: { kind: 'plan' } }, now?: number): RecordPlanContext;
|
|
126
|
+
export function readRecordPublished(input: RecordPublicationRequest, now?: number): RecordPublishedContext;
|
|
127
|
+
export function readRecordPublished({ root, record, result, phase, parts: sealedParts }: RecordPublicationRequest, now = Date.now()): RecordPublishedContext {
|
|
128
|
+
try {
|
|
129
|
+
const bytes = publicationFile(root, record.path);
|
|
130
|
+
if (record.path !== 'project.json' || jobHash(bytes) !== record.sha256) refuse('JOB_PUBLICATION_INVALID', 'project.json: the committed record binding changed; restore its exact sealed bytes');
|
|
131
|
+
const project: JsonObject = JSON.parse(bytes.toString('utf8'));
|
|
132
|
+
if (phase !== 'inventory' && phase !== 'retained') throw Error('phase must be inventory or retained');
|
|
133
|
+
const needsCollectionBinding = isLiveMemberRoot(root) || project.collection_binding !== undefined || projectUsesCollection(project);
|
|
134
|
+
let verifiedCollection: JsonObject | null = null;
|
|
135
|
+
if (phase === 'inventory' && needsCollectionBinding) {
|
|
136
|
+
const verified = verifyCollectionBinding(root, project, { requireBindingWhenExternal: true });
|
|
137
|
+
if (!verified.valid) {
|
|
138
|
+
refuse(
|
|
139
|
+
'JOB_RECORD_COLLECTION_EXTERNAL',
|
|
140
|
+
verified.finding?.message ??
|
|
141
|
+
"project.json: a member record's inherited answers depend on ../../collection.json, which the project-relative closure cannot seal; bind ../../collection.json with rig-create collection-binding <member dir>",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
verifiedCollection = verified.boundCollection;
|
|
145
|
+
}
|
|
146
|
+
// The check's waypoints come from the parts it loads, which no project file seals.
|
|
147
|
+
let parts = loadedPartsIdentity(root, phase === 'inventory' ? 'project' : 'host');
|
|
148
|
+
if (phase === 'retained') {
|
|
149
|
+
if (!isObject(sealedParts as Json | undefined) || typeof sealedParts!.name !== 'string' || typeof sealedParts!.version !== 'string' || typeof sealedParts!.integrity !== 'string') {
|
|
150
|
+
refuse('JOB_PUBLICATION_INVALID', 'project.json: the retained phase needs the parts identity the inventory phase returned; pass it unchanged');
|
|
151
|
+
}
|
|
152
|
+
if (sealedParts!.name !== parts.name || sealedParts!.version !== parts.version || sealedParts!.integrity !== parts.integrity) {
|
|
153
|
+
refuse('JOB_RECORD_PARTS_MISMATCH', `project.json: the project was checked with ${sealedParts!.name} ${sealedParts!.version} (${sealedParts!.integrity}), but the host loads ${parts.name} ${parts.version} (${parts.integrity}); rebuild the project on the host's ${parts.name} ${parts.version}, or run the check where ${sealedParts!.version} is installed`);
|
|
154
|
+
}
|
|
155
|
+
parts = { name: sealedParts!.name, version: sealedParts!.version, integrity: sealedParts!.integrity };
|
|
156
|
+
}
|
|
157
|
+
const seals = new Map<string, string>();
|
|
158
|
+
const seal = (raw: Json): { path: string; sha256: string } => {
|
|
159
|
+
if (!isObject(raw) || typeof raw.path !== 'string' || typeof raw.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(raw.sha256)) refuse('JOB_PUBLICATION_INVALID', 'project.json: retained file identity is malformed; restore its literal path and sha256');
|
|
160
|
+
const { path, sha256 } = raw;
|
|
161
|
+
if (seals.has(path) && seals.get(path) !== sha256) refuse('JOB_PUBLICATION_INVALID', `project.json: ${path} has conflicting hashes; restore one immutable identity`);
|
|
162
|
+
if (jobHash(publicationFile(root, path)) !== sha256) refuse('JOB_PUBLICATION_INVALID', `project.json: ${path} differs from its retained bytes; restore its sealed bytes`);
|
|
163
|
+
seals.set(path, sha256);
|
|
164
|
+
return { path, sha256 };
|
|
165
|
+
};
|
|
166
|
+
const list = (raw: Json | undefined): { path: string; sha256: string }[] => {
|
|
167
|
+
if (!Array.isArray(raw)) refuse('JOB_PUBLICATION_INVALID', 'project.json: retained file list is missing; restore its declared identities');
|
|
168
|
+
const paths = new Set<string>();
|
|
169
|
+
return raw.map(value => {
|
|
170
|
+
const identity = seal(value);
|
|
171
|
+
if (paths.has(identity.path)) refuse('JOB_PUBLICATION_INVALID', `project.json: ${identity.path} is declared twice in one list; retain a single declaration`);
|
|
172
|
+
paths.add(identity.path);
|
|
173
|
+
return identity;
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
const sealContext = (context: JsonObject): void => {
|
|
177
|
+
if (context.kind === 'inputs') list(context.files);
|
|
178
|
+
if (isObject(context.parts)) list(context.parts.files);
|
|
179
|
+
for (const binding of optionContextFiles(context)) seal(binding);
|
|
180
|
+
};
|
|
181
|
+
seal({ path: record.path, sha256: record.sha256 });
|
|
182
|
+
if (project.collection_binding !== undefined) {
|
|
183
|
+
if (
|
|
184
|
+
!isObject(project.collection_binding) ||
|
|
185
|
+
project.collection_binding.spec !== COLLECTION_BINDING_SPEC ||
|
|
186
|
+
typeof project.collection_binding.path !== 'string' ||
|
|
187
|
+
!isValidCollectionBindingPath(project.collection_binding.path) ||
|
|
188
|
+
typeof project.collection_binding.sha256 !== 'string' ||
|
|
189
|
+
!/^[a-f0-9]{64}$/.test(project.collection_binding.sha256) ||
|
|
190
|
+
typeof project.collection_binding.collection_sha256 !== 'string' ||
|
|
191
|
+
!/^[a-f0-9]{64}$/.test(project.collection_binding.collection_sha256)
|
|
192
|
+
) {
|
|
193
|
+
refuse('JOB_PUBLICATION_INVALID', 'project.json: retained collection_binding identity is malformed; restore its path, sha256, spec, and collection_sha256');
|
|
194
|
+
}
|
|
195
|
+
if (phase === 'inventory' || existsSync(join(root, project.collection_binding.path))) {
|
|
196
|
+
seal({ path: project.collection_binding.path, sha256: project.collection_binding.sha256 });
|
|
197
|
+
if (phase === 'retained') {
|
|
198
|
+
const verified = verifyCollectionBinding(root, project, { requireBindingWhenExternal: true });
|
|
199
|
+
if (!verified.valid) {
|
|
200
|
+
refuse(
|
|
201
|
+
isLiveMemberRoot(root) ? 'JOB_RECORD_COLLECTION_EXTERNAL' : 'JOB_PUBLICATION_INVALID',
|
|
202
|
+
verified.finding?.message ?? 'project.json: retained collection_binding is invalid',
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
verifiedCollection = verified.boundCollection;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else if (phase === 'retained' && needsCollectionBinding) {
|
|
209
|
+
refuse(
|
|
210
|
+
'JOB_RECORD_COLLECTION_EXTERNAL',
|
|
211
|
+
"project.json: a member record's inherited answers depend on ../../collection.json, which the project-relative closure cannot seal without collection_binding",
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const inputs = list(project.inputs);
|
|
215
|
+
const collection = verifiedCollection ?? loadCollection(root, project);
|
|
216
|
+
const items = collectItems(project, { collection });
|
|
217
|
+
const plan = isObject(project.plan) ? project.plan : null;
|
|
218
|
+
const steps = asArray(plan?.steps).filter(isObject);
|
|
219
|
+
const ids = result.kind === 'option-set' ? [result.questionId, result.setId] : steps.map(step => step.id);
|
|
220
|
+
if (ids.some(id => typeof id !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(id))) {
|
|
221
|
+
refuse('JOB_RECORD_SUBJECT_INVALID', 'project.json: a requested question, set or displayed step id is outside the record publication id domain; retain a representable producer id without aliases');
|
|
222
|
+
}
|
|
223
|
+
let selectedSet: JsonObject | undefined;
|
|
224
|
+
if (result.kind === 'option-set') {
|
|
225
|
+
const matches = items.filter(item => item.id === result.questionId);
|
|
226
|
+
if (matches.length !== 1) throw Error('the requested question is absent or ambiguous');
|
|
227
|
+
const binding = matches[0].record.pending_options;
|
|
228
|
+
// A question whose pick is already recorded has no pending set to publish.
|
|
229
|
+
if (!isObject(binding)) refuse('JOB_RECORD_SUBJECT_INVALID', 'project.json: the requested question has no pending option set; publish the exact current pending set');
|
|
230
|
+
seal(binding);
|
|
231
|
+
selectedSet = optionFile(root, binding).set;
|
|
232
|
+
sealContext(selectedSet.context as JsonObject);
|
|
233
|
+
} else if (result.kind === 'plan') {
|
|
234
|
+
if (!plan && project.stage === 'interview' && stageSignable(project, 'interview')) {
|
|
235
|
+
refuse('INTERVIEW_SIGNOFF_PLAN_REQUIRED', 'project.json.plan.steps: interview record sign-off needs displayed plan steps; draft the plan through the writers and publish it with job open --plan');
|
|
236
|
+
}
|
|
237
|
+
if (!plan) throw Error('the requested plan is absent');
|
|
238
|
+
// Seal current answer evidence for every displayed step before checking eligibility.
|
|
239
|
+
const basis = new Set(steps.flatMap(step => asArray(step.rests_on).map(String)));
|
|
240
|
+
for (const item of items.filter(item => basis.has(item.id))) {
|
|
241
|
+
if (item.state.state !== 'user' && item.state.state !== 'delegated') continue;
|
|
242
|
+
const choice = item.state.answer.choice;
|
|
243
|
+
if (!isObject(choice)) continue;
|
|
244
|
+
seal(choice.set!);
|
|
245
|
+
sealContext(optionFile(root, choice.set).set.context as JsonObject);
|
|
246
|
+
}
|
|
247
|
+
const approval = latestPartsApproval(project);
|
|
248
|
+
if (isObject(approval?.evidence)) seal(approval.evidence);
|
|
249
|
+
if (isObject(approval?.structure)) seal(approval.structure);
|
|
250
|
+
} else throw Error('unknown record result');
|
|
251
|
+
// Seal the decision record and every file its record check reads, rather than
|
|
252
|
+
// revalidating all producer artifacts from earlier versions and drafts.
|
|
253
|
+
const interviewSignoff = result.kind === 'plan' && project.stage === 'interview' && stageSignable(project, 'interview');
|
|
254
|
+
const report = checkProject(root, project, { now, readOnly: true, recordReads: true, recordPublication: !interviewSignoff });
|
|
255
|
+
for (const { path, sha256 } of report.reads ?? []) seal({ path, sha256 });
|
|
256
|
+
// A pick is how an open interview receives its first answer. Its own silent
|
|
257
|
+
// questions are todos, as in next/advance; they are not a plan-approval waiver.
|
|
258
|
+
const openQuestions = result.kind === 'option-set' && ['interview', 'rigging-interview', 'post-interview'].includes(String(report.effective_stage))
|
|
259
|
+
? new Set(items.filter(item => item.state.state === 'silent'
|
|
260
|
+
&& interviewStageOfGuide(item.id, item.kind === 'post', hasStageOrder(project)) === report.effective_stage).map(item => item.path))
|
|
261
|
+
: new Set<string>();
|
|
262
|
+
const blocking = report.refusals.filter(finding => finding.code !== 'INTERVIEW_INCOMPLETE' || !openQuestions.has(finding.path));
|
|
263
|
+
if (blocking.length && phase === 'retained') {
|
|
264
|
+
throw new RecordClosureIncomplete(blocking.map(({ code, path }) => ({ code, path })));
|
|
265
|
+
}
|
|
266
|
+
// A record publication necessarily owns an outbox entry while its official reader
|
|
267
|
+
// inventories it. Ignore only that exact record/result's pending transport hole;
|
|
268
|
+
// an unrelated, stale or malformed outbox continues to block publication.
|
|
269
|
+
const pending = phase === 'inventory' ? pendingPublications(root) : [];
|
|
270
|
+
const ownPending = pending.length > 0 && pending.every(entry => entry.phase === 'pending'
|
|
271
|
+
&& entry.recordHash === record.sha256 && isRecordResult(entry.result) && isDeepStrictEqual(entry.result, result));
|
|
272
|
+
const holes = ownPending ? report.holes?.filter(hole => hole.code !== 'JOB_PUBLISH_PENDING') : report.holes;
|
|
273
|
+
if (interviewSignoff) assertInterviewRecordSignable(project, { ...report, holes });
|
|
274
|
+
const finding = blocking[0] ?? holes?.[0];
|
|
275
|
+
if (finding) throw Error(`${finding.code} at ${finding.path}`);
|
|
276
|
+
const stage = report.effective_stage;
|
|
277
|
+
let subjects: RecordPlanSubject[] = [];
|
|
278
|
+
let label: string;
|
|
279
|
+
if (result.kind === 'option-set') {
|
|
280
|
+
const set = selectedSet!;
|
|
281
|
+
if (set.id !== result.setId || set.question !== result.questionId || set.stage !== stage) {
|
|
282
|
+
refuse('JOB_RECORD_SUBJECT_INVALID', 'project.json: requested option set does not match its question, set id and effective stage; publish the exact current pending set');
|
|
283
|
+
}
|
|
284
|
+
withCandidateProject(root, project, () => validateContext(root, project, set, now, true));
|
|
285
|
+
label = `option-set ${result.questionId} / ${result.setId}`;
|
|
286
|
+
} else if (result.kind === 'plan') {
|
|
287
|
+
if (!plan) throw Error('the requested plan is absent');
|
|
288
|
+
if (stage === 'direction-review') {
|
|
289
|
+
const reopens = collectReopens(project, []);
|
|
290
|
+
const post = asArray(project.versions).filter(isObject).filter(version => version.kind === 'post'
|
|
291
|
+
&& liveSignoff(reopens, 'post', Date.parse(String(version.at)))).at(-1);
|
|
292
|
+
const verdictIndex = post ? acceptedReviewVerdict(post, stage) : -1;
|
|
293
|
+
const verdict = verdictIndex >= 0 ? asArray(post?.verdicts)[verdictIndex] : undefined;
|
|
294
|
+
if (!isObject(verdict) || !liveSignoff(reopens, 'direction-review', Date.parse(String(verdict.at)))) {
|
|
295
|
+
refuse('JOB_DIRECTION_VERDICT_REQUIRED', 'project.json.versions: the current live post has no accepted direction-review verdict; open it with job open --rig <n> and ingest review <n> --from-review <actId> before requesting the direction sign-off');
|
|
296
|
+
}
|
|
297
|
+
requireRecordDirectionTarget(project, now);
|
|
298
|
+
}
|
|
299
|
+
const covered = steps.filter(step => stepCoveredByApproval(project, step, stage));
|
|
300
|
+
const signable = stage !== null && ['interview', 'plan-approved', 'rigging-interview', 'post-interview', 'direction-review'].includes(stage)
|
|
301
|
+
&& (stage !== 'interview' || interviewSignoff) && stageSignable(project, stage);
|
|
302
|
+
if (signable && stage === 'direction-review' && steps.length === 0) throw Error('direction-review record sign-off needs displayed plan steps');
|
|
303
|
+
const emptyPost = stage === 'post-interview' && !steps.some(step => step.phase === 'post') && postNotPassed({ items });
|
|
304
|
+
if (signable && stage !== 'interview' && stage !== 'direction-review' && !covered.length && !emptyPost) throw Error('the requested plan has no steps at this approval stage');
|
|
305
|
+
const signature: JsonObject = { stage, steps: covered.map(step => step.id!) };
|
|
306
|
+
if (signable && stage !== 'direction-review') withCandidateProject(root, project, () => selectAnswerHashes(root, project, signature));
|
|
307
|
+
if (signable && stage === 'rigging-interview') {
|
|
308
|
+
const errors: Parameters<typeof riggingContext>[3] = [];
|
|
309
|
+
const context = withCandidateProject(root, project, () => riggingContext(root, project, new Date(now).toISOString(), errors));
|
|
310
|
+
if (errors.length || !context) throw Error(errors[0]?.code ?? 'rigging evidence is missing');
|
|
311
|
+
}
|
|
312
|
+
subjects = steps.filter(step => {
|
|
313
|
+
if (step.status !== 'at-checkpoint' || (step.phase !== 'rig' && step.phase !== 'post') || stageOfStep(step.phase, String(step.id)) !== stage) return false;
|
|
314
|
+
return checkpointRequired(project, step, now);
|
|
315
|
+
}).map(step => ({ kind: 'checkpoint' as const, checkpointId: String(step.id) }));
|
|
316
|
+
if (signable) subjects = [...subjects, { kind: 'signoff', stage: stage as RecordSignoffStage }];
|
|
317
|
+
label = `plan / ${stage}`;
|
|
318
|
+
} else throw Error('unknown record result');
|
|
319
|
+
// Detect record changes during closure reads as well as changes before entry.
|
|
320
|
+
if (!publicationFile(root, record.path).equals(bytes)) throw Error('project.json changed while reading its closure');
|
|
321
|
+
const files = [...seals].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([path, sha256]) => ({ path, sha256 }));
|
|
322
|
+
if (result.kind === 'option-set') {
|
|
323
|
+
return { label, parts, inputs, files, plan: null,
|
|
324
|
+
subjects: [{ kind: 'interview-picks', questionId: result.questionId, setId: result.setId }] };
|
|
325
|
+
}
|
|
326
|
+
const evidence = checkpointEvidenceContext(root, project, subjects, files);
|
|
327
|
+
return { label, parts, inputs, files, ...evidence,
|
|
328
|
+
plan: { steps: steps.map(step => ({ id: String(step.id), hash: stepHash(step) })) }, subjects };
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (error instanceof Refusal && (phase === 'retained' || ['JOB_RECORD_SUBJECT_INVALID', 'JOB_RECORD_COLLECTION_EXTERNAL', 'JOB_RECORD_PARTS_LOCK_MISSING', 'JOB_DIRECTION_VERDICT_REQUIRED', 'REVIEW_ACT_RECORD_TARGET_INVALID', 'REVIEW_ACT_TARGET_MISMATCH', 'INTERVIEW_SIGNOFF_NOT_CLEAN', 'INTERVIEW_SIGNOFF_PLAN_REQUIRED'].includes(error.code))) throw error;
|
|
331
|
+
refuse('JOB_RECORD_INCOMPLETE', `project.json: ${error instanceof Error ? error.message : String(error)}; restore the requested record and retained evidence, resolve check refusals and reconcile pending publications before publishing`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function currentJobIdentity(saved: Pick<JobIdentity, 'root' | 'stateDir' | 'fingerprint' | 'reviewer' | 'publicationMode'>, root: string): JobIdentity {
|
|
336
|
+
return { ...saved, root: realpathSync(root), readerId: JOB_READER_ID, bindingId: JOB_BINDING_ID,
|
|
337
|
+
partsEnumeratorId: JOB_ENUMERATOR_ID, apiSchema: JOB_API_SCHEMA, actSchema: JOB_ACT_SCHEMA,
|
|
338
|
+
enumeratorVersion: partsEnumerator.version };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function publicationFile(root: string, path: string): Buffer {
|
|
342
|
+
if (!path || path.split('/').some(piece => !piece || piece === '.' || piece === '..') || /[\\:%?#\x00-\x1f]/.test(path)) {
|
|
343
|
+
refuse('JOB_PUBLICATION_INVALID', 'project.json: publication references an unsafe path; restore its literal project-relative source');
|
|
344
|
+
}
|
|
345
|
+
const absolute = join(root, path);
|
|
346
|
+
let regular = false;
|
|
347
|
+
try { regular = realpathSync(absolute) === absolute && lstatSync(absolute).isFile(); } catch { /* Named below. */ }
|
|
348
|
+
if (!regular) {
|
|
349
|
+
refuse('JOB_PUBLICATION_INVALID', `project.json: ${path} must be a regular source without symlinks; restore its retained bytes`);
|
|
350
|
+
}
|
|
351
|
+
return readFileSync(absolute);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function publicationSelector(project: JsonObject, n: number): Exclude<JobSelector, { kind: 'draft' }> {
|
|
355
|
+
const version = (project.versions as JsonObject[]).find(version => version.n === n);
|
|
356
|
+
if (!version) refuse('JOB_PUBLICATION_INVALID', `versions[v${n}]: result version is missing; restore the recorded version`);
|
|
357
|
+
return version.kind === 'post'
|
|
358
|
+
? { kind: 'post', version: n, rigVersion: Number(version.rig_version) }
|
|
359
|
+
: { kind: 'rig', version: n };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function originalPaintingReference(root: string, project: JsonObject, n: number) {
|
|
363
|
+
const painting = publishedSourcePainting(root, project, `versions[v${n}]`);
|
|
364
|
+
let size: { width?: number; height?: number } = {};
|
|
365
|
+
if (/\.png$/i.test(painting.path)) {
|
|
366
|
+
try { size = pngDimensions(publicationFile(root, painting.path), painting.path); }
|
|
367
|
+
catch { refuse('JOB_PUBLICATION_INVALID', `versions[v${n}]: ${painting.path} needs a valid PNG header; restore the recorded original painting before publishing`); }
|
|
368
|
+
}
|
|
369
|
+
return { id: 'original-painting', label: 'Original painting', type: 'image', path: painting.path, ...size };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Validate the same retained scene and plate identities for every publication writer and reader. */
|
|
373
|
+
export function sealedScene(root: string, project: JsonObject, directionPath: string, direction: JsonObject | undefined): {
|
|
374
|
+
path: string; sha256: string; plates: { path: string; sha256: string }[];
|
|
375
|
+
} | undefined {
|
|
376
|
+
if (direction?.schema !== 'rig-create/direction@2') return undefined;
|
|
377
|
+
const reference = direction.scene;
|
|
378
|
+
if (!isObject(reference) || typeof reference.path !== 'string' || typeof reference.sha256 !== 'string') refuse('JOB_PUBLICATION_SCENE_MISSING', `${directionPath}: direction.scene is missing; restore its committed scene reference before publishing`);
|
|
379
|
+
const entry = asArray(project.scenes).filter(isObject).find(value => value.n === reference.n && value.path === reference.path && value.sha256 === reference.sha256);
|
|
380
|
+
if (!entry || reference.path !== `evidence/scene/s${String(reference.n)}.json`) refuse('JOB_PUBLICATION_SCENE_CHANGED', `${directionPath}: direction.scene differs from project.scenes; restore the committed scene reference before publishing`);
|
|
381
|
+
let bytes: Buffer;
|
|
382
|
+
try { bytes = publicationFile(root, reference.path); }
|
|
383
|
+
catch { refuse('JOB_PUBLICATION_SCENE_MISSING', `${reference.path}: scene is missing or unreadable; restore its retained bytes before publishing`); }
|
|
384
|
+
if (jobHash(bytes) !== reference.sha256) refuse('JOB_PUBLICATION_SCENE_CHANGED', `${reference.path}: scene hash moved; restore its retained bytes before publishing`);
|
|
385
|
+
let doc: JsonObject;
|
|
386
|
+
try { doc = JSON.parse(bytes.toString('utf8')) as JsonObject; }
|
|
387
|
+
catch { refuse('JOB_PUBLICATION_SCENE_CHANGED', `${reference.path}: scene is not JSON; restore its retained bytes before publishing`); }
|
|
388
|
+
if (!isObject(doc) || doc.schema !== 'rig-create/scene@1' || doc.n !== reference.n || !Array.isArray(doc.plates) || !doc.plates.length) refuse('JOB_PUBLICATION_SCENE_CHANGED', `${reference.path}: scene identity or plates differ; restore the committed scene before publishing`);
|
|
389
|
+
const plates: { path: string; sha256: string }[] = [];
|
|
390
|
+
for (const plate of doc.plates) {
|
|
391
|
+
if (!isObject(plate) || typeof plate.path !== 'string' || typeof plate.sha256 !== 'string') refuse('JOB_PUBLICATION_PLATE_MISSING', `${reference.path}: plate identity is missing; restore every retained plate before publishing`);
|
|
392
|
+
let plateBytes: Buffer;
|
|
393
|
+
try { plateBytes = publicationFile(root, plate.path); }
|
|
394
|
+
catch { refuse('JOB_PUBLICATION_PLATE_MISSING', `${reference.path}: ${plate.path} is missing or unreadable; restore the retained plate before publishing`); }
|
|
395
|
+
if (jobHash(plateBytes) !== plate.sha256) refuse('JOB_PUBLICATION_PLATE_CHANGED', `${reference.path}: ${plate.path} hash moved; restore the retained plate bytes before publishing`);
|
|
396
|
+
plates.push({ path: plate.path, sha256: plate.sha256 });
|
|
397
|
+
}
|
|
398
|
+
return { path: reference.path, sha256: reference.sha256, plates };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** A manifest over the existing frozen model/direction, with project-relative paths.
|
|
402
|
+
* Source documents and compiled producer output remain byte-identical.
|
|
403
|
+
*/
|
|
404
|
+
export function prepareLiveManifest(root: string, project: JsonObject, n: number, warnings: Finding[] = []): string {
|
|
405
|
+
const selected = liveManifestPath(root, project, n);
|
|
406
|
+
if (selected !== `review-live-v${n}.json`) return selected;
|
|
407
|
+
const selector = publicationSelector(project, n);
|
|
408
|
+
const rig = selector.kind === 'post' ? selector.rigVersion : n;
|
|
409
|
+
const model = `derived/review/v${rig}/out/check/build/skeleton.model.json`;
|
|
410
|
+
publicationFile(root, model);
|
|
411
|
+
let scene: string | undefined;
|
|
412
|
+
if (selector.kind === 'post') {
|
|
413
|
+
const directionPath = `evidence/direction/v${n}.json`;
|
|
414
|
+
let direction: JsonObject | undefined;
|
|
415
|
+
try { direction = JSON.parse(publicationFile(root, directionPath).toString('utf8')) as JsonObject; } catch { /* Legacy manifests did not require a direction document. */ }
|
|
416
|
+
scene = sealedScene(root, project, directionPath, direction)?.path;
|
|
417
|
+
}
|
|
418
|
+
const legacyManifest = {
|
|
419
|
+
schema: 'rig-review/1', id: `create-v${n}`, revision: `v${n}`,
|
|
420
|
+
title: selector.kind === 'post' ? `post v${n} (rig v${rig})` : `rig v${n}`,
|
|
421
|
+
model, ...(selector.kind === 'post' ? { direction: `evidence/direction/v${n}.json` } : {}),
|
|
422
|
+
references: [], animations: [], checkpoints: [], authoring: [],
|
|
423
|
+
};
|
|
424
|
+
const path = `review-live-v${n}.json`;
|
|
425
|
+
const existing = existsSync(join(root, path)) ? publicationFile(root, path) : undefined;
|
|
426
|
+
// Reopening an exact old publication preserves both its bytes and its identity.
|
|
427
|
+
const legacyBytes = Buffer.from(`${JSON.stringify(legacyManifest, null, 2)}\n`);
|
|
428
|
+
const sceneManifest = { ...legacyManifest, ...(scene ? { scene } : {}) };
|
|
429
|
+
const sceneBytes = scene ? Buffer.from(`${JSON.stringify(sceneManifest, null, 2)}\n`) : legacyBytes;
|
|
430
|
+
if (existing?.equals(legacyBytes) || existing?.equals(sceneBytes)) return path;
|
|
431
|
+
// init permits projects without an original; publication did not require one.
|
|
432
|
+
if (project.inputs === undefined || (Array.isArray(project.inputs) && project.inputs.length === 0)) {
|
|
433
|
+
if (existing) refuse('JOB_PUBLICATION_INVALID', `versions[v${n}]: ${path} already differs; preserve it and restore the writer-generated manifest`);
|
|
434
|
+
writeFileSync(join(root, path), sceneBytes, { flag: 'wx' });
|
|
435
|
+
return path;
|
|
436
|
+
}
|
|
437
|
+
try { publishedSourcePainting(root, project, `versions[v${n}]`); }
|
|
438
|
+
catch (error) {
|
|
439
|
+
const findings = { dir: root, project, refusals: [] as Finding[], warnings: [] as Finding[] };
|
|
440
|
+
checkInputs(findings);
|
|
441
|
+
const legacy = findings.warnings.find(finding => finding.code === 'INPUT_WRITER_MISSING' && finding.path === 'inputs[0]');
|
|
442
|
+
if (!legacy) throw error;
|
|
443
|
+
if (existing) refuse('JOB_PUBLICATION_INVALID', `versions[v${n}]: ${path} already differs; preserve it and restore the writer-generated manifest`);
|
|
444
|
+
warnings.push(legacy);
|
|
445
|
+
writeFileSync(join(root, path), sceneBytes, { flag: 'wx' });
|
|
446
|
+
return path;
|
|
447
|
+
}
|
|
448
|
+
const reference = originalPaintingReference(root, project, n);
|
|
449
|
+
const manifest = { ...sceneManifest, references: [reference] };
|
|
450
|
+
const bytes = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
451
|
+
if (existing) {
|
|
452
|
+
const previous = Buffer.from(`${JSON.stringify({ ...legacyManifest, references: [reference] }, null, 2)}\n`);
|
|
453
|
+
if (!existing.equals(bytes) && !existing.equals(previous)) refuse('JOB_PUBLICATION_INVALID', `versions[v${n}]: ${path} already differs; preserve it and restore the writer-generated manifest`);
|
|
454
|
+
} else writeFileSync(join(root, path), bytes, { flag: 'wx' });
|
|
455
|
+
return path;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Actual public create read contract, called only under the writer's freeze. */
|
|
459
|
+
export const readPublished: JobPublicationReader = async ({ root, record, result }): Promise<JobPublishedContext> => {
|
|
460
|
+
const bytes = publicationFile(root, record.path);
|
|
461
|
+
if (record.path !== 'project.json' || jobHash(bytes) !== record.sha256 || !result.selector) {
|
|
462
|
+
refuse('JOB_PUBLICATION_INVALID', 'project.json: publication requires the exact committed rig/post record; restore its bound bytes');
|
|
463
|
+
}
|
|
464
|
+
const project = JSON.parse(bytes.toString('utf8')) as JsonObject;
|
|
465
|
+
if (result.selector.kind === 'draft') return readDraftPublished(root, project, result.selector.id, result.path);
|
|
466
|
+
const selector = publicationSelector(project, result.selector.version);
|
|
467
|
+
if (!isDeepStrictEqual(selector, result.selector) || result.path !== liveManifestPath(root, project, selector.version)) {
|
|
468
|
+
refuse('JOB_PUBLICATION_INVALID', 'project.json: publication result differs from its recorded version; restore the writer result');
|
|
469
|
+
}
|
|
470
|
+
const rig = selector.kind === 'post' ? selector.rigVersion : selector.version;
|
|
471
|
+
const rigVersion = (project.versions as JsonObject[]).find(version => version.n === rig);
|
|
472
|
+
const packageRecord = rigVersion?.review_package as JsonObject | undefined;
|
|
473
|
+
if (!Array.isArray(packageRecord?.files)) refuse('JOB_PUBLICATION_INVALID', `versions[v${rig}].review_package: retained package identity is missing; restore the recorded package before reconciling`);
|
|
474
|
+
const packageFiles = packageRecord.files;
|
|
475
|
+
const files = packageFiles.map(raw => {
|
|
476
|
+
const identity = raw as JsonObject;
|
|
477
|
+
const path = `derived/review/v${rig}/${String(identity.path)}`;
|
|
478
|
+
if (jobHash(publicationFile(root, path)) !== identity.sha256) {
|
|
479
|
+
refuse('JOB_PUBLICATION_INVALID', `versions[v${rig}].review_package: ${path} differs from its recorded hash; restore the retained bytes before reconciling`);
|
|
480
|
+
}
|
|
481
|
+
return { path, sha256: String(identity.sha256) };
|
|
482
|
+
});
|
|
483
|
+
// The package records its producer root. Its copied layout historically omits
|
|
484
|
+
// sibling painting/mask PNGs, which the official enumerator still references.
|
|
485
|
+
const partsRoot = typeof packageRecord.from === 'string' ? packageRecord.from : `derived/review/v${rig}/out`;
|
|
486
|
+
const manifestPath = `${partsRoot}/parts.json`;
|
|
487
|
+
const manifest = publicationFile(root, manifestPath);
|
|
488
|
+
const retainedManifest = packageFiles.find(raw => (raw as JsonObject).path === 'out/parts.json') as JsonObject | undefined;
|
|
489
|
+
if (retainedManifest?.sha256 !== jobHash(manifest)) refuse('JOB_PUBLICATION_INVALID', `versions[v${rig}].review_package.from: parts manifest differs from the retained package; restore the declared producer bytes`);
|
|
490
|
+
const directory = dirname(join(root, manifestPath));
|
|
491
|
+
let references: ReturnType<typeof partsFileReferences>;
|
|
492
|
+
try { references = partsFileReferences(directory); }
|
|
493
|
+
catch { refuse('JOB_PUBLICATION_INVALID', `versions[v${rig}].review_package.from: official Parts enumeration refused the manifest; restore the retained producer report before reconciling`); }
|
|
494
|
+
const images = references.map(row => {
|
|
495
|
+
const sha256 = jobHash(publicationFile(root, relative(root, resolve(directory, row.path)).split('\\').join('/')));
|
|
496
|
+
const retained = packageFiles.find(raw => (raw as JsonObject).path === `out/${row.path}`) as JsonObject | undefined;
|
|
497
|
+
if (retained && retained.sha256 !== sha256) refuse('JOB_PUBLICATION_INVALID', `versions[v${rig}].review_package.from: ${row.path} differs from its recorded package hash; restore the declared producer bytes`);
|
|
498
|
+
return { ...row, sha256 };
|
|
499
|
+
});
|
|
500
|
+
if (selector.kind === 'rig' && result.path !== `review-live-v${selector.version}.json`) files.push(publishedSourcePainting(root, project, `versions[v${selector.version}]`));
|
|
501
|
+
files.push({ path: result.path, sha256: jobHash(publicationFile(root, result.path)) });
|
|
502
|
+
if (selector.kind === 'post') {
|
|
503
|
+
const direction = `evidence/direction/v${selector.version}.json`;
|
|
504
|
+
const version = (project.versions as JsonObject[]).find(version => version.n === selector.version)!;
|
|
505
|
+
const binding = (version.provenance as JsonObject)?.direction as JsonObject | undefined;
|
|
506
|
+
const sha256 = jobHash(publicationFile(root, direction));
|
|
507
|
+
if (binding?.path !== direction || binding.sha256 !== sha256) refuse('JOB_PUBLICATION_INVALID', `versions[v${selector.version}].provenance.direction: retained direction changed; restore its recorded bytes before reconciling`);
|
|
508
|
+
files.push({ path: direction, sha256 });
|
|
509
|
+
let docDirection: JsonObject | undefined;
|
|
510
|
+
try { docDirection = JSON.parse(publicationFile(root, direction).toString('utf8')) as JsonObject; } catch { /* Preserve the legacy direction publication contract. */ }
|
|
511
|
+
const scene = sealedScene(root, project, direction, docDirection);
|
|
512
|
+
if (scene) files.push(...scene.plates, { path: scene.path, sha256: scene.sha256 });
|
|
513
|
+
}
|
|
514
|
+
const plan = project.plan as JsonObject | undefined;
|
|
515
|
+
const steps = Array.isArray(plan?.steps) ? plan.steps as JsonObject[] : [];
|
|
516
|
+
const subjects: JobPublishedContext['subjects'] = [{ kind: 'review-verdict', stage: selector.kind === 'post' ? 'direction-review' : 'rig-review' },
|
|
517
|
+
// The legacy direction panel accepts only the verdict on a post publication.
|
|
518
|
+
...(selector.kind === 'post' ? [] : packageSignSubjects(project, effectiveStage(root, project), selector, steps.length))];
|
|
519
|
+
return {
|
|
520
|
+
selector, plan: { steps: steps.map(step => ({ id: String(step.id), hash: stepHash(step) })) },
|
|
521
|
+
parts: { enumeratorVersion: partsEnumerator.version, manifest: { path: manifestPath, sha256: jobHash(manifest) }, images },
|
|
522
|
+
subjects, ...checkpointEvidenceContext(root, project, subjects, files),
|
|
523
|
+
partIds: (JSON.parse(manifest.toString('utf8')).parts as { name: string }[]).map(part => part.name), files,
|
|
524
|
+
};
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
export function publishedDraft(project: JsonObject, id: string): JsonObject {
|
|
528
|
+
const drafts = Array.isArray(project.drafts) ? project.drafts as JsonObject[] : [];
|
|
529
|
+
const found = drafts.filter(draft => draft.id === id);
|
|
530
|
+
if (found.length !== 1) refuse('JOB_PUBLICATION_INVALID', `drafts[${id}]: a unique retained draft is required; record a labelled draft with a new id`);
|
|
531
|
+
return found[0];
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function publishedSourcePainting(root: string, project: JsonObject, label: string): { path: string; sha256: string } {
|
|
535
|
+
const inputs = Array.isArray(project.inputs) ? project.inputs : [];
|
|
536
|
+
const first = inputs[0];
|
|
537
|
+
if (
|
|
538
|
+
!isObject(first) ||
|
|
539
|
+
typeof first.path !== 'string' ||
|
|
540
|
+
!first.path.trim() ||
|
|
541
|
+
first.path === 'project.json' ||
|
|
542
|
+
first.path === '.input.lock' ||
|
|
543
|
+
typeof first.sha256 !== 'string' ||
|
|
544
|
+
!/^[a-f0-9]{64}$/.test(first.sha256) ||
|
|
545
|
+
(first.bytes !== undefined && (typeof first.bytes !== 'number' || !Number.isSafeInteger(first.bytes) || first.bytes < 0))
|
|
546
|
+
) {
|
|
547
|
+
refuse('JOB_PUBLICATION_INVALID', `${label}: inputs[0] must record the original painting {path, sha256}; restore the declared inputs[0] entry in project.json before publishing`);
|
|
548
|
+
}
|
|
549
|
+
const path = first.path;
|
|
550
|
+
const sha256 = first.sha256;
|
|
551
|
+
const bytes = publicationFile(root, path);
|
|
552
|
+
if (jobHash(bytes) !== sha256 || (typeof first.bytes === 'number' && bytes.length !== first.bytes)) {
|
|
553
|
+
refuse('JOB_PUBLICATION_INVALID', `${label}: inputs[0] (${path}) differs from its registered sha256; restore the original painting bytes before publishing`);
|
|
554
|
+
}
|
|
555
|
+
return { path, sha256 };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Retain the official positional inventory, including duplicate paths/roles. */
|
|
559
|
+
export function draftPublishedContext(root: string, project: JsonObject, id: string): JobPublishedContext {
|
|
560
|
+
const draft = publishedDraft(project, id);
|
|
561
|
+
const painting = publishedSourcePainting(root, project, `drafts[${id}]`);
|
|
562
|
+
const parts = draft.parts as JsonObject;
|
|
563
|
+
const identities = [draft.config, ...(draft.config_inputs as JsonObject[] ?? []), ...(parts.files as JsonObject[]), ...(draft.files as JsonObject[])];
|
|
564
|
+
const files = identities.map(raw => {
|
|
565
|
+
const identity = raw as JsonObject;
|
|
566
|
+
const path = String(identity.path), sha256 = String(identity.sha256);
|
|
567
|
+
if (jobHash(publicationFile(root, path)) !== sha256) refuse('JOB_PUBLICATION_INVALID', `drafts[${id}]: ${path} differs from its retained hash; restore the draft or record a new id`);
|
|
568
|
+
return { path, sha256 };
|
|
569
|
+
});
|
|
570
|
+
if (!files.some(file => file.path === painting.path)) files.push(painting);
|
|
571
|
+
const manifestPath = `${String(parts.path)}/parts.json`;
|
|
572
|
+
const manifest = publicationFile(root, manifestPath);
|
|
573
|
+
const directory = dirname(join(root, manifestPath));
|
|
574
|
+
const images = partsEnumerator.enumerate(directory).map(row => ({ ...row,
|
|
575
|
+
sha256: jobHash(publicationFile(root, relative(root, resolve(directory, row.path)).split('\\').join('/'))) }));
|
|
576
|
+
const plan = project.plan as JsonObject | undefined;
|
|
577
|
+
return { selector: { kind: 'draft', id }, plan: plan ? { steps: (plan.steps as JsonObject[] ?? []).map(step => ({ id: String(step.id), hash: stepHash(step) })) } : null,
|
|
578
|
+
parts: { enumeratorVersion: partsEnumerator.version, manifest: { path: manifestPath, sha256: jobHash(manifest) }, images },
|
|
579
|
+
subjects: [{ kind: 'parts-verdict', draftId: id }],
|
|
580
|
+
partIds: (JSON.parse(manifest.toString('utf8')).parts as { name: string }[]).map(part => part.name), files };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export function prepareDraftManifest(root: string, project: JsonObject, id: string): string {
|
|
584
|
+
const context = draftPublishedContext(root, project, id);
|
|
585
|
+
const draft = publishedDraft(project, id);
|
|
586
|
+
const painting = publishedSourcePainting(root, project, `drafts[${id}]`);
|
|
587
|
+
const files = draft.files as JsonObject[];
|
|
588
|
+
const models = files.filter(file => String(file.path).endsWith('/skeleton.model.json'));
|
|
589
|
+
if (models.length > 1) refuse('JOB_PUBLICATION_INVALID', `drafts[${id}]: multiple compiled models are ambiguous; retain one draft output`);
|
|
590
|
+
const revision = jobHash(`${JSON.stringify(draft, null, 2)}\n`);
|
|
591
|
+
const legacyManifest = { schema: 'rig-review/1', id: `create-draft-${id}`, revision, title: `draft ${id}`,
|
|
592
|
+
...(models.length ? { model: models[0].path } : {}),
|
|
593
|
+
references: context.parts!.images.map((image, index) => ({ id: `image-${index}`, label: `${image.role} ${image.path}`, type: 'image',
|
|
594
|
+
path: relative(root, resolve(root, dirname(context.parts!.manifest.path), image.path)).split('\\').join('/') })),
|
|
595
|
+
animations: [], checkpoints: [], authoring: files.filter(file => /\/(rig|motion|mesh_report)\.json$/.test(String(file.path)))
|
|
596
|
+
.map(file => ({ label: String(file.path).split('/').at(-1), path: file.path })) };
|
|
597
|
+
const producer = { config: String((draft.config as JsonObject).path), parts: String((draft.parts as JsonObject).path), source: painting.path };
|
|
598
|
+
const panels = { schema: 'rig-review/panels@1', left: ['parts.list'], stage: ['parts.canvas'], toolbar: ['parts.layers'], side: ['parts.issues', 'act.submit'] };
|
|
599
|
+
const manifest = { ...legacyManifest, producer, panels };
|
|
600
|
+
const path = `review-draft-${id}.json`;
|
|
601
|
+
const bytes = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
602
|
+
const legacyPanelsBytes = Buffer.from(`${JSON.stringify({ ...legacyManifest, panels }, null, 2)}\n`);
|
|
603
|
+
const legacyBytes = Buffer.from(`${JSON.stringify(legacyManifest, null, 2)}\n`);
|
|
604
|
+
if (existsSync(join(root, path))) {
|
|
605
|
+
const existing = publicationFile(root, path);
|
|
606
|
+
if (!existing.equals(bytes)) {
|
|
607
|
+
if (!existing.equals(legacyPanelsBytes) && !existing.equals(legacyBytes)) {
|
|
608
|
+
refuse('JOB_PUBLICATION_INVALID', `drafts[${id}]: retained publication manifest differs; restore it before reconciling`);
|
|
609
|
+
}
|
|
610
|
+
const target = join(root, path);
|
|
611
|
+
const tmp = `${target}.${process.pid}-${crypto.randomUUID()}.tmp`;
|
|
612
|
+
try {
|
|
613
|
+
writeFileSync(tmp, bytes, { flag: 'wx' });
|
|
614
|
+
renameSync(tmp, target);
|
|
615
|
+
} finally {
|
|
616
|
+
rmSync(tmp, { force: true });
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
} else writeFileSync(join(root, path), bytes, { flag: 'wx' });
|
|
620
|
+
return path;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Register drafts independently of approval readiness. Only a prepared map declares approval. */
|
|
624
|
+
export function draftPublicationContext(root: string, project: JsonObject, id: string, warnings: Finding[] = []): JobPublishedContext {
|
|
625
|
+
const context = draftPublishedContext(root, project, id);
|
|
626
|
+
let subjects = [...context.subjects, ...packageSignSubjects(project, effectiveStage(root, project), context.selector!, context.plan?.steps.length ?? 0)];
|
|
627
|
+
if (subjects.some(subject => subject.kind === 'signoff' && subject.stage === 'parts-review')) {
|
|
628
|
+
const errors: Finding[] = [];
|
|
629
|
+
checkDiffMap(root, { stage: 'parts-review', checkpoint: null, draft_id: id }, `signoffs[${asArray(project.signoffs).length}]`,
|
|
630
|
+
resolve(root, context.parts!.manifest.path), errors, [], project);
|
|
631
|
+
if (errors.length) {
|
|
632
|
+
warnings.push(...errors);
|
|
633
|
+
subjects = subjects.filter(subject => subject.kind !== 'signoff' || subject.stage !== 'parts-review');
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return { ...context, subjects, ...checkpointEvidenceContext(root, project, subjects, context.files as { path: string; sha256: string }[]) };
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function readDraftPublished(root: string, project: JsonObject, id: string, path: string): JobPublishedContext {
|
|
640
|
+
if (path !== `review-draft-${id}.json`) refuse('JOB_PUBLICATION_INVALID', `drafts[${id}]: result path differs from its publication; restore the writer manifest`);
|
|
641
|
+
const context = draftPublicationContext(root, project, id);
|
|
642
|
+
return { ...context, files: [...context.files, { path, sha256: jobHash(publicationFile(root, path)) }] };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** The stage the check reads the published record in: the stage its `signoff` and `checkpoint` subjects sign. */
|
|
646
|
+
function effectiveStage(root: string, project: JsonObject): string | null {
|
|
647
|
+
return checkProject(root, project, { now: Date.now(), readOnly: true }).effective_stage;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** Optional, append-only legacy-to-painting upgrade. Absent history retains the old contract. */
|
|
651
|
+
export function liveManifestPath(root: string, project: JsonObject, n: number, identity?: { readonly id: string; readonly revision: string }): string {
|
|
652
|
+
const version = asArray(project.versions).filter(isObject).find(v => v.n === n);
|
|
653
|
+
const history = version?.publication_generations;
|
|
654
|
+
const legacy = `review-live-v${n}.json`;
|
|
655
|
+
if (history === undefined) return legacy;
|
|
656
|
+
const fail = (): never => refuse('PUBLICATION_GENERATION_INVALID', `versions[v${n}].publication_generations: broken generation chain; restore the immutable predecessor, manifest hashes, timestamp and reason`);
|
|
657
|
+
if ((version?.kind !== 'rig' && version?.kind !== 'post') || !Array.isArray(history) || history.length !== 1) return fail();
|
|
658
|
+
const entry = history[0];
|
|
659
|
+
if (!isObject(entry) || entry.generation !== 2 || !isObject(entry.supersedes) || !isObject(entry.manifest) ||
|
|
660
|
+
entry.supersedes.generation !== 1 || entry.supersedes.path !== legacy || entry.manifest.path !== `review-live-v${n}-g2.json` ||
|
|
661
|
+
typeof entry.at !== 'string' || !Number.isFinite(Date.parse(entry.at)) || new Date(entry.at).toISOString() !== entry.at || typeof entry.reason !== 'string' || !entry.reason.trim()) return fail();
|
|
662
|
+
let selected = entry.manifest.path;
|
|
663
|
+
try {
|
|
664
|
+
const old = publicationFile(root, legacy), bytes = publicationFile(root, entry.manifest.path);
|
|
665
|
+
if (jobHash(old) !== entry.supersedes.sha256 || jobHash(bytes) !== entry.manifest.sha256) return fail();
|
|
666
|
+
const previous = JSON.parse(old.toString('utf8'));
|
|
667
|
+
const current = JSON.parse(bytes.toString('utf8'));
|
|
668
|
+
if (version.kind === 'post') {
|
|
669
|
+
const directionPath = `evidence/direction/v${n}.json`;
|
|
670
|
+
const directionBytes = publicationFile(root, directionPath);
|
|
671
|
+
const binding = isObject(version.provenance) ? version.provenance.direction : undefined;
|
|
672
|
+
if (!isObject(binding) || binding.path !== directionPath || binding.sha256 !== jobHash(directionBytes)) return fail();
|
|
673
|
+
const direction = JSON.parse(directionBytes.toString('utf8')) as JsonObject;
|
|
674
|
+
const scene = direction.scene;
|
|
675
|
+
if (direction.schema !== 'rig-create/direction@2' || !isObject(scene) || typeof scene.path !== 'string' ||
|
|
676
|
+
!asArray(project.scenes).filter(isObject).some(value => value.n === scene.n && value.path === scene.path && value.sha256 === scene.sha256) ||
|
|
677
|
+
jobHash(publicationFile(root, scene.path)) !== scene.sha256) return fail();
|
|
678
|
+
if (previous.schema !== 'rig-review/1' || previous.id !== `create-v${n}` || previous.revision !== `v${n}` ||
|
|
679
|
+
previous.model !== `derived/review/v${String(version.rig_version)}/out/check/build/skeleton.model.json` ||
|
|
680
|
+
previous.direction !== directionPath || previous.scene !== undefined || !Array.isArray(previous.references) ||
|
|
681
|
+
!isDeepStrictEqual(current, { ...previous, revision: `v${n}-g2`, scene: scene.path })) return fail();
|
|
682
|
+
return identity && previous.id === identity.id && previous.revision === identity.revision ? legacy : selected;
|
|
683
|
+
}
|
|
684
|
+
if (previous.schema !== 'rig-review/1' || previous.model !== `derived/review/v${n}/out/check/build/skeleton.model.json` || previous.id !== `create-v${n}` || previous.revision !== `v${n}`) return fail();
|
|
685
|
+
if (!Array.isArray(previous.references) || previous.references.length !== 0 ||
|
|
686
|
+
!isDeepStrictEqual(current, { ...previous, revision: `v${n}-g2`, references: [originalPaintingReference(root, project, n)] })) return fail();
|
|
687
|
+
if (identity && previous.id === identity.id && previous.revision === identity.revision) selected = legacy;
|
|
688
|
+
} catch { return fail(); }
|
|
689
|
+
return selected;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export function checkPublicationGenerations(root: string, project: JsonObject, refusals: Finding[]): void {
|
|
693
|
+
for (const version of asArray(project.versions).filter(isObject)) {
|
|
694
|
+
if (version.publication_generations === undefined) continue;
|
|
695
|
+
try { liveManifestPath(root, project, Number(version.n)); }
|
|
696
|
+
catch (error) {
|
|
697
|
+
if (!(error instanceof Refusal)) throw error;
|
|
698
|
+
refusals.push({ code: error.code, path: `versions[v${String(version.n)}].publication_generations`, message: error.message });
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|