@rig-c/create 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +169 -2
- package/docs/CHECKS.md +1682 -0
- package/docs/COLLECTIONS.md +337 -0
- package/docs/CRAFT.md +277 -0
- package/docs/INTERVIEW.md +886 -0
- package/docs/LESSONS.md +172 -0
- package/docs/PROJECT.md +2657 -0
- package/docs/SOURCE.md +225 -0
- package/package.json +44 -4
- package/src/agent-bounce.ts +27 -0
- package/src/agent-guide.ts +237 -0
- package/src/amend.ts +193 -0
- package/src/applied-review.ts +104 -0
- package/src/ask-coverage.ts +69 -0
- package/src/assemble-answers.ts +53 -0
- package/src/cg-gates.ts +211 -0
- package/src/check-reads.ts +106 -0
- package/src/check.ts +1059 -0
- package/src/checkpoint-evidence-writer.ts +125 -0
- package/src/checkpoint-evidence.ts +152 -0
- package/src/choice-writers.ts +271 -0
- package/src/choices.ts +378 -0
- package/src/cli.ts +371 -0
- package/src/collection-answer.ts +136 -0
- package/src/collection-binding-reader.ts +496 -0
- package/src/collection-binding.ts +478 -0
- package/src/collection.ts +348 -0
- package/src/config-inputs.ts +155 -0
- package/src/config-lost.ts +135 -0
- package/src/correction-writers.ts +207 -0
- package/src/corrections.ts +685 -0
- package/src/diff-classes.ts +219 -0
- package/src/diff-map.ts +248 -0
- package/src/direction-citation.ts +13 -0
- package/src/direction-writers.ts +119 -0
- package/src/direction.ts +266 -0
- package/src/drafts.ts +363 -0
- package/src/feedback.ts +744 -0
- package/src/figure-mask.ts +143 -0
- package/src/fix-ledger.ts +355 -0
- package/src/init.ts +265 -0
- package/src/inputs.ts +160 -0
- package/src/intake.ts +158 -0
- package/src/interview.ts +53 -0
- package/src/invariants.ts +132 -0
- package/src/job-acts.ts +80 -0
- package/src/job-advance.ts +65 -0
- package/src/job-close.ts +226 -0
- package/src/job-host.ts +529 -0
- package/src/job-outbox.ts +383 -0
- package/src/job-publication.ts +701 -0
- package/src/job-runtime.ts +99 -0
- package/src/job-writer.ts +123 -0
- package/src/meshes-bust.ts +63 -0
- package/src/next.ts +1506 -0
- package/src/overrides.ts +42 -0
- package/src/package-upgrades.ts +32 -0
- package/src/packages-upgrade-check.ts +6 -0
- package/src/packages-upgrade.ts +168 -0
- package/src/parts-act.ts +96 -0
- package/src/parts-check.ts +242 -0
- package/src/parts-review.ts +196 -0
- package/src/parts-sections.ts +98 -0
- package/src/parts-verdict-record.ts +265 -0
- package/src/parts-verdict.ts +33 -0
- package/src/pause.ts +118 -0
- package/src/pins-check.ts +619 -0
- package/src/pins.ts +17 -0
- package/src/plan-bar-amendments.ts +54 -0
- package/src/plan-history.ts +147 -0
- package/src/plan-writers.ts +202 -0
- package/src/plate-receipt.ts +110 -0
- package/src/pre-interview.ts +302 -0
- package/src/prepared-inputs.ts +113 -0
- package/src/progress-writers.ts +181 -0
- package/src/provenance.ts +301 -0
- package/src/record-direction-target.ts +26 -0
- package/src/record-input.ts +18 -0
- package/src/refusal-baseline.ts +55 -0
- package/src/refusal.ts +30 -0
- package/src/reopen-lapses.ts +80 -0
- package/src/reopen.ts +144 -0
- package/src/republish.ts +93 -0
- package/src/restart.ts +141 -0
- package/src/review-act-runtime.ts +16 -0
- package/src/review-act.ts +592 -0
- package/src/review-package.ts +524 -0
- package/src/review-read.ts +238 -0
- package/src/review.ts +432 -0
- package/src/rig-version-date.ts +22 -0
- package/src/rig-version.ts +1047 -0
- package/src/run-record.ts +232 -0
- package/src/scene-authority.ts +410 -0
- package/src/scene-writers.ts +144 -0
- package/src/scene.ts +284 -0
- package/src/setup-painting.ts +46 -0
- package/src/signable.ts +65 -0
- package/src/signoff-answers.ts +76 -0
- package/src/signoff-checkpoint-act.ts +95 -0
- package/src/signoff-target.ts +127 -0
- package/src/signoff.ts +331 -0
- package/src/split-captions.ts +154 -0
- package/src/stage-progress.ts +379 -0
- package/src/stages.ts +148 -0
- package/src/step-hash.ts +19 -0
- package/src/structure-snapshot.ts +109 -0
- package/src/structure.ts +166 -0
- package/src/templates.ts +16 -0
- package/src/waypoints.ts +163 -0
- package/templates/agent-guide.md +108 -0
- package/templates/collection-agent-guide.md +72 -0
- package/templates/collection.json +252 -0
- package/templates/example-chain/AGENTS.md +134 -0
- package/templates/example-chain/CLAUDE.md +3 -0
- package/templates/example-chain/README.md +13 -0
- package/templates/example-chain/chain.json +2869 -0
- package/templates/example-chain/checkpoints/0a-interview-options/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/0a-interview-options/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/0a-interview-options/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/0a-interview-options/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/0a-interview-options/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/0a-interview-options/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/0a-interview-options/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/0a-interview-options/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/0a-interview-options/package.json +9 -0
- package/templates/example-chain/checkpoints/0a-interview-options/project.json +295 -0
- package/templates/example-chain/checkpoints/0a-interview-options.json +127 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/package.json +9 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/project.json +685 -0
- package/templates/example-chain/checkpoints/0b-plan-pending.json +81 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/parts-check.json +127 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/package.json +9 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/project.json +955 -0
- package/templates/example-chain/checkpoints/1-parts-verdict.json +204 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/diff-components.json +34 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/bones.json +190 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/parts-check.json +127 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/parts-review.json +88 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/structure.json +30 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/package.json +9 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/project.json +1198 -0
- package/templates/example-chain/checkpoints/2-rigging-answers.json +254 -0
- package/templates/example-chain/checkpoints/3-rig-review/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/3-rig-review/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/diff-components.json +34 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/bones.json +190 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/constraints.json +272 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/meshes.json +190 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/motion.json +272 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/parts-check.json +127 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/parts-review.json +88 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/setup.png +0 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/structure.json +30 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/v1/config.json +88 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/v1/interview-requirements.json +8 -0
- package/templates/example-chain/checkpoints/3-rig-review/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/3-rig-review/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/3-rig-review/package.json +9 -0
- package/templates/example-chain/checkpoints/3-rig-review/project.json +1909 -0
- package/templates/example-chain/checkpoints/3-rig-review.json +424 -0
- package/templates/example-chain/checkpoints/4-direction-post/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/4-direction-post/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/diff-components.json +34 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/bones.json +190 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/camera_depth.json +102 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/constraints.json +272 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/meshes.json +190 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/motion.json +272 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/post_composition.json +122 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/post_timing.json +102 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/parts-check.json +127 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/parts-review.json +88 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/setup.png +0 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/structure.json +30 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/v1/config.json +88 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/v1/interview-requirements.json +8 -0
- package/templates/example-chain/checkpoints/4-direction-post/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/4-direction-post/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/4-direction-post/package.json +9 -0
- package/templates/example-chain/checkpoints/4-direction-post/project.json +2302 -0
- package/templates/example-chain/checkpoints/4-direction-post.json +456 -0
- package/templates/example-chain/evidence/.gitkeep +0 -0
- package/templates/example-chain/evidence/approval.txt +1 -0
- package/templates/example-chain/evidence/captions.json +101 -0
- package/templates/example-chain/evidence/diff-components.json +34 -0
- package/templates/example-chain/evidence/direction/v2.json +74 -0
- package/templates/example-chain/evidence/direction-input.json +74 -0
- package/templates/example-chain/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/evidence/interview/bones.json +190 -0
- package/templates/example-chain/evidence/interview/camera_depth.json +102 -0
- package/templates/example-chain/evidence/interview/constraints.json +272 -0
- package/templates/example-chain/evidence/interview/meshes.json +190 -0
- package/templates/example-chain/evidence/interview/motion.json +272 -0
- package/templates/example-chain/evidence/interview/post_composition.json +122 -0
- package/templates/example-chain/evidence/interview/post_timing.json +102 -0
- package/templates/example-chain/evidence/interview/reference.json +59 -0
- package/templates/example-chain/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/evidence/parts-check.json +127 -0
- package/templates/example-chain/evidence/parts-review.json +88 -0
- package/templates/example-chain/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
- package/templates/example-chain/evidence/scene/s1.json +65 -0
- package/templates/example-chain/evidence/scene-citations.json +9 -0
- package/templates/example-chain/evidence/setup.png +0 -0
- package/templates/example-chain/evidence/split-config.json +1 -0
- package/templates/example-chain/evidence/structure.json +30 -0
- package/templates/example-chain/evidence/v1/config.json +88 -0
- package/templates/example-chain/evidence/v1/interview-requirements.json +8 -0
- package/templates/example-chain/inputs/background.png +0 -0
- package/templates/example-chain/inputs/painting.png +0 -0
- package/templates/example-chain/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/package.json +9 -0
- package/templates/example-chain/project.json +2593 -0
- package/templates/example-diff/diff-components.json +71 -0
- package/templates/example-diff/recomposite_rig.png +0 -0
- package/templates/example-direction/approval.txt +1 -0
- package/templates/example-direction/derived/review/v1/out/check/build/rectangle.png +0 -0
- package/templates/example-direction/derived/review/v1/out/check/build/run.json +40 -0
- package/templates/example-direction/derived/review/v1/out/check/build/skeleton.atlas +8 -0
- package/templates/example-direction/derived/review/v1/out/check/build/skeleton.json +73 -0
- package/templates/example-direction/derived/review/v1/out/check/build/skeleton.model.json +195 -0
- package/templates/example-direction/evidence/direction/v2.json +69 -0
- package/templates/example-direction/evidence/direction/v3.json +59 -0
- package/templates/example-direction/evidence/images/rectangle.png +0 -0
- package/templates/example-direction/evidence/input.json +69 -0
- package/templates/example-direction/evidence/motion.json +33 -0
- package/templates/example-direction/evidence/patch.json +23 -0
- package/templates/example-direction/evidence/rig-config.json +55 -0
- package/templates/example-direction/evidence/still.png +0 -0
- package/templates/example-direction/project.json +372 -0
- package/templates/example-direction/reasoning.txt +1 -0
- package/templates/example-direction/words.txt +1 -0
- package/templates/example-draft/README.md +28 -0
- package/templates/example-draft/config.json +71 -0
- package/templates/example-draft/derived/drafts/lantern/config.json +71 -0
- package/templates/example-draft/derived/drafts/lantern/input/painting_in_rig.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/background.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/eye_l.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/eye_r.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/face.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/lantern.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/torso.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts.json +188 -0
- package/templates/example-draft/derived/drafts/lantern/input/recomposite_rig.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/interview-requirements.json +8 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/run.json +40 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/background.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/eye_l.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/eye_r.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/face.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/lantern.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/torso.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/mesh_report.json +1 -0
- package/templates/example-draft/derived/drafts/lantern/out/motion.json +137 -0
- package/templates/example-draft/derived/drafts/lantern/out/rig.json +102 -0
- package/templates/example-draft/derived/drafts/lantern/out/run.json +52 -0
- package/templates/example-draft/derived/parts/rig/painting_in_rig.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/background.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/eye_l.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/eye_r.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/face.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/lantern.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/torso.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts.json +188 -0
- package/templates/example-draft/derived/parts/rig/recomposite_rig.png +0 -0
- package/templates/example-draft/derived/review/v1/config.json +71 -0
- package/templates/example-draft/derived/review/v1/interview-requirements.json +8 -0
- package/templates/example-draft/derived/review/v1/out/check/build/run.json +40 -0
- package/templates/example-draft/derived/review/v1/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/derived/review/v1/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/derived/review/v1/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/derived/review/v1/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/background.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/eye_l.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/eye_r.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/face.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/lantern.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/torso.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts.json +188 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/background.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/eye_l.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/eye_r.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/face.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/lantern.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/torso.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/mesh_report.json +1 -0
- package/templates/example-draft/derived/review/v1/out/rig/motion.json +137 -0
- package/templates/example-draft/derived/review/v1/out/rig/rig.json +102 -0
- package/templates/example-draft/derived/review/v1/out/rig/run.json +52 -0
- package/templates/example-draft/derived/review/v2/config.json +71 -0
- package/templates/example-draft/derived/review/v2/interview-requirements.json +8 -0
- package/templates/example-draft/derived/review/v2/out/check/build/run.json +40 -0
- package/templates/example-draft/derived/review/v2/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/derived/review/v2/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/derived/review/v2/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/derived/review/v2/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/background.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/eye_l.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/eye_r.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/face.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/lantern.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/torso.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts.json +188 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/background.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/eye_l.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/eye_r.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/face.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/lantern.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/torso.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/mesh_report.json +1 -0
- package/templates/example-draft/derived/review/v2/out/rig/motion.json +137 -0
- package/templates/example-draft/derived/review/v2/out/rig/rig.json +102 -0
- package/templates/example-draft/derived/review/v2/out/rig/run.json +52 -0
- package/templates/example-draft/evidence/diff-components.json +71 -0
- package/templates/example-draft/evidence/direction/v4.json +64 -0
- package/templates/example-draft/evidence/direction-input-v4.json +64 -0
- package/templates/example-draft/evidence/fix-ledger/lantern.json +17 -0
- package/templates/example-draft/evidence/parts-check.json +127 -0
- package/templates/example-draft/evidence/parts-review.json +88 -0
- package/templates/example-draft/evidence/review-reads/4ed0507381809746443021554e012032be5b5f8417da0f2e525e9b6df16e9f05.json +59 -0
- package/templates/example-draft/evidence/scene/s1.json +65 -0
- package/templates/example-draft/evidence/scene-background.png +0 -0
- package/templates/example-draft/evidence/scene-citations.json +9 -0
- package/templates/example-draft/evidence/structure-decisions.json +9 -0
- package/templates/example-draft/evidence/v1/interview-config.json +71 -0
- package/templates/example-draft/evidence/v1/interview-requirements.json +8 -0
- package/templates/example-draft/evidence/v1/review-config.json +71 -0
- package/templates/example-draft/evidence/v2/interview-config.json +71 -0
- package/templates/example-draft/evidence/v2/interview-requirements.json +8 -0
- package/templates/example-draft/evidence/v2/review-config.json +71 -0
- package/templates/example-draft/input/painting_in_rig.png +0 -0
- package/templates/example-draft/input/parts/background.png +0 -0
- package/templates/example-draft/input/parts/eye_l.png +0 -0
- package/templates/example-draft/input/parts/eye_r.png +0 -0
- package/templates/example-draft/input/parts/face.png +0 -0
- package/templates/example-draft/input/parts/lantern.png +0 -0
- package/templates/example-draft/input/parts/torso.png +0 -0
- package/templates/example-draft/input/parts.json +188 -0
- package/templates/example-draft/input/recomposite_rig.png +0 -0
- package/templates/example-draft/inputs/painting.png +0 -0
- package/templates/example-draft/interview-requirements.json +8 -0
- package/templates/example-draft/out/check/build/run.json +40 -0
- package/templates/example-draft/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/out/images/background.png +0 -0
- package/templates/example-draft/out/images/eye_l.png +0 -0
- package/templates/example-draft/out/images/eye_r.png +0 -0
- package/templates/example-draft/out/images/face.png +0 -0
- package/templates/example-draft/out/images/lantern.png +0 -0
- package/templates/example-draft/out/images/torso.png +0 -0
- package/templates/example-draft/out/mesh_report.json +1 -0
- package/templates/example-draft/out/motion.json +137 -0
- package/templates/example-draft/out/rig.json +102 -0
- package/templates/example-draft/out/run.json +52 -0
- package/templates/example-draft/outputs/v1/out/check/build/run.json +40 -0
- package/templates/example-draft/outputs/v1/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/outputs/v1/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/outputs/v1/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/outputs/v1/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/outputs/v1/out/painting_in_rig.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/background.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/eye_l.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/eye_r.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/face.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/lantern.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/torso.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts.json +188 -0
- package/templates/example-draft/outputs/v1/out/rig/images/background.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/eye_l.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/eye_r.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/face.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/lantern.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/torso.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/mesh_report.json +1 -0
- package/templates/example-draft/outputs/v1/out/rig/motion.json +137 -0
- package/templates/example-draft/outputs/v1/out/rig/rig.json +102 -0
- package/templates/example-draft/outputs/v1/out/rig/run.json +52 -0
- package/templates/example-draft/outputs/v2/out/check/build/run.json +40 -0
- package/templates/example-draft/outputs/v2/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/outputs/v2/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/outputs/v2/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/outputs/v2/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/outputs/v2/out/painting_in_rig.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/background.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/eye_l.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/eye_r.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/face.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/lantern.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/torso.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts.json +188 -0
- package/templates/example-draft/outputs/v2/out/rig/images/background.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/eye_l.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/eye_r.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/face.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/lantern.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/torso.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/mesh_report.json +1 -0
- package/templates/example-draft/outputs/v2/out/rig/motion.json +137 -0
- package/templates/example-draft/outputs/v2/out/rig/rig.json +102 -0
- package/templates/example-draft/outputs/v2/out/rig/run.json +52 -0
- package/templates/example-interview-choice.json +1068 -0
- package/templates/example-interview-options/constraint.json +376 -0
- package/templates/example-interview-options/example-assemble.json +376 -0
- package/templates/example-interview-options/example-bones.json +190 -0
- package/templates/example-interview-options/example-camera-depth.json +99 -0
- package/templates/example-interview-options/example-constraints.json +272 -0
- package/templates/example-interview-options/example-meshes.json +190 -0
- package/templates/example-interview-options/example-motion.json +272 -0
- package/templates/example-interview-options/example-picks.json +15 -0
- package/templates/example-interview-options/example-post-composition.json +110 -0
- package/templates/example-interview-options/example-post-timing.json +105 -0
- package/templates/example-interview-options/example-reference.json +63 -0
- package/templates/example-interview-options/example-rig-type.json +63 -0
- package/templates/example-interview-options/example-seethrough.json +190 -0
- package/templates/example-interview-options/fullscreen.json +63 -0
- package/templates/example-interview-options/other-explicit.json +63 -0
- package/templates/example-interview-options/other-pending.json +63 -0
- package/templates/example-interview-options/other-picks.json +16 -0
- package/templates/example-interview.md +213 -0
- package/templates/example-invariants/invariants.json +1339 -0
- package/templates/example-prepared/prepared.svg +1 -0
- package/templates/example-prepared/source.svg +1 -0
- package/templates/example-project.json +4259 -0
- package/templates/example-remesh/AGENTS.md +134 -0
- package/templates/example-remesh/CLAUDE.md +3 -0
- package/templates/example-remesh/evidence/.gitkeep +0 -0
- package/templates/example-remesh/evidence/approval.txt +1 -0
- package/templates/example-remesh/evidence/captions.json +101 -0
- package/templates/example-remesh/evidence/diff-components.json +34 -0
- package/templates/example-remesh/evidence/direction/v4.json +74 -0
- package/templates/example-remesh/evidence/direction/v5.json +74 -0
- package/templates/example-remesh/evidence/direction-input-v4.json +74 -0
- package/templates/example-remesh/evidence/direction-input-v5.json +74 -0
- package/templates/example-remesh/evidence/interview/assemble.json +309 -0
- package/templates/example-remesh/evidence/interview/bones.json +190 -0
- package/templates/example-remesh/evidence/interview/camera_depth.json +103 -0
- package/templates/example-remesh/evidence/interview/constraints.json +272 -0
- package/templates/example-remesh/evidence/interview/meshes.json +190 -0
- package/templates/example-remesh/evidence/interview/motion.json +272 -0
- package/templates/example-remesh/evidence/interview/post_composition.json +123 -0
- package/templates/example-remesh/evidence/interview/post_timing.json +103 -0
- package/templates/example-remesh/evidence/interview/reference.json +59 -0
- package/templates/example-remesh/evidence/interview/rig_type.json +59 -0
- package/templates/example-remesh/evidence/interview/seethrough.json +186 -0
- package/templates/example-remesh/evidence/parts-check.json +127 -0
- package/templates/example-remesh/evidence/parts-review.json +88 -0
- package/templates/example-remesh/evidence/reopen.txt +1 -0
- package/templates/example-remesh/evidence/review-reads/6a17db694db96f1b6049a61a017e0c8e66757b982b8851e0187e561cfcf9af68.json +59 -0
- package/templates/example-remesh/evidence/scene/s1.json +69 -0
- package/templates/example-remesh/evidence/scene-citations.json +9 -0
- package/templates/example-remesh/evidence/setup.png +0 -0
- package/templates/example-remesh/evidence/split-config.json +1 -0
- package/templates/example-remesh/evidence/structure.json +30 -0
- package/templates/example-remesh/evidence/v1/registration/at-source.json +44 -0
- package/templates/example-remesh/evidence/v1/registration/citations.json +68 -0
- package/templates/example-remesh/evidence/v1/registration/compile-report.json +368 -0
- package/templates/example-remesh/evidence/v1/registration/config-source.json +88 -0
- package/templates/example-remesh/evidence/v1/registration/config.json +88 -0
- package/templates/example-remesh/evidence/v1/registration/interview-requirements.json +8 -0
- package/templates/example-remesh/evidence/v1/registration/package-lock.json +11 -0
- package/templates/example-remesh/evidence/v1/registration/path-adapter.json +1 -0
- package/templates/example-remesh/evidence/v1/registration/rigc-run-1.json +44 -0
- package/templates/example-remesh/evidence/v2/registration/at-source.json +44 -0
- package/templates/example-remesh/evidence/v2/registration/citations.json +68 -0
- package/templates/example-remesh/evidence/v2/registration/compile-report.json +368 -0
- package/templates/example-remesh/evidence/v2/registration/config-source.json +88 -0
- package/templates/example-remesh/evidence/v2/registration/config.json +88 -0
- package/templates/example-remesh/evidence/v2/registration/interview-requirements.json +8 -0
- package/templates/example-remesh/evidence/v2/registration/package-lock.json +11 -0
- package/templates/example-remesh/evidence/v2/registration/path-adapter.json +1 -0
- package/templates/example-remesh/evidence/v2/registration/rigc-run-1.json +44 -0
- package/templates/example-remesh/evidence/v3/registration/at-source.json +44 -0
- package/templates/example-remesh/evidence/v3/registration/citations.json +68 -0
- package/templates/example-remesh/evidence/v3/registration/compile-report.json +368 -0
- package/templates/example-remesh/evidence/v3/registration/config-source.json +88 -0
- package/templates/example-remesh/evidence/v3/registration/config.json +88 -0
- package/templates/example-remesh/evidence/v3/registration/interview-requirements.json +8 -0
- package/templates/example-remesh/evidence/v3/registration/package-lock.json +11 -0
- package/templates/example-remesh/evidence/v3/registration/path-adapter.json +1 -0
- package/templates/example-remesh/evidence/v3/registration/rigc-run-1.json +44 -0
- package/templates/example-remesh/inputs/background.png +0 -0
- package/templates/example-remesh/inputs/painting.png +0 -0
- package/templates/example-remesh/inputs/references/.gitkeep +0 -0
- package/templates/example-remesh/package.json +9 -0
- package/templates/example-remesh/project.json +3916 -0
- package/templates/example-reopened/project.json +4568 -0
- package/templates/example-review/config.json +1 -0
- package/templates/example-review/out/check/build/skeleton.model.json +1 -0
- package/templates/example-review/out/parts/piece.svg +1 -0
- package/templates/example-review/out/parts.json +1 -0
- package/templates/example-review/out/rig/mesh_report.json +1 -0
- package/templates/example-review/out/rig/motion.json +1 -0
- package/templates/example-review/out/rig/rig.json +1 -0
- package/templates/example-split/captions.json +357 -0
- package/templates/example-split/config.json +215 -0
- package/templates/legacy-example-project.json +1071 -0
- package/templates/project.json +199 -0
- package/templates/shared/conventions.json +23 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
import { retainedReviewRead, reviewReadLegacy, reviewSessionSource, selectedReviewFeedback, ReviewReadError } from './review-read.ts';
|
|
2
|
+
import { configInputBindings, configInputsLegacy } from './config-inputs.ts';
|
|
3
|
+
import { DIRECTION_CORRECTION_SPEC, DirectionError, validateDirectionIdentity, validateDirectionOutcome } from './direction.ts';
|
|
4
|
+
/** Correction inventory reads retained producer evidence; it never applies a patch or builds a rig. */
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { lstatSync, readFileSync, readdirSync, realpathSync, statSync } from './check-reads.ts';
|
|
8
|
+
import { dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
9
|
+
import { isDeepStrictEqual as equal } from 'node:util';
|
|
10
|
+
import { asArray, collectSignoffs, isObject, type Finding } from './check.ts';
|
|
11
|
+
import { collectReopens, liveSignoff, reopenTime } from './reopen.ts';
|
|
12
|
+
import { collectAmendments } from './amend.ts';
|
|
13
|
+
import { stageIndex } from './stages.ts';
|
|
14
|
+
import type { Json, JsonObject } from './templates.ts';
|
|
15
|
+
import { reviewConfigInputs, reviewHandoffInputs } from './review-package.ts';
|
|
16
|
+
|
|
17
|
+
export const CORRECTIONS_SPEC = 'rig-create/corrections@1';
|
|
18
|
+
export const CORRECTION_SPEC = 'rig-create/correction@1';
|
|
19
|
+
export const DESCRIBED_DIRECTION_SINCE = '2026-11-06T00:00:00Z';
|
|
20
|
+
export const CORRECTION_STAGES = ['parts-review', 'rig-review', 'direction-review'];
|
|
21
|
+
// @rig-c/parts 2.16.0, src/check.ts:3094 (CheckStatus), read from the installed producer.
|
|
22
|
+
const PRODUCER_CHECK_STATUSES = new Set(['PASS', 'FAIL', 'SKIP', 'WARN', 'CLEAR', 'REPORTED', 'UNCLASSIFIED']);
|
|
23
|
+
const requireCore = createRequire(import.meta.url);
|
|
24
|
+
const { assertPng } = requireCore('@rig-c/core/png') as { assertPng: (bytes: Uint8Array, label: string) => void };
|
|
25
|
+
const { decodePng } = requireCore('@rig-c/core/plate') as { decodePng: (bytes: Uint8Array) => { width: number; height: number; data: Uint8Array } };
|
|
26
|
+
const partNamePattern = /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/;
|
|
27
|
+
|
|
28
|
+
/** Count retained PASS statuses only; preserve every other named status and the producer line. */
|
|
29
|
+
export function correctionCheckCoverage(summary: JsonObject): string {
|
|
30
|
+
const bars = Object.entries(summary.bars as JsonObject);
|
|
31
|
+
const measured = bars.filter(([, status]) => status === 'PASS').length;
|
|
32
|
+
const unmeasured = bars.filter(([, status]) => status !== 'PASS').map(([name, status]) => `${name}=${String(status)}`);
|
|
33
|
+
return `${measured} PASS bar(s) in the summary map; not measured: ${unmeasured.join(', ') || 'none'}; producer: ${String(summary.line)}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const correctionHash = (bytes: Uint8Array): string => createHash('sha256').update(bytes).digest('hex');
|
|
37
|
+
const hex = (v: unknown): v is string => typeof v === 'string' && /^[a-f0-9]{64}$/.test(v);
|
|
38
|
+
const text = (v: unknown): v is string => typeof v === 'string' && v.trim().length > 0;
|
|
39
|
+
const id = (v: unknown): v is string => typeof v === 'string' && /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(v);
|
|
40
|
+
const date = (v: unknown): number => typeof v === 'string' && /^\d{4}-\d\d-\d\dT.*(?:Z|[+-]\d\d:\d\d)$/.test(v) ? Date.parse(v) : NaN;
|
|
41
|
+
const closed = (v: JsonObject, required: string[], optional: string[] = []): boolean => required.every(k => k in v) && Object.keys(v).every(k => required.includes(k) || optional.includes(k));
|
|
42
|
+
export class CorrectionError extends Error {
|
|
43
|
+
constructor(readonly code: string, why: string) { super(why); }
|
|
44
|
+
}
|
|
45
|
+
function fail(code: string, why: string): never { throw new CorrectionError(code, why); }
|
|
46
|
+
export function correctionFinding(code: string, path: string, why: string): Finding {
|
|
47
|
+
return { code, path, message: `${path}: ${why}; retain the original correction evidence, resolve its named source/outcome, or reopen its review before recording again` };
|
|
48
|
+
}
|
|
49
|
+
/** Every ancestor must be a real directory, including under an otherwise contained symlink. */
|
|
50
|
+
export function correctionBytes(dir: string, path: string): Buffer {
|
|
51
|
+
try {
|
|
52
|
+
if (isAbsolute(path) || path.includes('\\') || path.split('/').some(p => !p || p === '.' || p === '..') || ['node_modules', '.git', 'scratch'].includes(path.split('/')[0] as string)) throw Error('expected literal retained project-relative file');
|
|
53
|
+
let current = realpathSync(dir);
|
|
54
|
+
for (const [i, part] of path.split('/').entries()) {
|
|
55
|
+
current = join(current, part);
|
|
56
|
+
const info = lstatSync(current);
|
|
57
|
+
if (info.isSymbolicLink() || (i === path.split('/').length - 1 ? !info.isFile() : !info.isDirectory())) throw Error('nonregular file or symlink');
|
|
58
|
+
}
|
|
59
|
+
const rel = relative(realpathSync(dir), realpathSync(current));
|
|
60
|
+
if (rel.startsWith(`..${sep}`) || rel === '..' || isAbsolute(rel)) throw Error('outside project');
|
|
61
|
+
return readFileSync(current);
|
|
62
|
+
} catch (e) { return fail('CORRECTION_FILE', `${path}: ${e instanceof Error ? e.message : String(e)}`); }
|
|
63
|
+
}
|
|
64
|
+
export function correctionBinding(dir: string, path: string): JsonObject { return { path, sha256: correctionHash(correctionBytes(dir, path)) }; }
|
|
65
|
+
export function correctionFile(dir: string, binding: Json | undefined): Buffer {
|
|
66
|
+
if (!isObject(binding) || !closed(binding, ['path', 'sha256']) || !text(binding.path) || !hex(binding.sha256)) fail('CORRECTION_MALFORMED', 'expected file {path,sha256}');
|
|
67
|
+
const bytes = correctionBytes(dir, binding.path);
|
|
68
|
+
if (correctionHash(bytes) !== binding.sha256) fail('CORRECTION_HASH', `${binding.path}: bytes differ from sha256`);
|
|
69
|
+
return bytes;
|
|
70
|
+
}
|
|
71
|
+
export function correctionJson(bytes: Buffer): JsonObject {
|
|
72
|
+
try { const v: Json = JSON.parse(bytes.toString('utf8')); if (!isObject(v)) throw Error('expected object'); return v; }
|
|
73
|
+
catch (e) { return fail('CORRECTION_FILE', `invalid retained JSON: ${e instanceof Error ? e.message : String(e)}`); }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Grammar only. Pointer resolution, JSON Patch replay, route computation and producer measurements are forbidden. */
|
|
77
|
+
export function validateCorrectionPatch(v: JsonObject): void {
|
|
78
|
+
const bad = (why: string): never => fail('CORRECTION_PATCH_INVALID', why);
|
|
79
|
+
if (!closed(v, ['schema', 'config_sha256', 'ops', 'producer', 'op']) || v.schema !== 'rig-review-config-patch/1' || !hex(v.config_sha256)) bad('expected exact config-patch/1 fields');
|
|
80
|
+
const producer = v.producer;
|
|
81
|
+
const command = v.op;
|
|
82
|
+
if (!isObject(producer) || !closed(producer, ['name', 'version']) || !text(producer.name) || !text(producer.version)) bad('producer requires name/version');
|
|
83
|
+
if (!isObject(command) || !closed(command, ['cmd', 'args']) || !['rig', 'remesh'].includes(String(command.cmd)) || !Array.isArray(command.args) || command.args.some(a => typeof a !== 'string')) bad('invalid patch command');
|
|
84
|
+
const cmd = command as JsonObject;
|
|
85
|
+
const args = cmd.args as string[];
|
|
86
|
+
const allowed: Record<string, string[]> = { '--idle-keys': ['ctl', 'direct'], '--page-edges': ['pot', 'free'], '--pack-shape': ['rect', 'polygon'] };
|
|
87
|
+
const seen = new Set<string>();
|
|
88
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
89
|
+
const flag = args[i] as string; const value = args[i + 1];
|
|
90
|
+
if (seen.has(flag) || !value || (flag === '--part' ? cmd.cmd !== 'remesh' || !/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(value) : !allowed[flag]?.includes(value))) bad(`invalid/duplicate non-path arg ${flag}`);
|
|
91
|
+
seen.add(flag);
|
|
92
|
+
}
|
|
93
|
+
if (cmd.cmd === 'remesh' && !seen.has('--part')) bad('remesh needs --part');
|
|
94
|
+
if (!Array.isArray(v.ops) || v.ops.length < 1 || v.ops.length > 256) bad('ops must hold 1–256 entries');
|
|
95
|
+
for (const raw of v.ops as Json[]) {
|
|
96
|
+
if (!isObject(raw) || !['add', 'replace', 'remove'].includes(String(raw.op)) || !closed(raw, raw.op === 'remove' ? ['op', 'path'] : ['op', 'path', 'value'], ['name']) || !text(raw.path) || !raw.path.startsWith('/') || /~(?![01])/.test(raw.path) || (raw.name !== undefined && !text(raw.name))) bad('invalid patch operation/pointer');
|
|
97
|
+
const op = raw as JsonObject;
|
|
98
|
+
if (String(op.path).split('/').slice(1).some(s => /^\d+$/.test(s) && s.length > 1 && s.startsWith('0') || s === '-' && op.op !== 'add')) bad('invalid array index or append');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function versionRecord(project: JsonObject, n: Json | undefined): JsonObject {
|
|
103
|
+
const matches = asArray(project.versions).filter(v => isObject(v) && v.n === n);
|
|
104
|
+
if (!Number.isSafeInteger(n) || matches.length !== 1 || !isObject(matches[0])) return fail('CORRECTION_SOURCE', 'version must exist uniquely');
|
|
105
|
+
return matches[0];
|
|
106
|
+
}
|
|
107
|
+
function draftRecord(project: JsonObject, value: Json | undefined): JsonObject {
|
|
108
|
+
const matches = asArray(project.drafts).filter(v => isObject(v) && v.id === value);
|
|
109
|
+
if (matches.length !== 1 || !isObject(matches[0])) return fail('CORRECTION_SOURCE', 'draft must exist uniquely');
|
|
110
|
+
return matches[0];
|
|
111
|
+
}
|
|
112
|
+
function bindings(v: Json | undefined): JsonObject[] { return asArray(v).filter(isObject); }
|
|
113
|
+
function sameHash(a: Json | undefined, b: Json | undefined): boolean { return isObject(a) && isObject(b) && hex(a.sha256) && a.sha256 === b.sha256; }
|
|
114
|
+
function partsIdentity(dir: string, value: JsonObject): void {
|
|
115
|
+
if (!(value.kind === 'parts' ? closed(value, ['kind', 'manifest', 'files']) : closed(value, ['manifest', 'files'])) || !isObject(value.manifest) || !String(value.manifest.path).endsWith('/parts.json') || !Array.isArray(value.files) || value.files.length === 0) fail('CORRECTION_SOURCE', 'parts identity needs manifest and complete files');
|
|
116
|
+
correctionJson(correctionFile(dir, value.manifest));
|
|
117
|
+
const files = bindings(value.files);
|
|
118
|
+
if (files.length !== value.files.length || files.some((f, i) => !text(f.path) || (i > 0 && String(files[i - 1]?.path) >= f.path))) fail('CORRECTION_SOURCE', 'parts files must be unique and sorted');
|
|
119
|
+
for (const f of files) correctionFile(dir, f);
|
|
120
|
+
const root = dirname(String(value.manifest.path));
|
|
121
|
+
const actual: string[] = [String(value.manifest.path)];
|
|
122
|
+
const walk = (path: string): void => {
|
|
123
|
+
for (const name of readdirSync(resolve(dir, path)).sort()) {
|
|
124
|
+
const p = `${path}/${name}`; const info = lstatSync(resolve(dir, p));
|
|
125
|
+
if (info.isSymbolicLink()) fail('CORRECTION_FILE', `${p}: symlink`);
|
|
126
|
+
if (info.isDirectory()) walk(p); else { correctionBytes(dir, p); actual.push(p); }
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
try { walk(`${root}/parts`); } catch (e) { if (e instanceof CorrectionError) throw e; fail('CORRECTION_FILE', `${root}/parts: missing/unreadable inventory`); }
|
|
130
|
+
// The raster parts.json declares under painting_in_rig belongs to the inventory when the identity carries it (drafts.ts partsInventory).
|
|
131
|
+
const painting = correctionJson(correctionFile(dir, value.manifest)).painting_in_rig;
|
|
132
|
+
const declared = isObject(painting) && typeof painting.path === 'string' ? `${root}/${painting.path}` : null;
|
|
133
|
+
if (declared !== null && files.some(f => f.path === declared)) actual.push(declared);
|
|
134
|
+
if (!equal(actual.sort(), files.map(f => f.path))) fail('CORRECTION_SOURCE', 'parts manifest/tree inventory is incomplete');
|
|
135
|
+
if (!files.some(f => equal(f, value.manifest))) fail('CORRECTION_SOURCE', 'manifest missing from inventory');
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* `partImage`: a part-image correction edits one part file at parts-review, so its draft
|
|
139
|
+
* identity binds the parts manifest and files with `model: null` (drafts recorded from a
|
|
140
|
+
* standalone `parts rig` output bind no compiled model). A non-null model is still read
|
|
141
|
+
* and must match the draft. Every other draft identity requires its retained model.
|
|
142
|
+
*/
|
|
143
|
+
export function validateCorrectionIdentity(dir: string, project: JsonObject, value: Json | undefined, stage: string, direction = false, partImage = false): JsonObject {
|
|
144
|
+
if (direction) {
|
|
145
|
+
if (stage !== "direction-review" || !isObject(value)) fail("DIRECTION_CORRECTION_OUTCOME", "direction identity requires post at direction-review");
|
|
146
|
+
try { validateDirectionIdentity(dir, project, value); } catch (e) { if (e instanceof DirectionError) fail(e.code,e.message); throw e; }
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
if (!isObject(value)) return fail('CORRECTION_SOURCE', 'missing source/outcome identity');
|
|
150
|
+
if (value.kind === 'parts') {
|
|
151
|
+
if (stage !== 'parts-review') fail('CORRECTION_SOURCE', 'parts identity belongs to parts-review');
|
|
152
|
+
partsIdentity(dir, value); return value;
|
|
153
|
+
}
|
|
154
|
+
if (value.kind === 'draft') {
|
|
155
|
+
if (stage !== 'parts-review' || !closed(value, ['kind', 'id', 'config', 'parts', 'model'])) fail('CORRECTION_SOURCE', 'draft identity belongs to parts-review');
|
|
156
|
+
const draft = draftRecord(project, value.id);
|
|
157
|
+
if (!sameHash(value.config, draft.config) || !isObject(value.parts) || !isObject(draft.parts)) fail('CORRECTION_SOURCE', 'draft config/parts do not match inventory');
|
|
158
|
+
const modelless = partImage && value.model === null;
|
|
159
|
+
correctionJson(correctionFile(dir, value.config)); if (!modelless) correctionFile(dir, value.model);
|
|
160
|
+
partsIdentity(dir, value.parts);
|
|
161
|
+
const wanted = bindings(draft.parts.files).map(f => f.sha256).sort();
|
|
162
|
+
if (!equal(bindings(value.parts.files).map(f => f.sha256).sort(), wanted) || !modelless && !bindings(draft.files).some(f => sameHash(f, value.model) && String(f.path).endsWith('/skeleton.model.json'))) fail('CORRECTION_SOURCE', 'draft parts/model hashes differ from inventory');
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
if (!['rig', 'post'].includes(String(value.kind)) || !closed(value, ['kind', 'n', 'config', 'model'])) fail('CORRECTION_SOURCE', 'unknown version identity');
|
|
166
|
+
const version = versionRecord(project, value.n);
|
|
167
|
+
if (version.kind !== value.kind || (value.kind === 'rig' ? stage !== 'rig-review' : stage !== 'direction-review')) fail('CORRECTION_SOURCE', 'review stage and version kind differ');
|
|
168
|
+
if (!isObject(version.provenance) || !sameHash(value.config, version.provenance.config)) fail('CORRECTION_SOURCE', 'config differs from version provenance');
|
|
169
|
+
correctionJson(correctionFile(dir, value.config)); correctionFile(dir, value.model);
|
|
170
|
+
if (value.kind === 'post') {
|
|
171
|
+
if (!bindings(version.outputs).some(f => sameHash(f, value.model) && isObject(value.model) && f.path === value.model.path)) fail('CORRECTION_SOURCE', 'post model must name a declared retained output');
|
|
172
|
+
} else {
|
|
173
|
+
const pkg = version.review_package;
|
|
174
|
+
if (!isObject(pkg) || !sameHash(pkg.config, value.config) || !bindings(pkg.files).some(f => f.path === 'out/check/build/skeleton.model.json' && sameHash(f, value.model))) fail('CORRECTION_SOURCE', 'rig identity differs from review package');
|
|
175
|
+
}
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
function identityTime(project: JsonObject, v: JsonObject): number {
|
|
179
|
+
return v.kind === 'rig' || v.kind === 'post' ? date(versionRecord(project, v.n).at) : v.kind === 'draft' ? date(draftRecord(project, v.id).at) : -Infinity;
|
|
180
|
+
}
|
|
181
|
+
function changed(a: JsonObject, b: JsonObject): boolean {
|
|
182
|
+
const inventory = (v: JsonObject): Json[] => bindings(v.kind === 'draft' && isObject(v.parts) ? v.parts.files : v.files).map(f => f.sha256).sort();
|
|
183
|
+
if (a.kind === 'parts' || b.kind === 'parts') return !equal(inventory(a), inventory(b));
|
|
184
|
+
// A part-image draft identity binds no model (null); its parts inventory alone says whether the draft changed.
|
|
185
|
+
if (a.kind === 'draft' && b.kind === 'draft') return (a.model !== null && b.model !== null && !sameHash(a.model, b.model)) || !equal(inventory(a), inventory(b));
|
|
186
|
+
return !sameHash(a.model, b.model);
|
|
187
|
+
}
|
|
188
|
+
export function correctionApplied(dir: string, artifact: JsonObject): { feedback: JsonObject; item: JsonObject; applied: JsonObject } {
|
|
189
|
+
const binding = artifact.feedback;
|
|
190
|
+
if (!isObject(binding) || !closed(binding, ['file', 'item_id']) || !text(binding.item_id)) return fail('CORRECTION_APPLIED_INVALID', 'feedback requires file and item_id');
|
|
191
|
+
let feedback: JsonObject;
|
|
192
|
+
try { feedback = retainedReviewRead(dir, artifact.review_read, binding.file); }
|
|
193
|
+
catch (error) { if (error instanceof ReviewReadError) return fail(error.code,error.message); throw error; }
|
|
194
|
+
const matches = selectedReviewFeedback(feedback, binding.item_id as string);
|
|
195
|
+
const item = matches[0];
|
|
196
|
+
if (reviewSessionSource(feedback) === null || matches.length !== 1 || !isObject(item) || !text(item.summary) || !isObject(item.context) || !isObject(item.context.applied)) return fail('CORRECTION_APPLIED_INVALID', 'expected one session feedback item with context.applied');
|
|
197
|
+
const applied = item.context.applied;
|
|
198
|
+
if (applied.schema !== 'rig-review-applied/2' || !hex(applied.configHash) || !hex(applied.modelHash) || !hex(applied.patchSha256) || !hex(applied.buildId) || !isObject(applied.patch) || !isObject(applied.producer) || !text(applied.producer.package) || !text(applied.producer.version) || !isObject(applied.compiler) || !text(applied.compiler.package) || !text(applied.compiler.version) || !(applied.line === null || typeof applied.line === 'string') || !isObject(applied.diff)) fail('CORRECTION_APPLIED_INVALID', 'expected complete published applied/2 identity');
|
|
199
|
+
if (!closed(applied, ['schema','buildId','patch','patchSha256','configHash','producer','compiler','line','check','diff','modelHash'], ['op','files','mesh','cloth','assembled','environment','stageInputHash'])) fail('CORRECTION_APPLIED_INVALID', 'unknown or missing applied/2 fields');
|
|
200
|
+
if (applied.stageInputHash !== undefined && !hex(applied.stageInputHash)) fail('CORRECTION_APPLIED_INVALID', 'stageInputHash must be a 64-lowercase-hex sha256');
|
|
201
|
+
if (!Number.isSafeInteger(applied.diff.count) || Number(applied.diff.count) < 0 || !Array.isArray(applied.diff.paths) || applied.diff.paths.length > 200 || applied.diff.paths.some(p => typeof p !== 'string') || applied.diff.paths.length > Number(applied.diff.count)) fail('CORRECTION_APPLIED_INVALID', 'malformed retained diff');
|
|
202
|
+
const summary = applied.check;
|
|
203
|
+
if (summary !== null && (!isObject(summary) || !closed(summary, ['schema','pass','exitCode','line','bars','gate'], ['source']) || summary.schema !== 'rig-review-check-summary/1' || ![true,false,null].includes(summary.pass as boolean|null) || !(summary.exitCode === null || Number.isInteger(summary.exitCode)) || typeof summary.line !== 'string' || !isObject(summary.bars) || Object.values(summary.bars).some(v => typeof v !== 'string') || typeof summary.gate !== 'boolean')) fail('CORRECTION_APPLIED_INVALID', 'malformed retained check summary');
|
|
204
|
+
if (applied.files !== undefined && applied.files !== null) {
|
|
205
|
+
const f = applied.files;
|
|
206
|
+
if (!isObject(f) || !closed(f,['documents','images']) || !Array.isArray(f.documents) || !isObject(f.images) || !closed(f.images,['changed','unchanged','names']) || !Number.isSafeInteger(f.images.changed) || Number(f.images.changed)<0 || !Number.isSafeInteger(f.images.unchanged) || Number(f.images.unchanged)<0 || !Array.isArray(f.images.names) || f.images.names.length>200 || f.images.names.some(n=>typeof n!=='string')) fail('CORRECTION_APPLIED_INVALID','malformed retained files report');
|
|
207
|
+
const names = new Set<string>();
|
|
208
|
+
for (const d of (f as JsonObject).documents as Json[]) {
|
|
209
|
+
if (!isObject(d) || !closed(d,['file','count','paths']) || !['rig.json','motion.json','sets.json','mesh_report.json'].includes(String(d.file)) || names.has(String(d.file)) || !Number.isSafeInteger(d.count) || Number(d.count)<0 || !Array.isArray(d.paths) || d.paths.length>200 || d.paths.some(p=>typeof p!=='string')) fail('CORRECTION_APPLIED_INVALID','malformed retained document diff');
|
|
210
|
+
names.add(String((d as JsonObject).file));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (applied.environment !== undefined && (!isObject(applied.environment) || !closed(applied.environment,['sha256','steps']) || !hex(applied.environment.sha256) || !Number.isSafeInteger(applied.environment.steps) || Number(applied.environment.steps)<1)) fail('CORRECTION_APPLIED_INVALID','malformed retained environment');
|
|
214
|
+
validateCorrectionPatch(applied.patch);
|
|
215
|
+
if (!isObject(applied.patch.producer) || applied.patch.config_sha256 !== applied.configHash || applied.producer.package !== applied.patch.producer.name || applied.producer.version !== applied.patch.producer.version) fail('CORRECTION_APPLIED_INVALID', 'patch/config/producer mismatch');
|
|
216
|
+
if (applied.op !== undefined && !['rig', 'retune', 'remesh', 'cloth', 'build'].includes(String(applied.op))) fail('CORRECTION_APPLIED_INVALID', 'unknown executed operation');
|
|
217
|
+
const command = applied.patch.op as JsonObject;
|
|
218
|
+
if (command.cmd === 'remesh' && applied.op !== undefined && applied.op !== 'remesh' || command.cmd === 'rig' && applied.op === 'remesh') fail('CORRECTION_APPLIED_INVALID','requested and executed operations disagree');
|
|
219
|
+
if (applied.op === 'cloth' && (!isObject(applied.cloth) || !['build','rig'].includes(String(applied.cloth.route)) || !isObject(applied.cloth.keypoints) || !hex(applied.cloth.keypoints.sha256) || typeof applied.cloth.keypoints.path !== 'string' || !isObject(applied.cloth.written))) fail('CORRECTION_APPLIED_INVALID','cloth route needs published context');
|
|
220
|
+
if ((applied.op === 'build' || isObject(applied.cloth) && applied.cloth.route === 'build') && (!isObject(applied.assembled) || typeof applied.assembled.wallMs !== 'number' || !isObject(applied.assembled.recomposite) || !Array.isArray(applied.assembled.partsChanged) || !Array.isArray(applied.assembled.seamPairs))) fail('CORRECTION_APPLIED_INVALID','build route needs published assembled context');
|
|
221
|
+
if (applied.mesh !== undefined) {
|
|
222
|
+
const mesh=applied.mesh;
|
|
223
|
+
if (!isObject(mesh) || typeof mesh.part!=='string' || !isObject(mesh.before) || !isObject(mesh.after) || ['before','after'].some(side=>['vertices','triangles','hull'].some(k=>!Number.isSafeInteger((mesh[side] as JsonObject)[k]) || Number((mesh[side] as JsonObject)[k])<0)) || command.cmd!=='remesh' || (command.args as string[])[(command.args as string[]).indexOf('--part')+1]!==mesh.part) fail('CORRECTION_APPLIED_INVALID','malformed or mismatched retained mesh counts');
|
|
224
|
+
}
|
|
225
|
+
return { feedback, item, applied };
|
|
226
|
+
}
|
|
227
|
+
const outsideProject = (p: string): boolean => p === '..' || p.startsWith('../') || posix.isAbsolute(p);
|
|
228
|
+
/**
|
|
229
|
+
* Review's input contract, @rig-c/review 2.7.0 src/manifest.ts:89-92 (`loadReview`):
|
|
230
|
+
* a directory input is the package, and any other input is a manifest file whose
|
|
231
|
+
* folder is the package. create reads which one a retained input was from its
|
|
232
|
+
* `.json` suffix; the writer refuses an input whose kind on disk disagrees.
|
|
233
|
+
*/
|
|
234
|
+
const manifestInput = (input: string): boolean => posix.extname(input) === '.json';
|
|
235
|
+
/**
|
|
236
|
+
* The `rig-review feedback` input as it is retained. Review runs with the project
|
|
237
|
+
* as its working directory (review-read.ts `runReview`), so an absolute input inside
|
|
238
|
+
* the project names the same package as its project-relative form, which survives
|
|
239
|
+
* the project being moved. A draft route's input must say by its form alone whether
|
|
240
|
+
* it is the package directory or its manifest file, since check reads it that way.
|
|
241
|
+
*/
|
|
242
|
+
export function feedbackPackageInput(dir: string, input: string, draft: boolean): string {
|
|
243
|
+
let value = input;
|
|
244
|
+
if (isAbsolute(input)) for (const root of [resolve(dir), realpathSync(dir)]) {
|
|
245
|
+
const rel = relative(root, resolve(input)).split(sep).join('/');
|
|
246
|
+
if (!outsideProject(rel)) { value = rel || '.'; break; }
|
|
247
|
+
}
|
|
248
|
+
if (draft) {
|
|
249
|
+
let directory: boolean | null = null;
|
|
250
|
+
try { directory = statSync(resolve(dir, value)).isDirectory(); } catch { /* Review refuses a missing input by name. */ }
|
|
251
|
+
if (directory !== null && directory === manifestInput(value)) fail('CORRECTION_APPLIED_INVALID', `--feedback-file ${value} is a ${directory ? 'directory named .json' : 'manifest file without a .json suffix'}; pass the package directory, or its manifest as a .json file`);
|
|
252
|
+
}
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Review writes handoff paths relative to the package it opened (`../../config.json`
|
|
257
|
+
* for a package at `<draft>/out/check`). The package is the retained input of the
|
|
258
|
+
* `rig-review feedback <input>` read, or the folder of a manifest file opened
|
|
259
|
+
* directly, read from the input's form and never from the current filesystem. Each
|
|
260
|
+
* path is rebased onto it to the project-relative form create binds; one that leaves
|
|
261
|
+
* the project is refused by name.
|
|
262
|
+
*/
|
|
263
|
+
function draftHandoffFiles(artifact: JsonObject, raw: Json[]): { base: string; files: JsonObject[] } {
|
|
264
|
+
const input = String(((artifact.review_read as JsonObject).argv as Json[])[2]);
|
|
265
|
+
if (isAbsolute(input) || input.includes('\\')) fail('CORRECTION_APPLIED_INVALID', `feedback package ${input} is not project-relative; record the correction again with --feedback-file relative to the project`);
|
|
266
|
+
const base = posix.normalize(manifestInput(input) ? posix.dirname(input) : input).replace(/\/+$/, '') || '.';
|
|
267
|
+
if (outsideProject(base)) fail('CORRECTION_APPLIED_INVALID', `feedback package ${input} is outside the project; read the review feedback from the package inside the project`);
|
|
268
|
+
return { base, files: bindings(raw).map(f => {
|
|
269
|
+
if (!text(f.path) || f.path.includes('\\') || posix.isAbsolute(f.path)) fail('CORRECTION_APPLIED_INVALID', `handoff path ${JSON.stringify(f.path)} is not a literal package-relative path`);
|
|
270
|
+
const path = posix.normalize(posix.join(base, f.path));
|
|
271
|
+
if (outsideProject(path)) fail('CORRECTION_APPLIED_INVALID', `handoff path ${f.path} resolves outside the project from package ${base}`);
|
|
272
|
+
return { ...f, path };
|
|
273
|
+
}) };
|
|
274
|
+
}
|
|
275
|
+
function validateApplied(dir: string, project: JsonObject, record: JsonObject, artifact: JsonObject, on: JsonObject, warnings: Finding[], path: string): void {
|
|
276
|
+
const { feedback, item, applied } = correctionApplied(dir, artifact);
|
|
277
|
+
if (record.verbatim !== item.summary || record.by !== 'user') fail('CORRECTION_APPLIED_INVALID', 'made human fields must retain summary and by:user');
|
|
278
|
+
if (!isObject(on.config) || on.config.sha256 !== applied.configHash || !isObject(on.model)) fail('CORRECTION_APPLIED_INVALID', 'Apply does not bind source config/model');
|
|
279
|
+
const reviewed = feedback.reviewedSource as JsonObject;
|
|
280
|
+
const handoff = reviewed.handoff;
|
|
281
|
+
if (!isObject(reviewed.rig) || reviewed.rig.sha256 !== on.model.sha256 || reviewed.motion !== null || !isObject(handoff) || !sameHash(handoff.config, on.config) || !Array.isArray(handoff.files) || !Array.isArray(handoff.missing) || handoff.missing.length !== 0) fail('CORRECTION_APPLIED_INVALID', 'feedback reviewed source/handoff differs');
|
|
282
|
+
const requireNamed = (named: JsonObject[], files: JsonObject[], from = ''): void => {
|
|
283
|
+
for (const input of named) {
|
|
284
|
+
const matches = files.filter(f => f.path === input.path);
|
|
285
|
+
if (matches.length && (!hex(input.sha256) || matches.length !== 1 || matches[0]!.sha256 !== input.sha256)) fail('CORRECTION_APPLIED_INVALID', `config-named input ${String(input.path)} has an altered hash`);
|
|
286
|
+
if (!matches.length) {
|
|
287
|
+
const reason = `config-named input ${String(input.path)} is missing from handoff${from}`;
|
|
288
|
+
if (!configInputsLegacy(record.at)) fail('CORRECTION_APPLIED_INVALID', reason);
|
|
289
|
+
warnings.push(correctionFinding('CORRECTION_APPLIED_INVALID', path, `${reason}; legacy input omission`));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
if (on.kind === 'rig') {
|
|
294
|
+
const version = versionRecord(project, on.n);
|
|
295
|
+
const pkg = version.review_package as JsonObject;
|
|
296
|
+
const packageIssue = (reason: string, at: unknown): void => {
|
|
297
|
+
if (!configInputsLegacy(at)) fail('CORRECTION_APPLIED_INVALID', reason);
|
|
298
|
+
warnings.push(correctionFinding('CORRECTION_APPLIED_INVALID', path, reason));
|
|
299
|
+
};
|
|
300
|
+
const named = reviewConfigInputs(pkg, dir, Number(on.n), name => packageIssue(`config-named input ${name} is missing from source package`, version.at), error => packageIssue(error.message, record.at));
|
|
301
|
+
const inputs = reviewHandoffInputs(pkg, dir, Number(on.n), named);
|
|
302
|
+
const files: JsonObject[] = bindings(handoff.files).map(f => ({ ...f, path: posix.normalize(`out/check/${String(f.path)}`) }));
|
|
303
|
+
requireNamed(named.filter(f => hex(f.sha256)), files);
|
|
304
|
+
for (const input of named.filter(f => !hex(f.sha256))) if (files.some(f => f.path === input.path)) fail('CORRECTION_APPLIED_INVALID', `config-named input ${String(input.path)} has no frozen package hash`);
|
|
305
|
+
const required = inputs.filter(f => !named.some(n => n.path === f.path));
|
|
306
|
+
if (files.length !== handoff.files.length || !required.every(f => files.some(h => h.path === f.path && h.sha256 === f.sha256)) || !files.every(h => inputs.some(f => f.path === h.path && f.sha256 === h.sha256)) || new Set(files.map(f => f.path)).size !== files.length) fail('CORRECTION_APPLIED_INVALID', 'feedback input inventory differs from source package');
|
|
307
|
+
} else if (on.kind === 'draft') {
|
|
308
|
+
const draft = draftRecord(project, on.id);
|
|
309
|
+
const named = configInputBindings(dir, String((draft.config as JsonObject).path), true, error => {
|
|
310
|
+
if (!configInputsLegacy(record.at)) fail('CORRECTION_APPLIED_INVALID', error.message);
|
|
311
|
+
warnings.push(correctionFinding('CORRECTION_APPLIED_INVALID', path, error.message));
|
|
312
|
+
}).map(input => {
|
|
313
|
+
// A missing directory input names its directory; its binding is the receipt that stands for it.
|
|
314
|
+
const bound = bindings(draft.config_inputs).find(f => f.path === input.path || f.directory === input.path);
|
|
315
|
+
if (bound && input.sha256 === null) return bound;
|
|
316
|
+
return input;
|
|
317
|
+
});
|
|
318
|
+
for (const input of bindings(draft.config_inputs)) {
|
|
319
|
+
if (!named.some(f => f.path === input.path && f.sha256 === input.sha256)) fail('CORRECTION_APPLIED_INVALID', `config-named input ${String(input.path)} differs from draft binding`);
|
|
320
|
+
}
|
|
321
|
+
const { base, files } = draftHandoffFiles(artifact, handoff.files);
|
|
322
|
+
requireNamed(named, files, ` (handoff paths read relative to package ${base})`);
|
|
323
|
+
const expected = [draft.config, ...bindings(isObject(draft.parts) ? draft.parts.files : undefined), ...bindings(draft.files), ...named];
|
|
324
|
+
const required = expected.filter(x => isObject(x) && (x === draft.config || String(x.path).endsWith('/parts.json') || String(x.path).includes('/parts/') || /\/(rig|motion|mesh_report)\.json$/.test(String(x.path))));
|
|
325
|
+
if (!required.every(x=>files.some(f=>sameHash(x,f))) || !files.every(f => expected.some(x => sameHash(x, f)))) fail('CORRECTION_APPLIED_INVALID', 'draft handoff contains foreign inputs');
|
|
326
|
+
} else fail('CORRECTION_APPLIED_INVALID', 'config patch Apply requires rig or reviewed draft');
|
|
327
|
+
const summary = applied.check;
|
|
328
|
+
if (artifact.state === 'applied-and-checked') {
|
|
329
|
+
if (!isObject(summary) || summary.schema !== 'rig-review-check-summary/1' || summary.pass !== true || summary.exitCode !== 0 || summary.gate !== true || !text(summary.line) || !isObject(summary.bars) || (summary.source !== undefined && !['remesh', 'retune', 'check'].includes(String(summary.source)))) fail('CORRECTION_APPLIED_INVALID', 'claimed checked Apply lacks a passing producer check summary');
|
|
330
|
+
const bars = summary.bars as JsonObject;
|
|
331
|
+
for (const [name, status] of Object.entries(bars)) {
|
|
332
|
+
if (status === 'FAIL' || typeof status !== 'string' || !PRODUCER_CHECK_STATUSES.has(status)) fail('CORRECTION_APPLIED_INVALID', `check bar ${name} has invalid status ${String(status)}`);
|
|
333
|
+
}
|
|
334
|
+
if (!Object.values(bars).includes('PASS')) fail('CORRECTION_APPLIED_INVALID', `check bars ${Object.keys(bars).join(', ') || '(empty)'} contain no PASS bar`);
|
|
335
|
+
// parts 2.16.0 src/check.ts:3069–3071 prints coverage's skipped names.
|
|
336
|
+
// Report-only SKIPs need not appear there, so compare only names the line supplies.
|
|
337
|
+
const coverage = /\b\d+ of \d+ bar\(s\) measured, \d+ skipped \(([^)]*)\)/.exec(String(summary.line));
|
|
338
|
+
for (const name of coverage?.[1]?.split(', ') ?? []) {
|
|
339
|
+
if (Object.hasOwn(bars, name) && bars[name] === 'PASS') fail('CORRECTION_APPLIED_INVALID', `check bar ${name} is PASS but the producer line names it skipped`);
|
|
340
|
+
}
|
|
341
|
+
if (!isObject(record.outcome) || !sameHash(record.outcome.model, { sha256: applied.modelHash })) fail('CORRECTION_OUTCOME', 'checked Apply outcome differs from applied.modelHash');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function decodePartPng(bytes: Uint8Array, label: string): { width: number; height: number; data: Uint8Array } {
|
|
346
|
+
try {
|
|
347
|
+
assertPng(bytes, label);
|
|
348
|
+
return decodePng(bytes);
|
|
349
|
+
} catch (e) {
|
|
350
|
+
return fail('CORRECTION_PART_IMAGE_MASK_SIZE', `${label}: invalid PNG (${e instanceof Error ? e.message : String(e)})`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function draftPartBinding(draft: JsonObject, part: string): JsonObject | null {
|
|
355
|
+
if (!isObject(draft.parts) || typeof draft.parts.path !== 'string' || !Array.isArray(draft.parts.files)) return null;
|
|
356
|
+
const expectedPath = `${draft.parts.path}/parts/${part}.png`;
|
|
357
|
+
const match = bindings(draft.parts.files).find(f => f.path === expectedPath && hex(f.sha256));
|
|
358
|
+
return match ?? null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function validatePartImageArtifact(dir: string, project: JsonObject, record: JsonObject, art: JsonObject, on: JsonObject): void {
|
|
362
|
+
if (record.stage !== 'parts-review' || on.kind !== 'draft') fail('CORRECTION_MALFORMED', 'part-image requires parts-review and a parent draft');
|
|
363
|
+
if (!closed(art, ['kind', 'draft', 'part', 'before', 'after', 'mask', 'tool']) || !id(art.draft) || typeof art.part !== 'string' || !partNamePattern.test(art.part) || !text(art.tool)) {
|
|
364
|
+
fail('CORRECTION_MALFORMED', 'part-image requires {kind,draft,part,before,after,mask,tool}');
|
|
365
|
+
}
|
|
366
|
+
if (art.draft !== on.id) fail('CORRECTION_PART_IMAGE_BEFORE', `artifact.draft ${String(art.draft)} differs from parent draft ${String(on.id)}`);
|
|
367
|
+
const parentDraft = draftRecord(project, on.id);
|
|
368
|
+
const expectedBefore = draftPartBinding(parentDraft, art.part);
|
|
369
|
+
if (!expectedBefore) fail('CORRECTION_PART_IMAGE_BEFORE', `parent draft ${String(on.id)} has no bound part ${art.part}`);
|
|
370
|
+
if (!isObject(art.before) || !closed(art.before, ['path', 'sha256']) || !text(art.before.path) || !hex(art.before.sha256)) {
|
|
371
|
+
fail('CORRECTION_MALFORMED', 'before requires file {path,sha256}');
|
|
372
|
+
}
|
|
373
|
+
const beforeBytes = correctionFile(dir, art.before);
|
|
374
|
+
if (art.before.path !== expectedBefore.path || art.before.sha256 !== expectedBefore.sha256) {
|
|
375
|
+
fail('CORRECTION_PART_IMAGE_BEFORE', `before ${String(art.before.path)} is not parent draft ${String(on.id)}'s bound ${String(expectedBefore.path)}`);
|
|
376
|
+
}
|
|
377
|
+
const afterBytes = correctionFile(dir, art.after);
|
|
378
|
+
const maskBytes = correctionFile(dir, art.mask);
|
|
379
|
+
const beforePng = decodePartPng(beforeBytes, String(art.before.path));
|
|
380
|
+
const afterPng = decodePartPng(afterBytes, String((art.after as JsonObject).path));
|
|
381
|
+
const maskPng = decodePartPng(maskBytes, String((art.mask as JsonObject).path));
|
|
382
|
+
if (maskPng.width !== beforePng.width || maskPng.height !== beforePng.height) {
|
|
383
|
+
fail('CORRECTION_PART_IMAGE_MASK_SIZE', `mask ${String((art.mask as JsonObject).path)} (${maskPng.width}x${maskPng.height}) differs from part ${String(art.before.path)} (${beforePng.width}x${beforePng.height})`);
|
|
384
|
+
}
|
|
385
|
+
if (afterPng.width !== beforePng.width || afterPng.height !== beforePng.height) {
|
|
386
|
+
fail('CORRECTION_PART_IMAGE_MASK_SIZE', `after ${String((art.after as JsonObject).path)} (${afterPng.width}x${afterPng.height}) differs from before ${String(art.before.path)} (${beforePng.width}x${beforePng.height})`);
|
|
387
|
+
}
|
|
388
|
+
const maskData = maskPng.data;
|
|
389
|
+
let hasAlphaVariation = false;
|
|
390
|
+
let hasNonZeroRgb = false;
|
|
391
|
+
for (let i = 0; i < maskData.length; i += 4) {
|
|
392
|
+
const a = maskData[i + 3]!;
|
|
393
|
+
if (a < 255) hasAlphaVariation = true;
|
|
394
|
+
if (a > 0 && (maskData[i]! > 0 || maskData[i + 1]! > 0 || maskData[i + 2]! > 0)) hasNonZeroRgb = true;
|
|
395
|
+
}
|
|
396
|
+
const insideMask = (offset: number): boolean => {
|
|
397
|
+
const a = maskData[offset + 3]!;
|
|
398
|
+
if (a === 0) return false;
|
|
399
|
+
if (hasNonZeroRgb) return maskData[offset]! > 0 || maskData[offset + 1]! > 0 || maskData[offset + 2]! > 0;
|
|
400
|
+
return hasAlphaVariation;
|
|
401
|
+
};
|
|
402
|
+
const beforeData = beforePng.data;
|
|
403
|
+
const afterData = afterPng.data;
|
|
404
|
+
let changedPixels = 0;
|
|
405
|
+
let outsideAt: { x: number; y: number } | null = null;
|
|
406
|
+
let addedAlphaAt: { x: number; y: number; beforeA: number; afterA: number } | null = null;
|
|
407
|
+
for (let y = 0; y < beforePng.height; y++) {
|
|
408
|
+
for (let x = 0; x < beforePng.width; x++) {
|
|
409
|
+
const offset = (y * beforePng.width + x) * 4;
|
|
410
|
+
const bR = beforeData[offset]!, bG = beforeData[offset + 1]!, bB = beforeData[offset + 2]!, bA = beforeData[offset + 3]!;
|
|
411
|
+
const aR = afterData[offset]!, aG = afterData[offset + 1]!, aB = afterData[offset + 2]!, aA = afterData[offset + 3]!;
|
|
412
|
+
if (bR === aR && bG === aG && bB === aB && bA === aA) continue;
|
|
413
|
+
changedPixels++;
|
|
414
|
+
if (!insideMask(offset) && outsideAt === null) outsideAt = { x, y };
|
|
415
|
+
if (aA > bA && addedAlphaAt === null) addedAlphaAt = { x, y, beforeA: bA, afterA: aA };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (outsideAt !== null) {
|
|
419
|
+
fail('CORRECTION_PART_IMAGE_OUTSIDE_MASK', `after ${String((art.after as JsonObject).path)} differs from before ${String(art.before.path)} outside mask ${String((art.mask as JsonObject).path)} at (${outsideAt.x},${outsideAt.y})`);
|
|
420
|
+
}
|
|
421
|
+
if (addedAlphaAt !== null) {
|
|
422
|
+
fail('CORRECTION_PART_IMAGE_ADDED_ALPHA', `after ${String((art.after as JsonObject).path)} adds alpha at (${addedAlphaAt.x},${addedAlphaAt.y}) (${addedAlphaAt.beforeA} -> ${addedAlphaAt.afterA})`);
|
|
423
|
+
}
|
|
424
|
+
if (changedPixels === 0) {
|
|
425
|
+
fail('CORRECTION_PART_IMAGE_BEFORE', `after ${String((art.after as JsonObject).path)} has no pixel changes from before ${String(art.before.path)}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function partImageResolvingDraft(project: JsonObject, record: JsonObject): JsonObject | null {
|
|
430
|
+
if (!isObject(record.artifact) || record.artifact.kind !== 'part-image' || record.verdict === 'withdrawn') return null;
|
|
431
|
+
const art = record.artifact;
|
|
432
|
+
if (!id(art.draft) || typeof art.part !== 'string' || !isObject(art.after) || !hex(art.after.sha256)) return null;
|
|
433
|
+
const drafts = bindings(project.drafts);
|
|
434
|
+
if (isObject(record.outcome)) {
|
|
435
|
+
if (record.outcome.kind !== 'draft' || record.outcome.id === art.draft) return null;
|
|
436
|
+
const target = drafts.find(d => d.id === (record.outcome as JsonObject).id);
|
|
437
|
+
if (!target) return null;
|
|
438
|
+
const partBinding = draftPartBinding(target, art.part);
|
|
439
|
+
return partBinding && partBinding.sha256 === art.after.sha256 ? target : null;
|
|
440
|
+
}
|
|
441
|
+
const parentIdx = drafts.findIndex(d => d.id === art.draft);
|
|
442
|
+
const candidates = (parentIdx >= 0 ? drafts.slice(parentIdx + 1) : drafts).filter(d =>
|
|
443
|
+
d.id !== art.draft && Number.isFinite(date(d.at)) && date(d.at) >= date(record.at)
|
|
444
|
+
);
|
|
445
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
446
|
+
const candidate = candidates[i]!;
|
|
447
|
+
const partBinding = draftPartBinding(candidate, art.part);
|
|
448
|
+
if (partBinding && partBinding.sha256 === art.after.sha256) return candidate;
|
|
449
|
+
}
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function checkUnrecordedPartRepaints(_dir: string, project: JsonObject, refusals: Finding[]): void {
|
|
454
|
+
if (!Array.isArray(project.drafts)) return;
|
|
455
|
+
const drafts = bindings(project.drafts);
|
|
456
|
+
const corrections = bindings(project.corrections);
|
|
457
|
+
const allPartImage = corrections.filter(c =>
|
|
458
|
+
isObject(c.artifact) && c.artifact.kind === 'part-image'
|
|
459
|
+
&& isObject(c.artifact.before) && isObject(c.artifact.after)
|
|
460
|
+
&& hex(c.artifact.after.sha256) && c.artifact.before.sha256 !== c.artifact.after.sha256
|
|
461
|
+
);
|
|
462
|
+
if (allPartImage.length === 0) return;
|
|
463
|
+
const reopens = collectReopens(project, []);
|
|
464
|
+
const firstResolutionTime = (c: JsonObject): number => {
|
|
465
|
+
const art = c.artifact as JsonObject;
|
|
466
|
+
const parentIdx = drafts.findIndex(d => d.id === art.draft);
|
|
467
|
+
for (const d of parentIdx >= 0 ? drafts.slice(parentIdx + 1) : drafts) {
|
|
468
|
+
if (d.id === art.draft || !(date(d.at) >= date(c.at))) continue;
|
|
469
|
+
const pb = draftPartBinding(d, String(art.part));
|
|
470
|
+
if (pb && pb.sha256 === (art.after as JsonObject).sha256) return date(d.at);
|
|
471
|
+
}
|
|
472
|
+
return Infinity;
|
|
473
|
+
};
|
|
474
|
+
const isWithdrawnAt = (c: JsonObject, draftAt: number): boolean =>
|
|
475
|
+
c.verdict === 'withdrawn' && (!Number.isFinite(date(c.withdrawn_at)) || date(c.withdrawn_at) <= draftAt);
|
|
476
|
+
const isSupersededAt = (c: JsonObject, draftAt: number): boolean => {
|
|
477
|
+
const checkUntil = Math.min(draftAt, firstResolutionTime(c));
|
|
478
|
+
const reopenByThen = reopenTime(reopens.filter(r => r.at <= checkUntil), String(c.stage));
|
|
479
|
+
if (date(c.at) <= reopenByThen) return true;
|
|
480
|
+
return corrections.some(r => r.resolves === c.id && r.verdict !== 'withdrawn' && date(r.at) <= draftAt);
|
|
481
|
+
};
|
|
482
|
+
for (const [index, draft] of drafts.entries()) {
|
|
483
|
+
if (!isObject(draft.parts) || typeof draft.parts.path !== 'string' || !Array.isArray(draft.parts.files)) continue;
|
|
484
|
+
const partsRoot = draft.parts.path;
|
|
485
|
+
const prefix = `${partsRoot}/parts/`;
|
|
486
|
+
const partFiles = bindings(draft.parts.files).filter(f => typeof f.path === 'string' && f.path.startsWith(prefix) && f.path.endsWith('.png') && hex(f.sha256));
|
|
487
|
+
for (const pf of partFiles) {
|
|
488
|
+
const partName = String(pf.path).slice(prefix.length, -4);
|
|
489
|
+
const matchingRepaints = allPartImage.filter(c => ((c.artifact as JsonObject).after as JsonObject).sha256 === pf.sha256);
|
|
490
|
+
if (matchingRepaints.length === 0) continue;
|
|
491
|
+
const draftAt = date(draft.at);
|
|
492
|
+
const covered = matchingRepaints.some(c => {
|
|
493
|
+
if (c.stage !== 'parts-review' || c.path !== 'made' || isWithdrawnAt(c, draftAt) || isSupersededAt(c, draftAt)) return false;
|
|
494
|
+
if (!(date(c.at) <= draftAt)) return false;
|
|
495
|
+
const art = c.artifact as JsonObject;
|
|
496
|
+
if (art.part !== partName || art.draft === draft.id) return false;
|
|
497
|
+
const parent = drafts.slice(0, index).find(prev => prev.id === art.draft);
|
|
498
|
+
if (!parent) return false;
|
|
499
|
+
const parentPf = draftPartBinding(parent, partName);
|
|
500
|
+
return parentPf !== null && isObject(art.before) && art.before.sha256 === parentPf.sha256;
|
|
501
|
+
});
|
|
502
|
+
if (!covered) {
|
|
503
|
+
const ref = matchingRepaints[0]!;
|
|
504
|
+
const refArt = ref.artifact as JsonObject;
|
|
505
|
+
const parentId = typeof refArt.draft === 'string' ? refArt.draft : '<draft-id>';
|
|
506
|
+
const path = `drafts[${index}].parts.files`;
|
|
507
|
+
if (!refusals.some(r => r.code === 'DRAFT_PART_REPAINT_UNRECORDED' && r.path === path)) {
|
|
508
|
+
refusals.push({
|
|
509
|
+
code: 'DRAFT_PART_REPAINT_UNRECORDED',
|
|
510
|
+
path,
|
|
511
|
+
message: `${path}: part ${partName} (${String(pf.path)}) binds part-image after bytes (${String(pf.sha256)}) from correction ${String(ref.id)} without a live covering part-image correction record; record a live masked repaint with correction record --path made --stage parts-review --draft ${parentId} --part ${partName} --part-image-file <after.png> --mask-file <mask.png> --tool <text> before recording the draft`,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function correctionsAdopted(project: JsonObject): boolean { return isObject(project.corrections_contract) && project.corrections_contract.schema === CORRECTIONS_SPEC; }
|
|
520
|
+
export function correctionIsLive(project: JsonObject, r: JsonObject): boolean {
|
|
521
|
+
return date(r.at) > reopenTime(collectReopens(project, []), String(r.stage));
|
|
522
|
+
}
|
|
523
|
+
export function correctionApprovalFindings(project: JsonObject, stage: string, signoff?: JsonObject): Finding[] {
|
|
524
|
+
const out: Finding[] = [];
|
|
525
|
+
if (!correctionsAdopted(project)) return out;
|
|
526
|
+
const records = bindings(project.corrections);
|
|
527
|
+
const relevant = records.filter(r => r.stage === stage && r.verdict !== 'withdrawn' && correctionIsLive(project, r) && date(r.at) >= date((project.corrections_contract as JsonObject).adopted_at));
|
|
528
|
+
for (const r of relevant) {
|
|
529
|
+
const replacement = relevant.find(next => next.resolves === r.id && (next.outcome !== null || partImageResolvingDraft(project, next) !== null));
|
|
530
|
+
if (replacement) continue;
|
|
531
|
+
const path = `corrections[${String(r.id)}]`;
|
|
532
|
+
if (isObject(r.artifact) && r.artifact.kind === 'part-image') {
|
|
533
|
+
const resolving = partImageResolvingDraft(project, r);
|
|
534
|
+
const resolvedAt = isObject(r.closure) ? date(r.closure.at) : resolving ? date(resolving.at) : Infinity;
|
|
535
|
+
if (!resolving || (signoff && resolvedAt > date(signoff.at))) {
|
|
536
|
+
out.push(correctionFinding('CORRECTION_PENDING', path, `resolve active correction before approving ${stage}`));
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (stage === 'parts-review' && signoff) {
|
|
540
|
+
const signedDraft = bindings(project.drafts).find(d => d.id === signoff.draft_id);
|
|
541
|
+
const signedPart = signedDraft ? draftPartBinding(signedDraft, String(r.artifact.part)) : null;
|
|
542
|
+
if (!signedDraft || (isObject(r.outcome) && r.outcome.id !== signoff.draft_id) || !signedPart || !isObject(r.artifact.after) || signedPart.sha256 !== r.artifact.after.sha256) {
|
|
543
|
+
out.push(correctionFinding('CORRECTION_OUTCOME', path, 'approved draft does not bind the repainted part image'));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
if (!isObject(r.outcome) || signoff && (!isObject(r.closure) || date(r.closure.at) > date(signoff.at))) { out.push(correctionFinding('CORRECTION_PENDING', path, `resolve active correction before approving ${stage}`)); continue; }
|
|
549
|
+
if (stage === 'parts-review' && signoff) {
|
|
550
|
+
const outcome = r.outcome;
|
|
551
|
+
if (outcome.kind === 'draft' && outcome.id !== signoff.draft_id) out.push(correctionFinding('CORRECTION_OUTCOME', path, 'approved draft is not the corrected draft'));
|
|
552
|
+
else if (outcome.kind === 'parts') {
|
|
553
|
+
const draft = bindings(project.drafts).find(d => d.id === signoff.draft_id);
|
|
554
|
+
if (!draft || !isObject(draft.parts) || !equal(bindings(draft.parts.files).map(f => f.sha256).sort(), bindings(outcome.files).map(f => f.sha256).sort())) out.push(correctionFinding('CORRECTION_OUTCOME', path, 'approved draft does not use corrected parts'));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return out;
|
|
559
|
+
}
|
|
560
|
+
export function checkCorrections(ctx: { dir: string; project: JsonObject; refusals: Finding[]; warnings: Finding[] }, now: number): void {
|
|
561
|
+
const { dir, project, refusals, warnings } = ctx;
|
|
562
|
+
const contract = project.corrections_contract;
|
|
563
|
+
if (contract === undefined) warnings.push(correctionFinding('CORRECTION_PATH_LEGACY', 'corrections_contract', 'correction paths are not adopted; adopt explicitly without rewriting history'));
|
|
564
|
+
else if (!isObject(contract) || !closed(contract, ['schema', 'adopted_at']) || contract.schema !== CORRECTIONS_SPEC || !Number.isFinite(date(contract.adopted_at)) || date(contract.adopted_at) > now) refusals.push(correctionFinding('CORRECTION_CONTRACT_INVALID', 'corrections_contract', 'invalid explicit adoption'));
|
|
565
|
+
if (project.corrections === undefined) { if (contract !== undefined) refusals.push(correctionFinding('CORRECTION_MALFORMED', 'corrections', 'adopted contract needs an explicit list')); return; }
|
|
566
|
+
if (!Array.isArray(project.corrections)) { refusals.push(correctionFinding('CORRECTION_MALFORMED', 'corrections', 'expected list')); return; }
|
|
567
|
+
if (!correctionsAdopted(project) && project.corrections.length) refusals.push(correctionFinding('CORRECTION_CONTRACT_INVALID', 'corrections_contract', 'records require explicit adoption'));
|
|
568
|
+
const seen = new Map<string, JsonObject>();
|
|
569
|
+
for (const [index, raw] of project.corrections.entries()) {
|
|
570
|
+
const path = `corrections[${index}]`;
|
|
571
|
+
try {
|
|
572
|
+
const partImage = isObject(raw) && isObject(raw.artifact) && raw.artifact.kind === 'part-image';
|
|
573
|
+
if (!isObject(raw) || !closed(raw, ['schema', 'id', 'path', 'by', 'at', 'verbatim', 'stage', 'on', 'artifact', 'outcome', 'resolves'], ['source', 'closure', 'verdict']) || ![CORRECTION_SPEC,DIRECTION_CORRECTION_SPEC].includes(String(raw.schema)) || !id(raw.id) || seen.has(raw.id) || !['described', 'made'].includes(String(raw.path)) || (partImage ? raw.by !== 'agent' || raw.verbatim !== null : !text(raw.by) || raw.by.toLowerCase() === 'agent' || !text(raw.verbatim)) || !Number.isFinite(date(raw.at)) || date(raw.at) > now || !CORRECTION_STAGES.includes(String(raw.stage)) || (raw.source !== undefined && typeof raw.source !== 'string') || (raw.verdict !== undefined && raw.verdict !== 'withdrawn')) fail('CORRECTION_MALFORMED', 'invalid common fields, duplicate id or human timestamp');
|
|
574
|
+
const record = raw as JsonObject;
|
|
575
|
+
if (record.resolves !== null) {
|
|
576
|
+
const prior = seen.get(String(record.resolves));
|
|
577
|
+
if (!prior || prior.stage !== record.stage || date(prior.at) >= date(record.at)) fail('CORRECTION_MALFORMED', 'replacement must name an earlier same-stage record');
|
|
578
|
+
}
|
|
579
|
+
seen.set(String(record.id), record);
|
|
580
|
+
const direction = record.schema === DIRECTION_CORRECTION_SPEC;
|
|
581
|
+
const describedDirection = record.path === 'described' && record.stage === 'direction-review' && isObject(record.on) && record.on.kind === 'post';
|
|
582
|
+
const directionPatch = isObject(record.artifact) && record.artifact.kind === 'direction-patch';
|
|
583
|
+
if (direction && !describedDirection && !directionPatch || !direction && directionPatch || direction && partImage) fail('CORRECTION_MALFORMED', 'correction@2 requires a direction description or direction-patch');
|
|
584
|
+
if (!direction && describedDirection) {
|
|
585
|
+
const version = versionRecord(project, (record.on as JsonObject).n);
|
|
586
|
+
if (isObject(version.provenance) && isObject(version.provenance.direction)) {
|
|
587
|
+
if (date(record.at) >= Date.parse(DESCRIBED_DIRECTION_SINCE)) fail('DESCRIBED_DIRECTION_REQUIRED', 'authored direction description requires correction@2 and exact direction identity');
|
|
588
|
+
warnings.push(correctionFinding('DESCRIBED_DIRECTION_LEGACY', path, 'historical description lacks the exact direction identity; preserve it and capture a new correction@2 request'));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const on = validateCorrectionIdentity(dir, project, record.on, String(record.stage), direction, partImage);
|
|
592
|
+
if (identityTime(project, on) > date(record.at)) fail('CORRECTION_SOURCE', 'correction predates its source');
|
|
593
|
+
if (record.resolves !== null && (on.kind === 'rig' || on.kind === 'post') && bindings(project.versions).some(v => v.kind === on.kind && Number(v.n) > Number(on.n) && date(v.at) <= date(record.at))) fail('CORRECTION_SOURCE', 'correction must name the latest reviewed version at capture');
|
|
594
|
+
const artifact = record.artifact;
|
|
595
|
+
if (!isObject(artifact)) fail('CORRECTION_MALFORMED', 'missing artifact');
|
|
596
|
+
const art = artifact as JsonObject;
|
|
597
|
+
const apiArtifact = record.path === 'made' && !direction && !partImage;
|
|
598
|
+
const legacyRead = apiArtifact && art.review_read === undefined && reviewReadLegacy(record.at);
|
|
599
|
+
if (legacyRead) warnings.push(correctionFinding('REVIEW_READ_LEGACY', path, 'historical correction has no CLI response receipt; preserve history and capture new results through Review feedback'));
|
|
600
|
+
if (legacyRead) {
|
|
601
|
+
if(record.by !== 'user') fail('CORRECTION_MALFORMED','made requires by:user');
|
|
602
|
+
if(art.kind === 'unsupported') {
|
|
603
|
+
if(!closed(art,['kind','operation','reason','files','producer_schema']) || !['mesh-delta','retarget','keyed-animation','other'].includes(String(art.operation)) || !text(art.reason) || !text(art.producer_schema) || !Array.isArray(art.files) || !art.files.length) fail('CORRECTION_MALFORMED','invalid historical unsupported artifact');
|
|
604
|
+
if(record.outcome !== null) fail('CORRECTION_UNSUPPORTED_OUTCOME','unsupported operations cannot claim completion');
|
|
605
|
+
} else if(art.kind !== 'config-patch' || !closed(art,['kind','state','feedback'],['patch']) || !['preview/pending','applied-and-checked'].includes(String(art.state)) || on.kind === 'post' || on.kind === 'parts' || record.outcome !== null && art.state !== 'applied-and-checked') fail('CORRECTION_MALFORMED','invalid historical config patch artifact');
|
|
606
|
+
}
|
|
607
|
+
if (apiArtifact && !legacyRead) {
|
|
608
|
+
try { retainedReviewRead(dir,art.review_read,art.kind === 'unsupported' ? asArray(art.files)[0] : isObject(art.feedback) ? art.feedback.file : undefined); }
|
|
609
|
+
catch(error) { if(error instanceof ReviewReadError) fail(error.code,error.message); throw error; }
|
|
610
|
+
}
|
|
611
|
+
if (record.path === 'described') {
|
|
612
|
+
if (!closed(art, ['kind']) || art.kind !== 'description') fail('CORRECTION_MALFORMED', 'described artifact is description');
|
|
613
|
+
} else if (direction) {
|
|
614
|
+
if (record.by !== 'user') fail('CORRECTION_MALFORMED','made requires by:user');
|
|
615
|
+
try { validateDirectionOutcome(dir,project,record,art); } catch(e) { if(e instanceof DirectionError) fail(e.code,e.message); throw e; }
|
|
616
|
+
} else if (partImage) {
|
|
617
|
+
if (record.path !== 'made') fail('CORRECTION_MALFORMED', 'part-image requires made path');
|
|
618
|
+
validatePartImageArtifact(dir, project, record, art, on);
|
|
619
|
+
} else if (!legacyRead) {
|
|
620
|
+
if (record.by !== 'user') fail('CORRECTION_MALFORMED', 'made requires by:user');
|
|
621
|
+
if (art.kind === 'unsupported') {
|
|
622
|
+
if (!closed(art, ['kind', 'operation', 'reason', 'files', 'producer_schema'], ['review_read']) || !['mesh-delta', 'retarget', 'keyed-animation', 'other'].includes(String(art.operation)) || !text(art.reason) || !text(art.producer_schema) || !Array.isArray(art.files) || art.files.length!==1) fail('CORRECTION_MALFORMED', 'invalid unsupported artifact');
|
|
623
|
+
if (record.outcome !== null) fail('CORRECTION_UNSUPPORTED_OUTCOME', 'unsupported operations cannot claim completion');
|
|
624
|
+
} else {
|
|
625
|
+
if (art.kind !== 'config-patch' || !closed(art, ['kind', 'state', 'feedback','review_read']) || !['preview/pending', 'applied-and-checked'].includes(String(art.state)) || on.kind === 'post' || on.kind === 'parts') fail('CORRECTION_MALFORMED', 'config patch needs rig/draft and known state');
|
|
626
|
+
if (art.feedback !== null) {
|
|
627
|
+
try { validateApplied(dir, project, record, art, on, warnings, path); }
|
|
628
|
+
catch (error) { if (error instanceof CorrectionError) throw error; fail('CORRECTION_APPLIED_INVALID', `config-named inputs unreadable: ${error instanceof Error ? error.message : String(error)}`); }
|
|
629
|
+
}
|
|
630
|
+
else fail('CORRECTION_APPLIED_INVALID', 'missing preview patch or checked session');
|
|
631
|
+
if (record.outcome !== null && art.state !== 'applied-and-checked') fail('CORRECTION_APPLIED_INVALID', 'pending preview cannot claim completed outcome');
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (record.outcome !== null) {
|
|
635
|
+
let outcome: JsonObject;
|
|
636
|
+
try { outcome = validateCorrectionIdentity(dir, project, record.outcome, String(record.stage), direction, partImage); } catch (e) { if (e instanceof CorrectionError && e.code === 'CORRECTION_SOURCE') fail('CORRECTION_OUTCOME', e.message); throw e; }
|
|
637
|
+
const closure = record.closure;
|
|
638
|
+
if (!isObject(closure) || !closed(closure, ['by', 'at', 'reasoning']) || closure.by !== 'agent' || !text(closure.reasoning) || !Number.isFinite(date(closure.at)) || date(closure.at) < date(record.at) || date(closure.at) > now || outcome.kind !== 'parts' && identityTime(project, outcome) < date(record.at) || identityTime(project, outcome) > date(closure.at) || !(direction && on.kind === 'post' && outcome.kind === 'post' ? (record.path === 'described' || sameHash(on.model,outcome.model)) && !sameHash(on.direction,outcome.direction) : changed(on, outcome)) || equal(on, outcome) || (on.kind === 'rig' || on.kind === 'post') && (outcome.kind !== on.kind || Number(outcome.n) <= Number(on.n)) || on.kind === 'draft' && outcome.kind === 'draft' && on.id === outcome.id) fail('CORRECTION_OUTCOME', 'outcome must be distinct/later/changed with one labelled agent closure');
|
|
639
|
+
if (partImage && (outcome.kind !== 'draft' || !partImageResolvingDraft(project, record))) {
|
|
640
|
+
fail('CORRECTION_OUTCOME', 'part-image outcome must be a later draft whose bound part file equals after');
|
|
641
|
+
}
|
|
642
|
+
if (record.path === 'made' && !legacyRead && on.kind === 'rig' && outcome.kind === 'rig') {
|
|
643
|
+
const target = versionRecord(project, outcome.n); const { applied } = correctionApplied(dir, art);
|
|
644
|
+
const origin = target.review_origin;
|
|
645
|
+
if (!isObject(origin) || origin.source_version !== on.n || !equal(origin.applied, applied) || !isObject(art.feedback) || !equal(origin.feedback, art.feedback.file) || origin.feedback_id !== art.feedback.item_id) fail('CORRECTION_OUTCOME', 'result version must preserve the same review_origin');
|
|
646
|
+
}
|
|
647
|
+
} else if (record.closure !== undefined) fail('CORRECTION_MALFORMED', 'open record cannot carry closure');
|
|
648
|
+
const replacement = bindings(project.corrections).some(r => r.resolves === record.id && r.verdict !== 'withdrawn' && (r.outcome !== null || partImageResolvingDraft(project, r) !== null) && correctionIsLive(project, r));
|
|
649
|
+
if (record.verdict === 'withdrawn') {
|
|
650
|
+
// Withdrawn corrections impose no open/pending obligations; checkUnrecordedPartRepaints refuses drafts that still bind their after bytes.
|
|
651
|
+
} else if (partImage) {
|
|
652
|
+
const resolving = partImageResolvingDraft(project, record);
|
|
653
|
+
if (!resolving && correctionIsLive(project, record) && !replacement) {
|
|
654
|
+
const laterDraftExists = bindings(project.drafts).some(d => d.id !== art.draft && Number.isFinite(date(d.at)) && date(d.at) >= date(record.at));
|
|
655
|
+
if (laterDraftExists) fail('CORRECTION_PENDING', 'open part-image correction has no resolving draft binding its after bytes');
|
|
656
|
+
warnings.push(correctionFinding('CORRECTION_OPEN', path, 'part-image correction awaits its resolving draft'));
|
|
657
|
+
}
|
|
658
|
+
} else if (record.outcome === null && correctionIsLive(project, record) && !replacement) {
|
|
659
|
+
warnings.push(correctionFinding(art.kind === 'unsupported' ? 'CORRECTION_UNSUPPORTED' : 'CORRECTION_OPEN', path, art.kind === 'unsupported' ? 'operation has no checked adapter' : 'correction awaits its outcome'));
|
|
660
|
+
}
|
|
661
|
+
} catch (e) {
|
|
662
|
+
if (!(e instanceof CorrectionError)) throw e;
|
|
663
|
+
refusals.push(correctionFinding(e.code, path, e.message));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
checkUnrecordedPartRepaints(dir, project, refusals);
|
|
667
|
+
const reopens = collectReopens(project, []);
|
|
668
|
+
for (const signoff of collectSignoffs(project, []).filter(s => s.checkpoint === null && liveSignoff(reopens, s.stage, s.at))) {
|
|
669
|
+
if (!CORRECTION_STAGES.includes(signoff.stage) || !isObject(contract) || signoff.at <= date(contract.adopted_at)) continue;
|
|
670
|
+
refusals.push(...correctionApprovalFindings(project, signoff.stage, signoff.record));
|
|
671
|
+
}
|
|
672
|
+
for (const version of bindings(project.versions)) for (const verdict of bindings(version.verdicts)) {
|
|
673
|
+
if (verdict.verdict === 'accepted' && CORRECTION_STAGES.includes(String(verdict.review)) && liveSignoff(reopens, String(verdict.review), date(verdict.at)) && isObject(contract) && date(verdict.at) > date(contract.adopted_at)) refusals.push(...correctionApprovalFindings(project, String(verdict.review), verdict));
|
|
674
|
+
}
|
|
675
|
+
// Accepted verdicts and downstream stages also cannot use an unresolved current correction.
|
|
676
|
+
const amendments = collectAmendments(project, []);
|
|
677
|
+
for (const stage of CORRECTION_STAGES) {
|
|
678
|
+
const accepted = bindings(project.versions).some(v => bindings(v.verdicts).some(d => d.review === stage && d.verdict === 'accepted' && date(d.at) > date(isObject(contract) ? contract.adopted_at : undefined) && liveSignoff(reopens, stage, date(d.at))));
|
|
679
|
+
const awaitsReapproval = amendments.some(a => stageIndex(a.stage) <= stageIndex(stage))
|
|
680
|
+
&& !collectSignoffs(project, []).some(s => s.stage === stage && s.checkpoint === null && liveSignoff(reopens, stage, s.at));
|
|
681
|
+
if (accepted || (stageIndex(String(project.stage)) > stageIndex(stage) && !awaitsReapproval)) {
|
|
682
|
+
for (const f of correctionApprovalFindings(project, stage)) if (!refusals.some(x => x.code === f.code && x.path === f.path)) refusals.push(f);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|