rig-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,95 @@
|
|
|
1
|
+
/** `signoff --checkpoint <id> --from-review <actId>`: a passed Review checkpoint act becomes the checkpoint sign-off. */
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
7
|
+
import { asArray, isObject } from './check.ts';
|
|
8
|
+
import { jobHash } from './job-outbox.ts';
|
|
9
|
+
import { jobRuntimeExecArgv } from './job-writer.ts';
|
|
10
|
+
import { refuse } from './refusal.ts';
|
|
11
|
+
import { assertActSubjectDeclared, checkpointActSignoff, decodeReviewAct, reviewActFindings, reviewActTransport, type CheckpointAct, type ReviewActTransport } from './review-act.ts';
|
|
12
|
+
import { stepHash } from './step-hash.ts';
|
|
13
|
+
import { readCheckpointEvidence } from './checkpoint-evidence.ts';
|
|
14
|
+
import type { JsonObject } from './templates.ts';
|
|
15
|
+
|
|
16
|
+
export interface SignoffHooks {
|
|
17
|
+
/** Tests inject the job API response; the command otherwise fetches through the private runtime. */
|
|
18
|
+
fetch?: (transport: ReviewActTransport) => { bytes: string };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CheckpointIngest {
|
|
22
|
+
act: CheckpointAct;
|
|
23
|
+
bytes: string;
|
|
24
|
+
by: string;
|
|
25
|
+
at: string;
|
|
26
|
+
verbatim: string;
|
|
27
|
+
source: string;
|
|
28
|
+
/** The sign-off this act already recorded: the command replays it and writes nothing. */
|
|
29
|
+
replay?: JsonObject;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function fetchBytes(transport: ReviewActTransport, hooks: SignoffHooks): string {
|
|
33
|
+
if (hooks.fetch) return hooks.fetch(transport).bytes;
|
|
34
|
+
const child = Bun.spawnSync([process.execPath, ...jobRuntimeExecArgv(), fileURLToPath(new URL('./review-act-runtime.ts', import.meta.url))], {
|
|
35
|
+
stdin: Buffer.from(JSON.stringify(transport)), stdout: 'pipe', stderr: 'pipe',
|
|
36
|
+
});
|
|
37
|
+
let response: { ok: boolean; bytes: string; code: string; message: string };
|
|
38
|
+
try { response = JSON.parse(child.stdout.toString()); }
|
|
39
|
+
catch { return refuse('REVIEW_ACT_FETCH_REFUSED', 'project.json.review_acts: fetch adapter failed; restore the live Review job'); }
|
|
40
|
+
if (!response.ok || child.exitCode) refuse(response.code, response.message);
|
|
41
|
+
return response.bytes;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Fetches, decodes and binds the act to the `--checkpoint` step the command already accepted. */
|
|
45
|
+
export function ingestCheckpointAct(dir: string, project: JsonObject, actId: string, checkpointId: string, now: number, hooks: SignoffHooks): CheckpointIngest {
|
|
46
|
+
const invalid = reviewActFindings(project)[0];
|
|
47
|
+
if (invalid) refuse(invalid.code, invalid.message);
|
|
48
|
+
const transport: ReviewActTransport = { ...reviewActTransport(dir, actId), kind: 'checkpoint' };
|
|
49
|
+
const bytes = fetchBytes(transport, hooks);
|
|
50
|
+
const act = decodeReviewAct(bytes, 'checkpoint');
|
|
51
|
+
if (act.jobId !== transport.jobId || act.actId !== transport.actId || Date.parse(act.at) > now) {
|
|
52
|
+
refuse('REVIEW_ACT_INVALID', `signoffs.checkpoint ${checkpointId}: fetched act identity or date differs from the request; fetch the checkpoint act again`);
|
|
53
|
+
}
|
|
54
|
+
if (act.value.planStepId !== checkpointId || act.value.checkpointId !== act.value.planStepId) {
|
|
55
|
+
refuse('REVIEW_ACT_CHECKPOINT_MISMATCH', `signoffs.checkpoint ${checkpointId}: act ${act.jobId}/${act.actId} names checkpoint ${act.value.checkpointId} on plan step ${act.value.planStepId}; sign the checkpoint the act names, or ask Review for checkpoint ${checkpointId}`);
|
|
56
|
+
}
|
|
57
|
+
assertActSubjectDeclared(dir, act, `signoffs.checkpoint ${checkpointId}`);
|
|
58
|
+
const fields = { act, bytes, ...checkpointActSignoff(act) };
|
|
59
|
+
const existing = asArray(project.review_acts).filter(isObject).find(entry => entry.jobId === act.jobId && entry.actId === act.actId);
|
|
60
|
+
if (existing) {
|
|
61
|
+
if (existing.bytes !== bytes) refuse('REVIEW_ACT_REPLAY_CONFLICT', 'project.json.review_acts: immutable checkpoint act changed; restore the original receipt');
|
|
62
|
+
const signoff = asArray(project.signoffs)[Number(existing.signoff)];
|
|
63
|
+
if (existing.kind !== 'checkpoint' || !isObject(signoff)) refuse('REVIEW_ACT_LEDGER_INVALID', 'project.json.review_acts: checkpoint receipt has no recorded sign-off; restore the original receipt and sign-off together');
|
|
64
|
+
return { ...fields, replay: signoff };
|
|
65
|
+
}
|
|
66
|
+
if (act.value.outcome !== 'pass') {
|
|
67
|
+
refuse('REVIEW_ACT_CHECKPOINT_FAILED', `signoffs.checkpoint ${checkpointId}: the person failed this checkpoint in Review (act ${act.jobId}/${act.actId}); a failed checkpoint is feedback, not a sign-off, so fix the step from its words and ask for the checkpoint again`);
|
|
68
|
+
}
|
|
69
|
+
const plan = isObject(project.plan) ? project.plan : null;
|
|
70
|
+
const current = asArray(plan?.steps).map(raw => ({ id: isObject(raw) ? raw.id : null, hash: isObject(raw) ? stepHash(raw) : null }));
|
|
71
|
+
if (!isDeepStrictEqual(act.source.plan.steps, current)) {
|
|
72
|
+
refuse('REVIEW_ACT_SOURCE_STALE', `signoffs.checkpoint ${checkpointId}: displayed plan step identities changed since act ${act.jobId}/${act.actId}; ask for the checkpoint again on the current plan`);
|
|
73
|
+
}
|
|
74
|
+
const displayedEvidence = (act.source as { checkpointEvidence?: readonly { checkpointId: string }[] }).checkpointEvidence
|
|
75
|
+
?.find(packet => packet.checkpointId === checkpointId);
|
|
76
|
+
const currentEvidence = readCheckpointEvidence(dir, project)?.find(packet => packet.checkpointId === checkpointId);
|
|
77
|
+
if (!isDeepStrictEqual(displayedEvidence, currentEvidence)) {
|
|
78
|
+
refuse('REVIEW_ACT_SOURCE_STALE', `signoffs.checkpoint ${checkpointId}: sealed checkpoint presentation changed since act ${act.jobId}/${act.actId}; ask for the checkpoint again on the current evidence`);
|
|
79
|
+
}
|
|
80
|
+
return fields;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function checkpointReceipt(ingest: CheckpointIngest, signoff: number): JsonObject {
|
|
84
|
+
const displayed = ingest.act.source.plan.steps.find(step => step.id === ingest.act.value.planStepId)!;
|
|
85
|
+
return { schema: 'rig-create/review-act@1', kind: 'checkpoint', jobId: ingest.act.jobId, actId: ingest.act.actId,
|
|
86
|
+
checkpoint: ingest.act.value.planStepId, signoff, bytes: ingest.bytes, sha256: jobHash(ingest.bytes), version_hash: displayed.hash };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Writes only if the record is the one the fetch was validated against, through an exclusive temporary file. */
|
|
90
|
+
export function commitIngestedSignoff(file: string, original: Buffer, project: JsonObject): void {
|
|
91
|
+
if (!readFileSync(file).equals(original)) refuse('REVIEW_ACT_SOURCE_STALE', 'project.json: record changed during checkpoint fetch; retry against the current live job');
|
|
92
|
+
const temporary = join(dirname(file), `.rig-create-review-act-${randomUUID()}.tmp`);
|
|
93
|
+
writeFileSync(temporary, `${JSON.stringify(project, null, 2)}\n`, { flag: 'wx' });
|
|
94
|
+
renameSync(temporary, file);
|
|
95
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { acceptedReviewVerdict, readVersions } from './review.ts';
|
|
2
|
+
import { checkFigureMask } from './figure-mask.ts';
|
|
3
|
+
/** Review signatures bind accepted verdicts and the reviewed version's retained inputs. */
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
6
|
+
import { asArray, collectSignoffs, isObject, type Finding } from './check.ts';
|
|
7
|
+
import { directionBinding } from './direction.ts';
|
|
8
|
+
import { collectReopens, liveSignoff } from './reopen.ts';
|
|
9
|
+
import { checkReviewPackages } from './review-package.ts';
|
|
10
|
+
import { refuse } from './refusal.ts';
|
|
11
|
+
import { SIGNOFF_TARGET_KINDS } from './review-act.ts';
|
|
12
|
+
import { sorted } from './step-hash.ts';
|
|
13
|
+
import type { Json, JsonObject } from './templates.ts';
|
|
14
|
+
|
|
15
|
+
export const SIGNOFF_TARGET_SINCE = '2026-11-06T00:00:00Z';
|
|
16
|
+
|
|
17
|
+
export function outputsHash(outputs: Json[]): string {
|
|
18
|
+
return createHash('sha256').update(JSON.stringify(sorted(outputs))).digest('hex');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function reviewPackageHash(record: JsonObject): string {
|
|
22
|
+
return createHash('sha256').update(JSON.stringify(sorted({ config: record.config, files: record.files }))).digest('hex');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validBinding(value: Json | undefined): value is JsonObject {
|
|
26
|
+
return isObject(value) && typeof value.path === 'string' && value.path.length > 0
|
|
27
|
+
&& typeof value.sha256 === 'string' && /^[a-f0-9]{64}$/.test(value.sha256);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function targetFor(dir: string, project: JsonObject, stage: string, version: JsonObject, index: number, verdict: number): JsonObject {
|
|
31
|
+
const target: JsonObject = { version: version.n, verdict: `versions[${index}].verdicts[${verdict}]` };
|
|
32
|
+
if (stage === 'rig-review' && version.pre_interview === true) {
|
|
33
|
+
if (version.outputs_lost !== undefined) throw Error('outputs lost: nothing to bind');
|
|
34
|
+
if (!Array.isArray(version.outputs) || version.outputs.length === 0) throw Error('outputs missing: nothing to bind');
|
|
35
|
+
target.outputs = { path: `versions[${index}].outputs`, sha256: outputsHash(version.outputs) };
|
|
36
|
+
} else if (stage === 'rig-review') {
|
|
37
|
+
if (!isObject(version.review_package) || !isObject(version.review_package.config) || !Array.isArray(version.review_package.files)) throw Error('review_package missing or malformed');
|
|
38
|
+
target.review_package = { path: `versions[${index}].review_package`, sha256: reviewPackageHash(version.review_package) };
|
|
39
|
+
} else {
|
|
40
|
+
const provenance = isObject(version.provenance) ? version.provenance : {};
|
|
41
|
+
if (project.direction_contract !== undefined && provenance.direction !== undefined) {
|
|
42
|
+
if (!validBinding(provenance.direction)) throw Error('direction binding missing or malformed');
|
|
43
|
+
const actual = directionBinding(dir, provenance.direction.path as string);
|
|
44
|
+
if (!isDeepStrictEqual(actual, provenance.direction)) throw Error('direction file hash differs from recorded binding');
|
|
45
|
+
target.direction = actual;
|
|
46
|
+
} else {
|
|
47
|
+
if (!validBinding(provenance.config)) throw Error('post config binding missing or malformed');
|
|
48
|
+
target.post_config = provenance.config;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return target;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Same resolution is used by the reader and by the writer before its commit point. */
|
|
55
|
+
export function checkSignoffTarget(dir: string, project: JsonObject, signoff: JsonObject, path: string, refusals: Finding[], warnings: Finding[]): void {
|
|
56
|
+
const stage = String(signoff.stage);
|
|
57
|
+
if (!(stage in SIGNOFF_TARGET_KINDS)) return;
|
|
58
|
+
const missing = signoff.target === undefined || signoff.target === null;
|
|
59
|
+
let reason = missing ? 'target missing' : '';
|
|
60
|
+
if (!missing) {
|
|
61
|
+
try {
|
|
62
|
+
const target = signoff.target;
|
|
63
|
+
if (!isObject(target) || !Number.isSafeInteger(target.version) || Number(target.version) < 1) throw Error('target version missing or malformed');
|
|
64
|
+
const versions = asArray(project.versions);
|
|
65
|
+
const matches = versions.map((v, i) => ({ v, i })).filter(({ v }) => isObject(v) && v.n === target.version);
|
|
66
|
+
if (matches.length !== 1) throw Error('target version does not exist uniquely');
|
|
67
|
+
const { v, i } = matches[0] as { v: JsonObject; i: number };
|
|
68
|
+
if (v.kind !== SIGNOFF_TARGET_KINDS[stage]) throw Error('target version belongs to another stage kind');
|
|
69
|
+
const match = typeof target.verdict === 'string' ? /^versions\[(\d+)\]\.verdicts\[(\d+)\]$/.exec(target.verdict) : null;
|
|
70
|
+
if (!match || Number(match[1]) !== i) throw Error('verdict path does not resolve to the target version');
|
|
71
|
+
const verdictIndex = Number(match[2]);
|
|
72
|
+
const verdict = asArray(v.verdicts)[verdictIndex];
|
|
73
|
+
if (!isObject(verdict) || verdictIndex !== acceptedReviewVerdict(v, stage)) throw Error('named verdict is missing, not accepted or belongs to another review stage');
|
|
74
|
+
const expected = targetFor(dir, project, stage, v, i, verdictIndex);
|
|
75
|
+
if (!isDeepStrictEqual(target, expected)) throw Error(expected.outputs ? 'outputs changed since signing' : stage === 'rig-review' ? 'review_package changed since signing' : expected.direction ? 'direction changed since signing' : 'post config changed since signing');
|
|
76
|
+
} catch (error) { reason = error instanceof Error ? error.message : String(error); }
|
|
77
|
+
}
|
|
78
|
+
if (!reason) return;
|
|
79
|
+
const legacy = Date.parse(String(signoff.at)) < Date.parse(SIGNOFF_TARGET_SINCE);
|
|
80
|
+
const code = `SIGNOFF_TARGET_${missing ? 'MISSING' : 'STALE'}${legacy ? '_LEGACY' : ''}`;
|
|
81
|
+
const at = `${path}.target`;
|
|
82
|
+
(legacy ? warnings : refusals).push({ code, path: at, message: `${at}: ${reason}; preserve the reviewed inputs and sign ${stage} again with --version` });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function selectSignoffTarget(dir: string, project: JsonObject, stage: string, requested?: string): JsonObject | undefined {
|
|
86
|
+
if (!(stage in SIGNOFF_TARGET_KINDS)) {
|
|
87
|
+
if (requested !== undefined) refuse('SIGNOFF_OPTION_INVALID', 'signoffs.target: --version requires rig-review or direction-review; remove --version');
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
const reopens = collectReopens(project, []);
|
|
91
|
+
const signed = collectSignoffs(project, []).filter(s => s.stage === stage && liveSignoff(reopens, stage, s.at));
|
|
92
|
+
const versions = asArray(project.versions);
|
|
93
|
+
// A rig a reopen or amendment made history is no candidate (rig-c/create#243).
|
|
94
|
+
const history = stage === 'rig-review' ? new Set(readVersions(project).rigs.filter(r => !r.live).map(r => r.n)) : new Set<number>();
|
|
95
|
+
const candidates = versions.filter((v): v is JsonObject => isObject(v) && v.kind === SIGNOFF_TARGET_KINDS[stage]
|
|
96
|
+
&& acceptedReviewVerdict(v, stage) >= 0 && !history.has(v.n as number)
|
|
97
|
+
&& !signed.some(s => isObject(s.record.target) && s.record.target.version === v.n));
|
|
98
|
+
const fail = (reason: string): never => refuse('SIGNOFF_TARGET_STALE', `signoffs[${asArray(project.signoffs).length}].target: ${reason}; candidates [${candidates.map(v => String(v.n)).join(', ')}]; select an accepted version with --version and restore its reviewed inputs`);
|
|
99
|
+
let version: JsonObject;
|
|
100
|
+
if (requested === undefined) {
|
|
101
|
+
if (candidates.length !== 1) fail('exactly one unsigned accepted candidate is required');
|
|
102
|
+
version = candidates[0]!;
|
|
103
|
+
} else {
|
|
104
|
+
const matches = versions.filter((v): v is JsonObject => isObject(v) && v.n === Number(requested));
|
|
105
|
+
if (!/^[1-9][0-9]*$/.test(requested) || matches.length !== 1 || matches[0]?.kind !== SIGNOFF_TARGET_KINDS[stage]) fail('requested version does not exist uniquely with this stage kind');
|
|
106
|
+
version = matches[0]!;
|
|
107
|
+
}
|
|
108
|
+
if (history.has(version.n as number)) fail('rig version is history since a reopen or amendment of rig-review or an earlier stage');
|
|
109
|
+
const verdictIndex = acceptedReviewVerdict(version, stage);
|
|
110
|
+
if (verdictIndex < 0) fail('version has no accepted verdict');
|
|
111
|
+
let target: JsonObject;
|
|
112
|
+
try { target = targetFor(dir, project, stage, version, versions.indexOf(version), verdictIndex); }
|
|
113
|
+
catch (error) { return fail(error instanceof Error ? error.message : String(error)); }
|
|
114
|
+
if (stage === 'rig-review' && version.pre_interview !== true) {
|
|
115
|
+
const ctx = { dir, project: { ...project, versions: [{ ...version, pre_interview: false }] }, refusals: [] as Finding[], warnings: [] as Finding[] };
|
|
116
|
+
checkReviewPackages(ctx);
|
|
117
|
+
checkFigureMask(ctx);
|
|
118
|
+
const mask = ctx.refusals.find(f => f.code.startsWith('FIGURE_MASK_')
|
|
119
|
+
|| ['REVIEW_PACKAGE_PART_FILE_MISMATCH', 'RIG_VERSION_AT_INVALID'].includes(f.code));
|
|
120
|
+
if (mask) refuse(mask.code, mask.message);
|
|
121
|
+
if (ctx.refusals.length || ctx.warnings.some(w => w.code === 'REVIEW_PACKAGE_MISSING')) fail('review package is missing or differs from its recorded binding');
|
|
122
|
+
}
|
|
123
|
+
const errors: Finding[] = [];
|
|
124
|
+
checkSignoffTarget(dir, project, { stage, target, at: SIGNOFF_TARGET_SINCE }, `signoffs[${asArray(project.signoffs).length}]`, errors, []);
|
|
125
|
+
if (errors[0]) refuse(errors[0].code, errors[0].message);
|
|
126
|
+
return target;
|
|
127
|
+
}
|
package/src/signoff.ts
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { latestPartsVerdictFinding, recordedPartsVerdict } from './parts-verdict-record.ts';
|
|
2
|
+
import { selectAnswerHashes } from './signoff-answers.ts';
|
|
3
|
+
import { selectSignoffTarget } from './signoff-target.ts';
|
|
4
|
+
import { assertDirectionApproval } from './direction.ts';
|
|
5
|
+
import { assertCorrectionApproval } from './correction-writers.ts';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { acquireBoundJobWrite, jobRuntimeExecArgv } from './job-writer.ts';
|
|
9
|
+
import { assertInterviewRecordSignable, checkpointProblem } from './signable.ts';
|
|
10
|
+
import { checkpointReceipt, commitIngestedSignoff, ingestCheckpointAct, type CheckpointIngest, type SignoffHooks } from './signoff-checkpoint-act.ts';
|
|
11
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { jobHash } from './job-outbox.ts';
|
|
14
|
+
import { assertActSubjectDeclared, decodeReviewAct, isRecordDirectionSignoff, isRecordInterviewSignoff, recordSignoffActVersion, PLAN_BOUND_SIGNOFF_STAGES, reviewActFindings, reviewActSignoff, reviewActTransport, SIGNOFF_TARGET_KINDS, signoffActIdentity, signoffActVersion, type ReviewActTransport, type SignoffAct } from './review-act.ts';
|
|
15
|
+
|
|
16
|
+
import { asArray, checkProject, classify, collectItems, collectSignoffs, isObject, loadCollection, postNotPassed, type Finding } from './check.ts';
|
|
17
|
+
import { checkDiffMap } from './diff-map.ts';
|
|
18
|
+
import { readProject } from './feedback.ts';
|
|
19
|
+
import { checkStructure, riggingContext, structurePathIsIgnored } from './structure.ts';
|
|
20
|
+
import { readVerbatim } from './record-input.ts';
|
|
21
|
+
import { refuse, USAGE_EXIT } from './refusal.ts';
|
|
22
|
+
import { baselineOf, newFindings } from './refusal-baseline.ts';
|
|
23
|
+
import { approvalStageOfStep, hasStageOrder, PARTS_REVIEW, RIG_REVIEW, STAGE_ORDER_SPEC, STAGE_VALUES } from './stages.ts';
|
|
24
|
+
import { assertRigReviewSignable } from './review.ts';
|
|
25
|
+
import { furthestStageSignoff } from './stage-progress.ts';
|
|
26
|
+
import { collectReopens, liveSignoff } from './reopen.ts';
|
|
27
|
+
import { stepHash } from './step-hash.ts';
|
|
28
|
+
import type { JsonObject } from './templates.ts';
|
|
29
|
+
|
|
30
|
+
export const SIGNOFF_HELP = `signoff:
|
|
31
|
+
<stage> [<project dir>] record a human signature without moving project.stage
|
|
32
|
+
--version <n> reviewed rig or post version (single candidate defaults)
|
|
33
|
+
--from-review <actId> without --checkpoint: record the person's Review signoff act for <stage>
|
|
34
|
+
(excludes --by, --verbatim-file and --steps on both routes)
|
|
35
|
+
--by <who> the person signing (agent is refused)
|
|
36
|
+
--verbatim-file <file> UTF-8 words unchanged, or - for stdin (default: stdin)
|
|
37
|
+
--steps <id,id,...> narrow parts plan, rigging-interview rig steps, or post steps
|
|
38
|
+
--checkpoint <id> sign an enabled checkpoint in this execution stage
|
|
39
|
+
--from-review <actId> with --checkpoint: record the passed Review checkpoint act (no --by or --verbatim-file)
|
|
40
|
+
--draft <id> labelled draft with a recorded parts-review API receipt
|
|
41
|
+
--structure-file <path> retained structure decisions snapshot
|
|
42
|
+
--json print the signature as JSON
|
|
43
|
+
The clock supplies at. With no --steps, signs all steps of the stage's phase.
|
|
44
|
+
Fills null checkpoints from checkpoint_proposed and hashes covered steps after filling.
|
|
45
|
+
Existing checkpoints are preserved. Parts-review validates evidence before recording.`;
|
|
46
|
+
|
|
47
|
+
interface FetchedSignoff { act: SignoffAct; bytes: string; human: JsonObject }
|
|
48
|
+
|
|
49
|
+
/** Fetch and decode a finalized signoff act; its stage, words, identity and date are the person's, never flags. */
|
|
50
|
+
function fetchSignoffAct(dir: string, stage: string, actId: string, now: number, hooks: SignoffHooks): FetchedSignoff {
|
|
51
|
+
const invalid = reviewActFindings(readProject(dir).project)[0];
|
|
52
|
+
if (invalid) refuse(invalid.code, invalid.message);
|
|
53
|
+
const transport: ReviewActTransport = { ...reviewActTransport(dir, actId), kind: 'signoff' };
|
|
54
|
+
let bytes: string;
|
|
55
|
+
if (hooks.fetch) bytes = hooks.fetch(transport).bytes;
|
|
56
|
+
else {
|
|
57
|
+
const child = Bun.spawnSync([process.execPath, ...jobRuntimeExecArgv(), fileURLToPath(new URL('./review-act-runtime.ts', import.meta.url))], {
|
|
58
|
+
stdin: Buffer.from(JSON.stringify(transport)), stdout: 'pipe', stderr: 'pipe',
|
|
59
|
+
});
|
|
60
|
+
let response: { ok: boolean; bytes: string; code: string; message: string };
|
|
61
|
+
try { response = JSON.parse(child.stdout.toString()); }
|
|
62
|
+
catch { return refuse('REVIEW_ACT_FETCH_REFUSED', 'project.json.review_acts: fetch adapter failed; restore the live Review job'); }
|
|
63
|
+
if (!response.ok || child.exitCode) refuse(response.code, response.message);
|
|
64
|
+
bytes = response.bytes;
|
|
65
|
+
}
|
|
66
|
+
const act = decodeReviewAct(bytes, 'signoff');
|
|
67
|
+
if (act.jobId !== transport.jobId || act.actId !== transport.actId || Date.parse(act.at) > now) {
|
|
68
|
+
refuse('REVIEW_ACT_INVALID', 'project.json.review_acts: fetched signoff identity or date differs from the request; fetch the correct signoff act');
|
|
69
|
+
}
|
|
70
|
+
if (act.value.stage !== stage) refuse('REVIEW_ACT_KIND_MISMATCH', `project.json.review_acts: act signs ${act.value.stage}, not ${stage}; invoke signoff for the stage the person approved`);
|
|
71
|
+
assertActSubjectDeclared(dir, act, 'project.json.review_acts');
|
|
72
|
+
return { act, bytes, human: reviewActSignoff(act) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function signoffCommand(args: string[], now: number, hooks: SignoffHooks = {}): { stdout: string; exit: number } {
|
|
76
|
+
if (args.some(arg => arg === '--evidence' || arg.startsWith('--evidence='))) refuse('PARTS_VERDICT_FILE_ROUTE_REMOVED', 'project.json.signoffs.evidence: --evidence was removed; record the API response with parts-review and select it with --draft', USAGE_EXIT);
|
|
77
|
+
const opts: Record<string, string> = {};
|
|
78
|
+
const positional: string[] = [];
|
|
79
|
+
let json = false;
|
|
80
|
+
let hasDiffFlag = false;
|
|
81
|
+
for (let i = 0; i < args.length; i++) {
|
|
82
|
+
const arg = args[i];
|
|
83
|
+
if (arg === '--json') json = true;
|
|
84
|
+
else if (arg === '--component' || arg === '--diff-report' || arg === '--triage-file') {
|
|
85
|
+
hasDiffFlag = true;
|
|
86
|
+
i++;
|
|
87
|
+
} else if (arg.startsWith('--component=') || arg.startsWith('--diff-report=') || arg.startsWith('--triage-file=')) {
|
|
88
|
+
hasDiffFlag = true;
|
|
89
|
+
} else if (['--version', '--draft', '--structure-file', '--by', '--verbatim-file', '--steps', '--checkpoint', '--from-review'].includes(arg)) {
|
|
90
|
+
const value = args[++i];
|
|
91
|
+
if (arg in opts || value === undefined || value === '') refuse('SIGNOFF_OPTION_INVALID', `${arg} needs one non-empty value`, USAGE_EXIT);
|
|
92
|
+
opts[arg] = value;
|
|
93
|
+
} else if (arg.startsWith('-')) refuse('UNKNOWN_OPTION', `signoff does not take ${arg}`, USAGE_EXIT);
|
|
94
|
+
else positional.push(arg);
|
|
95
|
+
}
|
|
96
|
+
const stage = positional[0];
|
|
97
|
+
if (positional.length > 2 || stage === undefined || !STAGE_VALUES.includes(stage)) refuse('SIGNOFF_OPTION_INVALID', 'signoff takes a known stage and one optional project folder', USAGE_EXIT);
|
|
98
|
+
if (hasDiffFlag) {
|
|
99
|
+
if (stage !== PARTS_REVIEW) refuse('SIGNOFF_OPTION_INVALID', '--diff-report, --triage-file and --component are not accepted on signoff', USAGE_EXIT);
|
|
100
|
+
refuse(
|
|
101
|
+
'SIGNOFF_DIFF_MAP_REMOVED',
|
|
102
|
+
"project.json.signoffs.diff_map: --diff-report, --triage-file and --component were removed from signoff; component classification is agent-authored — record it with 'rig-create diff-classes --diff-report <path> --triage-file <file> --by agent' (or '--component <id>=<status>:<why> --by agent')",
|
|
103
|
+
USAGE_EXIT,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const actId = opts['--from-review'];
|
|
107
|
+
if (actId !== undefined) {
|
|
108
|
+
const mixed = ['--by', '--verbatim-file', '--steps'].filter(flag => opts[flag] !== undefined);
|
|
109
|
+
if (mixed.length > 0) refuse('SIGNOFF_OPTION_INVALID', `project.json.review_acts: --from-review excludes ${mixed.join(', ')}; the person's identity, words and covered steps come from the review act`, USAGE_EXIT);
|
|
110
|
+
}
|
|
111
|
+
if (actId === undefined) return recordSignoff(stage, positional, opts, json, now, hooks);
|
|
112
|
+
// One bound write lock per route covers the fetch, validation and the write.
|
|
113
|
+
const root = resolve(positional[1] ?? process.cwd());
|
|
114
|
+
const release = acquireBoundJobWrite(root);
|
|
115
|
+
try {
|
|
116
|
+
// With --checkpoint the passed checkpoint act is fetched inside the writer; without it, the stage signoff act.
|
|
117
|
+
const review = opts['--checkpoint'] === undefined ? fetchSignoffAct(root, stage, actId, now, hooks) : undefined;
|
|
118
|
+
return recordSignoff(stage, positional, opts, json, now, hooks, review);
|
|
119
|
+
} finally { release(); }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function recordSignoff(stage: string, positional: string[], opts: Record<string, string>, json: boolean, now: number, hooks: SignoffHooks, review?: FetchedSignoff): { stdout: string; exit: number } {
|
|
123
|
+
const actId = opts['--from-review'];
|
|
124
|
+
let by = review === undefined ? opts['--by'] ?? '' : String(review.human.by);
|
|
125
|
+
if (opts['--by'] === undefined && actId === undefined) refuse('SIGNOFF_OPTION_INVALID', 'signoff needs --by', USAGE_EXIT);
|
|
126
|
+
if (by.toLowerCase() === 'agent') refuse('SIGNOFF_NOT_HUMAN', `stage ${stage}: signed by agent`);
|
|
127
|
+
const dir = resolve(positional[1] ?? process.cwd());
|
|
128
|
+
const { file, project } = readProject(dir);
|
|
129
|
+
const original = actId === undefined ? undefined : readFileSync(file);
|
|
130
|
+
let ingest: CheckpointIngest | undefined;
|
|
131
|
+
// Recorded checkpoint receipts replay before current eligibility, through the same immutable-byte validator.
|
|
132
|
+
if (actId !== undefined && opts['--checkpoint'] !== undefined && asArray(project.review_acts).some(entry =>
|
|
133
|
+
isObject(entry) && entry.kind === 'checkpoint' && entry.actId === actId)) {
|
|
134
|
+
ingest = ingestCheckpointAct(dir, project, actId, opts['--checkpoint'], now, hooks);
|
|
135
|
+
if (ingest.replay) {
|
|
136
|
+
if (ingest.replay.stage !== stage) refuse('SIGNOFF_CHECKPOINT_INVALID', `signoffs.checkpoint ${opts['--checkpoint']}: recorded act belongs to ${String(ingest.replay.stage)}; replay it for its original stage`);
|
|
137
|
+
return { stdout: json ? JSON.stringify(ingest.replay, null, 2) : `signoff: ${stage} checkpoint ${opts['--checkpoint']} already recorded from ${ingest.source}; nothing written`, exit: 0 };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// A target stage signs Review's selector or the exact displayed record, never a flag-selected replacement.
|
|
141
|
+
let targetVersion = opts['--version'];
|
|
142
|
+
if (review !== undefined && stage in SIGNOFF_TARGET_KINDS && !isRecordDirectionSignoff(review.act)) {
|
|
143
|
+
const shown = signoffActVersion(review.act);
|
|
144
|
+
const at = `signoffs[${asArray(project.signoffs).length}].target.version`;
|
|
145
|
+
if (shown === null) refuse('REVIEW_ACT_TARGET_MISMATCH', `${at}: the review act names no ${SIGNOFF_TARGET_KINDS[stage]} version it displayed; ask for ${stage} again in Review on the reviewed version`);
|
|
146
|
+
if (targetVersion !== undefined && targetVersion !== String(shown)) refuse('REVIEW_ACT_TARGET_MISMATCH', `${at}: --version ${targetVersion} differs from version ${shown} the person approved in Review; drop --version or ask for ${stage} on version ${targetVersion} in Review`);
|
|
147
|
+
targetVersion = String(shown);
|
|
148
|
+
}
|
|
149
|
+
if (review !== undefined) {
|
|
150
|
+
// Replays dedupe before current-source checks: the recorded signoff is returned unchanged.
|
|
151
|
+
const existing = asArray(project.review_acts).filter(isObject).find(entry => entry.jobId === review.act.jobId && entry.actId === review.act.actId);
|
|
152
|
+
if (existing !== undefined) {
|
|
153
|
+
if (existing.bytes !== review.bytes) refuse('REVIEW_ACT_REPLAY_CONFLICT', 'project.json.review_acts: immutable signoff act changed; restore the original receipt');
|
|
154
|
+
if (isRecordDirectionSignoff(review.act)) {
|
|
155
|
+
const invalid = reviewActFindings(project)[0];
|
|
156
|
+
if (invalid !== undefined) refuse(invalid.code, invalid.message);
|
|
157
|
+
}
|
|
158
|
+
const recorded = asArray(project.signoffs).filter(isObject).find(entry => entry.source === review.human.source);
|
|
159
|
+
if (recorded === undefined) refuse('REVIEW_ACT_LEDGER_INVALID', 'project.json.review_acts: the signoff this act recorded is absent; restore its signoffs[] entry');
|
|
160
|
+
return { stdout: json ? JSON.stringify(recorded, null, 2) : `signoff: replayed ${stage} by ${by} at ${String(recorded.at)}; stage unchanged`, exit: 0 };
|
|
161
|
+
}
|
|
162
|
+
if (review.act.schema === 'rig-review/act@4' && review.act.source.projectHash !== jobHash(original!)) refuse('REVIEW_ACT_SOURCE_STALE', 'project.json: displayed signoff record differs; publish the current plan and ask again');
|
|
163
|
+
if (isRecordDirectionSignoff(review.act)) {
|
|
164
|
+
const shown = recordSignoffActVersion(review.act, original!.toString('utf8'));
|
|
165
|
+
if (shown === null) refuse('REVIEW_ACT_RECORD_TARGET_INVALID', 'project.json.versions: the frozen sign-off record has no single current live post with an accepted direction-review verdict; publish the corrected record after reviewing that post');
|
|
166
|
+
if (targetVersion !== undefined && targetVersion !== String(shown)) refuse('REVIEW_ACT_TARGET_MISMATCH', `signoffs.target.version: --version ${targetVersion} differs from post ${shown} bound by the frozen record; drop --version and sign the displayed record`);
|
|
167
|
+
targetVersion = String(shown);
|
|
168
|
+
}
|
|
169
|
+
if (PLAN_BOUND_SIGNOFF_STAGES.includes(stage) || isRecordInterviewSignoff(review.act)) {
|
|
170
|
+
const displayed = review.act.source.plan?.steps ?? [];
|
|
171
|
+
const current = asArray(isObject(project.plan) ? project.plan.steps : undefined).map(raw => ({ id: isObject(raw) ? raw.id ?? null : null, hash: isObject(raw) ? stepHash(raw) : null }));
|
|
172
|
+
if (displayed.length !== current.length || displayed.some((step, index) => step.id !== current[index]!.id || step.hash !== current[index]!.hash)) {
|
|
173
|
+
refuse('REVIEW_ACT_SOURCE_STALE', `signoffs[${asArray(project.signoffs).length}].step_hashes: the plan steps the person approved differ from the current plan; publish the current plan and ask for ${stage} again`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (review !== undefined && stage === PARTS_REVIEW) {
|
|
178
|
+
const selector = review.act.source.selector;
|
|
179
|
+
const shown = isObject(selector) && selector.kind === 'draft' && typeof selector.id === 'string' && selector.id !== '' ? selector.id : null;
|
|
180
|
+
const at = `signoffs[${asArray(project.signoffs).length}].draft_id`;
|
|
181
|
+
if (selector !== null && (shown === null || (opts['--draft'] !== undefined && opts['--draft'] !== shown))) {
|
|
182
|
+
refuse('REVIEW_ACT_DRAFT_MISMATCH', `${at}: --draft ${opts['--draft'] ?? '<absent>'} differs from draft ${shown ?? '<non-draft>'} the person approved in Review; drop --draft or ask for parts-review on the reviewed draft in Review`);
|
|
183
|
+
}
|
|
184
|
+
if (shown !== null) opts['--draft'] = shown;
|
|
185
|
+
}
|
|
186
|
+
if (stage === PARTS_REVIEW && opts['--draft'] !== undefined) {
|
|
187
|
+
const finding = latestPartsVerdictFinding(project, opts['--draft'], `signoffs[${asArray(project.signoffs).length}].draft_id`);
|
|
188
|
+
if (finding) refuse(finding.code, finding.message);
|
|
189
|
+
}
|
|
190
|
+
// The record as read: its refusals are the baseline the candidate is judged against below.
|
|
191
|
+
const asRead = checkProject(dir, project, { now });
|
|
192
|
+
if (review !== undefined && isRecordInterviewSignoff(review.act)) assertInterviewRecordSignable(project, asRead);
|
|
193
|
+
const existingLag = asRead.refusals.find(f => f.code === 'STAGE_LAGGING');
|
|
194
|
+
if (existingLag !== undefined) refuse(existingLag.code, existingLag.message);
|
|
195
|
+
if (!Array.isArray(project.signoffs)) refuse('SIGNOFF_MALFORMED', 'signoffs must be a list');
|
|
196
|
+
const checkpointId = opts['--checkpoint'];
|
|
197
|
+
if (checkpointId !== undefined) {
|
|
198
|
+
if (opts['--steps'] !== undefined) refuse('SIGNOFF_OPTION_INVALID', '--checkpoint and --steps cannot be combined', USAGE_EXIT);
|
|
199
|
+
// The publications declare checkpoint subjects with this same rule (src/signable.ts).
|
|
200
|
+
const problem = checkpointProblem(project, stage, checkpointId);
|
|
201
|
+
if (problem !== null) refuse('SIGNOFF_CHECKPOINT_INVALID', problem);
|
|
202
|
+
if (actId !== undefined) {
|
|
203
|
+
ingest = ingestCheckpointAct(dir, project, actId, checkpointId, now, hooks);
|
|
204
|
+
if (ingest.replay) return { stdout: json ? JSON.stringify(ingest.replay, null, 2) : `signoff: ${stage} checkpoint ${checkpointId} already recorded from ${ingest.source}; nothing written`, exit: 0 };
|
|
205
|
+
by = ingest.by;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (stage === PARTS_REVIEW && !opts['--draft']) refuse('PARTS_VERDICT_RECEIPT_REQUIRED', 'project.json.signoffs.draft_id: parts-review needs --draft with a recorded API receipt');
|
|
209
|
+
let evidence: JsonObject | undefined;
|
|
210
|
+
const recorded = stage === PARTS_REVIEW ? recordedPartsVerdict(dir, project, opts['--draft']) : undefined;
|
|
211
|
+
evidence = recorded?.evidence;
|
|
212
|
+
if ((opts['--draft'] !== undefined || opts['--structure-file'] !== undefined) && stage !== PARTS_REVIEW) refuse('SIGNOFF_OPTION_INVALID', '--draft and --structure-file require parts-review', USAGE_EXIT);
|
|
213
|
+
const phase = stage === 'post-interview' ? 'post' : ['plan-approved', 'rigging-interview'].includes(stage) ? 'rig' : undefined;
|
|
214
|
+
if (phase === undefined && opts['--steps'] !== undefined) refuse('SIGNOFF_OPTION_INVALID', '--steps requires plan-approved, rigging-interview or post-interview', USAGE_EXIT);
|
|
215
|
+
const requested = opts['--steps']?.split(',');
|
|
216
|
+
if (requested !== undefined && (requested.some((id) => id === '') || new Set(requested).size !== requested.length)) refuse('SIGNOFF_STEP_INVALID', '--steps needs distinct non-empty step ids', USAGE_EXIT);
|
|
217
|
+
const steps = phase === undefined ? [] : asArray(isObject(project.plan) ? project.plan.steps : undefined)
|
|
218
|
+
.filter((step): step is JsonObject => isObject(step) && step.phase === phase && approvalStageOfStep(phase as 'rig' | 'post', String(step.id), hasStageOrder(project) || stage === 'rigging-interview') === stage);
|
|
219
|
+
for (const id of requested ?? []) {
|
|
220
|
+
if (!steps.some((step) => step.id === id)) refuse('SIGNOFF_STEP_INVALID', `${id}: no step in phase ${phase}`);
|
|
221
|
+
}
|
|
222
|
+
const covered = steps.filter((step) => requested === undefined || requested.includes(step.id as string));
|
|
223
|
+
const emptyPost = stage === 'post-interview' && requested === undefined && steps.length === 0
|
|
224
|
+
&& postNotPassed({ items: collectItems(project, { collection: loadCollection(dir) }) });
|
|
225
|
+
if (phase !== undefined && covered.length === 0 && !emptyPost) refuse('SIGNOFF_STEP_INVALID', `no ${phase} steps to sign`);
|
|
226
|
+
const ids = covered.map((step) => step.id);
|
|
227
|
+
if (ids.some((id) => typeof id !== 'string' || id === '') || new Set(ids).size !== ids.length) refuse('SIGNOFF_STEP_INVALID', 'covered steps need distinct non-empty string ids');
|
|
228
|
+
for (const step of covered) {
|
|
229
|
+
if (typeof step.checkpoint_proposed !== 'boolean') refuse('SIGNOFF_CHECKPOINT_INVALID', `${String(step.id)}: checkpoint_proposed must be boolean`);
|
|
230
|
+
const checkpoint = classify(step.checkpoint, { collection: null });
|
|
231
|
+
if (checkpoint.state === 'silent') continue;
|
|
232
|
+
const value = checkpoint.state === 'user' ? checkpoint.answer.value
|
|
233
|
+
: checkpoint.state === 'delegated' && checkpoint.decided && isObject(checkpoint.answer.agent_decision) ? checkpoint.answer.agent_decision.value : undefined;
|
|
234
|
+
if (typeof value !== 'boolean') refuse('SIGNOFF_CHECKPOINT_INVALID', `${String(step.id)}: checkpoint needs a boolean user answer or a decided boolean delegation`);
|
|
235
|
+
}
|
|
236
|
+
const verbatim = review !== undefined ? String(review.human.verbatim) : ingest ? ingest.verbatim : readVerbatim(opts['--verbatim-file'] ?? '-');
|
|
237
|
+
const at = review !== undefined ? String(review.human.at) : ingest ? ingest.at : new Date(now).toISOString();
|
|
238
|
+
const hashes: JsonObject = Object.create(null) as JsonObject;
|
|
239
|
+
for (const step of covered) {
|
|
240
|
+
if (step.checkpoint === null || step.checkpoint === undefined) {
|
|
241
|
+
step.checkpoint = { delegated: false, by, at, verbatim, value: step.checkpoint_proposed };
|
|
242
|
+
}
|
|
243
|
+
hashes[step.id as string] = stepHash(step);
|
|
244
|
+
}
|
|
245
|
+
const signoff: JsonObject = { stage, checkpoint: checkpointId ?? null, by, at, verbatim };
|
|
246
|
+
if (review !== undefined) signoff.source = review.human.source!;
|
|
247
|
+
if (evidence !== undefined) signoff.evidence = evidence;
|
|
248
|
+
if (phase !== undefined) {
|
|
249
|
+
signoff.steps = ids;
|
|
250
|
+
signoff.step_hashes = hashes;
|
|
251
|
+
}
|
|
252
|
+
const answerHashes = selectAnswerHashes(dir, project, signoff);
|
|
253
|
+
if (answerHashes !== undefined) signoff.answer_hashes = answerHashes;
|
|
254
|
+
if (stage === PARTS_REVIEW && opts['--draft'] !== undefined) signoff.draft_id = opts['--draft'];
|
|
255
|
+
if (stage === PARTS_REVIEW && checkpointId === undefined) {
|
|
256
|
+
const errors: Finding[] = [];
|
|
257
|
+
checkDiffMap(dir, signoff, `signoffs[${project.signoffs.length}]`, recorded?.partsFile, errors, [], project);
|
|
258
|
+
if (errors[0] !== undefined) refuse(errors[0].code, errors[0].message);
|
|
259
|
+
}
|
|
260
|
+
if (stage === PARTS_REVIEW && (hasStageOrder(project) || opts['--structure-file'] !== undefined)) {
|
|
261
|
+
const path = opts['--structure-file'];
|
|
262
|
+
if (path !== undefined) {
|
|
263
|
+
try {
|
|
264
|
+
if (structurePathIsIgnored(dir, path)) throw new Error('structure snapshots must survive a clone; retain the file outside ignored roots, for example evidence/');
|
|
265
|
+
const canonical = realpathSync(resolve(dir, path));
|
|
266
|
+
const rel = relative(realpathSync(dir), canonical).split(sep).join('/');
|
|
267
|
+
if (isAbsolute(path) || rel === '' || rel === '..' || rel.startsWith('../')) throw new Error('structure file must stay under the project');
|
|
268
|
+
signoff.structure = { path: rel, sha256: createHash('sha256').update(readFileSync(canonical)).digest('hex') };
|
|
269
|
+
} catch (error) { refuse('STRUCTURE_DECISIONS_INVALID', `signoffs.structure: ${error instanceof Error ? error.message : String(error)}; retain the structure snapshot under the project`); }
|
|
270
|
+
}
|
|
271
|
+
const errors: Finding[] = [];
|
|
272
|
+
checkStructure(dir, project, signoff, `signoffs[${project.signoffs.length}]`, errors);
|
|
273
|
+
if (errors[0] !== undefined) refuse(errors[0].code, errors[0].message);
|
|
274
|
+
}
|
|
275
|
+
if (stage === 'rigging-interview') {
|
|
276
|
+
const errors: Finding[] = [];
|
|
277
|
+
const context = riggingContext(dir, project, at, errors);
|
|
278
|
+
if (errors[0] !== undefined) refuse(errors[0].code, errors[0].message);
|
|
279
|
+
if (context !== null) { signoff.accepted_parts = context.accepted_parts as JsonObject; signoff.structure = context.structure as JsonObject; }
|
|
280
|
+
if (!isObject(project.interview)) refuse('RIGGING_INTERVIEW_CONTEXT', 'interview: restore the interview record before adoption');
|
|
281
|
+
project.interview.rigging_context = context;
|
|
282
|
+
if (!hasStageOrder(project)) project.interview.order = { spec: STAGE_ORDER_SPEC, adopted_at: at };
|
|
283
|
+
project.stage_values = [...STAGE_VALUES];
|
|
284
|
+
}
|
|
285
|
+
assertDirectionApproval(dir, project, stage, now);
|
|
286
|
+
assertCorrectionApproval(dir, project, stage, now, signoff);
|
|
287
|
+
if (stage === RIG_REVIEW) assertRigReviewSignable(project, `signoffs[${project.signoffs.length}]`);
|
|
288
|
+
const target = selectSignoffTarget(dir, project, stage, targetVersion);
|
|
289
|
+
if (target !== undefined) signoff.target = target;
|
|
290
|
+
if (ingest) signoff.source = ingest.source;
|
|
291
|
+
project.signoffs.push(signoff);
|
|
292
|
+
if (review !== undefined) {
|
|
293
|
+
project.review_acts = [...asArray(project.review_acts), { schema: 'rig-create/review-act@1', kind: 'signoff', jobId: review.act.jobId, actId: review.act.actId,
|
|
294
|
+
stage, bytes: review.bytes, sha256: jobHash(review.bytes), version_hash: jobHash(JSON.stringify(signoffActIdentity(review.act))),
|
|
295
|
+
...(isRecordDirectionSignoff(review.act) ? { record_sha256: review.act.source.projectHash,
|
|
296
|
+
target_version: target!.version!, verdict: target!.verdict! } : {}) }];
|
|
297
|
+
const invalid = reviewActFindings(project)[0];
|
|
298
|
+
if (invalid !== undefined) refuse(invalid.code, invalid.message);
|
|
299
|
+
}
|
|
300
|
+
if (ingest) {
|
|
301
|
+
project.review_acts = [...asArray(project.review_acts), checkpointReceipt(ingest, project.signoffs.length - 1)];
|
|
302
|
+
const invalid = reviewActFindings(project)[0];
|
|
303
|
+
if (invalid) refuse(invalid.code, invalid.message);
|
|
304
|
+
}
|
|
305
|
+
const reopens = collectReopens(project, []);
|
|
306
|
+
const frontier = furthestStageSignoff(collectSignoffs(project, []).filter(s => liveSignoff(reopens, s.stage, s.at)));
|
|
307
|
+
const ownLag = signoff.checkpoint === null && frontier?.index === project.signoffs.length - 1;
|
|
308
|
+
const candidateRefusals = checkProject(dir, project, { now }).refusals;
|
|
309
|
+
// Refusals the record already carried do not block the signature (docs/PROJECT.md); a new one does.
|
|
310
|
+
const fresh = new Set(newFindings(baselineOf(asRead), candidateRefusals));
|
|
311
|
+
const pendingOwnAdvance = (finding: Finding): boolean => ownLag && finding.code === 'STAGE_LAGGING';
|
|
312
|
+
const progress = candidateRefusals.find(f => ['STAGE_LAGGING', 'STAGE_SKIPPED', 'STAGE_SIGNOFF_DATE_INVALID', 'ADVANCE_MALFORMED', 'STEP_STATUS_TRANSITION', 'STEP_STATUS_SIGNOFF'].includes(f.code) && !pendingOwnAdvance(f) && fresh.has(f));
|
|
313
|
+
if (progress !== undefined) refuse(progress.code, progress.message);
|
|
314
|
+
if (hasStageOrder(project) || signoff.structure !== undefined || project.amendments !== undefined) {
|
|
315
|
+
const path = `signoffs[${project.signoffs.length - 1}]`;
|
|
316
|
+
const first = candidateRefusals.find(f => {
|
|
317
|
+
if (pendingOwnAdvance(f)) return false;
|
|
318
|
+
if (f.code.startsWith('SIGNOFF_ANSWERS_')) return f.path.startsWith(`${path}.`);
|
|
319
|
+
// A covered step this signature leaves unapproved is its own failure, even when the record carried it.
|
|
320
|
+
if (f.code === 'GUIDE_CHANGED_UNSIGNED') return covered.some(step => f.path === `plan.steps[${String(step.id)}]`);
|
|
321
|
+
return fresh.has(f);
|
|
322
|
+
});
|
|
323
|
+
if (first !== undefined) refuse(first.code, first.message);
|
|
324
|
+
}
|
|
325
|
+
if (ingest) commitIngestedSignoff(file, original!, project);
|
|
326
|
+
else {
|
|
327
|
+
if (review !== undefined && !readFileSync(file).equals(original!)) refuse('REVIEW_ACT_SOURCE_STALE', 'project.json: record changed during signoff validation; retry against the current live job');
|
|
328
|
+
writeFileSync(file, `${JSON.stringify(project, null, 2)}\n`);
|
|
329
|
+
}
|
|
330
|
+
return { stdout: json ? JSON.stringify(signoff, null, 2) : `signoff: recorded ${stage} by ${by} at ${at}; stage unchanged`, exit: 0 };
|
|
331
|
+
}
|