@rig-c/create 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +163 -2
- package/docs/CHECKS.md +1665 -0
- package/docs/COLLECTIONS.md +337 -0
- package/docs/CRAFT.md +277 -0
- package/docs/INTERVIEW.md +886 -0
- package/docs/LESSONS.md +172 -0
- package/docs/PROJECT.md +2657 -0
- package/docs/SOURCE.md +225 -0
- package/package.json +44 -4
- package/src/agent-bounce.ts +27 -0
- package/src/agent-guide.ts +237 -0
- package/src/amend.ts +193 -0
- package/src/applied-review.ts +104 -0
- package/src/ask-coverage.ts +69 -0
- package/src/assemble-answers.ts +53 -0
- package/src/cg-gates.ts +211 -0
- package/src/check-reads.ts +106 -0
- package/src/check.ts +1059 -0
- package/src/checkpoint-evidence-writer.ts +125 -0
- package/src/checkpoint-evidence.ts +152 -0
- package/src/choice-writers.ts +271 -0
- package/src/choices.ts +378 -0
- package/src/cli.ts +371 -0
- package/src/collection-answer.ts +136 -0
- package/src/collection-binding-reader.ts +496 -0
- package/src/collection-binding.ts +478 -0
- package/src/collection.ts +348 -0
- package/src/config-inputs.ts +155 -0
- package/src/config-lost.ts +135 -0
- package/src/correction-writers.ts +207 -0
- package/src/corrections.ts +685 -0
- package/src/diff-classes.ts +219 -0
- package/src/diff-map.ts +248 -0
- package/src/direction-citation.ts +13 -0
- package/src/direction-writers.ts +119 -0
- package/src/direction.ts +266 -0
- package/src/drafts.ts +363 -0
- package/src/feedback.ts +744 -0
- package/src/figure-mask.ts +143 -0
- package/src/fix-ledger.ts +355 -0
- package/src/init.ts +265 -0
- package/src/inputs.ts +160 -0
- package/src/intake.ts +158 -0
- package/src/interview.ts +53 -0
- package/src/invariants.ts +132 -0
- package/src/job-acts.ts +80 -0
- package/src/job-advance.ts +65 -0
- package/src/job-close.ts +226 -0
- package/src/job-host.ts +529 -0
- package/src/job-outbox.ts +383 -0
- package/src/job-publication.ts +701 -0
- package/src/job-runtime.ts +99 -0
- package/src/job-writer.ts +123 -0
- package/src/meshes-bust.ts +63 -0
- package/src/next.ts +1506 -0
- package/src/overrides.ts +42 -0
- package/src/package-upgrades.ts +32 -0
- package/src/packages-upgrade-check.ts +6 -0
- package/src/packages-upgrade.ts +168 -0
- package/src/parts-act.ts +96 -0
- package/src/parts-check.ts +242 -0
- package/src/parts-review.ts +196 -0
- package/src/parts-sections.ts +98 -0
- package/src/parts-verdict-record.ts +265 -0
- package/src/parts-verdict.ts +33 -0
- package/src/pause.ts +118 -0
- package/src/pins-check.ts +619 -0
- package/src/pins.ts +17 -0
- package/src/plan-bar-amendments.ts +54 -0
- package/src/plan-history.ts +147 -0
- package/src/plan-writers.ts +202 -0
- package/src/plate-receipt.ts +110 -0
- package/src/pre-interview.ts +302 -0
- package/src/prepared-inputs.ts +113 -0
- package/src/progress-writers.ts +181 -0
- package/src/provenance.ts +301 -0
- package/src/record-direction-target.ts +26 -0
- package/src/record-input.ts +18 -0
- package/src/refusal-baseline.ts +55 -0
- package/src/refusal.ts +30 -0
- package/src/reopen-lapses.ts +80 -0
- package/src/reopen.ts +144 -0
- package/src/republish.ts +93 -0
- package/src/restart.ts +141 -0
- package/src/review-act-runtime.ts +16 -0
- package/src/review-act.ts +592 -0
- package/src/review-package.ts +524 -0
- package/src/review-read.ts +238 -0
- package/src/review.ts +432 -0
- package/src/rig-version-date.ts +22 -0
- package/src/rig-version.ts +1047 -0
- package/src/run-record.ts +232 -0
- package/src/scene-authority.ts +410 -0
- package/src/scene-writers.ts +144 -0
- package/src/scene.ts +284 -0
- package/src/setup-painting.ts +46 -0
- package/src/signable.ts +65 -0
- package/src/signoff-answers.ts +76 -0
- package/src/signoff-checkpoint-act.ts +95 -0
- package/src/signoff-target.ts +127 -0
- package/src/signoff.ts +331 -0
- package/src/split-captions.ts +154 -0
- package/src/stage-progress.ts +379 -0
- package/src/stages.ts +148 -0
- package/src/step-hash.ts +19 -0
- package/src/structure-snapshot.ts +109 -0
- package/src/structure.ts +166 -0
- package/src/templates.ts +16 -0
- package/src/waypoints.ts +163 -0
- package/templates/agent-guide.md +108 -0
- package/templates/collection-agent-guide.md +72 -0
- package/templates/collection.json +252 -0
- package/templates/example-chain/AGENTS.md +134 -0
- package/templates/example-chain/CLAUDE.md +3 -0
- package/templates/example-chain/README.md +13 -0
- package/templates/example-chain/chain.json +2869 -0
- package/templates/example-chain/checkpoints/0a-interview-options/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/0a-interview-options/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/0a-interview-options/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/0a-interview-options/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/0a-interview-options/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/0a-interview-options/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/0a-interview-options/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/0a-interview-options/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/0a-interview-options/package.json +9 -0
- package/templates/example-chain/checkpoints/0a-interview-options/project.json +295 -0
- package/templates/example-chain/checkpoints/0a-interview-options.json +127 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/package.json +9 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/project.json +685 -0
- package/templates/example-chain/checkpoints/0b-plan-pending.json +81 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/parts-check.json +127 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/package.json +9 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/project.json +955 -0
- package/templates/example-chain/checkpoints/1-parts-verdict.json +204 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/diff-components.json +34 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/bones.json +190 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/parts-check.json +127 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/parts-review.json +88 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/structure.json +30 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/package.json +9 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/project.json +1198 -0
- package/templates/example-chain/checkpoints/2-rigging-answers.json +254 -0
- package/templates/example-chain/checkpoints/3-rig-review/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/3-rig-review/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/diff-components.json +34 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/bones.json +190 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/constraints.json +272 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/meshes.json +190 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/motion.json +272 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/parts-check.json +127 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/parts-review.json +88 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/setup.png +0 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/structure.json +30 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/v1/config.json +88 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/v1/interview-requirements.json +8 -0
- package/templates/example-chain/checkpoints/3-rig-review/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/3-rig-review/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/3-rig-review/package.json +9 -0
- package/templates/example-chain/checkpoints/3-rig-review/project.json +1909 -0
- package/templates/example-chain/checkpoints/3-rig-review.json +424 -0
- package/templates/example-chain/checkpoints/4-direction-post/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/4-direction-post/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/diff-components.json +34 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/bones.json +190 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/camera_depth.json +102 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/constraints.json +272 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/meshes.json +190 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/motion.json +272 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/post_composition.json +122 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/post_timing.json +102 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/parts-check.json +127 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/parts-review.json +88 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/setup.png +0 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/structure.json +30 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/v1/config.json +88 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/v1/interview-requirements.json +8 -0
- package/templates/example-chain/checkpoints/4-direction-post/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/4-direction-post/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/4-direction-post/package.json +9 -0
- package/templates/example-chain/checkpoints/4-direction-post/project.json +2302 -0
- package/templates/example-chain/checkpoints/4-direction-post.json +456 -0
- package/templates/example-chain/evidence/.gitkeep +0 -0
- package/templates/example-chain/evidence/approval.txt +1 -0
- package/templates/example-chain/evidence/captions.json +101 -0
- package/templates/example-chain/evidence/diff-components.json +34 -0
- package/templates/example-chain/evidence/direction/v2.json +74 -0
- package/templates/example-chain/evidence/direction-input.json +74 -0
- package/templates/example-chain/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/evidence/interview/bones.json +190 -0
- package/templates/example-chain/evidence/interview/camera_depth.json +102 -0
- package/templates/example-chain/evidence/interview/constraints.json +272 -0
- package/templates/example-chain/evidence/interview/meshes.json +190 -0
- package/templates/example-chain/evidence/interview/motion.json +272 -0
- package/templates/example-chain/evidence/interview/post_composition.json +122 -0
- package/templates/example-chain/evidence/interview/post_timing.json +102 -0
- package/templates/example-chain/evidence/interview/reference.json +59 -0
- package/templates/example-chain/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/evidence/parts-check.json +127 -0
- package/templates/example-chain/evidence/parts-review.json +88 -0
- package/templates/example-chain/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
- package/templates/example-chain/evidence/scene/s1.json +65 -0
- package/templates/example-chain/evidence/scene-citations.json +9 -0
- package/templates/example-chain/evidence/setup.png +0 -0
- package/templates/example-chain/evidence/split-config.json +1 -0
- package/templates/example-chain/evidence/structure.json +30 -0
- package/templates/example-chain/evidence/v1/config.json +88 -0
- package/templates/example-chain/evidence/v1/interview-requirements.json +8 -0
- package/templates/example-chain/inputs/background.png +0 -0
- package/templates/example-chain/inputs/painting.png +0 -0
- package/templates/example-chain/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/package.json +9 -0
- package/templates/example-chain/project.json +2593 -0
- package/templates/example-diff/diff-components.json +71 -0
- package/templates/example-diff/recomposite_rig.png +0 -0
- package/templates/example-direction/approval.txt +1 -0
- package/templates/example-direction/derived/review/v1/out/check/build/rectangle.png +0 -0
- package/templates/example-direction/derived/review/v1/out/check/build/run.json +40 -0
- package/templates/example-direction/derived/review/v1/out/check/build/skeleton.atlas +8 -0
- package/templates/example-direction/derived/review/v1/out/check/build/skeleton.json +73 -0
- package/templates/example-direction/derived/review/v1/out/check/build/skeleton.model.json +195 -0
- package/templates/example-direction/evidence/direction/v2.json +69 -0
- package/templates/example-direction/evidence/direction/v3.json +59 -0
- package/templates/example-direction/evidence/images/rectangle.png +0 -0
- package/templates/example-direction/evidence/input.json +69 -0
- package/templates/example-direction/evidence/motion.json +33 -0
- package/templates/example-direction/evidence/patch.json +23 -0
- package/templates/example-direction/evidence/rig-config.json +55 -0
- package/templates/example-direction/evidence/still.png +0 -0
- package/templates/example-direction/project.json +372 -0
- package/templates/example-direction/reasoning.txt +1 -0
- package/templates/example-direction/words.txt +1 -0
- package/templates/example-draft/README.md +28 -0
- package/templates/example-draft/config.json +71 -0
- package/templates/example-draft/derived/drafts/lantern/config.json +71 -0
- package/templates/example-draft/derived/drafts/lantern/input/painting_in_rig.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/background.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/eye_l.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/eye_r.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/face.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/lantern.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/torso.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts.json +188 -0
- package/templates/example-draft/derived/drafts/lantern/input/recomposite_rig.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/interview-requirements.json +8 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/run.json +40 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/background.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/eye_l.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/eye_r.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/face.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/lantern.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/torso.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/mesh_report.json +1 -0
- package/templates/example-draft/derived/drafts/lantern/out/motion.json +137 -0
- package/templates/example-draft/derived/drafts/lantern/out/rig.json +102 -0
- package/templates/example-draft/derived/drafts/lantern/out/run.json +52 -0
- package/templates/example-draft/derived/parts/rig/painting_in_rig.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/background.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/eye_l.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/eye_r.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/face.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/lantern.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/torso.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts.json +188 -0
- package/templates/example-draft/derived/parts/rig/recomposite_rig.png +0 -0
- package/templates/example-draft/derived/review/v1/config.json +71 -0
- package/templates/example-draft/derived/review/v1/interview-requirements.json +8 -0
- package/templates/example-draft/derived/review/v1/out/check/build/run.json +40 -0
- package/templates/example-draft/derived/review/v1/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/derived/review/v1/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/derived/review/v1/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/derived/review/v1/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/background.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/eye_l.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/eye_r.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/face.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/lantern.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/torso.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts.json +188 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/background.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/eye_l.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/eye_r.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/face.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/lantern.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/torso.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/mesh_report.json +1 -0
- package/templates/example-draft/derived/review/v1/out/rig/motion.json +137 -0
- package/templates/example-draft/derived/review/v1/out/rig/rig.json +102 -0
- package/templates/example-draft/derived/review/v1/out/rig/run.json +52 -0
- package/templates/example-draft/derived/review/v2/config.json +71 -0
- package/templates/example-draft/derived/review/v2/interview-requirements.json +8 -0
- package/templates/example-draft/derived/review/v2/out/check/build/run.json +40 -0
- package/templates/example-draft/derived/review/v2/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/derived/review/v2/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/derived/review/v2/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/derived/review/v2/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/background.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/eye_l.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/eye_r.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/face.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/lantern.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/torso.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts.json +188 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/background.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/eye_l.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/eye_r.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/face.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/lantern.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/torso.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/mesh_report.json +1 -0
- package/templates/example-draft/derived/review/v2/out/rig/motion.json +137 -0
- package/templates/example-draft/derived/review/v2/out/rig/rig.json +102 -0
- package/templates/example-draft/derived/review/v2/out/rig/run.json +52 -0
- package/templates/example-draft/evidence/diff-components.json +71 -0
- package/templates/example-draft/evidence/direction/v4.json +64 -0
- package/templates/example-draft/evidence/direction-input-v4.json +64 -0
- package/templates/example-draft/evidence/fix-ledger/lantern.json +17 -0
- package/templates/example-draft/evidence/parts-check.json +127 -0
- package/templates/example-draft/evidence/parts-review.json +88 -0
- package/templates/example-draft/evidence/review-reads/4ed0507381809746443021554e012032be5b5f8417da0f2e525e9b6df16e9f05.json +59 -0
- package/templates/example-draft/evidence/scene/s1.json +65 -0
- package/templates/example-draft/evidence/scene-background.png +0 -0
- package/templates/example-draft/evidence/scene-citations.json +9 -0
- package/templates/example-draft/evidence/structure-decisions.json +9 -0
- package/templates/example-draft/evidence/v1/interview-config.json +71 -0
- package/templates/example-draft/evidence/v1/interview-requirements.json +8 -0
- package/templates/example-draft/evidence/v1/review-config.json +71 -0
- package/templates/example-draft/evidence/v2/interview-config.json +71 -0
- package/templates/example-draft/evidence/v2/interview-requirements.json +8 -0
- package/templates/example-draft/evidence/v2/review-config.json +71 -0
- package/templates/example-draft/input/painting_in_rig.png +0 -0
- package/templates/example-draft/input/parts/background.png +0 -0
- package/templates/example-draft/input/parts/eye_l.png +0 -0
- package/templates/example-draft/input/parts/eye_r.png +0 -0
- package/templates/example-draft/input/parts/face.png +0 -0
- package/templates/example-draft/input/parts/lantern.png +0 -0
- package/templates/example-draft/input/parts/torso.png +0 -0
- package/templates/example-draft/input/parts.json +188 -0
- package/templates/example-draft/input/recomposite_rig.png +0 -0
- package/templates/example-draft/inputs/painting.png +0 -0
- package/templates/example-draft/interview-requirements.json +8 -0
- package/templates/example-draft/out/check/build/run.json +40 -0
- package/templates/example-draft/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/out/images/background.png +0 -0
- package/templates/example-draft/out/images/eye_l.png +0 -0
- package/templates/example-draft/out/images/eye_r.png +0 -0
- package/templates/example-draft/out/images/face.png +0 -0
- package/templates/example-draft/out/images/lantern.png +0 -0
- package/templates/example-draft/out/images/torso.png +0 -0
- package/templates/example-draft/out/mesh_report.json +1 -0
- package/templates/example-draft/out/motion.json +137 -0
- package/templates/example-draft/out/rig.json +102 -0
- package/templates/example-draft/out/run.json +52 -0
- package/templates/example-draft/outputs/v1/out/check/build/run.json +40 -0
- package/templates/example-draft/outputs/v1/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/outputs/v1/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/outputs/v1/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/outputs/v1/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/outputs/v1/out/painting_in_rig.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/background.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/eye_l.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/eye_r.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/face.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/lantern.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/torso.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts.json +188 -0
- package/templates/example-draft/outputs/v1/out/rig/images/background.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/eye_l.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/eye_r.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/face.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/lantern.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/torso.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/mesh_report.json +1 -0
- package/templates/example-draft/outputs/v1/out/rig/motion.json +137 -0
- package/templates/example-draft/outputs/v1/out/rig/rig.json +102 -0
- package/templates/example-draft/outputs/v1/out/rig/run.json +52 -0
- package/templates/example-draft/outputs/v2/out/check/build/run.json +40 -0
- package/templates/example-draft/outputs/v2/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/outputs/v2/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/outputs/v2/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/outputs/v2/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/outputs/v2/out/painting_in_rig.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/background.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/eye_l.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/eye_r.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/face.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/lantern.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/torso.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts.json +188 -0
- package/templates/example-draft/outputs/v2/out/rig/images/background.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/eye_l.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/eye_r.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/face.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/lantern.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/torso.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/mesh_report.json +1 -0
- package/templates/example-draft/outputs/v2/out/rig/motion.json +137 -0
- package/templates/example-draft/outputs/v2/out/rig/rig.json +102 -0
- package/templates/example-draft/outputs/v2/out/rig/run.json +52 -0
- package/templates/example-interview-choice.json +1068 -0
- package/templates/example-interview-options/constraint.json +376 -0
- package/templates/example-interview-options/example-assemble.json +376 -0
- package/templates/example-interview-options/example-bones.json +190 -0
- package/templates/example-interview-options/example-camera-depth.json +99 -0
- package/templates/example-interview-options/example-constraints.json +272 -0
- package/templates/example-interview-options/example-meshes.json +190 -0
- package/templates/example-interview-options/example-motion.json +272 -0
- package/templates/example-interview-options/example-picks.json +15 -0
- package/templates/example-interview-options/example-post-composition.json +110 -0
- package/templates/example-interview-options/example-post-timing.json +105 -0
- package/templates/example-interview-options/example-reference.json +63 -0
- package/templates/example-interview-options/example-rig-type.json +63 -0
- package/templates/example-interview-options/example-seethrough.json +190 -0
- package/templates/example-interview-options/fullscreen.json +63 -0
- package/templates/example-interview-options/other-explicit.json +63 -0
- package/templates/example-interview-options/other-pending.json +63 -0
- package/templates/example-interview-options/other-picks.json +16 -0
- package/templates/example-interview.md +213 -0
- package/templates/example-invariants/invariants.json +1339 -0
- package/templates/example-prepared/prepared.svg +1 -0
- package/templates/example-prepared/source.svg +1 -0
- package/templates/example-project.json +4259 -0
- package/templates/example-remesh/AGENTS.md +134 -0
- package/templates/example-remesh/CLAUDE.md +3 -0
- package/templates/example-remesh/evidence/.gitkeep +0 -0
- package/templates/example-remesh/evidence/approval.txt +1 -0
- package/templates/example-remesh/evidence/captions.json +101 -0
- package/templates/example-remesh/evidence/diff-components.json +34 -0
- package/templates/example-remesh/evidence/direction/v4.json +74 -0
- package/templates/example-remesh/evidence/direction/v5.json +74 -0
- package/templates/example-remesh/evidence/direction-input-v4.json +74 -0
- package/templates/example-remesh/evidence/direction-input-v5.json +74 -0
- package/templates/example-remesh/evidence/interview/assemble.json +309 -0
- package/templates/example-remesh/evidence/interview/bones.json +190 -0
- package/templates/example-remesh/evidence/interview/camera_depth.json +103 -0
- package/templates/example-remesh/evidence/interview/constraints.json +272 -0
- package/templates/example-remesh/evidence/interview/meshes.json +190 -0
- package/templates/example-remesh/evidence/interview/motion.json +272 -0
- package/templates/example-remesh/evidence/interview/post_composition.json +123 -0
- package/templates/example-remesh/evidence/interview/post_timing.json +103 -0
- package/templates/example-remesh/evidence/interview/reference.json +59 -0
- package/templates/example-remesh/evidence/interview/rig_type.json +59 -0
- package/templates/example-remesh/evidence/interview/seethrough.json +186 -0
- package/templates/example-remesh/evidence/parts-check.json +127 -0
- package/templates/example-remesh/evidence/parts-review.json +88 -0
- package/templates/example-remesh/evidence/reopen.txt +1 -0
- package/templates/example-remesh/evidence/review-reads/6a17db694db96f1b6049a61a017e0c8e66757b982b8851e0187e561cfcf9af68.json +59 -0
- package/templates/example-remesh/evidence/scene/s1.json +69 -0
- package/templates/example-remesh/evidence/scene-citations.json +9 -0
- package/templates/example-remesh/evidence/setup.png +0 -0
- package/templates/example-remesh/evidence/split-config.json +1 -0
- package/templates/example-remesh/evidence/structure.json +30 -0
- package/templates/example-remesh/evidence/v1/registration/at-source.json +44 -0
- package/templates/example-remesh/evidence/v1/registration/citations.json +68 -0
- package/templates/example-remesh/evidence/v1/registration/compile-report.json +368 -0
- package/templates/example-remesh/evidence/v1/registration/config-source.json +88 -0
- package/templates/example-remesh/evidence/v1/registration/config.json +88 -0
- package/templates/example-remesh/evidence/v1/registration/interview-requirements.json +8 -0
- package/templates/example-remesh/evidence/v1/registration/package-lock.json +11 -0
- package/templates/example-remesh/evidence/v1/registration/path-adapter.json +1 -0
- package/templates/example-remesh/evidence/v1/registration/rigc-run-1.json +44 -0
- package/templates/example-remesh/evidence/v2/registration/at-source.json +44 -0
- package/templates/example-remesh/evidence/v2/registration/citations.json +68 -0
- package/templates/example-remesh/evidence/v2/registration/compile-report.json +368 -0
- package/templates/example-remesh/evidence/v2/registration/config-source.json +88 -0
- package/templates/example-remesh/evidence/v2/registration/config.json +88 -0
- package/templates/example-remesh/evidence/v2/registration/interview-requirements.json +8 -0
- package/templates/example-remesh/evidence/v2/registration/package-lock.json +11 -0
- package/templates/example-remesh/evidence/v2/registration/path-adapter.json +1 -0
- package/templates/example-remesh/evidence/v2/registration/rigc-run-1.json +44 -0
- package/templates/example-remesh/evidence/v3/registration/at-source.json +44 -0
- package/templates/example-remesh/evidence/v3/registration/citations.json +68 -0
- package/templates/example-remesh/evidence/v3/registration/compile-report.json +368 -0
- package/templates/example-remesh/evidence/v3/registration/config-source.json +88 -0
- package/templates/example-remesh/evidence/v3/registration/config.json +88 -0
- package/templates/example-remesh/evidence/v3/registration/interview-requirements.json +8 -0
- package/templates/example-remesh/evidence/v3/registration/package-lock.json +11 -0
- package/templates/example-remesh/evidence/v3/registration/path-adapter.json +1 -0
- package/templates/example-remesh/evidence/v3/registration/rigc-run-1.json +44 -0
- package/templates/example-remesh/inputs/background.png +0 -0
- package/templates/example-remesh/inputs/painting.png +0 -0
- package/templates/example-remesh/inputs/references/.gitkeep +0 -0
- package/templates/example-remesh/package.json +9 -0
- package/templates/example-remesh/project.json +3916 -0
- package/templates/example-reopened/project.json +4568 -0
- package/templates/example-review/config.json +1 -0
- package/templates/example-review/out/check/build/skeleton.model.json +1 -0
- package/templates/example-review/out/parts/piece.svg +1 -0
- package/templates/example-review/out/parts.json +1 -0
- package/templates/example-review/out/rig/mesh_report.json +1 -0
- package/templates/example-review/out/rig/motion.json +1 -0
- package/templates/example-review/out/rig/rig.json +1 -0
- package/templates/example-split/captions.json +357 -0
- package/templates/example-split/config.json +215 -0
- package/templates/legacy-example-project.json +1071 -0
- package/templates/project.json +199 -0
- package/templates/shared/conventions.json +23 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { assertPartsCliArgs, assertPartsReadDraft, decodePartsRead, partsReadFindings, partsVerdictNames, PARTS_CLI_RECEIPT } from './parts-verdict-record.ts';
|
|
2
|
+
/** A parts verdict is recorded independently of signoff and stage progression. */
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { closeSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { readProject } from './feedback.ts';
|
|
8
|
+
import { readPartsReview, ReviewReadError, storeReviewRead, type ReviewReadRunner } from './review-read.ts';
|
|
9
|
+
import { acquireBoundJobWrite, jobRuntimeExecArgv } from './job-writer.ts';
|
|
10
|
+
import { acquireJobLock, jobHash, readAcknowledgedPublication, readWriterBinding } from './job-outbox.ts';
|
|
11
|
+
import { decodeReviewAct, reviewActFindings, reviewActTransport, reviewActVerbatim } from './review-act.ts';
|
|
12
|
+
import { assertPartsActSource, partsActDraftHash, partsActFindings, partsActReceipt } from './parts-act.ts';
|
|
13
|
+
import { newRefusals } from './refusal-baseline.ts';
|
|
14
|
+
import { refuse, USAGE_EXIT } from './refusal.ts';
|
|
15
|
+
import type { JsonObject } from './templates.ts';
|
|
16
|
+
|
|
17
|
+
export const PARTS_REVIEW_HELP = 'parts-review [<project>] --from-review <actId> [--json] | parts-review [<project>] --draft <id> --from-review-cli --by <name> [--json] -- <rig-review parts arguments>';
|
|
18
|
+
export interface PartsReviewHooks {
|
|
19
|
+
afterEvidence?: (path: string) => void;
|
|
20
|
+
/** Replaces Review's resolution and spawn; the exit code and tool identity are still checked. */
|
|
21
|
+
runCli?: ReviewReadRunner;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function syncDirectory(path: string): void {
|
|
25
|
+
const fd = openSync(path, 'r');
|
|
26
|
+
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function partsReviewCommand(args: string[], now: number, hooks: PartsReviewHooks = {}): { stdout: string; exit: number } {
|
|
30
|
+
let actId: string | undefined;
|
|
31
|
+
const options: Record<string, string> = {};
|
|
32
|
+
let json = false;
|
|
33
|
+
let fromCli = false;
|
|
34
|
+
let cliArgs: string[] = [];
|
|
35
|
+
const positional: string[] = [];
|
|
36
|
+
const usage = (): never => refuse('PARTS_REVIEW_OPTION_INVALID', `project.json.review_acts: ${PARTS_REVIEW_HELP}; submit human values through Review`, USAGE_EXIT);
|
|
37
|
+
for (let i = 0; i < args.length; i++) {
|
|
38
|
+
const arg = args[i];
|
|
39
|
+
if (arg === '--') { cliArgs = args.slice(i + 1); break; }
|
|
40
|
+
if (arg === '--from-review-cli') { if (fromCli) usage(); fromCli = true; }
|
|
41
|
+
else if (arg === '--json') { if (json) usage(); json = true; }
|
|
42
|
+
else if (arg === '--from-review') { if (actId) usage(); actId = args[++i]; if (!actId || actId.startsWith('-')) usage(); }
|
|
43
|
+
else if (['--draft', '--by'].includes(arg)) {
|
|
44
|
+
const value = args[++i];
|
|
45
|
+
if (options[arg] !== undefined || !value || value.startsWith('-')) usage();
|
|
46
|
+
options[arg] = value;
|
|
47
|
+
}
|
|
48
|
+
else if (arg.startsWith('-')) usage();
|
|
49
|
+
else positional.push(arg);
|
|
50
|
+
}
|
|
51
|
+
if (positional.length > 1 || (actId ? fromCli || Object.keys(options).length > 0 || cliArgs.length > 0 : !fromCli || Object.keys(options).length !== 2 || cliArgs.length === 0)) usage();
|
|
52
|
+
if (fromCli && cliArgs.some(arg => arg === '--format' || arg.startsWith('--format='))) usage();
|
|
53
|
+
const root = resolve(positional[0] ?? process.cwd());
|
|
54
|
+
if (fromCli) assertPartsCliArgs(cliArgs);
|
|
55
|
+
if (!actId) return recordCliVerdict(root, options, cliArgs, json, now, hooks);
|
|
56
|
+
const release = acquireBoundJobWrite(root);
|
|
57
|
+
let created: string | undefined;
|
|
58
|
+
let committed = false;
|
|
59
|
+
const temporary = join(root, `.parts-review-${randomUUID()}.tmp`);
|
|
60
|
+
try {
|
|
61
|
+
const transport = { ...reviewActTransport(root, actId!), kind: 'parts-verdict' };
|
|
62
|
+
const child = Bun.spawnSync([process.execPath, ...jobRuntimeExecArgv(), fileURLToPath(new URL('./review-act-runtime.ts', import.meta.url))], {
|
|
63
|
+
stdin: Buffer.from(JSON.stringify(transport)), stdout: 'pipe', stderr: 'pipe',
|
|
64
|
+
});
|
|
65
|
+
let response: { ok: boolean; bytes: string; code: string; message: string };
|
|
66
|
+
try { response = JSON.parse(child.stdout.toString()); }
|
|
67
|
+
catch { return refuse('REVIEW_ACT_FETCH_REFUSED', 'project.json.review_acts: fetch adapter failed; restore the live Review job'); }
|
|
68
|
+
if (!response.ok || child.exitCode) refuse(response.code, response.message);
|
|
69
|
+
const act = decodeReviewAct(response.bytes, 'parts-verdict');
|
|
70
|
+
if (Date.parse(act.at) > now) refuse('REVIEW_ACT_INVALID', 'project.json.review_acts: act is dated in the future; restore the clock before recording');
|
|
71
|
+
const { file } = readProject(root);
|
|
72
|
+
const original = readFileSync(file);
|
|
73
|
+
const project = JSON.parse(original.toString('utf8')) as JsonObject;
|
|
74
|
+
const invalid = [...reviewActFindings(project), ...partsActFindings(root, project), ...partsReadFindings(root, project)][0];
|
|
75
|
+
if (invalid) refuse(invalid.code, invalid.message);
|
|
76
|
+
assertPartsActSource(root, project, act);
|
|
77
|
+
const hash = partsActDraftHash(project, act);
|
|
78
|
+
const existing = partsActReceipt(project, act);
|
|
79
|
+
const evidence = { path: `evidence/review-acts/${jobHash(JSON.stringify([act.jobId, act.actId]))}.json`, sha256: jobHash(response.bytes) };
|
|
80
|
+
const warnings: string[] = [];
|
|
81
|
+
if (existing) {
|
|
82
|
+
if (existing.bytes !== response.bytes) refuse('REVIEW_ACT_REPLAY_CONFLICT', 'project.json.review_acts: immutable parts act changed; restore the original receipt');
|
|
83
|
+
if (existing.version_hash !== hash) refuse('REVIEW_ACT_SOURCE_STALE', 'project.json.review_acts: reviewed draft entry changed; review a new draft id');
|
|
84
|
+
} else {
|
|
85
|
+
const publication = readAcknowledgedPublication(root, act.jobId, act.publicationId);
|
|
86
|
+
if (publication && (publication.versionHash !== hash || publication.result.selector?.kind !== 'draft' || publication.result.selector.id !== act.source.selector.id)) {
|
|
87
|
+
refuse('REVIEW_ACT_SOURCE_STALE', 'project.json.review_acts: draft differs from its acknowledged publication; record and review a new id');
|
|
88
|
+
}
|
|
89
|
+
if (!publication) warnings.push('REVIEW_ACT_VERSION_UNVERIFIED: project.json.review_acts: publication receipt is absent, so the first-ingest draft-entry check was skipped; retain acknowledged receipts');
|
|
90
|
+
const directory = dirname(join(root, evidence.path));
|
|
91
|
+
for (const segment of ['evidence', 'evidence/review-acts']) {
|
|
92
|
+
const path = join(root, segment);
|
|
93
|
+
let info;
|
|
94
|
+
try { info = lstatSync(path); }
|
|
95
|
+
catch (error) {
|
|
96
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
97
|
+
mkdirSync(path);
|
|
98
|
+
syncDirectory(dirname(path));
|
|
99
|
+
info = lstatSync(path);
|
|
100
|
+
}
|
|
101
|
+
if (!info.isDirectory() || info.isSymbolicLink()) refuse('PARTS_REVIEW_EVIDENCE_CONFLICT', 'project.json.review_acts: evidence directory is unsafe; restore its original real directory');
|
|
102
|
+
}
|
|
103
|
+
const path = join(root, evidence.path);
|
|
104
|
+
let present = false;
|
|
105
|
+
try { lstatSync(path); present = true; }
|
|
106
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
|
|
107
|
+
if (present) {
|
|
108
|
+
if (!lstatSync(path).isFile() || lstatSync(path).isSymbolicLink() || !readFileSync(path).equals(Buffer.from(response.bytes))) {
|
|
109
|
+
refuse('PARTS_REVIEW_EVIDENCE_CONFLICT', `project.json.review_acts: ${evidence.path} contains another act; preserve it and restore the original evidence before retrying`);
|
|
110
|
+
}
|
|
111
|
+
const fd = openSync(path, 'r');
|
|
112
|
+
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
113
|
+
} else {
|
|
114
|
+
const fd = openSync(path, 'wx', 0o600);
|
|
115
|
+
created = path;
|
|
116
|
+
try { writeFileSync(fd, response.bytes); fsyncSync(fd); } finally { closeSync(fd); }
|
|
117
|
+
syncDirectory(directory);
|
|
118
|
+
}
|
|
119
|
+
hooks.afterEvidence?.(path);
|
|
120
|
+
const next = structuredClone(project);
|
|
121
|
+
next.review_acts = [...(project.review_acts as JsonObject[] ?? []), { schema: 'rig-create/review-act@1', kind: 'parts-verdict',
|
|
122
|
+
jobId: act.jobId, actId: act.actId, draft_id: act.source.selector.id, version_hash: hash,
|
|
123
|
+
bytes: response.bytes, sha256: evidence.sha256, evidence }];
|
|
124
|
+
const failure = [...reviewActFindings(next), ...partsActFindings(root, next), ...newRefusals(root, project, next, { now })][0];
|
|
125
|
+
if (failure) refuse(failure.code, failure.message);
|
|
126
|
+
assertPartsActSource(root, project, act);
|
|
127
|
+
if (!readFileSync(file).equals(original)) refuse('REVIEW_ACT_SOURCE_STALE', 'project.json: record changed during parts verdict validation; retry against the current job');
|
|
128
|
+
const fd = openSync(temporary, 'wx', 0o600);
|
|
129
|
+
try { writeFileSync(fd, `${JSON.stringify(next, null, 2)}\n`); fsyncSync(fd); } finally { closeSync(fd); }
|
|
130
|
+
renameSync(temporary, file);
|
|
131
|
+
committed = true;
|
|
132
|
+
syncDirectory(root);
|
|
133
|
+
}
|
|
134
|
+
const result = { draft_id: act.source.selector.id, evidence, verdict: act.value.verdict,
|
|
135
|
+
by: act.authority.reviewer.role === 'owner' ? 'user' : 'delegate', at: act.at, verbatim: reviewActVerbatim(act),
|
|
136
|
+
...(warnings.length ? { warnings } : {}) };
|
|
137
|
+
return { stdout: json ? JSON.stringify(result, null, 2) : [...warnings, `parts-review: ${result.draft_id} ${result.verdict}, evidence ${evidence.path}`].join('\n'), exit: 0 };
|
|
138
|
+
} finally {
|
|
139
|
+
if (created && !committed) rmSync(created, { force: true });
|
|
140
|
+
rmSync(temporary, { force: true });
|
|
141
|
+
release();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function recordCliVerdict(root: string, options: Record<string, string>, cliArgs: string[], json: boolean, now: number, hooks: PartsReviewHooks): { stdout: string; exit: number } {
|
|
146
|
+
if (!options['--by'].trim() || options['--by'].toLowerCase() === 'agent') refuse('SIGNOFF_NOT_HUMAN', 'project.json.review_acts.by: supply the human reviewer with --by');
|
|
147
|
+
const release = readWriterBinding(root) ? acquireBoundJobWrite(root) : acquireJobLock(root)!;
|
|
148
|
+
const temporary = join(root, `.parts-review-${randomUUID()}.tmp`);
|
|
149
|
+
let created: string | undefined;
|
|
150
|
+
let committed = false;
|
|
151
|
+
try {
|
|
152
|
+
const { file } = readProject(root);
|
|
153
|
+
const original = readFileSync(file);
|
|
154
|
+
const project = JSON.parse(original.toString('utf8')) as JsonObject;
|
|
155
|
+
const invalid = [...reviewActFindings(project), ...partsActFindings(root, project), ...partsReadFindings(root, project)][0];
|
|
156
|
+
if (invalid) refuse(invalid.code, invalid.message);
|
|
157
|
+
const draftId = options['--draft'];
|
|
158
|
+
let bytes: Buffer;
|
|
159
|
+
try { bytes = readPartsReview(root, cliArgs, hooks.runCli).stdout; }
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (!(error instanceof ReviewReadError)) throw error;
|
|
162
|
+
return refuse('PARTS_REVIEW_CLI_FAILED', `project.json.review_acts: rig-review ${error.why}; restore the pinned rig-review installation and the saved verdict, then retry its original display arguments`);
|
|
163
|
+
}
|
|
164
|
+
const verdict = decodePartsRead(bytes);
|
|
165
|
+
assertPartsReadDraft(root, project, verdict, draftId);
|
|
166
|
+
const hash = jobHash(bytes);
|
|
167
|
+
const receipts = project.review_acts as JsonObject[] | undefined;
|
|
168
|
+
const existing = receipts?.find(entry => entry.schema === PARTS_CLI_RECEIPT && entry.draft_id === draftId && (entry.evidence as JsonObject).sha256 === hash);
|
|
169
|
+
if (existing) return { stdout: json ? JSON.stringify(existing, null, 2) : `parts-review: ${draftId} ${existing.status}, already recorded`, exit: 0 };
|
|
170
|
+
const evidence = { kind: 'parts-verdict', path: `evidence/review-reads/${hash}.json`, sha256: hash };
|
|
171
|
+
const receipt: JsonObject = { schema: PARTS_CLI_RECEIPT, kind: 'parts-verdict', draft_id: draftId, evidence,
|
|
172
|
+
route: 'cli', argv: cliArgs, tool: verdict.tool, status: verdict.status, ...partsVerdictNames(verdict), by: options['--by'], at: new Date(now).toISOString() };
|
|
173
|
+
const path = join(root, evidence.path);
|
|
174
|
+
try { if (storeReviewRead(root, bytes).created) created = path; }
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (!(error instanceof ReviewReadError)) throw error;
|
|
177
|
+
refuse('PARTS_REVIEW_EVIDENCE_CONFLICT', `project.json.review_acts.evidence: ${error.why}; preserve and restore the original content-addressed verdict in a real evidence directory`);
|
|
178
|
+
}
|
|
179
|
+
hooks.afterEvidence?.(path);
|
|
180
|
+
const next = structuredClone(project);
|
|
181
|
+
next.review_acts = [...(receipts ?? []), receipt];
|
|
182
|
+
const failure = [...partsReadFindings(root, next), ...newRefusals(root, project, next, { now })][0];
|
|
183
|
+
if (failure) refuse(failure.code, failure.message);
|
|
184
|
+
if (!readFileSync(file).equals(original)) refuse('REVIEW_ACT_SOURCE_STALE', 'project.json: record changed during validation; retry against the current record');
|
|
185
|
+
const fd = openSync(temporary, 'wx', 0o600);
|
|
186
|
+
try { writeFileSync(fd, `${JSON.stringify(next, null, 2)}\n`); fsyncSync(fd); } finally { closeSync(fd); }
|
|
187
|
+
renameSync(temporary, file);
|
|
188
|
+
committed = true;
|
|
189
|
+
syncDirectory(root);
|
|
190
|
+
return { stdout: json ? JSON.stringify(receipt, null, 2) : `parts-review: ${draftId} ${receipt.status}, evidence ${evidence.path}`, exit: 0 };
|
|
191
|
+
} finally {
|
|
192
|
+
if (created && !committed) rmSync(created, { force: true });
|
|
193
|
+
rmSync(temporary, { force: true });
|
|
194
|
+
release();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The top-level sections of a `@rig-c/parts` config, taken from the installed
|
|
3
|
+
* package's own export — the pipeline's list, not a hand copy.
|
|
4
|
+
*
|
|
5
|
+
* parts exports the list as `CONFIG_SECTIONS` from `src/config.ts` (from
|
|
6
|
+
* 2.3.0; its package.json has no `exports` map, so the subpath is importable).
|
|
7
|
+
* It is loaded at run time from the parts found for the project being checked
|
|
8
|
+
* (`locateParts`), not imported from this repository's own dependency, because
|
|
9
|
+
* `interview --check` reports the sections of the version installed there.
|
|
10
|
+
* There is no static import either, not even a type-only one: it would make
|
|
11
|
+
* this repository's typecheck compile the parts and core sources, which do not
|
|
12
|
+
* typecheck outside their own repositories. A test loads the export from this
|
|
13
|
+
* repository's pinned install instead.
|
|
14
|
+
*
|
|
15
|
+
* Bun's `require` loads that ES module synchronously, so the check stays
|
|
16
|
+
* synchronous. A parts with no such export (older than 2.3.0, or a release that
|
|
17
|
+
* moves it) is `PARTS_SECTIONS_UNREADABLE`, so it fails loudly instead of
|
|
18
|
+
* silently deriving no waypoints.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync } from './check-reads.ts';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import { basename, dirname, join } from 'node:path';
|
|
23
|
+
|
|
24
|
+
export const PARTS_PACKAGE = '@rig-c/parts';
|
|
25
|
+
const CONFIG_SOURCE = join('src', 'config.ts');
|
|
26
|
+
const SECTIONS_EXPORT = 'CONFIG_SECTIONS';
|
|
27
|
+
|
|
28
|
+
/** The export's shape: section names, in parts' order. */
|
|
29
|
+
type Sections = readonly string[];
|
|
30
|
+
|
|
31
|
+
export interface PartsSections {
|
|
32
|
+
readonly version: string;
|
|
33
|
+
/** The package folder the sections were read from. */
|
|
34
|
+
readonly dir: string;
|
|
35
|
+
/** The export the list was taken from. */
|
|
36
|
+
readonly read_from: typeof SECTIONS_EXPORT;
|
|
37
|
+
readonly sections: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class PartsUnreadable extends Error {
|
|
41
|
+
readonly code: 'PARTS_NOT_INSTALLED' | 'PARTS_SECTIONS_UNREADABLE';
|
|
42
|
+
constructor(code: 'PARTS_NOT_INSTALLED' | 'PARTS_SECTIONS_UNREADABLE', message: string) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = 'PartsUnreadable';
|
|
45
|
+
this.code = code;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The create repository's own install, used when a project has none (running the check in this repo, or before `bun install` in a project). */
|
|
50
|
+
export const OWN_NODE_MODULES = join(import.meta.dir, '..', 'node_modules');
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Where the installed package is looked for, in order: the project's own
|
|
54
|
+
* install, the collection's for a member (`<collection>/members/<name>/`, which
|
|
55
|
+
* installs nothing itself: docs/COLLECTIONS.md), then create's.
|
|
56
|
+
*/
|
|
57
|
+
export function partsCandidates(projectDir: string): string[] {
|
|
58
|
+
const pkg = PARTS_PACKAGE.split('/');
|
|
59
|
+
const collection = basename(dirname(projectDir)) === 'members' ? [join(dirname(dirname(projectDir)), 'node_modules', ...pkg)] : [];
|
|
60
|
+
return [join(projectDir, 'node_modules', ...pkg), ...collection, join(OWN_NODE_MODULES, ...pkg)];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function locateParts(projectDir: string): string | null {
|
|
64
|
+
for (const dir of partsCandidates(projectDir)) {
|
|
65
|
+
if (existsSync(join(dir, 'package.json')) && existsSync(join(dir, CONFIG_SOURCE))) return dir;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const load = createRequire(import.meta.url);
|
|
71
|
+
|
|
72
|
+
function isSections(v: unknown): v is Sections {
|
|
73
|
+
return Array.isArray(v) && v.length > 0 && v.every((s) => typeof s === 'string');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function readPartsSections(projectDir: string): PartsSections {
|
|
77
|
+
const dir = locateParts(projectDir);
|
|
78
|
+
if (dir === null) {
|
|
79
|
+
throw new PartsUnreadable(
|
|
80
|
+
'PARTS_NOT_INSTALLED',
|
|
81
|
+
`${PARTS_PACKAGE} is not installed (looked in ${partsCandidates(projectDir).join(', ')}); run bun install in the project`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const pkg: unknown = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
85
|
+
const version =
|
|
86
|
+
pkg !== null && typeof pkg === 'object' && 'version' in pkg && typeof pkg.version === 'string' ? pkg.version : 'unknown';
|
|
87
|
+
const unreadable = (why: string): PartsUnreadable =>
|
|
88
|
+
new PartsUnreadable('PARTS_SECTIONS_UNREADABLE', `${PARTS_PACKAGE} ${version} at ${dir}: ${why}; the section list cannot be derived (it is exported from 2.3.0)`);
|
|
89
|
+
let mod: unknown;
|
|
90
|
+
try {
|
|
91
|
+
mod = load(join(dir, CONFIG_SOURCE));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
throw unreadable(`${CONFIG_SOURCE} does not load (${e instanceof Error ? e.message : String(e)})`);
|
|
94
|
+
}
|
|
95
|
+
const sections = mod !== null && typeof mod === 'object' && SECTIONS_EXPORT in mod ? (mod as Record<string, unknown>)[SECTIONS_EXPORT] : undefined;
|
|
96
|
+
if (!isSections(sections)) throw unreadable(`${CONFIG_SOURCE} has no ${SECTIONS_EXPORT} export holding section names`);
|
|
97
|
+
return { version, dir, read_from: SECTIONS_EXPORT, sections: [...sections] };
|
|
98
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { draftFileUnavailable, draftInputUnavailable } from './drafts.ts';
|
|
2
|
+
import { checkPartsActEvidence, partsActDraftHash, partsActFindings } from './parts-act.ts';
|
|
3
|
+
import { realpathSync } from './check-reads.ts';
|
|
4
|
+
import { isAbsolute, posix, resolve } from 'node:path';
|
|
5
|
+
import { comparePartsVerdictBindings, type FileBinding } from './parts-verdict.ts';
|
|
6
|
+
/** Review CLI stdout is retained separately from Review's private saved verdict. */
|
|
7
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
8
|
+
import { publicationFile } from './job-publication.ts';
|
|
9
|
+
import { jobHash } from './job-outbox.ts';
|
|
10
|
+
import { decodeReviewAct, reviewActFindings } from './review-act.ts';
|
|
11
|
+
import { Refusal, refuse } from './refusal.ts';
|
|
12
|
+
import { ReviewReadError, storedReviewRead } from './review-read.ts';
|
|
13
|
+
import type { Finding } from './check.ts';
|
|
14
|
+
import type { JsonObject } from './templates.ts';
|
|
15
|
+
import { PARTS_COMMENTS_VERDICT_SCHEMA, parseCommentsPartsVerdict } from '@rig-c/review/job-host';
|
|
16
|
+
|
|
17
|
+
export const PARTS_CLI_RECEIPT = 'rig-create/parts-verdict-cli@1';
|
|
18
|
+
export const PARTS_VERDICT_LATEST_SINCE = '2026-11-08T00:00:00Z';
|
|
19
|
+
const object = (value: unknown): value is JsonObject => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
20
|
+
const sha = (value: unknown): value is string => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
21
|
+
const fail = (code: string, detail: string): never => refuse(code, `project.json.review_acts: ${detail}; query the labelled draft with rig-review parts --format json and record its response again`);
|
|
22
|
+
|
|
23
|
+
export function decodePartsRead(bytes: Buffer): JsonObject {
|
|
24
|
+
let value: unknown;
|
|
25
|
+
try { value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)); }
|
|
26
|
+
catch { return fail('PARTS_REVIEW_CLI_INVALID', 'CLI stdout must be a UTF-8 JSON object'); }
|
|
27
|
+
if (object(value) && value.schema === PARTS_COMMENTS_VERDICT_SCHEMA) {
|
|
28
|
+
try { return parseCommentsPartsVerdict(value) as unknown as JsonObject; }
|
|
29
|
+
catch { return fail('PARTS_REVIEW_CLI_INVALID', 'comments verdict or its declared parts binding is malformed'); }
|
|
30
|
+
}
|
|
31
|
+
if (!object(value) || !['rig-review-parts-verdict/1', 'rig-review-parts-verdict/2', 'rig-review-parts-verdict/4'].includes(String(value.schema))
|
|
32
|
+
|| !['approved', 'changes-requested', 'pending'].includes(String(value.status))
|
|
33
|
+
|| !object(value.tool) || typeof value.tool.name !== 'string' || !value.tool.name || typeof value.tool.version !== 'string' || !value.tool.version
|
|
34
|
+
|| !object(value.source) || !sha(value.source.sha256) || !Array.isArray(value.source.files) || !value.source.files.length
|
|
35
|
+
|| !value.source.files.every(file => object(file) && sha(file.sha256))) {
|
|
36
|
+
return fail('PARTS_REVIEW_CLI_INVALID', 'CLI response needs a supported schema, status, tool, source hashes and per-part outcomes');
|
|
37
|
+
}
|
|
38
|
+
if (value.schema === 'rig-review-parts-verdict/4') {
|
|
39
|
+
if (typeof value.mode !== 'string' || !value.mode || typeof value.updatedAt !== 'string' || !value.updatedAt
|
|
40
|
+
|| !['approved', 'changes-requested'].includes(String(value.status)) || !Array.isArray(value.issues)
|
|
41
|
+
|| (value.status === 'approved') !== (value.issues.length === 0)) {
|
|
42
|
+
return fail('PARTS_REVIEW_CLI_INVALID', 'CLI response needs a supported schema, status, tool, source hashes and issues');
|
|
43
|
+
}
|
|
44
|
+
for (const issue of value.issues) {
|
|
45
|
+
if (!object(issue) || typeof issue.id !== 'string' || !issue.id || typeof issue.summary !== 'string' || !issue.summary.trim()
|
|
46
|
+
|| !Array.isArray(issue.parts) || !issue.parts.length
|
|
47
|
+
|| !issue.parts.every(part => object(part) && typeof part.name === 'string' && part.name.length > 0 && sha(part.imageHash))
|
|
48
|
+
|| new Set(issue.parts.map(part => (part as JsonObject).name)).size !== issue.parts.length) {
|
|
49
|
+
return fail('PARTS_REVIEW_CLI_INVALID', 'each CLI issue needs id, summary and unique part bindings');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
if (!object(value.parts)) {
|
|
55
|
+
return fail('PARTS_REVIEW_CLI_INVALID', 'CLI response needs a supported schema, status, tool, source hashes and per-part outcomes');
|
|
56
|
+
}
|
|
57
|
+
for (const part of Object.values(value.parts)) {
|
|
58
|
+
if (!object(part) || !['clean', 'flagged', 'pending'].includes(String(part.outcome)) || typeof part.note !== 'string') {
|
|
59
|
+
return fail('PARTS_REVIEW_CLI_INVALID', 'each CLI part needs its outcome and note');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Bind a parts receipt's stdout through the shared stored-read validator, keeping this route's codes. */
|
|
66
|
+
function partsStdout(root: string, evidence: JsonObject, path: string): Buffer {
|
|
67
|
+
try { return storedReviewRead(root, evidence); }
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (!(error instanceof ReviewReadError)) throw error;
|
|
70
|
+
if (error.reason === 'unavailable') return refuse('JOB_PUBLICATION_INVALID', `project.json: ${String(evidence.path)} must be a regular source without symlinks; restore its retained bytes`);
|
|
71
|
+
return fail('PARTS_VERDICT_RECEIPT_INVALID', `${path}.evidence ${error.reason === 'changed' ? 'bytes changed' : error.why}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function partsVerdictNames(verdict: JsonObject): { flagged: string[]; noted: string[] } {
|
|
76
|
+
if (verdict.schema === PARTS_COMMENTS_VERDICT_SCHEMA) {
|
|
77
|
+
const parsed = parseCommentsPartsVerdict(verdict);
|
|
78
|
+
const names = [...new Set(parsed.comments.flatMap(comment => comment.anchor && 'schema' in comment.anchor
|
|
79
|
+
? comment.anchor.parts.map(part => part.name) : []))];
|
|
80
|
+
return { flagged: names, noted: [...names] };
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(verdict.issues)) {
|
|
83
|
+
const names = [...new Set(verdict.issues.flatMap(issue => object(issue) && Array.isArray(issue.parts)
|
|
84
|
+
? issue.parts.filter(object).map(part => String(part.name)) : []))];
|
|
85
|
+
return { flagged: names, noted: [...names] };
|
|
86
|
+
}
|
|
87
|
+
const entries = Object.entries(verdict.parts as JsonObject);
|
|
88
|
+
return {
|
|
89
|
+
flagged: entries.filter(([, part]) => (part as JsonObject).outcome === 'flagged').map(([name]) => name),
|
|
90
|
+
noted: entries.filter(([, part]) => (part as JsonObject).note !== '').map(([name]) => name),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Compare only recorded hashes. Output paths remain in Review's own frame. */
|
|
95
|
+
export function assertPartsReadDraft(root: string, project: JsonObject, verdict: JsonObject, draftId: string, options: { requireAvailable?: boolean } = {}): void {
|
|
96
|
+
const drafts = Array.isArray(project.drafts) ? project.drafts.filter(d => object(d) && d.id === draftId) : [];
|
|
97
|
+
const draft = drafts[0];
|
|
98
|
+
if (drafts.length !== 1 || !object(draft) || !object(draft.parts) || !Array.isArray(draft.parts.files)
|
|
99
|
+
|| !draft.parts.files.length || !draft.parts.files.every(file => object(file) && sha(file.sha256))) {
|
|
100
|
+
return fail('PARTS_VERDICT_RECEIPT_INVALID', `draft ${draftId} needs unique recorded parts file hashes`);
|
|
101
|
+
}
|
|
102
|
+
const parts = draft.parts;
|
|
103
|
+
const recorded = parts.files as FileBinding[];
|
|
104
|
+
const manifest = recorded.find(file => file.path === `${parts.path}/parts.json`);
|
|
105
|
+
if (!manifest) return fail('PARTS_VERDICT_RECEIPT_INVALID', `draft ${draftId} has no bound parts.json`);
|
|
106
|
+
if (draftFileUnavailable(root, manifest.path, `derived/drafts/${draftId}`)) {
|
|
107
|
+
const source = (verdict.source as JsonObject).files as JsonObject[];
|
|
108
|
+
if (options.requireAvailable === false && source.some(file => file.sha256 === manifest.sha256)) return;
|
|
109
|
+
return fail('PARTS_VERDICT_RECEIPT_INVALID', `draft ${draftId}'s parts.json is unavailable; restore its recorded bytes`);
|
|
110
|
+
}
|
|
111
|
+
const bytes = publicationFile(realpathSync(root), manifest.path);
|
|
112
|
+
if (jobHash(bytes) !== manifest.sha256) return fail('PARTS_VERDICT_RECEIPT_INVALID', `draft ${draftId}'s parts.json differs from its recorded hash`);
|
|
113
|
+
const declared: unknown = JSON.parse(bytes.toString('utf8'));
|
|
114
|
+
const painting = object(declared) && object(declared.painting_in_rig) ? declared.painting_in_rig.path : undefined;
|
|
115
|
+
const paintingFile = typeof painting === 'string' ? recorded.find(file => file.path === posix.join(String(parts.path), painting)) : undefined;
|
|
116
|
+
if (painting !== undefined && !paintingFile) return fail('PARTS_VERDICT_RECEIPT_INVALID', `draft ${draftId}'s declared painting has no recorded hash`);
|
|
117
|
+
const keep = (file: FileBinding): boolean => paintingFile === undefined || file.sha256 !== paintingFile.sha256;
|
|
118
|
+
const reviewed = ((verdict.source as JsonObject).files as JsonObject[]).map((file, index) => ({ path: `source.files[${index}]`, sha256: String(file.sha256) }));
|
|
119
|
+
const outputs = Array.isArray(draft.files) ? draft.files as FileBinding[] : [];
|
|
120
|
+
const reviewedHashes = new Set(reviewed.map(file => file.sha256));
|
|
121
|
+
for (const file of [...recorded, ...outputs.filter(file => reviewedHashes.has(file.sha256))]) {
|
|
122
|
+
if (options.requireAvailable === false && draftFileUnavailable(root, file.path, `derived/drafts/${draftId}`)) continue;
|
|
123
|
+
if (jobHash(publicationFile(realpathSync(root), file.path)) !== file.sha256) {
|
|
124
|
+
fail('PARTS_VERDICT_RECEIPT_INVALID', `draft ${draftId}'s reviewed file ${file.path} differs from its recorded hash`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const { verdictOnly, draftOnly } = comparePartsVerdictBindings(reviewed.filter(keep), recorded.filter(keep), outputs);
|
|
128
|
+
if (verdictOnly.length || draftOnly.length) fail('PARTS_VERDICT_DRAFT_MISMATCH', `CLI source hashes do not match draft ${draftId}: ${verdictOnly.length} unbound source file(s), ${draftOnly.length} missing part file(s)`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function partsReadFindings(root: string, project: JsonObject): Finding[] {
|
|
132
|
+
if (!Array.isArray(project.review_acts)) return [];
|
|
133
|
+
const seen = new Set<string>();
|
|
134
|
+
return project.review_acts.flatMap((entry, index) => {
|
|
135
|
+
if (!object(entry) || entry.schema !== PARTS_CLI_RECEIPT) return [];
|
|
136
|
+
const path = `review_acts[${index}]`;
|
|
137
|
+
try {
|
|
138
|
+
if (entry.kind !== 'parts-verdict' || entry.route !== 'cli' || typeof entry.draft_id !== 'string'
|
|
139
|
+
|| typeof entry.by !== 'string' || !entry.by.trim() || entry.by.toLowerCase() === 'agent'
|
|
140
|
+
|| typeof entry.at !== 'string' || !Number.isFinite(Date.parse(entry.at))
|
|
141
|
+
|| !Array.isArray(entry.argv) || !entry.argv.length || !entry.argv.every(arg => typeof arg === 'string')
|
|
142
|
+
|| !object(entry.evidence) || !sha(entry.evidence.sha256)
|
|
143
|
+
|| entry.evidence.path !== `evidence/review-reads/${entry.evidence.sha256}.json`) {
|
|
144
|
+
fail('PARTS_VERDICT_RECEIPT_INVALID', `${path} has invalid CLI receipt metadata`);
|
|
145
|
+
}
|
|
146
|
+
assertPartsCliArgs(entry.argv as string[]);
|
|
147
|
+
const evidence = entry.evidence as JsonObject;
|
|
148
|
+
const verdict = decodePartsRead(partsStdout(root, evidence, path));
|
|
149
|
+
const names = partsVerdictNames(verdict);
|
|
150
|
+
if (entry.status !== verdict.status || !isDeepStrictEqual(entry.tool, verdict.tool)
|
|
151
|
+
|| !isDeepStrictEqual(entry.flagged, names.flagged) || !isDeepStrictEqual(entry.noted, names.noted)) {
|
|
152
|
+
fail('PARTS_VERDICT_RECEIPT_INVALID', `${path} differs from its retained CLI response`);
|
|
153
|
+
}
|
|
154
|
+
assertPartsReadDraft(root, project, verdict, String(entry.draft_id), { requireAvailable: false });
|
|
155
|
+
const key = JSON.stringify([entry.draft_id, evidence.sha256]);
|
|
156
|
+
if (seen.has(key)) fail('PARTS_VERDICT_RECEIPT_INVALID', `${path} repeats an earlier CLI receipt`);
|
|
157
|
+
seen.add(key);
|
|
158
|
+
return [];
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return [{ code: error instanceof Refusal ? error.code : 'PARTS_VERDICT_RECEIPT_INVALID', path,
|
|
161
|
+
message: error instanceof Refusal ? error.message : `${path}: retained CLI response is unavailable; restore its exact bytes` }];
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Both routes append to one ledger, so its order also determines the current verdict. */
|
|
167
|
+
export function latestPartsVerdictFinding(project: JsonObject, draftId: string, path: string): Finding | undefined {
|
|
168
|
+
const entry = latestPartsReceipt(project, draftId);
|
|
169
|
+
if (!object(entry)) return;
|
|
170
|
+
let status = entry.status;
|
|
171
|
+
if (entry.schema !== PARTS_CLI_RECEIPT) {
|
|
172
|
+
try { status = decodeReviewAct(String(entry.bytes), 'parts-verdict').value.verdict; }
|
|
173
|
+
catch { return; } // The act reader reports malformed receipts.
|
|
174
|
+
}
|
|
175
|
+
if (status === 'changes-requested') return { code: 'PARTS_VERDICT_LATEST_NOT_APPROVED', path,
|
|
176
|
+
message: `${path}: draft ${draftId}'s latest parts verdict requests changes; record a newer approved verdict with parts-review before signing` };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function assertPartsCliArgs(args: string[]): void {
|
|
180
|
+
for (const arg of args) {
|
|
181
|
+
const value = arg.startsWith('--') && arg.includes('=') ? arg.slice(arg.indexOf('=') + 1) : arg;
|
|
182
|
+
const normalized = posix.normalize(value.replaceAll('\\', '/'));
|
|
183
|
+
if (arg === '--' || arg === '--format' || arg.startsWith('--format=') || isAbsolute(value) || /^[A-Za-z]:/.test(value)
|
|
184
|
+
|| normalized === '..' || normalized.startsWith('../') || normalized.startsWith('/')) {
|
|
185
|
+
refuse('PARTS_REVIEW_CLI_ARGUMENT_INVALID', 'project.json.review_acts.argv: pass project-relative CLI arguments without escapes and let create supply --format json');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Legacy file bindings remain historical records without opening Review's saved files. */
|
|
191
|
+
export const PARTS_VERDICT_RECEIPT_SINCE = '2026-11-08T00:00:00Z';
|
|
192
|
+
|
|
193
|
+
export function latestPartsReceipt(project: JsonObject, draftId: string): JsonObject | undefined {
|
|
194
|
+
return Array.isArray(project.review_acts) ? [...project.review_acts].reverse().find(
|
|
195
|
+
entry => object(entry) && entry.kind === 'parts-verdict' && entry.draft_id === draftId,
|
|
196
|
+
) as JsonObject | undefined : undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Shared by both writers and check. Reads only the response retained by parts-review. */
|
|
200
|
+
export function recordedPartsVerdict(root: string, project: JsonObject, draftId: unknown, evidence?: unknown, options: { requireAvailable?: boolean } = {}): { evidence: JsonObject; partsFile: string } {
|
|
201
|
+
if (typeof draftId !== 'string' || !draftId) return fail('PARTS_VERDICT_RECEIPT_REQUIRED', 'signoffs.parts-review.draft_id needs a recorded draft id');
|
|
202
|
+
const entries = Array.isArray(project.review_acts) ? project.review_acts : [];
|
|
203
|
+
const receipt = evidence === undefined ? latestPartsReceipt(project, draftId) : entries.find(entry => object(entry)
|
|
204
|
+
&& entry.kind === 'parts-verdict' && entry.draft_id === draftId && object(entry.evidence) && object(evidence)
|
|
205
|
+
&& entry.evidence.path === evidence.path && entry.evidence.sha256 === evidence.sha256);
|
|
206
|
+
if (!object(receipt) || !object(receipt.evidence)) {
|
|
207
|
+
const boundPath = object(evidence) && entries.some(entry => object(entry) && entry.kind === 'parts-verdict'
|
|
208
|
+
&& entry.draft_id === draftId && object(entry.evidence) && entry.evidence.path === evidence.path);
|
|
209
|
+
if (object(evidence) && (evidence.spec !== undefined || boundPath)) {
|
|
210
|
+
return fail('PARTS_VERDICT_RECEIPT_INVALID', `signoffs.parts-review.evidence differs from its recorded receipt for draft ${draftId}`);
|
|
211
|
+
}
|
|
212
|
+
return fail('PARTS_VERDICT_RECEIPT_REQUIRED', `signoffs.parts-review.evidence needs a parts-review receipt for draft ${draftId}`);
|
|
213
|
+
}
|
|
214
|
+
const invalid = [...reviewActFindings(project), ...partsReadFindings(root, project), ...partsActFindings(root, project)][0];
|
|
215
|
+
if (invalid) refuse(invalid.code, invalid.message);
|
|
216
|
+
const binding = receipt.evidence;
|
|
217
|
+
const bytes = receipt.schema === PARTS_CLI_RECEIPT ? partsStdout(root, binding, 'signoffs.parts-review')
|
|
218
|
+
: publicationFile(realpathSync(root), String(binding.path));
|
|
219
|
+
let partsFile: string;
|
|
220
|
+
let spec: string;
|
|
221
|
+
if (receipt.schema === PARTS_CLI_RECEIPT) {
|
|
222
|
+
spec = PARTS_CLI_RECEIPT;
|
|
223
|
+
if (evidence !== undefined && (!object(evidence) || evidence.kind !== 'parts-verdict'
|
|
224
|
+
|| (evidence.spec !== undefined && evidence.spec !== spec))) return fail('PARTS_VERDICT_RECEIPT_INVALID', 'signoffs.parts-review.evidence has an invalid receipt spec');
|
|
225
|
+
const verdict = decodePartsRead(bytes);
|
|
226
|
+
assertPartsReadDraft(root, project, verdict, draftId, options);
|
|
227
|
+
if (verdict.status !== 'approved') return fail('PARTS_VERDICT_NOT_APPROVED', `signoffs.parts-review.evidence for draft ${draftId} is not approved`);
|
|
228
|
+
const draft = (project.drafts as JsonObject[]).find(draft => draft.id === draftId)!;
|
|
229
|
+
partsFile = resolve(root, String((draft.parts as JsonObject).path), 'parts.json');
|
|
230
|
+
} else {
|
|
231
|
+
if (receipt.schema !== 'rig-create/review-act@1') return fail('PARTS_VERDICT_RECEIPT_INVALID', 'signoffs.parts-review.evidence names an unsupported receipt spec');
|
|
232
|
+
const act = decodeReviewAct(bytes.toString('utf8'), 'parts-verdict');
|
|
233
|
+
spec = act.schema;
|
|
234
|
+
if (evidence !== undefined && (!object(evidence) || evidence.kind !== 'parts-verdict'
|
|
235
|
+
|| (evidence.spec !== undefined && evidence.spec !== spec))) return fail('PARTS_VERDICT_RECEIPT_INVALID', 'signoffs.parts-review.evidence has an invalid receipt spec');
|
|
236
|
+
if (act.source.selector.id !== draftId) return fail('PARTS_VERDICT_DRAFT_MISMATCH', `signoffs.parts-review.draft_id differs from the retained act for ${draftId}`);
|
|
237
|
+
const draft = (project.drafts as JsonObject[]).find(draft => draft.id === draftId);
|
|
238
|
+
const draftRoot = `derived/drafts/${draftId}`;
|
|
239
|
+
const bound = object(draft) && object(draft.parts) ? [draft.config, ...(draft.parts.files as JsonObject[]), ...(draft.files as JsonObject[])] : [];
|
|
240
|
+
const inputs = object(draft) && Array.isArray(draft.config_inputs) ? draft.config_inputs as JsonObject[] : [];
|
|
241
|
+
const unavailable = options.requireAvailable === false && (bound.some(file => object(file)
|
|
242
|
+
&& typeof file.path === 'string' && draftFileUnavailable(root, file.path, draftRoot))
|
|
243
|
+
|| inputs.some(file => object(file) && typeof file.path === 'string' && draftInputUnavailable(root, file.path, draftRoot)));
|
|
244
|
+
if (unavailable) {
|
|
245
|
+
if (partsActDraftHash(project, act) !== receipt.version_hash) return fail('PARTS_VERDICT_RECEIPT_INVALID', `draft ${draftId}'s recorded identity changed`);
|
|
246
|
+
if (act.value.verdict !== 'approved') return fail('PARTS_VERDICT_NOT_APPROVED', `signoffs.parts-review.evidence for draft ${draftId} is not approved`);
|
|
247
|
+
partsFile = resolve(root, act.source.parts.manifest.path);
|
|
248
|
+
} else partsFile = checkPartsActEvidence(root, bytes, String(binding.sha256), project);
|
|
249
|
+
}
|
|
250
|
+
return { evidence: { kind: 'parts-verdict', path: binding.path, sha256: binding.sha256, spec }, partsFile };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function recordedPartsFinding(root: string, project: JsonObject, signoff: JsonObject, path: string): Finding | undefined {
|
|
254
|
+
try {
|
|
255
|
+
if (!object(signoff.evidence)) fail('PARTS_VERDICT_RECEIPT_REQUIRED', 'signoffs.parts-review.evidence needs a recorded API receipt');
|
|
256
|
+
recordedPartsVerdict(root, project, signoff.draft_id, signoff.evidence, { requireAvailable: false });
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
const legacy = error instanceof Refusal && error.code === 'PARTS_VERDICT_RECEIPT_REQUIRED'
|
|
260
|
+
&& typeof signoff.at === 'string' && Date.parse(signoff.at) < Date.parse(PARTS_VERDICT_RECEIPT_SINCE);
|
|
261
|
+
return { code: legacy ? 'PARTS_VERDICT_FILE_LEGACY' : error instanceof Refusal ? error.code : 'PARTS_VERDICT_RECEIPT_INVALID', path: `${path}.evidence`,
|
|
262
|
+
message: legacy ? `${path}.evidence: legacy review-saved file binding is preserved; query the reviewed draft through parts-review and record a new signature`
|
|
263
|
+
: error instanceof Refusal ? error.message : `${path}.evidence: retained API response is unavailable; restore the parts-review receipt and its response` };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Hash-list comparison for retained parts-review API responses. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
export const PARTS_VERDICT_SCHEMA = 'rig-review-parts-verdict/1';
|
|
5
|
+
export const PARTS_VERDICT_SCHEMA_V2 = 'rig-review-parts-verdict/2';
|
|
6
|
+
export const PARTS_VERDICT_KIND = 'parts-verdict';
|
|
7
|
+
export const PARTS_VERDICT_DRAFT_SINCE = '2026-11-08T00:00:00Z';
|
|
8
|
+
|
|
9
|
+
export type FileBinding = { path: string; sha256: string };
|
|
10
|
+
|
|
11
|
+
/** Synthetic producer fixtures use Review's canonical source-list hash. */
|
|
12
|
+
export function sourceSha256(files: readonly { path: string; sha256: string }[]): string {
|
|
13
|
+
const sorted = [...files].sort((a, b) => Buffer.compare(Buffer.from(a.path, 'utf8'), Buffer.from(b.path, 'utf8')));
|
|
14
|
+
const hash = createHash('sha256');
|
|
15
|
+
for (const f of sorted) hash.update(`${f.path}\t${f.sha256}\n`, 'utf8');
|
|
16
|
+
return hash.digest('hex');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function unmatched(side: FileBinding[], other: FileBinding[]): string[] {
|
|
20
|
+
const counts = new Map<string, number>();
|
|
21
|
+
for (const f of other) counts.set(f.sha256, (counts.get(f.sha256) ?? 0) + 1);
|
|
22
|
+
const out: string[] = [];
|
|
23
|
+
for (const f of side) {
|
|
24
|
+
const n = counts.get(f.sha256) ?? 0;
|
|
25
|
+
if (n > 0) counts.set(f.sha256, n - 1);
|
|
26
|
+
else out.push(f.path);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function comparePartsVerdictBindings(reviewed: FileBinding[], parts: FileBinding[], outputs: FileBinding[]): { verdictOnly: string[]; draftOnly: string[] } {
|
|
32
|
+
return { verdictOnly: unmatched(reviewed, [...parts, ...outputs]), draftOnly: unmatched(parts, reviewed) };
|
|
33
|
+
}
|