@rig-c/create 0.0.0-stage → 1.0.1
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 +169 -2
- package/docs/CHECKS.md +1682 -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,144 @@
|
|
|
1
|
+
/** Immutable scene publication. The scenes[] append is the commit point. */
|
|
2
|
+
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, rmdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { checkProject, isObject, type Finding } from './check.ts';
|
|
5
|
+
import { closed, directionHash } from './direction.ts';
|
|
6
|
+
import { checkPlateRegistrations } from './plate-receipt.ts';
|
|
7
|
+
import { refuse, USAGE_EXIT } from './refusal.ts';
|
|
8
|
+
import { baselineOf, newFindings } from './refusal-baseline.ts';
|
|
9
|
+
import { SCENE_CHOICE_MISMATCH, resolveAgentCitationSpec } from './scene-authority.ts';
|
|
10
|
+
import { SCENE_SPEC, SceneError, checkScenes, plateRegistration, pngDimensions, sceneBytes, sceneEntries, sceneFail, scenePath, sceneRig, validateScene } from './scene.ts';
|
|
11
|
+
import type { JsonObject } from './templates.ts';
|
|
12
|
+
|
|
13
|
+
export const SCENE_HELP = `scene write --rig <R> --background <path> [--foreground <path>]
|
|
14
|
+
[--background-rect x,y,w,h] [--foreground-rect x,y,w,h]
|
|
15
|
+
--citations-file <file> [<dir>]
|
|
16
|
+
Citations are a role map {background: citation, foreground?: citation}.
|
|
17
|
+
Each citation names post_composition, asks:["post_composition"], and quote or delegated:true.
|
|
18
|
+
Paths and rects must match the current ingested post_composition choice.
|
|
19
|
+
Records by:agent; typed --by is refused. Earlier human scenes remain readable.
|
|
20
|
+
Appends evidence/scene/s<N>.json and scenes[]; stage stays unchanged.`;
|
|
21
|
+
export interface SceneWriteHooks { beforeCommit?: () => void; }
|
|
22
|
+
export function sceneCommand(args: string[], now: number, hooks: SceneWriteHooks = {}): { stdout: string; exit: number } {
|
|
23
|
+
try { return writeScene(args, now, hooks); }
|
|
24
|
+
catch (error) { if (error instanceof SceneError) refuse(error.code, error.message); throw error; }
|
|
25
|
+
}
|
|
26
|
+
function writeScene(args: string[], now: number, hooks: SceneWriteHooks): { stdout: string; exit: number } {
|
|
27
|
+
const opts: Record<string, string> = {};
|
|
28
|
+
const positional: string[] = [];
|
|
29
|
+
const usage = (): never => refuse('SCENE_MALFORMED', `scene: ${SCENE_HELP}`, USAGE_EXIT);
|
|
30
|
+
if (args[0] !== 'write') usage();
|
|
31
|
+
for (let index = 1; index < args.length; index++) {
|
|
32
|
+
const arg = args[index]!;
|
|
33
|
+
if (['--rig', '--background', '--foreground', '--background-rect', '--foreground-rect', '--citations-file', '--by'].includes(arg)) {
|
|
34
|
+
const value = args[++index];
|
|
35
|
+
if (!value || value.startsWith('--') || opts[arg] !== undefined) usage();
|
|
36
|
+
opts[arg] = value;
|
|
37
|
+
} else if (arg.startsWith('-')) usage();
|
|
38
|
+
else positional.push(arg);
|
|
39
|
+
}
|
|
40
|
+
if (opts['--by'] !== undefined) sceneFail('SCENE_AUTHOR_TYPED', 'scene.by', 'typed scene authority is not supported', 'omit --by and cite the current post_composition Review choice');
|
|
41
|
+
if (positional.length > 1 || ['--rig', '--background', '--citations-file'].some(key => !opts[key])
|
|
42
|
+
|| !/^[1-9][0-9]*$/.test(opts['--rig']!) || !Number.isSafeInteger(Number(opts['--rig']))
|
|
43
|
+
|| opts['--foreground-rect'] && !opts['--foreground']) usage();
|
|
44
|
+
const dir = resolve(positional[0] ?? process.cwd());
|
|
45
|
+
const snapshots = new Map<string, Buffer>();
|
|
46
|
+
const original = sceneBytes(dir, 'project.json', 'SCENE_MALFORMED', snapshots);
|
|
47
|
+
let project: JsonObject;
|
|
48
|
+
try { project = JSON.parse(original.toString('utf8')) as JsonObject; }
|
|
49
|
+
catch { return sceneFail('SCENE_MALFORMED', 'project.json', 'expected a project JSON object'); }
|
|
50
|
+
if (!isObject(project) || project.schema !== 'rig-create/project@0') sceneFail('SCENE_MALFORMED', 'project.json.schema', 'expected rig-create/project@0');
|
|
51
|
+
const collectionRoot = basename(dirname(dir)) === 'members' ? dirname(dirname(dir)) : null;
|
|
52
|
+
const collection = collectionRoot && existsSync(join(collectionRoot, 'collection.json')) ? sceneBytes(collectionRoot, 'collection.json', 'SCENE_MALFORMED') : null;
|
|
53
|
+
// Shares the direction lock because a direction derives the current scene.
|
|
54
|
+
const lock = join(dir, '.direction.lock');
|
|
55
|
+
let descriptor: number;
|
|
56
|
+
try { descriptor = openSync(lock, 'wx'); }
|
|
57
|
+
catch { return sceneFail('SCENE_MALFORMED', 'project.json', 'another scene or direction writer holds the lock'); }
|
|
58
|
+
const createdDirectories: string[] = [];
|
|
59
|
+
let document: string | null = null;
|
|
60
|
+
let temporary: string | null = null;
|
|
61
|
+
try {
|
|
62
|
+
const entries = sceneEntries(project);
|
|
63
|
+
const n = entries.length + 1;
|
|
64
|
+
const binding = sceneRig(dir, project, Number(opts['--rig']), snapshots);
|
|
65
|
+
const citationsBytes = sceneBytes(dir, opts['--citations-file']!, 'SCENE_CITATION_MISSING', snapshots);
|
|
66
|
+
let citations: JsonObject;
|
|
67
|
+
try { citations = JSON.parse(citationsBytes.toString('utf8')) as JsonObject; }
|
|
68
|
+
catch { return sceneFail('SCENE_CITATION_MISSING', opts['--citations-file']!, 'expected a plate-role citation map'); }
|
|
69
|
+
if (!closed(citations, ['background'], opts['--foreground'] ? ['foreground'] : []) || opts['--foreground'] && citations.foreground === undefined) sceneFail('SCENE_CITATION_MISSING', opts['--citations-file']!, 'provide exactly the declared plate-role citations');
|
|
70
|
+
const plates: JsonObject[] = [];
|
|
71
|
+
for (const role of ['background', 'foreground']) {
|
|
72
|
+
const path = opts[`--${role}`];
|
|
73
|
+
if (path === undefined) continue;
|
|
74
|
+
const bytes = sceneBytes(dir, path, 'SCENE_PLATE_CHANGED', snapshots);
|
|
75
|
+
const size = pngDimensions(bytes, path);
|
|
76
|
+
const identity = { path, sha256: directionHash(bytes) };
|
|
77
|
+
const registered = plateRegistration(project, identity);
|
|
78
|
+
if (!registered) sceneFail('SCENE_PLATE_UNREGISTERED', path, 'register this plate as an input or a receipted prepared-input chain');
|
|
79
|
+
let rect = binding.stage;
|
|
80
|
+
const value = opts[`--${role}-rect`];
|
|
81
|
+
if (value !== undefined) {
|
|
82
|
+
const parts = value.split(',');
|
|
83
|
+
if (parts.length !== 4 || parts.some(part => !part.trim())) sceneFail('SCENE_PLACEMENT_INVALID', `${path}.rect`, 'supply x,y,width,height');
|
|
84
|
+
const [x, y, width, height] = parts.map(Number);
|
|
85
|
+
rect = { x: x!, y: y!, width: width!, height: height! };
|
|
86
|
+
} else {
|
|
87
|
+
const spec = resolveAgentCitationSpec(dir, project, citations[role]!, snapshots);
|
|
88
|
+
if (role === 'foreground') {
|
|
89
|
+
if (!spec.foreground) {
|
|
90
|
+
sceneFail(
|
|
91
|
+
SCENE_CHOICE_MISMATCH,
|
|
92
|
+
'scene.plates[1]',
|
|
93
|
+
'scene declares a foreground plate not present in post_composition value.scene',
|
|
94
|
+
'match the exact roles, paths and rects from the cited post_composition choice in the Review tool (or record a new post_composition choice in the Review tool) and run scene write',
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
rect = spec.foreground.rect;
|
|
98
|
+
} else {
|
|
99
|
+
rect = spec.background.rect;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
plates.push({ role, ...identity, ...size, rect, registered, citation: citations[role]! });
|
|
103
|
+
}
|
|
104
|
+
const doc: JsonObject = { schema: SCENE_SPEC, n, rig: binding.rig, frame: { space: 'rig-world-y-up', stage: binding.stage }, plates, layers: opts['--foreground'] ? ['background', 'rig', 'foreground'] : ['background', 'rig'] };
|
|
105
|
+
validateScene(dir, project, doc, snapshots, true, 'agent');
|
|
106
|
+
const bytes = Buffer.from(`${JSON.stringify(doc, null, 2)}\n`);
|
|
107
|
+
const entry = { n, path: scenePath(n), sha256: directionHash(bytes), at: new Date(now).toISOString(), by: 'agent' };
|
|
108
|
+
const next = { ...project, scenes: [...entries, entry] };
|
|
109
|
+
const baseline = baselineOf(checkProject(dir, project, { now }));
|
|
110
|
+
for (const part of ['evidence', 'scene']) {
|
|
111
|
+
const parent = part === 'evidence' ? join(dir, part) : join(dir, 'evidence', part);
|
|
112
|
+
if (existsSync(parent)) {
|
|
113
|
+
if (!lstatSync(parent).isDirectory() || lstatSync(parent).isSymbolicLink()) sceneFail('SCENE_MALFORMED', entry.path, 'scene parents must be real directories');
|
|
114
|
+
} else { mkdirSync(parent); createdDirectories.push(parent); }
|
|
115
|
+
}
|
|
116
|
+
const destination = join(dir, entry.path);
|
|
117
|
+
if (existsSync(destination)) sceneFail('SCENE_FILE_CHANGED', entry.path, 'immutable scene destination already exists');
|
|
118
|
+
writeFileSync(destination, bytes, { flag: 'wx' });
|
|
119
|
+
document = destination;
|
|
120
|
+
const own: { dir: string; project: JsonObject; refusals: Finding[] } = { dir, project: next, refusals: [] };
|
|
121
|
+
checkScenes(own);
|
|
122
|
+
checkPlateRegistrations({ ...own, warnings: [] });
|
|
123
|
+
if (own.refusals[0]) sceneFail(own.refusals[0].code, own.refusals[0].path, own.refusals[0].message);
|
|
124
|
+
const fresh = newFindings(baseline, checkProject(dir, next, { now }).refusals)[0];
|
|
125
|
+
if (fresh) sceneFail(fresh.code, fresh.path, fresh.message);
|
|
126
|
+
hooks.beforeCommit?.();
|
|
127
|
+
if (collectionRoot && collection && !sceneBytes(collectionRoot, 'collection.json', 'SCENE_MALFORMED').equals(collection)) sceneFail('SCENE_MALFORMED', 'collection.json', 'inherited citation changed during publication');
|
|
128
|
+
for (const [path, expected] of snapshots) {
|
|
129
|
+
if (!sceneBytes(dir, path, 'SCENE_FILE_CHANGED').equals(expected)) sceneFail('SCENE_FILE_CHANGED', path, 'source changed before the scenes[] commit');
|
|
130
|
+
}
|
|
131
|
+
if (!readFileSync(destination).equals(bytes)) sceneFail('SCENE_FILE_CHANGED', entry.path, 'scene document changed before the commit');
|
|
132
|
+
temporary = join(dir, `.scene-${crypto.randomUUID()}.tmp`);
|
|
133
|
+
writeFileSync(temporary, `${JSON.stringify(next, null, 2)}\n`, { flag: 'wx' });
|
|
134
|
+
renameSync(temporary, join(dir, 'project.json'));
|
|
135
|
+
document = null;
|
|
136
|
+
return { stdout: `scene: s${n} recorded; stage unchanged`, exit: 0 };
|
|
137
|
+
} finally {
|
|
138
|
+
if (temporary) rmSync(temporary, { force: true });
|
|
139
|
+
if (document) rmSync(document, { force: true });
|
|
140
|
+
for (const directory of createdDirectories.reverse()) { try { rmdirSync(directory); } catch { /* Keep committed evidence. */ } }
|
|
141
|
+
closeSync(descriptor);
|
|
142
|
+
rmSync(lock);
|
|
143
|
+
}
|
|
144
|
+
}
|
package/src/scene.ts
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/** Create-owned composition declarations. Producer measurements are only read. */
|
|
2
|
+
import { readFileSync } from './check-reads.ts';
|
|
3
|
+
import { isDeepStrictEqual as equal } from 'node:util';
|
|
4
|
+
import { asArray, collectItems, collectSignoffs, isObject, loadCollection, type Ctx, type Finding } from './check.ts';
|
|
5
|
+
import { literalFile } from './choices.ts';
|
|
6
|
+
import { closed, directionHash, hex, objects } from './direction.ts';
|
|
7
|
+
import { answersOf, checkEntry } from './provenance.ts';
|
|
8
|
+
import { collectReopens, liveSignoff } from './reopen.ts';
|
|
9
|
+
import { humanReviewVerdicts, latestAccepted, readVersions } from './review.ts';
|
|
10
|
+
import { SCENE_CHOICE_MISMATCH, SCENE_CHOICE_MISSING, sceneAuthor, validateAgentSceneAuthority } from './scene-authority.ts';
|
|
11
|
+
import { reviewPackageHash } from './signoff-target.ts';
|
|
12
|
+
import type { Json, JsonObject } from './templates.ts';
|
|
13
|
+
|
|
14
|
+
export { SCENE_CHOICE_MISMATCH, SCENE_CHOICE_MISSING, sceneAuthor };
|
|
15
|
+
export const SCENE_SPEC = 'rig-create/scene@1';
|
|
16
|
+
export const scenePath = (n: number): string => `evidence/scene/s${n}.json`;
|
|
17
|
+
const positive = (value: unknown): value is number => Number.isSafeInteger(value) && Number(value) > 0;
|
|
18
|
+
const text = (value: unknown): value is string => typeof value === 'string' && value.trim().length > 0;
|
|
19
|
+
const finite = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value);
|
|
20
|
+
const pathShape = (value: unknown): value is string => text(value) && !value.startsWith('/') && !value.includes('\\') && !/^[A-Za-z]:/.test(value) && value.split('/').every(part => part !== '' && part !== '.' && part !== '..');
|
|
21
|
+
export const sceneBinding = (value: unknown): value is JsonObject => closed(value, ['path', 'sha256']) && pathShape(value.path) && hex(value.sha256);
|
|
22
|
+
export class SceneError extends Error {
|
|
23
|
+
constructor(
|
|
24
|
+
readonly code: string,
|
|
25
|
+
readonly path: string,
|
|
26
|
+
why: string,
|
|
27
|
+
remedy = 'restore the retained sources and record a new scene with scene write',
|
|
28
|
+
) {
|
|
29
|
+
super(`${path}: ${why}; ${remedy}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function sceneFail(code: string, path: string, why: string, remedy?: string): never { throw new SceneError(code, path, why, remedy); }
|
|
33
|
+
export function sceneFinding(error: SceneError): Finding { return { code: error.code, path: error.path, message: error.message }; }
|
|
34
|
+
|
|
35
|
+
/** Every read is relative to this one project root, including optional source snapshots for writers. */
|
|
36
|
+
export function sceneBytes(dir: string, path: string, code: string, snapshots?: Map<string, Buffer>): Buffer {
|
|
37
|
+
try {
|
|
38
|
+
if (!pathShape(path)) throw Error('a literal project-relative path is required');
|
|
39
|
+
const bytes = readFileSync(literalFile(dir, path));
|
|
40
|
+
const previous = snapshots?.get(path);
|
|
41
|
+
if (previous && !previous.equals(bytes)) throw Error('source changed during validation');
|
|
42
|
+
snapshots?.set(path, bytes);
|
|
43
|
+
return bytes;
|
|
44
|
+
} catch (error) { return sceneFail(code, path, error instanceof Error ? error.message : String(error)); }
|
|
45
|
+
}
|
|
46
|
+
function boundBytes(dir: string, binding: JsonObject, code: string, snapshots?: Map<string, Buffer>): Buffer {
|
|
47
|
+
const bytes = sceneBytes(dir, String(binding.path), code, snapshots);
|
|
48
|
+
if (directionHash(bytes) !== binding.sha256) sceneFail(code, String(binding.path), 'raw bytes differ from the recorded sha256');
|
|
49
|
+
return bytes;
|
|
50
|
+
}
|
|
51
|
+
function json(bytes: Buffer, path: string): JsonObject {
|
|
52
|
+
try { const value: Json = JSON.parse(bytes.toString('utf8')); if (isObject(value)) return value; } catch { /* Report the scene code below. */ }
|
|
53
|
+
return sceneFail('SCENE_MALFORMED', path, 'expected a JSON object');
|
|
54
|
+
}
|
|
55
|
+
export function sceneEntries(project: JsonObject): JsonObject[] {
|
|
56
|
+
if (project.scenes === undefined) return [];
|
|
57
|
+
if (!Array.isArray(project.scenes)) sceneFail('SCENE_MALFORMED', 'scenes', 'expected an append-only array');
|
|
58
|
+
for (const [index, entry] of project.scenes.entries()) {
|
|
59
|
+
if (!closed(entry, ['n', 'path', 'sha256', 'at', 'by']) || entry.n !== index + 1 || !positive(entry.n)
|
|
60
|
+
|| entry.path !== scenePath(entry.n) || !hex(entry.sha256) || !text(entry.at) || !/Z$/.test(entry.at) || !Number.isFinite(Date.parse(entry.at)) || !sceneAuthor(entry.by)) {
|
|
61
|
+
sceneFail('SCENE_MALFORMED', `scenes[${index}]`, 'expected consecutive n, immutable scene path, sha256, UTC at and by ("agent" or a human identifier)');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return project.scenes as JsonObject[];
|
|
65
|
+
}
|
|
66
|
+
export function currentScene(project: JsonObject, at = Infinity): JsonObject | undefined {
|
|
67
|
+
return sceneEntries(project).filter(entry => Date.parse(String(entry.at)) <= at).at(-1);
|
|
68
|
+
}
|
|
69
|
+
export function sceneReference(entry: JsonObject): JsonObject { return { n: entry.n!, path: entry.path!, sha256: entry.sha256! }; }
|
|
70
|
+
|
|
71
|
+
function boxShape(value: unknown): value is JsonObject {
|
|
72
|
+
return closed(value, ['x', 'y', 'width', 'height']) && Object.values(value).every(v => typeof v === 'number');
|
|
73
|
+
}
|
|
74
|
+
export function validateSceneShape(doc: JsonObject, path = 'scene'): void {
|
|
75
|
+
const bad = (why: string): never => sceneFail('SCENE_MALFORMED', path, why);
|
|
76
|
+
if (!closed(doc, ['schema', 'n', 'rig', 'frame', 'plates', 'layers']) || doc.schema !== SCENE_SPEC || !positive(doc.n)) bad('expected closed scene@1 and positive n');
|
|
77
|
+
const rig = doc.rig;
|
|
78
|
+
if (!closed(rig, ['version', 'acceptance', 'model', 'animation']) || !positive(rig.version) || !sceneBinding(rig.model)
|
|
79
|
+
|| !closed(rig.animation, ['name'], ['frames']) || !text(rig.animation.name) || rig.animation.frames !== undefined && !sceneBinding(rig.animation.frames)) bad('rig needs version, model, animation name and optional frames binding');
|
|
80
|
+
const acceptance = (rig as JsonObject).acceptance;
|
|
81
|
+
if (!closed(acceptance, ['signoff', 'verdict', 'review_package']) || !closed(acceptance.signoff, ['stage', 'checkpoint', 'at'])
|
|
82
|
+
|| acceptance.signoff.stage !== 'rig-review' || acceptance.signoff.checkpoint !== null || !text(acceptance.signoff.at) || !Number.isFinite(Date.parse(acceptance.signoff.at))
|
|
83
|
+
|| !text(acceptance.verdict) || !/^versions\[\d+\]\.verdicts\[\d+\]$/.test(acceptance.verdict)
|
|
84
|
+
|| !closed(acceptance.review_package, ['path', 'sha256']) || !text(acceptance.review_package.path) || !hex(acceptance.review_package.sha256)) bad('acceptance needs the exact rig-review signoff identity, verdict and review-package target');
|
|
85
|
+
if (!closed(doc.frame, ['space', 'stage']) || doc.frame.space !== 'rig-world-y-up' || !boxShape(doc.frame.stage)) bad('frame needs rig-world-y-up and a stage box');
|
|
86
|
+
if (!Array.isArray(doc.plates) || !Array.isArray(doc.layers) || doc.layers.some(layer => typeof layer !== 'string')) bad('plates and layers must be arrays');
|
|
87
|
+
for (const [index, plate] of (doc.plates as Json[]).entries()) {
|
|
88
|
+
if (!closed(plate, ['role', 'path', 'sha256', 'width', 'height', 'rect', 'registered'], ['citation'])
|
|
89
|
+
|| !['background', 'foreground'].includes(String(plate.role)) || !pathShape(plate.path) || !hex(plate.sha256)
|
|
90
|
+
|| !positive(plate.width) || !positive(plate.height) || !boxShape(plate.rect)
|
|
91
|
+
|| !closed(plate.registered, ['kind', 'ref']) || !['input', 'derived'].includes(String(plate.registered.kind)) || !text(plate.registered.ref)) bad(`plates[${index}] has unknown keys or invalid fields`);
|
|
92
|
+
const citation = (plate as JsonObject).citation;
|
|
93
|
+
if (citation !== undefined) {
|
|
94
|
+
if (!isObject(citation) || Object.keys(citation).some(key => !['guide', 'asks', 'quote', 'delegated'].includes(key))
|
|
95
|
+
|| citation.guide !== undefined && typeof citation.guide !== 'string' || citation.asks !== undefined && (!Array.isArray(citation.asks) || citation.asks.some(ask => typeof ask !== 'string'))
|
|
96
|
+
|| citation.quote !== undefined && typeof citation.quote !== 'string' || citation.delegated !== undefined && typeof citation.delegated !== 'boolean') bad(`plates[${index}].citation has unknown keys or wrong type`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Prepared-input receipts may fan in. Every branch terminates at a registered input. */
|
|
102
|
+
function sameFile(entry: JsonObject, binding: JsonObject): boolean { return entry.path === binding.path && entry.sha256 === binding.sha256; }
|
|
103
|
+
function receiptChain(project: JsonObject, entry: JsonObject, visiting: Set<JsonObject>): boolean {
|
|
104
|
+
if (entry.kind !== 'prepared-input' || !pathShape(entry.path) || !hex(entry.sha256) || visiting.has(entry)) return false;
|
|
105
|
+
const sources = entry.sources === undefined ? [entry.source] : asArray(entry.sources);
|
|
106
|
+
const next = new Set([...visiting, entry]);
|
|
107
|
+
return sources.length > 0 && sources.every(source => sceneBinding(source) && (objects(project.inputs).some(input => sameFile(input, source))
|
|
108
|
+
|| objects(project.derived).some(parent => sameFile(parent, source) && receiptChain(project, parent, next))));
|
|
109
|
+
}
|
|
110
|
+
export function plateRegistration(project: JsonObject, binding: JsonObject): JsonObject | null {
|
|
111
|
+
const input = asArray(project.inputs).findIndex(entry => isObject(entry) && sameFile(entry, binding));
|
|
112
|
+
if (input >= 0) return { kind: 'input', ref: `inputs[${input}]` };
|
|
113
|
+
const derived = asArray(project.derived).findIndex(entry => isObject(entry) && sameFile(entry, binding) && receiptChain(project, entry, new Set()));
|
|
114
|
+
return derived < 0 ? null : { kind: 'derived', ref: `derived[${derived}]` };
|
|
115
|
+
}
|
|
116
|
+
function registeredPlate(project: JsonObject, plate: JsonObject): boolean {
|
|
117
|
+
const registration = plate.registered as JsonObject;
|
|
118
|
+
const match = /^(inputs|derived)\[(\d+)\]$/.exec(String(registration.ref));
|
|
119
|
+
if (!match || match[1] !== (registration.kind === 'input' ? 'inputs' : 'derived')) return false;
|
|
120
|
+
const entry = asArray(project[match[1]!])[Number(match[2])];
|
|
121
|
+
return isObject(entry) && sameFile(entry, plate) && (registration.kind === 'input' || receiptChain(project, entry, new Set()));
|
|
122
|
+
}
|
|
123
|
+
/** One gate for the three authoring-time staleness rules, per the signed HQ ruling. */
|
|
124
|
+
export function validateSceneCurrent(project: JsonObject, rig: JsonObject, current: boolean, reference?: JsonObject): void {
|
|
125
|
+
if (!current) return;
|
|
126
|
+
const versions = readVersions(project);
|
|
127
|
+
const accepted = latestAccepted(versions);
|
|
128
|
+
if (accepted?.n !== rig.version || versions.rigs.some(value => value.live && value.n > Number(rig.version))) sceneFail('SCENE_RIG_STALE', 'scene.rig.version', 'select the latest live accepted rig');
|
|
129
|
+
const reopens = collectReopens(project, []);
|
|
130
|
+
const acceptance = rig.acceptance as JsonObject;
|
|
131
|
+
const signatures = collectSignoffs(project, []).filter(value => value.stage === 'rig-review' && value.checkpoint === null && liveSignoff(reopens, value.stage, value.at)
|
|
132
|
+
&& isObject(value.record.target) && value.record.target.version === rig.version).sort((a, b) => b.at - a.at);
|
|
133
|
+
const signature = signatures[0];
|
|
134
|
+
if (!signature || !equal(acceptance.signoff, { stage: 'rig-review', checkpoint: null, at: signature.record.at! })
|
|
135
|
+
|| !isObject(signature.record.target) || signature.record.target.verdict !== acceptance.verdict || !equal(signature.record.target.review_package, acceptance.review_package)) sceneFail('SCENE_ACCEPTANCE_STALE', 'scene.rig.acceptance', 'retain the current live rig-review signoff target');
|
|
136
|
+
if (reference) {
|
|
137
|
+
const latest = currentScene(project);
|
|
138
|
+
if (!latest || !equal(sceneReference(latest), reference)) sceneFail('DIRECTION_SCENE_STALE', 'direction.scene', 'new direction must cite the current scene');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** The records validateSceneCurrent would have read at `at`: every dated entry at or before it. */
|
|
142
|
+
function recordsAt(project: JsonObject, at: number): JsonObject {
|
|
143
|
+
const dated = (value: Json | undefined): JsonObject[] => asArray(value).filter((entry): entry is JsonObject => isObject(entry) && Date.parse(String(entry.at)) <= at);
|
|
144
|
+
const view: JsonObject = { ...project, signoffs: dated(project.signoffs), versions: dated(project.versions).map(version => ({ ...version, verdicts: dated(version.verdicts) })) };
|
|
145
|
+
if (project.reopens !== undefined) view.reopens = dated(project.reopens);
|
|
146
|
+
if (project.amendments !== undefined) view.amendments = dated(project.amendments);
|
|
147
|
+
return view;
|
|
148
|
+
}
|
|
149
|
+
/** The last scene dated at or before `at`, while it passes validateSceneCurrent on the records of that instant. */
|
|
150
|
+
export function sceneCurrentAt(dir: string, project: JsonObject, at: number): JsonObject | undefined {
|
|
151
|
+
const entry = currentScene(project, at);
|
|
152
|
+
if (!entry) return undefined;
|
|
153
|
+
const doc = json(boundBytes(dir, entry, 'SCENE_FILE_CHANGED'), String(entry.path));
|
|
154
|
+
if (doc.n !== entry.n) sceneFail('SCENE_MALFORMED', String(entry.path), 'document n differs from its committed entry');
|
|
155
|
+
validateSceneShape(doc, String(entry.path));
|
|
156
|
+
if (entry.by === 'agent') validateScene(dir, recordsAt(project, at), doc, undefined, false, 'agent');
|
|
157
|
+
try { validateSceneCurrent(recordsAt(project, at), doc.rig as JsonObject, true); }
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (error instanceof SceneError && ['SCENE_RIG_STALE', 'SCENE_ACCEPTANCE_STALE'].includes(error.code)) return undefined;
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
return entry;
|
|
163
|
+
}
|
|
164
|
+
function rigVersion(project: JsonObject, n: number): { version: JsonObject; index: number } {
|
|
165
|
+
const matches = asArray(project.versions).map((version, index) => ({ version, index })).filter(value => isObject(value.version) && value.version.kind === 'rig' && value.version.n === n);
|
|
166
|
+
if (matches.length !== 1) sceneFail('SCENE_RIG_STALE', 'scene.rig.version', 'select a uniquely recorded rig version');
|
|
167
|
+
return matches[0] as { version: JsonObject; index: number };
|
|
168
|
+
}
|
|
169
|
+
function modelForRig(dir: string, version: JsonObject, snapshots?: Map<string, Buffer>): { binding: JsonObject; model: JsonObject } {
|
|
170
|
+
const path = `derived/review/v${String(version.n)}/out/check/build/skeleton.model.json`;
|
|
171
|
+
const pkg = version.review_package;
|
|
172
|
+
const row = isObject(pkg) ? objects(pkg.files).find(file => file.path === 'out/check/build/skeleton.model.json') : undefined;
|
|
173
|
+
if (!row || !hex(row.sha256)) sceneFail('SCENE_MODEL_MISMATCH', path, 'retain the compiled model in the accepted review-package inventory');
|
|
174
|
+
const binding = { path, sha256: row.sha256 };
|
|
175
|
+
return { binding, model: json(boundBytes(dir, binding, 'SCENE_MODEL_MISMATCH', snapshots), path) };
|
|
176
|
+
}
|
|
177
|
+
export function sceneRig(dir: string, project: JsonObject, n: number, snapshots?: Map<string, Buffer>): { rig: JsonObject; stage: JsonObject } {
|
|
178
|
+
const { version } = rigVersion(project, n);
|
|
179
|
+
const reopens = collectReopens(project, []);
|
|
180
|
+
const signature = collectSignoffs(project, []).filter(value => value.stage === 'rig-review' && value.checkpoint === null && liveSignoff(reopens, value.stage, value.at)
|
|
181
|
+
&& isObject(value.record.target) && value.record.target.version === n).sort((a, b) => b.at - a.at)[0];
|
|
182
|
+
if (!signature || !isObject(signature.record.target)) sceneFail('SCENE_ACCEPTANCE_STALE', 'scene.rig.acceptance', 'sign the accepted rig before scene write');
|
|
183
|
+
const target = signature.record.target;
|
|
184
|
+
const { binding, model } = modelForRig(dir, version, snapshots);
|
|
185
|
+
const registration = isObject(version.registration) ? version.registration : {};
|
|
186
|
+
const rig: JsonObject = { version: n, acceptance: { signoff: { stage: 'rig-review', checkpoint: null, at: signature.record.at! }, verdict: target.verdict!, review_package: target.review_package! },
|
|
187
|
+
model: binding, animation: { name: 'idle', ...(registration.idle === undefined ? {} : { frames: registration.idle }) } };
|
|
188
|
+
validateSceneCurrent(project, rig, true);
|
|
189
|
+
if (model.stage === null || model.stage === undefined) sceneFail('SCENE_STAGE_MISSING', String(binding.path), 'compile a model with a stage box');
|
|
190
|
+
return { rig, stage: model.stage as JsonObject };
|
|
191
|
+
}
|
|
192
|
+
/** Historical acceptance and its package seal are checked even after a reopen or later verdict. */
|
|
193
|
+
function validateSceneAcceptance(project: JsonObject, rig: JsonObject): JsonObject {
|
|
194
|
+
const { version, index } = rigVersion(project, Number(rig.version));
|
|
195
|
+
const acceptance = rig.acceptance as JsonObject;
|
|
196
|
+
const identity = acceptance.signoff as JsonObject;
|
|
197
|
+
const signature = collectSignoffs(project, []).find(value => value.stage === identity.stage && value.checkpoint === identity.checkpoint && value.record.at === identity.at)?.record;
|
|
198
|
+
const match = /^versions\[(\d+)\]\.verdicts\[(\d+)\]$/.exec(String(acceptance.verdict));
|
|
199
|
+
const verdict = match && Number(match[1]) === index ? humanReviewVerdicts(version.verdicts, 'rig-review').find(value => value.index === Number(match[2]) && value.record.verdict === 'accepted') : undefined;
|
|
200
|
+
if (!verdict || !Number.isFinite(Date.parse(String(verdict.record.at))) || Date.parse(String(verdict.record.at)) > Date.parse(String(identity.at))) sceneFail('SCENE_RIG_STALE', 'scene.rig.acceptance.verdict', 'the cited rig must have been human-accepted before this signoff');
|
|
201
|
+
const pkg = version.review_package;
|
|
202
|
+
const target = { version: version.n!, verdict: acceptance.verdict!, review_package: { path: `versions[${index}].review_package`, sha256: isObject(pkg) ? reviewPackageHash(pkg) : '' } };
|
|
203
|
+
if (!signature || !equal(signature.target, target) || !equal(acceptance.review_package, target.review_package)) sceneFail('SCENE_ACCEPTANCE_STALE', 'scene.rig.acceptance.review_package', 'restore the recorded acceptance target and its matching package sha256');
|
|
204
|
+
return version;
|
|
205
|
+
}
|
|
206
|
+
export function pngDimensions(bytes: Buffer, path: string): { width: number; height: number } {
|
|
207
|
+
if (bytes.length < 33 || !bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) || bytes.readUInt32BE(8) !== 13 || bytes.toString('ascii', 12, 16) !== 'IHDR'
|
|
208
|
+
|| bytes.readUInt32BE(16) === 0 || bytes.readUInt32BE(20) === 0) sceneFail('SCENE_PLATE_DIMENSIONS', path, 'retain a PNG with a valid IHDR');
|
|
209
|
+
return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };
|
|
210
|
+
}
|
|
211
|
+
export function validateScene(dir: string, project: JsonObject, doc: JsonObject, snapshots?: Map<string, Buffer>, current = false, by?: string): void {
|
|
212
|
+
validateSceneShape(doc);
|
|
213
|
+
const rig = doc.rig as JsonObject;
|
|
214
|
+
const version = validateSceneAcceptance(project, rig);
|
|
215
|
+
validateSceneCurrent(project, rig, current);
|
|
216
|
+
const { binding, model } = modelForRig(dir, version, snapshots);
|
|
217
|
+
if (!equal(rig.model, binding)) sceneFail('SCENE_MODEL_MISMATCH', 'scene.rig.model', 'use the accepted package model path and hash');
|
|
218
|
+
const animation = rig.animation as JsonObject;
|
|
219
|
+
if (!objects(model.animations).some(value => value.name === animation.name)) sceneFail('SCENE_ANIMATION_MISSING', 'scene.rig.animation.name', 'choose an animation named by the model');
|
|
220
|
+
const registration = isObject(version.registration) ? version.registration : {};
|
|
221
|
+
if (registration.idle !== undefined && !sceneBinding(registration.idle) || !equal(animation.frames, registration.idle)) sceneFail('SCENE_MALFORMED', 'scene.rig.animation.frames', 'frames must be present exactly when registration.idle exists and equal that binding');
|
|
222
|
+
if (isObject(animation.frames)) boundBytes(dir, animation.frames, 'SCENE_FILE_CHANGED', snapshots);
|
|
223
|
+
if (model.stage === null || model.stage === undefined) sceneFail('SCENE_STAGE_MISSING', String(binding.path), 'compile a model with a stage box');
|
|
224
|
+
if (!boxShape(model.stage) || !Object.values(model.stage).every(finite) || Number(model.stage.width) <= 0 || Number(model.stage.height) <= 0) sceneFail('SCENE_MALFORMED', `${String(binding.path)}.stage`, 'the model stage must be a finite positive box');
|
|
225
|
+
if (!equal((doc.frame as JsonObject).stage, model.stage)) sceneFail('SCENE_STAGE_MISMATCH', 'scene.frame.stage', 'copy the model stage field for field');
|
|
226
|
+
const plates = doc.plates as JsonObject[];
|
|
227
|
+
if (plates.filter(plate => plate.role === 'background').length !== 1 || plates.filter(plate => plate.role === 'foreground').length > 1
|
|
228
|
+
|| plates.length === 2 && (plates[0]!.path === plates[1]!.path || plates[0]!.sha256 === plates[1]!.sha256)) sceneFail('SCENE_PLATE_CONFLICT', 'scene.plates', 'declare one background and at most one distinct foreground');
|
|
229
|
+
const collection = loadCollection(dir);
|
|
230
|
+
const ctx: Ctx = { dir, project, collection, items: collectItems(project, { collection }), refusals: [], warnings: [], signoffs: [], steps: [], stage: String(project.stage), effectiveStage: String(project.stage), predatesPartsReview: null };
|
|
231
|
+
if (snapshots) {
|
|
232
|
+
// Freeze the same current and historical answers checkEntry reads, including inherited history.
|
|
233
|
+
for (const item of ctx.items.filter(value => value.id === 'post_composition')) {
|
|
234
|
+
for (const answer of [item.raw, ...answersOf(item, ctx)]) {
|
|
235
|
+
if (isObject(answer) && isObject(answer.choice) && isObject(answer.choice.set) && typeof answer.choice.set.path === 'string') {
|
|
236
|
+
sceneBytes(dir, answer.choice.set.path, 'SCENE_CITATION_MISSING', snapshots);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (const [index, plate] of plates.entries()) {
|
|
242
|
+
const path = `scene.plates[${index}]`;
|
|
243
|
+
const bytes = boundBytes(dir, plate, 'SCENE_PLATE_CHANGED', snapshots);
|
|
244
|
+
const size = pngDimensions(bytes, String(plate.path));
|
|
245
|
+
if (size.width !== plate.width || size.height !== plate.height) sceneFail('SCENE_PLATE_DIMENSIONS', path, 'declared dimensions must equal PNG IHDR dimensions');
|
|
246
|
+
const rect = plate.rect as JsonObject;
|
|
247
|
+
const a = Number(rect.width) * size.height;
|
|
248
|
+
const b = Number(rect.height) * size.width;
|
|
249
|
+
if (!Object.values(rect).every(finite) || Number(rect.width) <= 0 || Number(rect.height) <= 0 || !Number.isFinite(a) || !Number.isFinite(b)
|
|
250
|
+
|| Math.abs(a - b) > 1e-9 * Math.max(Math.abs(a), Math.abs(b))) sceneFail('SCENE_PLACEMENT_INVALID', `${path}.rect`, 'use a finite positive box and uniform scale within relative 1e-9');
|
|
251
|
+
if (!registeredPlate(project, plate)) sceneFail('SCENE_PLATE_UNREGISTERED', `${path}.registered`, 'register this exact path and sha256 as an input or a complete prepared-input receipt chain');
|
|
252
|
+
const citation = plate.citation;
|
|
253
|
+
if (!(closed(citation, ['guide', 'asks', 'quote']) && text(citation.quote) || closed(citation, ['guide', 'asks', 'delegated']) && citation.delegated === true)
|
|
254
|
+
|| citation.guide !== 'post_composition' || !equal(citation.asks, ['post_composition'])) sceneFail('SCENE_CITATION_MISSING', `${path}.citation`, 'cite post_composition with asks and a quote or delegated:true');
|
|
255
|
+
checkEntry(ctx, 'post', `${path}.citation`, citation);
|
|
256
|
+
if (ctx.refusals.length || ctx.warnings.length) sceneFail('SCENE_CITATION_MISSING', `${path}.citation`, 'cite a recorded post_composition answer or decided delegation');
|
|
257
|
+
}
|
|
258
|
+
const layers = plates.some(plate => plate.role === 'foreground') ? ['background', 'rig', 'foreground'] : ['background', 'rig'];
|
|
259
|
+
if (!equal(doc.layers, layers)) sceneFail('SCENE_LAYER_ORDER', 'scene.layers', 'use background, rig and foreground only when declared, in that order');
|
|
260
|
+
const author = by ?? sceneEntries(project).find(entry => entry.n === doc.n)?.by;
|
|
261
|
+
if (author === 'agent') validateAgentSceneAuthority(dir, project, doc, ctx, snapshots, current);
|
|
262
|
+
}
|
|
263
|
+
/** The committed document a reference names, sealed by its scenes[] entry and not yet validated. */
|
|
264
|
+
export function committedScene(dir: string, project: JsonObject, reference: JsonObject, snapshots?: Map<string, Buffer>): JsonObject {
|
|
265
|
+
const entry = sceneEntries(project).find(value => equal(sceneReference(value), reference));
|
|
266
|
+
if (!entry) sceneFail('SCENE_MALFORMED', 'scenes', 'reference must equal a committed scenes[] entry');
|
|
267
|
+
const doc = json(boundBytes(dir, entry, 'SCENE_FILE_CHANGED', snapshots), String(entry.path));
|
|
268
|
+
if (doc.n !== entry.n) sceneFail('SCENE_MALFORMED', String(entry.path), 'document n differs from its committed entry');
|
|
269
|
+
return doc;
|
|
270
|
+
}
|
|
271
|
+
export function readScene(dir: string, project: JsonObject, reference: JsonObject, snapshots?: Map<string, Buffer>, current = false): JsonObject {
|
|
272
|
+
const entry = sceneEntries(project).find(value => equal(sceneReference(value), reference));
|
|
273
|
+
const doc = committedScene(dir, project, reference, snapshots);
|
|
274
|
+
validateScene(dir, project, doc, snapshots, current, entry ? String(entry.by) : undefined);
|
|
275
|
+
return doc;
|
|
276
|
+
}
|
|
277
|
+
export function checkScenes(ctx: Pick<Ctx, 'dir' | 'project' | 'refusals'>): void {
|
|
278
|
+
try {
|
|
279
|
+
for (const entry of sceneEntries(ctx.project)) {
|
|
280
|
+
try { readScene(ctx.dir, ctx.project, sceneReference(entry)); }
|
|
281
|
+
catch (error) { if (!(error instanceof SceneError)) throw error; ctx.refusals.push(sceneFinding(error)); }
|
|
282
|
+
}
|
|
283
|
+
} catch (error) { if (!(error instanceof SceneError)) throw error; ctx.refusals.push(sceneFinding(error)); }
|
|
284
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** The setup-painting declaration on retained build configs (create#32). */
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { asArray, asString, isObject, type Ctx } from './check.ts';
|
|
5
|
+
import { readObject } from './cg-gates.ts';
|
|
6
|
+
|
|
7
|
+
export const SETUP_PAINTING_SINCE = '2026-11-06T00:00:00Z';
|
|
8
|
+
|
|
9
|
+
/** Unread retained configs keep their existing provenance owner. */
|
|
10
|
+
export function checkSetupPainting(ctx: Ctx): void {
|
|
11
|
+
asArray(ctx.project.versions).forEach((version, index) => {
|
|
12
|
+
if (!isObject(version) || version.pre_interview === true) return;
|
|
13
|
+
const provenance = isObject(version.provenance) ? version.provenance : {};
|
|
14
|
+
// Authored direction is consumer camera/timing, not a parts build config.
|
|
15
|
+
// Its closed schema and hash are checked by checkDirections. Legacy post configs keep their contract.
|
|
16
|
+
if (version.kind === 'post' && isObject(provenance.direction)) return;
|
|
17
|
+
const config = isObject(provenance.config) ? provenance.config : {};
|
|
18
|
+
const configPath = asString(config.path);
|
|
19
|
+
if (configPath === null || configPath === '') return;
|
|
20
|
+
const cfg = readObject(ctx.dir, resolve(ctx.dir, configPath), asString(config.sha256));
|
|
21
|
+
if (!cfg.ok) return;
|
|
22
|
+
const label = typeof version.n === 'number' ? `versions[v${version.n}]` : `versions[${index}]`;
|
|
23
|
+
const path = `${label}.provenance.config requirements`;
|
|
24
|
+
// CG already owns an absent or unread requirements file at this path.
|
|
25
|
+
if (ctx.refusals.some((finding) => finding.path === path
|
|
26
|
+
&& (finding.code === 'CG_GATE_UNDECLARED' || finding.code === 'CG_GATES_UNREAD'))) return;
|
|
27
|
+
const requirements = asString(cfg.value.requirements);
|
|
28
|
+
const req = requirements === null || requirements === '' ? null
|
|
29
|
+
: readObject(ctx.dir, resolve(dirname(cfg.file), requirements), null);
|
|
30
|
+
const setup = req?.ok && isObject(req.value.setup_painting) ? req.value.setup_painting : {};
|
|
31
|
+
if (req?.ok && setup.required === true) return;
|
|
32
|
+
const at = typeof version.at === 'string' ? Date.parse(version.at) : Number.NaN;
|
|
33
|
+
const legacy = !Number.isFinite(at) || at < Date.parse(SETUP_PAINTING_SINCE);
|
|
34
|
+
const missing = req === null || !req.ok;
|
|
35
|
+
const code = missing ? 'SETUP_PAINTING_REQUIREMENTS_MISSING' : 'SETUP_PAINTING_REQUIRED_MISSING';
|
|
36
|
+
const detail = req === null ? `${configPath} names no requirements file. Write requirements in that config naming the checks file`
|
|
37
|
+
: !req.ok ? `${configPath} names ${requirements}, but ${req.why}. Retain a readable requirements file at that config-relative path`
|
|
38
|
+
: `${requirements} lacks setup_painting.required: true. Write that declaration in the requirements file named by ${configPath}`;
|
|
39
|
+
const finding = {
|
|
40
|
+
code: legacy ? 'SETUP_PAINTING_LEGACY' : code,
|
|
41
|
+
path: missing ? path : `${path} setup_painting.required`,
|
|
42
|
+
message: `${label}: ${detail}. Measure limits with parts' own pixel_count per region on the accepted split build and record their source.${legacy ? ` This record predates ${SETUP_PAINTING_SINCE} or has no readable date (${code}).` : ''}`,
|
|
43
|
+
};
|
|
44
|
+
(legacy ? ctx.warnings : ctx.refusals).push(finding);
|
|
45
|
+
});
|
|
46
|
+
}
|
package/src/signable.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { checkpointRequired } from './plan-history.ts';
|
|
2
|
+
/**
|
|
3
|
+
* What a person can sign on the record as it stands. `signoff` uses these to
|
|
4
|
+
* accept a checkpoint, and the publications use them to declare the
|
|
5
|
+
* `signoff` and `checkpoint` subjects Review may ask for (docs/LIVE-REVIEW.md).
|
|
6
|
+
*/
|
|
7
|
+
import { JOB_PLAN_BOUND_SIGNOFF_STAGES, type JobSelector, type JobSubject } from '@rig-c/review/job-host';
|
|
8
|
+
import { asArray, collectSignoffs, isObject, type CheckReport } from './check.ts';
|
|
9
|
+
import { refuse } from './refusal.ts';
|
|
10
|
+
import { collectReopens, liveSignoff } from './reopen.ts';
|
|
11
|
+
import { SIGNOFF_TARGET_KINDS } from './review-act.ts';
|
|
12
|
+
import { STAGES, stageOfStep } from './stages.ts';
|
|
13
|
+
import type { JsonObject } from './templates.ts';
|
|
14
|
+
|
|
15
|
+
/** Record-mode interview approval uses the full interview check, before any write. */
|
|
16
|
+
export function assertInterviewRecordSignable(project: JsonObject, report: CheckReport): void {
|
|
17
|
+
if (project.stage !== 'interview' || report.effective_stage !== 'interview') {
|
|
18
|
+
refuse('REVIEW_ACT_TARGET_MISMATCH', 'project.json.stage: the displayed interview sign-off requires stage interview; publish and sign the current stage in Review');
|
|
19
|
+
}
|
|
20
|
+
if (!asArray(isObject(project.plan) ? project.plan.steps : undefined).length) {
|
|
21
|
+
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');
|
|
22
|
+
}
|
|
23
|
+
if (report.refusals.length || report.holes?.length) {
|
|
24
|
+
const finding = report.refusals[0] ?? report.holes![0]!;
|
|
25
|
+
refuse('INTERVIEW_SIGNOFF_NOT_CLEAN', `project.json: interview --check reports ${finding.code} at ${finding.path}; resolve the interview check and publish the current record again`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Why `signoff <stage> --checkpoint <id>` refuses the step, or null when it can sign it. */
|
|
30
|
+
export function checkpointProblem(project: JsonObject, stage: string, id: string): string | null {
|
|
31
|
+
const spec = STAGES.find(spec => spec.id === stage);
|
|
32
|
+
const matches = asArray(isObject(project.plan) ? project.plan.steps : undefined)
|
|
33
|
+
.filter((step): step is JsonObject => isObject(step) && step.id === id);
|
|
34
|
+
const step = matches[0];
|
|
35
|
+
if (matches.length !== 1 || spec?.phase === undefined || step.phase !== spec.phase || stageOfStep(spec.phase, id) !== stage) {
|
|
36
|
+
return `${id}: no unique step runs in stage ${stage}`;
|
|
37
|
+
}
|
|
38
|
+
return checkpointRequired(project, step) ? null : `${id}: checkpoint must be enabled before it can be signed`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A stage closed by a sign-off that has no live stage-level signature yet. */
|
|
42
|
+
export function stageSignable(project: JsonObject, stage: string | null): boolean {
|
|
43
|
+
if (stage === null || !STAGES.some(spec => spec.id === stage && spec.closed_by === 'signoff')) return false;
|
|
44
|
+
const reopens = collectReopens(project, []);
|
|
45
|
+
return !collectSignoffs(project, []).some(signature => signature.stage === stage && signature.checkpoint === null
|
|
46
|
+
&& liveSignoff(reopens, signature.stage, signature.at));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The `checkpoint` and `signoff` subjects a package publication declares, for the
|
|
51
|
+
* record's effective stage: every checkpoint `signoff` accepts in that stage, and
|
|
52
|
+
* the stage itself while it is signable. rig-review and direction-review sign the
|
|
53
|
+
* displayed version, so only a rig or post selector carries them, and a plan-bound
|
|
54
|
+
* stage needs displayed plan steps (Review's `bindJobAsk` refuses one without).
|
|
55
|
+
*/
|
|
56
|
+
export function packageSignSubjects(project: JsonObject, stage: string | null, selector: JobSelector, planSteps: number): JobSubject[] {
|
|
57
|
+
if (stage === null) return [];
|
|
58
|
+
const subjects: JobSubject[] = asArray(isObject(project.plan) ? project.plan.steps : undefined)
|
|
59
|
+
.filter((step): step is JsonObject => isObject(step) && typeof step.id === 'string' && checkpointProblem(project, stage, step.id) === null)
|
|
60
|
+
.map(step => ({ kind: 'checkpoint', checkpointId: String(step.id) }));
|
|
61
|
+
const target = SIGNOFF_TARGET_KINDS[stage];
|
|
62
|
+
if (stageSignable(project, stage) && (target === undefined || selector.kind === target)
|
|
63
|
+
&& (!(JOB_PLAN_BOUND_SIGNOFF_STAGES as readonly string[]).includes(stage) || planSteps > 0)) subjects.push({ kind: 'signoff', stage });
|
|
64
|
+
return subjects;
|
|
65
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Approval bindings for local and inherited answer revisions. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { answered, asArray, collectItems, isObject, itemFor, loadCollection, postNotPassed, type Ctx, type Finding, type Item } from './check.ts';
|
|
4
|
+
import { refuse } from './refusal.ts';
|
|
5
|
+
import { approvalStageOfStep, hasStageOrder } from './stages.ts';
|
|
6
|
+
import { sorted } from './step-hash.ts';
|
|
7
|
+
import type { Json, JsonObject } from './templates.ts';
|
|
8
|
+
|
|
9
|
+
export const SIGNOFF_ANSWERS_SINCE = '2026-11-06T00:00:00Z';
|
|
10
|
+
const stages = ['interview', 'plan-approved', 'rigging-interview', 'post-interview'];
|
|
11
|
+
const excluded = new Set(['history', 'prompts', 'config', 'text', 'source', 'required_for']);
|
|
12
|
+
|
|
13
|
+
function retained(value: Json, interview: boolean): Json {
|
|
14
|
+
if (!isObject(value)) return null;
|
|
15
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => !excluded.has(key) && !(interview && key === 'agent_decision')));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Interview approves the delegation; plan approvals also approve its decision. */
|
|
19
|
+
export function answerRevisionHash(item: Item, stage = 'plan-approved'): string {
|
|
20
|
+
const resolved = answered(item.state) && 'answer' in item.state ? item.state.answer : null;
|
|
21
|
+
const envelope = { raw: retained(item.raw, stage === 'interview'), resolved: retained(resolved, stage === 'interview') };
|
|
22
|
+
return createHash('sha256').update(JSON.stringify(sorted(envelope))).digest('hex');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Plan completeness follows the signature's own covered steps. */
|
|
26
|
+
function requiredIds(project: JsonObject, signoff: JsonObject, items: Item[]): string[] {
|
|
27
|
+
if (signoff.stage === 'interview') return [];
|
|
28
|
+
const phase = signoff.stage === 'post-interview' ? 'post' : 'rig';
|
|
29
|
+
const phaseSteps = asArray(isObject(project.plan) ? project.plan.steps : undefined).filter(isObject).filter(step => step.phase === phase);
|
|
30
|
+
if (phase === 'post' && phaseSteps.length === 0 && postNotPassed({ items })) return items.filter(item => item.kind === 'post').map(item => item.id);
|
|
31
|
+
const steps = phaseSteps.filter(step => (Array.isArray(signoff.steps) ? signoff.steps.includes(step.id as Json)
|
|
32
|
+
: approvalStageOfStep(phase, String(step.id), hasStageOrder(project) || signoff.stage === 'rigging-interview') === signoff.stage));
|
|
33
|
+
return [...new Set(steps.flatMap(step => asArray(step.rests_on).filter((id): id is string => typeof id === 'string')))];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function selectAnswerHashes(dir: string, project: JsonObject, signoff: JsonObject): JsonObject | undefined {
|
|
37
|
+
if (!stages.includes(String(signoff.stage))) return undefined;
|
|
38
|
+
const items = collectItems(project, { collection: loadCollection(dir) });
|
|
39
|
+
// The writer captures every answer present now, independent of its recorded date.
|
|
40
|
+
const ids = signoff.stage === 'interview' ? items.filter(item => answered(item.state)).map(item => item.id) : requiredIds(project, signoff, items);
|
|
41
|
+
const hashes = Object.create(null) as JsonObject;
|
|
42
|
+
for (const id of ids) {
|
|
43
|
+
const item = items.find(item => item.id === id);
|
|
44
|
+
if (!item || !answered(item.state)) refuse('SIGNOFF_ANSWERS_UNANSWERED', `signoffs[${asArray(project.signoffs).length}].answer_hashes[${id}]: answer is missing or unresolved; answer ${id} before signing ${String(signoff.stage)}`);
|
|
45
|
+
hashes[id] = answerRevisionHash(item, String(signoff.stage));
|
|
46
|
+
}
|
|
47
|
+
return hashes;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Called only for live signatures, sharing the writer's coverage resolver. */
|
|
51
|
+
export function checkSignoffAnswers(ctx: Ctx, signoff: JsonObject, path: string): void {
|
|
52
|
+
if (!stages.includes(String(signoff.stage))) return;
|
|
53
|
+
const ids = requiredIds(ctx.project, signoff, ctx.items);
|
|
54
|
+
const hashes = isObject(signoff.answer_hashes) ? signoff.answer_hashes : null;
|
|
55
|
+
const missing = hashes === null || ids.some(id => !Object.hasOwn(hashes, id));
|
|
56
|
+
if (missing) {
|
|
57
|
+
const legacy = Date.parse(String(signoff.at)) < Date.parse(SIGNOFF_ANSWERS_SINCE);
|
|
58
|
+
const code = `SIGNOFF_ANSWERS_MISSING${legacy ? '_LEGACY' : ''}`;
|
|
59
|
+
const finding: Finding = { code, path: `${path}.answer_hashes`, message: `${path}.answer_hashes: missing answer revisions${hashes === null ? '' : ` for ${ids.filter(id => !Object.hasOwn(hashes, id)).join(', ')}`}; sign ${String(signoff.stage)} again to bind its answers` };
|
|
60
|
+
(legacy ? ctx.warnings : ctx.refusals).push(finding);
|
|
61
|
+
}
|
|
62
|
+
const current = (ctx.signoffs ?? []).find(s => s.record === signoff);
|
|
63
|
+
const at = Date.parse(String(signoff.at));
|
|
64
|
+
const newer = (ctx.signoffs ?? []).filter(s => s.stage === signoff.stage
|
|
65
|
+
&& (s.at > at || (s.at === at && s.index > (current?.index ?? -1))));
|
|
66
|
+
for (const [id, hash] of Object.entries(hashes ?? {})) {
|
|
67
|
+
const superseded = newer.some(s => signoff.stage === 'interview'
|
|
68
|
+
|| (isObject(s.record.answer_hashes) && Object.hasOwn(s.record.answer_hashes, id)));
|
|
69
|
+
if (superseded) continue;
|
|
70
|
+
const item = itemFor(ctx, id);
|
|
71
|
+
if (!item || !answered(item.state) || hash !== answerRevisionHash(item, String(signoff.stage))) {
|
|
72
|
+
const at = `${path}.answer_hashes[${id}]`;
|
|
73
|
+
ctx.refusals.push({ code: 'SIGNOFF_ANSWERS_STALE', path: at, message: `${at}: ${id} is missing, unresolved or changed since signing; sign ${String(signoff.stage)} again after restoring the intended answer` });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|