@rig-c/create 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +163 -2
- package/docs/CHECKS.md +1665 -0
- package/docs/COLLECTIONS.md +337 -0
- package/docs/CRAFT.md +277 -0
- package/docs/INTERVIEW.md +886 -0
- package/docs/LESSONS.md +172 -0
- package/docs/PROJECT.md +2657 -0
- package/docs/SOURCE.md +225 -0
- package/package.json +44 -4
- package/src/agent-bounce.ts +27 -0
- package/src/agent-guide.ts +237 -0
- package/src/amend.ts +193 -0
- package/src/applied-review.ts +104 -0
- package/src/ask-coverage.ts +69 -0
- package/src/assemble-answers.ts +53 -0
- package/src/cg-gates.ts +211 -0
- package/src/check-reads.ts +106 -0
- package/src/check.ts +1059 -0
- package/src/checkpoint-evidence-writer.ts +125 -0
- package/src/checkpoint-evidence.ts +152 -0
- package/src/choice-writers.ts +271 -0
- package/src/choices.ts +378 -0
- package/src/cli.ts +371 -0
- package/src/collection-answer.ts +136 -0
- package/src/collection-binding-reader.ts +496 -0
- package/src/collection-binding.ts +478 -0
- package/src/collection.ts +348 -0
- package/src/config-inputs.ts +155 -0
- package/src/config-lost.ts +135 -0
- package/src/correction-writers.ts +207 -0
- package/src/corrections.ts +685 -0
- package/src/diff-classes.ts +219 -0
- package/src/diff-map.ts +248 -0
- package/src/direction-citation.ts +13 -0
- package/src/direction-writers.ts +119 -0
- package/src/direction.ts +266 -0
- package/src/drafts.ts +363 -0
- package/src/feedback.ts +744 -0
- package/src/figure-mask.ts +143 -0
- package/src/fix-ledger.ts +355 -0
- package/src/init.ts +265 -0
- package/src/inputs.ts +160 -0
- package/src/intake.ts +158 -0
- package/src/interview.ts +53 -0
- package/src/invariants.ts +132 -0
- package/src/job-acts.ts +80 -0
- package/src/job-advance.ts +65 -0
- package/src/job-close.ts +226 -0
- package/src/job-host.ts +529 -0
- package/src/job-outbox.ts +383 -0
- package/src/job-publication.ts +701 -0
- package/src/job-runtime.ts +99 -0
- package/src/job-writer.ts +123 -0
- package/src/meshes-bust.ts +63 -0
- package/src/next.ts +1506 -0
- package/src/overrides.ts +42 -0
- package/src/package-upgrades.ts +32 -0
- package/src/packages-upgrade-check.ts +6 -0
- package/src/packages-upgrade.ts +168 -0
- package/src/parts-act.ts +96 -0
- package/src/parts-check.ts +242 -0
- package/src/parts-review.ts +196 -0
- package/src/parts-sections.ts +98 -0
- package/src/parts-verdict-record.ts +265 -0
- package/src/parts-verdict.ts +33 -0
- package/src/pause.ts +118 -0
- package/src/pins-check.ts +619 -0
- package/src/pins.ts +17 -0
- package/src/plan-bar-amendments.ts +54 -0
- package/src/plan-history.ts +147 -0
- package/src/plan-writers.ts +202 -0
- package/src/plate-receipt.ts +110 -0
- package/src/pre-interview.ts +302 -0
- package/src/prepared-inputs.ts +113 -0
- package/src/progress-writers.ts +181 -0
- package/src/provenance.ts +301 -0
- package/src/record-direction-target.ts +26 -0
- package/src/record-input.ts +18 -0
- package/src/refusal-baseline.ts +55 -0
- package/src/refusal.ts +30 -0
- package/src/reopen-lapses.ts +80 -0
- package/src/reopen.ts +144 -0
- package/src/republish.ts +93 -0
- package/src/restart.ts +141 -0
- package/src/review-act-runtime.ts +16 -0
- package/src/review-act.ts +592 -0
- package/src/review-package.ts +524 -0
- package/src/review-read.ts +238 -0
- package/src/review.ts +432 -0
- package/src/rig-version-date.ts +22 -0
- package/src/rig-version.ts +1047 -0
- package/src/run-record.ts +232 -0
- package/src/scene-authority.ts +410 -0
- package/src/scene-writers.ts +144 -0
- package/src/scene.ts +284 -0
- package/src/setup-painting.ts +46 -0
- package/src/signable.ts +65 -0
- package/src/signoff-answers.ts +76 -0
- package/src/signoff-checkpoint-act.ts +95 -0
- package/src/signoff-target.ts +127 -0
- package/src/signoff.ts +331 -0
- package/src/split-captions.ts +154 -0
- package/src/stage-progress.ts +379 -0
- package/src/stages.ts +148 -0
- package/src/step-hash.ts +19 -0
- package/src/structure-snapshot.ts +109 -0
- package/src/structure.ts +166 -0
- package/src/templates.ts +16 -0
- package/src/waypoints.ts +163 -0
- package/templates/agent-guide.md +108 -0
- package/templates/collection-agent-guide.md +72 -0
- package/templates/collection.json +252 -0
- package/templates/example-chain/AGENTS.md +134 -0
- package/templates/example-chain/CLAUDE.md +3 -0
- package/templates/example-chain/README.md +13 -0
- package/templates/example-chain/chain.json +2869 -0
- package/templates/example-chain/checkpoints/0a-interview-options/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/0a-interview-options/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/0a-interview-options/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/0a-interview-options/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/0a-interview-options/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/0a-interview-options/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/0a-interview-options/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/0a-interview-options/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/0a-interview-options/package.json +9 -0
- package/templates/example-chain/checkpoints/0a-interview-options/project.json +295 -0
- package/templates/example-chain/checkpoints/0a-interview-options.json +127 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/package.json +9 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/project.json +685 -0
- package/templates/example-chain/checkpoints/0b-plan-pending.json +81 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/parts-check.json +127 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/package.json +9 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/project.json +955 -0
- package/templates/example-chain/checkpoints/1-parts-verdict.json +204 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/diff-components.json +34 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/bones.json +190 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/parts-check.json +127 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/parts-review.json +88 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/evidence/structure.json +30 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/package.json +9 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/project.json +1198 -0
- package/templates/example-chain/checkpoints/2-rigging-answers.json +254 -0
- package/templates/example-chain/checkpoints/3-rig-review/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/3-rig-review/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/diff-components.json +34 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/bones.json +190 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/constraints.json +272 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/meshes.json +190 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/motion.json +272 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/parts-check.json +127 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/parts-review.json +88 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/setup.png +0 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/structure.json +30 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/v1/config.json +88 -0
- package/templates/example-chain/checkpoints/3-rig-review/evidence/v1/interview-requirements.json +8 -0
- package/templates/example-chain/checkpoints/3-rig-review/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/3-rig-review/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/3-rig-review/package.json +9 -0
- package/templates/example-chain/checkpoints/3-rig-review/project.json +1909 -0
- package/templates/example-chain/checkpoints/3-rig-review.json +424 -0
- package/templates/example-chain/checkpoints/4-direction-post/AGENTS.md +134 -0
- package/templates/example-chain/checkpoints/4-direction-post/CLAUDE.md +3 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/approval.txt +1 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/diff-components.json +34 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/bones.json +190 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/camera_depth.json +102 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/constraints.json +272 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/meshes.json +190 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/motion.json +272 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/post_composition.json +122 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/post_timing.json +102 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/reference.json +59 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/parts-check.json +127 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/parts-review.json +88 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/setup.png +0 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/split-config.json +1 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/structure.json +30 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/v1/config.json +88 -0
- package/templates/example-chain/checkpoints/4-direction-post/evidence/v1/interview-requirements.json +8 -0
- package/templates/example-chain/checkpoints/4-direction-post/inputs/painting.png +0 -0
- package/templates/example-chain/checkpoints/4-direction-post/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/checkpoints/4-direction-post/package.json +9 -0
- package/templates/example-chain/checkpoints/4-direction-post/project.json +2302 -0
- package/templates/example-chain/checkpoints/4-direction-post.json +456 -0
- package/templates/example-chain/evidence/.gitkeep +0 -0
- package/templates/example-chain/evidence/approval.txt +1 -0
- package/templates/example-chain/evidence/captions.json +101 -0
- package/templates/example-chain/evidence/diff-components.json +34 -0
- package/templates/example-chain/evidence/direction/v2.json +74 -0
- package/templates/example-chain/evidence/direction-input.json +74 -0
- package/templates/example-chain/evidence/interview/assemble.json +309 -0
- package/templates/example-chain/evidence/interview/bones.json +190 -0
- package/templates/example-chain/evidence/interview/camera_depth.json +102 -0
- package/templates/example-chain/evidence/interview/constraints.json +272 -0
- package/templates/example-chain/evidence/interview/meshes.json +190 -0
- package/templates/example-chain/evidence/interview/motion.json +272 -0
- package/templates/example-chain/evidence/interview/post_composition.json +122 -0
- package/templates/example-chain/evidence/interview/post_timing.json +102 -0
- package/templates/example-chain/evidence/interview/reference.json +59 -0
- package/templates/example-chain/evidence/interview/rig_type.json +59 -0
- package/templates/example-chain/evidence/interview/seethrough.json +186 -0
- package/templates/example-chain/evidence/parts-check.json +127 -0
- package/templates/example-chain/evidence/parts-review.json +88 -0
- package/templates/example-chain/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
- package/templates/example-chain/evidence/scene/s1.json +65 -0
- package/templates/example-chain/evidence/scene-citations.json +9 -0
- package/templates/example-chain/evidence/setup.png +0 -0
- package/templates/example-chain/evidence/split-config.json +1 -0
- package/templates/example-chain/evidence/structure.json +30 -0
- package/templates/example-chain/evidence/v1/config.json +88 -0
- package/templates/example-chain/evidence/v1/interview-requirements.json +8 -0
- package/templates/example-chain/inputs/background.png +0 -0
- package/templates/example-chain/inputs/painting.png +0 -0
- package/templates/example-chain/inputs/references/.gitkeep +0 -0
- package/templates/example-chain/package.json +9 -0
- package/templates/example-chain/project.json +2593 -0
- package/templates/example-diff/diff-components.json +71 -0
- package/templates/example-diff/recomposite_rig.png +0 -0
- package/templates/example-direction/approval.txt +1 -0
- package/templates/example-direction/derived/review/v1/out/check/build/rectangle.png +0 -0
- package/templates/example-direction/derived/review/v1/out/check/build/run.json +40 -0
- package/templates/example-direction/derived/review/v1/out/check/build/skeleton.atlas +8 -0
- package/templates/example-direction/derived/review/v1/out/check/build/skeleton.json +73 -0
- package/templates/example-direction/derived/review/v1/out/check/build/skeleton.model.json +195 -0
- package/templates/example-direction/evidence/direction/v2.json +69 -0
- package/templates/example-direction/evidence/direction/v3.json +59 -0
- package/templates/example-direction/evidence/images/rectangle.png +0 -0
- package/templates/example-direction/evidence/input.json +69 -0
- package/templates/example-direction/evidence/motion.json +33 -0
- package/templates/example-direction/evidence/patch.json +23 -0
- package/templates/example-direction/evidence/rig-config.json +55 -0
- package/templates/example-direction/evidence/still.png +0 -0
- package/templates/example-direction/project.json +372 -0
- package/templates/example-direction/reasoning.txt +1 -0
- package/templates/example-direction/words.txt +1 -0
- package/templates/example-draft/README.md +28 -0
- package/templates/example-draft/config.json +71 -0
- package/templates/example-draft/derived/drafts/lantern/config.json +71 -0
- package/templates/example-draft/derived/drafts/lantern/input/painting_in_rig.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/background.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/eye_l.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/eye_r.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/face.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/lantern.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts/torso.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/input/parts.json +188 -0
- package/templates/example-draft/derived/drafts/lantern/input/recomposite_rig.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/interview-requirements.json +8 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/run.json +40 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/background.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/eye_l.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/eye_r.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/face.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/lantern.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/images/torso.png +0 -0
- package/templates/example-draft/derived/drafts/lantern/out/mesh_report.json +1 -0
- package/templates/example-draft/derived/drafts/lantern/out/motion.json +137 -0
- package/templates/example-draft/derived/drafts/lantern/out/rig.json +102 -0
- package/templates/example-draft/derived/drafts/lantern/out/run.json +52 -0
- package/templates/example-draft/derived/parts/rig/painting_in_rig.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/background.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/eye_l.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/eye_r.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/face.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/lantern.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts/torso.png +0 -0
- package/templates/example-draft/derived/parts/rig/parts.json +188 -0
- package/templates/example-draft/derived/parts/rig/recomposite_rig.png +0 -0
- package/templates/example-draft/derived/review/v1/config.json +71 -0
- package/templates/example-draft/derived/review/v1/interview-requirements.json +8 -0
- package/templates/example-draft/derived/review/v1/out/check/build/run.json +40 -0
- package/templates/example-draft/derived/review/v1/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/derived/review/v1/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/derived/review/v1/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/derived/review/v1/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/background.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/eye_l.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/eye_r.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/face.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/lantern.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts/torso.png +0 -0
- package/templates/example-draft/derived/review/v1/out/parts.json +188 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/background.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/eye_l.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/eye_r.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/face.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/lantern.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/images/torso.png +0 -0
- package/templates/example-draft/derived/review/v1/out/rig/mesh_report.json +1 -0
- package/templates/example-draft/derived/review/v1/out/rig/motion.json +137 -0
- package/templates/example-draft/derived/review/v1/out/rig/rig.json +102 -0
- package/templates/example-draft/derived/review/v1/out/rig/run.json +52 -0
- package/templates/example-draft/derived/review/v2/config.json +71 -0
- package/templates/example-draft/derived/review/v2/interview-requirements.json +8 -0
- package/templates/example-draft/derived/review/v2/out/check/build/run.json +40 -0
- package/templates/example-draft/derived/review/v2/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/derived/review/v2/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/derived/review/v2/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/derived/review/v2/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/background.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/eye_l.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/eye_r.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/face.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/lantern.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts/torso.png +0 -0
- package/templates/example-draft/derived/review/v2/out/parts.json +188 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/background.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/eye_l.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/eye_r.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/face.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/lantern.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/images/torso.png +0 -0
- package/templates/example-draft/derived/review/v2/out/rig/mesh_report.json +1 -0
- package/templates/example-draft/derived/review/v2/out/rig/motion.json +137 -0
- package/templates/example-draft/derived/review/v2/out/rig/rig.json +102 -0
- package/templates/example-draft/derived/review/v2/out/rig/run.json +52 -0
- package/templates/example-draft/evidence/diff-components.json +71 -0
- package/templates/example-draft/evidence/direction/v4.json +64 -0
- package/templates/example-draft/evidence/direction-input-v4.json +64 -0
- package/templates/example-draft/evidence/fix-ledger/lantern.json +17 -0
- package/templates/example-draft/evidence/parts-check.json +127 -0
- package/templates/example-draft/evidence/parts-review.json +88 -0
- package/templates/example-draft/evidence/review-reads/4ed0507381809746443021554e012032be5b5f8417da0f2e525e9b6df16e9f05.json +59 -0
- package/templates/example-draft/evidence/scene/s1.json +65 -0
- package/templates/example-draft/evidence/scene-background.png +0 -0
- package/templates/example-draft/evidence/scene-citations.json +9 -0
- package/templates/example-draft/evidence/structure-decisions.json +9 -0
- package/templates/example-draft/evidence/v1/interview-config.json +71 -0
- package/templates/example-draft/evidence/v1/interview-requirements.json +8 -0
- package/templates/example-draft/evidence/v1/review-config.json +71 -0
- package/templates/example-draft/evidence/v2/interview-config.json +71 -0
- package/templates/example-draft/evidence/v2/interview-requirements.json +8 -0
- package/templates/example-draft/evidence/v2/review-config.json +71 -0
- package/templates/example-draft/input/painting_in_rig.png +0 -0
- package/templates/example-draft/input/parts/background.png +0 -0
- package/templates/example-draft/input/parts/eye_l.png +0 -0
- package/templates/example-draft/input/parts/eye_r.png +0 -0
- package/templates/example-draft/input/parts/face.png +0 -0
- package/templates/example-draft/input/parts/lantern.png +0 -0
- package/templates/example-draft/input/parts/torso.png +0 -0
- package/templates/example-draft/input/parts.json +188 -0
- package/templates/example-draft/input/recomposite_rig.png +0 -0
- package/templates/example-draft/inputs/painting.png +0 -0
- package/templates/example-draft/interview-requirements.json +8 -0
- package/templates/example-draft/out/check/build/run.json +40 -0
- package/templates/example-draft/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/out/images/background.png +0 -0
- package/templates/example-draft/out/images/eye_l.png +0 -0
- package/templates/example-draft/out/images/eye_r.png +0 -0
- package/templates/example-draft/out/images/face.png +0 -0
- package/templates/example-draft/out/images/lantern.png +0 -0
- package/templates/example-draft/out/images/torso.png +0 -0
- package/templates/example-draft/out/mesh_report.json +1 -0
- package/templates/example-draft/out/motion.json +137 -0
- package/templates/example-draft/out/rig.json +102 -0
- package/templates/example-draft/out/run.json +52 -0
- package/templates/example-draft/outputs/v1/out/check/build/run.json +40 -0
- package/templates/example-draft/outputs/v1/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/outputs/v1/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/outputs/v1/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/outputs/v1/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/outputs/v1/out/painting_in_rig.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/background.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/eye_l.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/eye_r.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/face.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/lantern.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts/torso.png +0 -0
- package/templates/example-draft/outputs/v1/out/parts.json +188 -0
- package/templates/example-draft/outputs/v1/out/rig/images/background.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/eye_l.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/eye_r.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/face.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/lantern.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/images/torso.png +0 -0
- package/templates/example-draft/outputs/v1/out/rig/mesh_report.json +1 -0
- package/templates/example-draft/outputs/v1/out/rig/motion.json +137 -0
- package/templates/example-draft/outputs/v1/out/rig/rig.json +102 -0
- package/templates/example-draft/outputs/v1/out/rig/run.json +52 -0
- package/templates/example-draft/outputs/v2/out/check/build/run.json +40 -0
- package/templates/example-draft/outputs/v2/out/check/build/skeleton.atlas +28 -0
- package/templates/example-draft/outputs/v2/out/check/build/skeleton.json +193 -0
- package/templates/example-draft/outputs/v2/out/check/build/skeleton.model.json +483 -0
- package/templates/example-draft/outputs/v2/out/check/build/skeleton.png +0 -0
- package/templates/example-draft/outputs/v2/out/painting_in_rig.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/background.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/eye_l.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/eye_r.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/face.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/lantern.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts/torso.png +0 -0
- package/templates/example-draft/outputs/v2/out/parts.json +188 -0
- package/templates/example-draft/outputs/v2/out/rig/images/background.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/eye_l.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/eye_r.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/face.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/lantern.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/images/torso.png +0 -0
- package/templates/example-draft/outputs/v2/out/rig/mesh_report.json +1 -0
- package/templates/example-draft/outputs/v2/out/rig/motion.json +137 -0
- package/templates/example-draft/outputs/v2/out/rig/rig.json +102 -0
- package/templates/example-draft/outputs/v2/out/rig/run.json +52 -0
- package/templates/example-interview-choice.json +1068 -0
- package/templates/example-interview-options/constraint.json +376 -0
- package/templates/example-interview-options/example-assemble.json +376 -0
- package/templates/example-interview-options/example-bones.json +190 -0
- package/templates/example-interview-options/example-camera-depth.json +99 -0
- package/templates/example-interview-options/example-constraints.json +272 -0
- package/templates/example-interview-options/example-meshes.json +190 -0
- package/templates/example-interview-options/example-motion.json +272 -0
- package/templates/example-interview-options/example-picks.json +15 -0
- package/templates/example-interview-options/example-post-composition.json +110 -0
- package/templates/example-interview-options/example-post-timing.json +105 -0
- package/templates/example-interview-options/example-reference.json +63 -0
- package/templates/example-interview-options/example-rig-type.json +63 -0
- package/templates/example-interview-options/example-seethrough.json +190 -0
- package/templates/example-interview-options/fullscreen.json +63 -0
- package/templates/example-interview-options/other-explicit.json +63 -0
- package/templates/example-interview-options/other-pending.json +63 -0
- package/templates/example-interview-options/other-picks.json +16 -0
- package/templates/example-interview.md +213 -0
- package/templates/example-invariants/invariants.json +1339 -0
- package/templates/example-prepared/prepared.svg +1 -0
- package/templates/example-prepared/source.svg +1 -0
- package/templates/example-project.json +4259 -0
- package/templates/example-remesh/AGENTS.md +134 -0
- package/templates/example-remesh/CLAUDE.md +3 -0
- package/templates/example-remesh/evidence/.gitkeep +0 -0
- package/templates/example-remesh/evidence/approval.txt +1 -0
- package/templates/example-remesh/evidence/captions.json +101 -0
- package/templates/example-remesh/evidence/diff-components.json +34 -0
- package/templates/example-remesh/evidence/direction/v4.json +74 -0
- package/templates/example-remesh/evidence/direction/v5.json +74 -0
- package/templates/example-remesh/evidence/direction-input-v4.json +74 -0
- package/templates/example-remesh/evidence/direction-input-v5.json +74 -0
- package/templates/example-remesh/evidence/interview/assemble.json +309 -0
- package/templates/example-remesh/evidence/interview/bones.json +190 -0
- package/templates/example-remesh/evidence/interview/camera_depth.json +103 -0
- package/templates/example-remesh/evidence/interview/constraints.json +272 -0
- package/templates/example-remesh/evidence/interview/meshes.json +190 -0
- package/templates/example-remesh/evidence/interview/motion.json +272 -0
- package/templates/example-remesh/evidence/interview/post_composition.json +123 -0
- package/templates/example-remesh/evidence/interview/post_timing.json +103 -0
- package/templates/example-remesh/evidence/interview/reference.json +59 -0
- package/templates/example-remesh/evidence/interview/rig_type.json +59 -0
- package/templates/example-remesh/evidence/interview/seethrough.json +186 -0
- package/templates/example-remesh/evidence/parts-check.json +127 -0
- package/templates/example-remesh/evidence/parts-review.json +88 -0
- package/templates/example-remesh/evidence/reopen.txt +1 -0
- package/templates/example-remesh/evidence/review-reads/6a17db694db96f1b6049a61a017e0c8e66757b982b8851e0187e561cfcf9af68.json +59 -0
- package/templates/example-remesh/evidence/scene/s1.json +69 -0
- package/templates/example-remesh/evidence/scene-citations.json +9 -0
- package/templates/example-remesh/evidence/setup.png +0 -0
- package/templates/example-remesh/evidence/split-config.json +1 -0
- package/templates/example-remesh/evidence/structure.json +30 -0
- package/templates/example-remesh/evidence/v1/registration/at-source.json +44 -0
- package/templates/example-remesh/evidence/v1/registration/citations.json +68 -0
- package/templates/example-remesh/evidence/v1/registration/compile-report.json +368 -0
- package/templates/example-remesh/evidence/v1/registration/config-source.json +88 -0
- package/templates/example-remesh/evidence/v1/registration/config.json +88 -0
- package/templates/example-remesh/evidence/v1/registration/interview-requirements.json +8 -0
- package/templates/example-remesh/evidence/v1/registration/package-lock.json +11 -0
- package/templates/example-remesh/evidence/v1/registration/path-adapter.json +1 -0
- package/templates/example-remesh/evidence/v1/registration/rigc-run-1.json +44 -0
- package/templates/example-remesh/evidence/v2/registration/at-source.json +44 -0
- package/templates/example-remesh/evidence/v2/registration/citations.json +68 -0
- package/templates/example-remesh/evidence/v2/registration/compile-report.json +368 -0
- package/templates/example-remesh/evidence/v2/registration/config-source.json +88 -0
- package/templates/example-remesh/evidence/v2/registration/config.json +88 -0
- package/templates/example-remesh/evidence/v2/registration/interview-requirements.json +8 -0
- package/templates/example-remesh/evidence/v2/registration/package-lock.json +11 -0
- package/templates/example-remesh/evidence/v2/registration/path-adapter.json +1 -0
- package/templates/example-remesh/evidence/v2/registration/rigc-run-1.json +44 -0
- package/templates/example-remesh/evidence/v3/registration/at-source.json +44 -0
- package/templates/example-remesh/evidence/v3/registration/citations.json +68 -0
- package/templates/example-remesh/evidence/v3/registration/compile-report.json +368 -0
- package/templates/example-remesh/evidence/v3/registration/config-source.json +88 -0
- package/templates/example-remesh/evidence/v3/registration/config.json +88 -0
- package/templates/example-remesh/evidence/v3/registration/interview-requirements.json +8 -0
- package/templates/example-remesh/evidence/v3/registration/package-lock.json +11 -0
- package/templates/example-remesh/evidence/v3/registration/path-adapter.json +1 -0
- package/templates/example-remesh/evidence/v3/registration/rigc-run-1.json +44 -0
- package/templates/example-remesh/inputs/background.png +0 -0
- package/templates/example-remesh/inputs/painting.png +0 -0
- package/templates/example-remesh/inputs/references/.gitkeep +0 -0
- package/templates/example-remesh/package.json +9 -0
- package/templates/example-remesh/project.json +3916 -0
- package/templates/example-reopened/project.json +4568 -0
- package/templates/example-review/config.json +1 -0
- package/templates/example-review/out/check/build/skeleton.model.json +1 -0
- package/templates/example-review/out/parts/piece.svg +1 -0
- package/templates/example-review/out/parts.json +1 -0
- package/templates/example-review/out/rig/mesh_report.json +1 -0
- package/templates/example-review/out/rig/motion.json +1 -0
- package/templates/example-review/out/rig/rig.json +1 -0
- package/templates/example-split/captions.json +357 -0
- package/templates/example-split/config.json +215 -0
- package/templates/legacy-example-project.json +1071 -0
- package/templates/project.json +199 -0
- package/templates/shared/conventions.json +23 -0
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection pins (docs/COLLECTIONS.md § Pins) and `collection --check`.
|
|
3
|
+
*
|
|
4
|
+
* `collection pin` records a collection version: every member's selected or latest rig
|
|
5
|
+
* version and the sha256 of every rig file that version lists (model, atlas,
|
|
6
|
+
* config, per variant), hashed on disk where the file is present, plus the
|
|
7
|
+
* collection's own `outputs/v<N>/` files. It is run when a collection output
|
|
8
|
+
* is made.
|
|
9
|
+
*
|
|
10
|
+
* `collection --check` (also what `interview --check` runs on a collection)
|
|
11
|
+
* refuses, by member name, a member whose selected/default build no longer hashes to
|
|
12
|
+
* the pins of the latest collection version that has outputs — the output a
|
|
13
|
+
* person would be shown. It also checks the collection record itself: schema,
|
|
14
|
+
* stage, sign-offs, the collection's own answers, and that every member in
|
|
15
|
+
* `members[]` has a project record. It does not run each member's interview
|
|
16
|
+
* check: run `interview --check` in the member.
|
|
17
|
+
*/
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { basename, extname, join, relative, resolve } from 'node:path';
|
|
21
|
+
|
|
22
|
+
import { classify, collectSignoffs, COLLECTION_SCHEMA, readJsonObject, type Finding } from './check.ts';
|
|
23
|
+
import { COLLECTION_STAGES, COLLECTION_STAGE_VALUES } from './collection.ts';
|
|
24
|
+
import type { CommandResult } from './interview.ts';
|
|
25
|
+
import { refuse, USAGE_EXIT } from './refusal.ts';
|
|
26
|
+
import { collectionAnswerCommand } from './collection-answer.ts';
|
|
27
|
+
import type { Json, JsonObject } from './templates.ts';
|
|
28
|
+
|
|
29
|
+
export interface PinFile {
|
|
30
|
+
role: string;
|
|
31
|
+
path: string;
|
|
32
|
+
sha256: string;
|
|
33
|
+
}
|
|
34
|
+
export interface PinRig {
|
|
35
|
+
variant: string;
|
|
36
|
+
files: PinFile[];
|
|
37
|
+
}
|
|
38
|
+
export interface Pin {
|
|
39
|
+
member: string;
|
|
40
|
+
version: number | null;
|
|
41
|
+
rigs: PinRig[];
|
|
42
|
+
note?: string;
|
|
43
|
+
selection?: 'explicit';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A rig file of a member build, as the member records it and as it is on disk now. */
|
|
47
|
+
interface BuildFile {
|
|
48
|
+
variant: string;
|
|
49
|
+
role: string;
|
|
50
|
+
path: string;
|
|
51
|
+
recorded: string | null;
|
|
52
|
+
on_disk: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface CollectionReport {
|
|
56
|
+
readonly kind: 'collection';
|
|
57
|
+
readonly ok: boolean;
|
|
58
|
+
readonly collection_dir: string;
|
|
59
|
+
readonly stage: string | null;
|
|
60
|
+
readonly effective_stage: string | null;
|
|
61
|
+
readonly members: string[];
|
|
62
|
+
/** The collection version whose pins were checked: the latest one with outputs. */
|
|
63
|
+
readonly pins_checked: number | null;
|
|
64
|
+
readonly refusals: Finding[];
|
|
65
|
+
readonly warnings: Finding[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const DEFAULT_VARIANT = 'default';
|
|
69
|
+
const MODEL_EXT = new Set(['.json', '.skel']);
|
|
70
|
+
const ATLAS_EXT = new Set(['.atlas', '.png', '.webp', '.jpg', '.jpeg', '.ktx2']);
|
|
71
|
+
|
|
72
|
+
function isObject(v: Json | undefined): v is JsonObject {
|
|
73
|
+
return v !== null && v !== undefined && typeof v === 'object' && !Array.isArray(v);
|
|
74
|
+
}
|
|
75
|
+
function asArray(v: Json | undefined): Json[] {
|
|
76
|
+
return Array.isArray(v) ? v : [];
|
|
77
|
+
}
|
|
78
|
+
function asString(v: Json | undefined): string | null {
|
|
79
|
+
return typeof v === 'string' ? v : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function sha256File(file: string): string {
|
|
83
|
+
return createHash('sha256').update(readFileSync(file)).digest('hex');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function hashIfPresent(dir: string, path: string): string | null {
|
|
87
|
+
const file = join(dir, path);
|
|
88
|
+
return existsSync(file) && statSync(file).isFile() ? sha256File(file) : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The role of an output: its own `role` field when it carries one, else by
|
|
93
|
+
* extension — `.json`/`.skel` is the model, `.atlas` and page images are the
|
|
94
|
+
* atlas. Anything else (a take, a contact sheet) is not a rig file and is not
|
|
95
|
+
* pinned unless it says it is.
|
|
96
|
+
*/
|
|
97
|
+
export function outputRole(output: JsonObject): string | null {
|
|
98
|
+
const role = asString(output.role);
|
|
99
|
+
if (role !== null) return role;
|
|
100
|
+
const path = asString(output.path);
|
|
101
|
+
if (path === null) return null;
|
|
102
|
+
const ext = extname(path).toLowerCase();
|
|
103
|
+
if (MODEL_EXT.has(ext)) return 'model';
|
|
104
|
+
if (ATLAS_EXT.has(ext)) return 'atlas';
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A member's latest rig version (`kind: "rig"`, or no kind), by `n`. */
|
|
109
|
+
export function latestRigVersion(project: JsonObject): JsonObject | null {
|
|
110
|
+
let best: JsonObject | null = null;
|
|
111
|
+
for (const v of asArray(project.versions)) {
|
|
112
|
+
if (!isObject(v) || typeof v.n !== 'number') continue;
|
|
113
|
+
if (v.kind !== undefined && v.kind !== 'rig') continue;
|
|
114
|
+
if (best === null || (v.n as number) > (best.n as number)) best = v;
|
|
115
|
+
}
|
|
116
|
+
return best;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The rig files a member version lists: its model and atlas outputs, and the config it was built from, for every variant. */
|
|
120
|
+
function buildFiles(memberDir: string, version: JsonObject): BuildFile[] {
|
|
121
|
+
const files: BuildFile[] = [];
|
|
122
|
+
for (const o of asArray(version.outputs)) {
|
|
123
|
+
if (!isObject(o)) continue;
|
|
124
|
+
const role = outputRole(o);
|
|
125
|
+
const path = asString(o.path);
|
|
126
|
+
if (role === null || path === null) continue;
|
|
127
|
+
files.push({ variant: asString(o.variant) ?? DEFAULT_VARIANT, role, path, recorded: asString(o.sha256), on_disk: hashIfPresent(memberDir, path) });
|
|
128
|
+
}
|
|
129
|
+
const provenance = isObject(version.provenance) ? version.provenance : {};
|
|
130
|
+
const config = isObject(provenance.config) ? provenance.config : null;
|
|
131
|
+
const configPath = config === null ? null : asString(config.path);
|
|
132
|
+
if (config !== null && configPath !== null) {
|
|
133
|
+
const variants = [...new Set(files.map((f) => f.variant))];
|
|
134
|
+
for (const variant of variants.length > 0 ? variants : [DEFAULT_VARIANT]) {
|
|
135
|
+
files.push({ variant, role: 'config', path: configPath, recorded: asString(config.sha256), on_disk: hashIfPresent(memberDir, configPath) });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return files;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const current = (f: BuildFile): string | null => f.on_disk ?? f.recorded;
|
|
142
|
+
const fileKey = (variant: string, role: string, path: string): string => `${variant}\u0000${role}\u0000${path}`;
|
|
143
|
+
|
|
144
|
+
interface Member {
|
|
145
|
+
name: string;
|
|
146
|
+
dir: string;
|
|
147
|
+
project: JsonObject | null;
|
|
148
|
+
path: string;
|
|
149
|
+
why: string | null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function readMembers(dir: string, collection: JsonObject, refusals: Finding[]): Member[] {
|
|
153
|
+
const out: Member[] = [];
|
|
154
|
+
asArray(collection.members).forEach((m, index) => {
|
|
155
|
+
const name = isObject(m) ? asString(m.name) : null;
|
|
156
|
+
if (!isObject(m) || name === null) {
|
|
157
|
+
refusals.push({ code: 'MEMBER_MALFORMED', path: `members[${index}]`, message: `members[${index}]: a member is an object with a string name` });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const projectRel = asString(m.project) ?? `members/${name}/project.json`;
|
|
161
|
+
const file = resolve(dir, projectRel);
|
|
162
|
+
const memberDir = resolve(file, '..');
|
|
163
|
+
const path = `members[${name}]`;
|
|
164
|
+
if (!existsSync(file)) {
|
|
165
|
+
out.push({ name, dir: memberDir, project: null, path, why: `${projectRel} does not exist` });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const read = readJsonObject(file);
|
|
169
|
+
out.push(read.ok ? { name, dir: memberDir, project: read.value, path, why: null } : { name, dir: memberDir, project: null, path, why: `${projectRel}: ${read.error}` });
|
|
170
|
+
});
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ------------------------------------------------------------------- pinning
|
|
175
|
+
|
|
176
|
+
export interface PinResult {
|
|
177
|
+
version: number;
|
|
178
|
+
pins: Pin[];
|
|
179
|
+
outputs: { path: string; sha256: string; bytes: number }[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function listFiles(root: string): string[] {
|
|
183
|
+
if (!existsSync(root)) return [];
|
|
184
|
+
const out: string[] = [];
|
|
185
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
186
|
+
const full = join(root, entry.name);
|
|
187
|
+
if (entry.isDirectory()) out.push(...listFiles(full));
|
|
188
|
+
else if (entry.isFile()) out.push(full);
|
|
189
|
+
}
|
|
190
|
+
return out.sort();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Builds pins from an explicitly selected rig version or the member's latest.
|
|
195
|
+
* Refuses by member name — all of them, not the first — a member with
|
|
196
|
+
* no project record, no rig version, no rig file, or a file on disk that no
|
|
197
|
+
* longer hashes to the version that lists it (rebuilt in place: record a new
|
|
198
|
+
* member version first, so the pin names a build that exists).
|
|
199
|
+
*/
|
|
200
|
+
export function buildPins(dir: string, collection: JsonObject, selectors: string[] = []): { pins: Pin[]; refusals: Finding[] } {
|
|
201
|
+
const refusals: Finding[] = [];
|
|
202
|
+
const members = readMembers(dir, collection, refusals);
|
|
203
|
+
const selected = new Map<string, number>();
|
|
204
|
+
for (const selector of selectors) {
|
|
205
|
+
const match = /^([^=]+)=([1-9][0-9]*)$/.exec(selector);
|
|
206
|
+
const name = match?.[1];
|
|
207
|
+
const n = Number(match?.[2]);
|
|
208
|
+
if (name === undefined || !Number.isSafeInteger(n) || selected.has(name) || !members.some(m => m.name === name)) {
|
|
209
|
+
refusals.push({ code: 'PIN_SELECTION_INVALID', path: 'members', message: `members: invalid or duplicate --member ${selector}; supply each registered name once as name=positive-integer-version` });
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
selected.set(name, n);
|
|
213
|
+
}
|
|
214
|
+
if (members.length === 0 && refusals.length === 0) {
|
|
215
|
+
refusals.push({ code: 'PIN_NO_MEMBERS', path: 'members', message: 'collection.json members[] is empty; there is nothing to pin' });
|
|
216
|
+
}
|
|
217
|
+
const pins: Pin[] = [];
|
|
218
|
+
for (const m of members) {
|
|
219
|
+
if (m.project === null) {
|
|
220
|
+
refusals.push({ code: 'MEMBER_PROJECT_MISSING', path: m.path, message: `${m.path}: ${m.why}` });
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const wanted = selected.get(m.name);
|
|
224
|
+
const version = wanted === undefined ? latestRigVersion(m.project) : asArray(m.project.versions).filter(isObject).find(v => v.n === wanted && (v.kind === undefined || v.kind === 'rig')) ?? null;
|
|
225
|
+
if (wanted !== undefined && version === null) {
|
|
226
|
+
refusals.push({ code: 'PIN_SELECTION_INVALID', path: m.path, message: `${m.path}: selected rig version ${wanted} is absent from versions[]; select an existing rig version` });
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (version === null) {
|
|
230
|
+
refusals.push({ code: 'PIN_NO_BUILD', path: m.path, message: `${m.path}: has no rig version in versions[]; build and record one before pinning` });
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const n = version.n as number;
|
|
234
|
+
const files = buildFiles(m.dir, version);
|
|
235
|
+
if (!files.some((f) => f.role !== 'config')) {
|
|
236
|
+
refusals.push({ code: 'PIN_NO_BUILD', path: m.path, message: `${m.path}: version ${n} lists no model or atlas output` });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
let ok = true;
|
|
240
|
+
for (const f of files) {
|
|
241
|
+
if (current(f) === null) {
|
|
242
|
+
refusals.push({ code: 'PIN_NO_BUILD', path: m.path, message: `${m.path}: ${f.role} ${f.path} has no sha256 in version ${n} and is not on disk` });
|
|
243
|
+
ok = false;
|
|
244
|
+
} else if (f.on_disk !== null && f.recorded !== null && f.on_disk !== f.recorded) {
|
|
245
|
+
refusals.push({
|
|
246
|
+
code: 'PIN_SOURCE_DRIFT',
|
|
247
|
+
path: m.path,
|
|
248
|
+
message: `${m.path}: ${f.role} ${f.path} on disk does not hash to version ${n}'s record; it was rebuilt in place — record a new member version, then pin`,
|
|
249
|
+
});
|
|
250
|
+
ok = false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (!ok) continue;
|
|
254
|
+
const rigs: PinRig[] = [];
|
|
255
|
+
for (const f of files) {
|
|
256
|
+
let rig = rigs.find((r) => r.variant === f.variant);
|
|
257
|
+
if (rig === undefined) {
|
|
258
|
+
rig = { variant: f.variant, files: [] };
|
|
259
|
+
rigs.push(rig);
|
|
260
|
+
}
|
|
261
|
+
rig.files.push({ role: f.role, path: f.path, sha256: current(f) as string });
|
|
262
|
+
}
|
|
263
|
+
pins.push({ member: m.name, version: n, rigs, ...(wanted === undefined ? {} : { selection: 'explicit' as const }) });
|
|
264
|
+
}
|
|
265
|
+
return { pins, refusals };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Writes a new `versions[]` entry with pins and the hashed `outputs/v<N>/` files. */
|
|
269
|
+
export function pinCollection(dirArg: string, changed: string | null, now: Date, selectors: string[] = []): { result: PinResult | null; refusals: Finding[]; dir: string } {
|
|
270
|
+
const dir = resolve(dirArg);
|
|
271
|
+
const file = join(dir, 'collection.json');
|
|
272
|
+
if (!existsSync(file)) refuse('COLLECTION_JSON_MISSING', `${file} does not exist; run in a collection folder or name one`);
|
|
273
|
+
const read = readJsonObject(file);
|
|
274
|
+
if (!read.ok) refuse('COLLECTION_JSON_INVALID', `${file}: ${read.error}`);
|
|
275
|
+
const collection = read.value;
|
|
276
|
+
if (collection.schema !== COLLECTION_SCHEMA) refuse('SCHEMA_UNKNOWN', `${file}: schema is ${JSON.stringify(collection.schema ?? null)}; this reads ${COLLECTION_SCHEMA}`);
|
|
277
|
+
const { pins, refusals } = buildPins(dir, collection, selectors);
|
|
278
|
+
if (refusals.length > 0) return { result: null, refusals, dir };
|
|
279
|
+
const versions = asArray(collection.versions);
|
|
280
|
+
const n = versions.reduce<number>((max, v) => (isObject(v) && typeof v.n === 'number' && v.n > max ? v.n : max), 0) + 1;
|
|
281
|
+
const outputs = listFiles(join(dir, 'outputs', `v${n}`)).map((f) => ({ path: relative(dir, f).split('\\').join('/'), sha256: sha256File(f), bytes: statSync(f).size }));
|
|
282
|
+
const entry: JsonObject = {
|
|
283
|
+
n,
|
|
284
|
+
at: now.toISOString(),
|
|
285
|
+
pins: pins as unknown as Json,
|
|
286
|
+
composition_inputs: [],
|
|
287
|
+
outputs,
|
|
288
|
+
evidence: [],
|
|
289
|
+
verdicts: [],
|
|
290
|
+
changed,
|
|
291
|
+
answers_feedback: [],
|
|
292
|
+
};
|
|
293
|
+
collection.versions = [...versions, entry];
|
|
294
|
+
writeFileSync(file, `${JSON.stringify(collection, null, 2)}\n`);
|
|
295
|
+
return { result: { version: n, pins, outputs }, refusals: [], dir };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ------------------------------------------------------------------ checking
|
|
299
|
+
|
|
300
|
+
function readPins(raw: Json | undefined, version: number, refusals: Finding[]): Pin[] {
|
|
301
|
+
const pins: Pin[] = [];
|
|
302
|
+
asArray(raw).forEach((p, index) => {
|
|
303
|
+
const member = isObject(p) ? asString(p.member) : null;
|
|
304
|
+
if (!isObject(p) || member === null || (p.version !== null && typeof p.version !== 'number') || !Array.isArray(p.rigs)) {
|
|
305
|
+
refusals.push({ code: 'PIN_MALFORMED', path: `versions[${version}].pins[${index}]`, message: `versions[n=${version}].pins[${index}]: a pin is { member, version (number or null), rigs[] }` });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (p.selection !== undefined && (p.selection !== 'explicit' || typeof p.version !== 'number' || !Number.isSafeInteger(p.version) || p.version < 1)) {
|
|
309
|
+
refusals.push({ code: 'PIN_SELECTION_INVALID', path: `versions[${version}].pins[${index}]`, message: `versions[n=${version}].pins[${index}]: selection must be explicit with a positive integer rig version; restore the pin or record a new collection pin --member name=version` });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// A pin that cannot be compared is malformed, never a silent pass: every rig
|
|
313
|
+
// carries a non-empty files[] and every file a role, path and sha256.
|
|
314
|
+
const rigs: PinRig[] = [];
|
|
315
|
+
let unreadable: string | null = p.rigs.length === 0 ? 'rigs[] is empty' : null;
|
|
316
|
+
p.rigs.forEach((r, ri) => {
|
|
317
|
+
if (unreadable !== null) return;
|
|
318
|
+
if (!isObject(r) || !Array.isArray(r.files) || r.files.length === 0) {
|
|
319
|
+
unreadable = `rigs[${ri}] has no files[] (each rig is { variant, files[{ role, path, sha256 }] })`;
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const files: PinFile[] = [];
|
|
323
|
+
for (const [fi, f] of r.files.entries()) {
|
|
324
|
+
const role = isObject(f) ? asString(f.role) : null;
|
|
325
|
+
const path = isObject(f) ? asString(f.path) : null;
|
|
326
|
+
const sha256 = isObject(f) ? asString(f.sha256) : null;
|
|
327
|
+
if (role === null || path === null || sha256 === null) {
|
|
328
|
+
unreadable = `rigs[${ri}].files[${fi}] needs role, path and sha256`;
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
files.push({ role, path, sha256 });
|
|
332
|
+
}
|
|
333
|
+
rigs.push({ variant: asString(r.variant) ?? DEFAULT_VARIANT, files });
|
|
334
|
+
});
|
|
335
|
+
if (unreadable !== null) {
|
|
336
|
+
refusals.push({ code: 'PIN_MALFORMED', path: `versions[${version}].pins[${index}]`, message: `versions[n=${version}].pins[${index}] (${member}): ${unreadable}; a pin that cannot be compared verifies nothing` });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
pins.push({ member, version: p.version as number | null, rigs, ...(p.selection === 'explicit' ? { selection: 'explicit' as const } : {}) });
|
|
340
|
+
});
|
|
341
|
+
return pins;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Every way a member's current build differs from its pin, one refusal each, all naming the member. */
|
|
345
|
+
function comparePin(m: Member, project: JsonObject, pin: Pin, cv: number, refusals: Finding[]): void {
|
|
346
|
+
const stale = (message: string): void => {
|
|
347
|
+
refusals.push({ code: 'PIN_STALE', path: m.path, message: `${m.path}: ${message}; collection v${cv}'s output is stale until a new collection version pins it` });
|
|
348
|
+
};
|
|
349
|
+
const version = pin.selection === 'explicit' ? asArray(project.versions).filter(isObject).find(v => v.n === pin.version && (v.kind === undefined || v.kind === 'rig')) ?? null : latestRigVersion(project);
|
|
350
|
+
if (pin.version === null) {
|
|
351
|
+
if (version !== null) {
|
|
352
|
+
stale(`has rig version ${version.n as number}, which is not pinned (pin v${cv} predates the member's versions[])`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
for (const rig of pin.rigs) {
|
|
356
|
+
for (const f of rig.files) {
|
|
357
|
+
const disk = hashIfPresent(m.dir, f.path);
|
|
358
|
+
if (disk !== null && disk !== f.sha256) stale(`${f.role} sha256 differs from pin v${cv} (${f.path}, variant ${rig.variant})`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (version === null) {
|
|
364
|
+
stale(pin.selection === 'explicit' ? `recorded rig version ${pin.version} no longer exists for explicit pin v${cv}` : `has no rig version; pin v${cv} names version ${pin.version}`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const n = version.n as number;
|
|
368
|
+
if (n !== pin.version) {
|
|
369
|
+
stale(`rig version ${n} is not pinned; pin v${cv} names version ${pin.version}`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const files = buildFiles(m.dir, version);
|
|
373
|
+
const now = new Map(files.map((f) => [fileKey(f.variant, f.role, f.path), f]));
|
|
374
|
+
const pinned = new Set<string>();
|
|
375
|
+
for (const rig of pin.rigs) {
|
|
376
|
+
for (const f of rig.files) {
|
|
377
|
+
const key = fileKey(rig.variant, f.role, f.path);
|
|
378
|
+
pinned.add(key);
|
|
379
|
+
const build = now.get(key);
|
|
380
|
+
if (build === undefined) {
|
|
381
|
+
stale(`${f.role} ${f.path} (variant ${rig.variant}) is pinned in v${cv} but version ${n} no longer lists it`);
|
|
382
|
+
} else if (current(build) !== f.sha256 || (pin.selection === 'explicit' && build.recorded !== null && build.recorded !== f.sha256)) {
|
|
383
|
+
stale(`${f.role} sha256 differs from pin v${cv} (${f.path}, variant ${rig.variant}${build.on_disk !== null ? ', hashed on disk' : ''})`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
for (const f of files) {
|
|
388
|
+
if (!pinned.has(fileKey(f.variant, f.role, f.path))) stale(`${f.role} ${f.path} (variant ${f.variant}) of version ${n} is not pinned in v${cv}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function checkPins(dir: string, collection: JsonObject, members: Member[], refusals: Finding[], warnings: Finding[]): number | null {
|
|
393
|
+
const versions = asArray(collection.versions).filter(isObject);
|
|
394
|
+
const shown = versions
|
|
395
|
+
.filter((v) => typeof v.n === 'number' && asArray(v.outputs).length > 0)
|
|
396
|
+
.sort((a, b) => (b.n as number) - (a.n as number))[0];
|
|
397
|
+
if (shown === undefined) return null;
|
|
398
|
+
const cv = shown.n as number;
|
|
399
|
+
const pins = readPins(shown.pins, cv, refusals);
|
|
400
|
+
for (const m of members) {
|
|
401
|
+
if (m.project === null) continue;
|
|
402
|
+
const pin = pins.find((p) => p.member === m.name);
|
|
403
|
+
if (pin === undefined) {
|
|
404
|
+
refusals.push({ code: 'PIN_MISSING', path: m.path, message: `${m.path}: is a member but collection v${cv} has no pin for it; its output was not made from a pinned build of this member` });
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
comparePin(m, m.project, pin, cv, refusals);
|
|
408
|
+
}
|
|
409
|
+
for (const p of pins) {
|
|
410
|
+
if (!members.some((m) => m.name === p.member)) {
|
|
411
|
+
refusals.push({ code: 'PIN_UNKNOWN_MEMBER', path: `members[${p.member}]`, message: `collection v${cv} pins '${p.member}', which is not in members[]` });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
for (const o of asArray(shown.outputs)) {
|
|
415
|
+
if (!isObject(o)) continue;
|
|
416
|
+
const path = asString(o.path);
|
|
417
|
+
if (path === null) continue;
|
|
418
|
+
const disk = hashIfPresent(dir, path);
|
|
419
|
+
if (disk !== null && disk !== asString(o.sha256)) {
|
|
420
|
+
warnings.push({ code: 'OUTPUT_CHANGED', path: `versions[${cv}].outputs`, message: `${path} on disk does not hash to collection v${cv}'s record; an output is frozen when pinned` });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return cv;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Collection answers as `interview --check` reads a project's, against the collection's own stages. */
|
|
427
|
+
export function checkCollectionRecord(collection: JsonObject, stage: string, refusals: Finding[], warnings: Finding[] = []): string {
|
|
428
|
+
const signoffs = collectSignoffs(collection, refusals);
|
|
429
|
+
const order: readonly string[] = COLLECTION_STAGES.map((s) => s.id);
|
|
430
|
+
const effective = stage === 'paused'
|
|
431
|
+
? (COLLECTION_STAGES.find((s) => s.closed_by === 'signoff' && !signoffs.some((x) => x.stage === s.id && x.checkpoint === null))?.id ?? 'accepted')
|
|
432
|
+
: stage;
|
|
433
|
+
const cur = order.indexOf(effective);
|
|
434
|
+
for (const s of signoffs) {
|
|
435
|
+
if (!order.includes(s.stage) && s.stage !== 'paused') {
|
|
436
|
+
refusals.push({ code: 'SIGNOFF_UNKNOWN_STAGE', path: `signoffs[${s.index}]`, message: `signoffs[${s.index}]: stage '${s.stage}' is not a collection stage (${COLLECTION_STAGE_VALUES.join(', ')})` });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (stage === 'paused' && !signoffs.some((s) => s.stage === 'paused')) {
|
|
440
|
+
refusals.push({ code: 'PAUSED_UNSIGNED', path: 'stage', message: 'stage is paused, but no { "stage": "paused" } sign-off stopped it' });
|
|
441
|
+
}
|
|
442
|
+
COLLECTION_STAGES.forEach((spec, idx) => {
|
|
443
|
+
if (idx >= cur || spec.closed_by !== 'signoff') return;
|
|
444
|
+
if (!signoffs.some((s) => s.stage === spec.id && s.checkpoint === null)) {
|
|
445
|
+
refusals.push({ code: 'STAGE_UNSIGNED', path: 'stage', message: `stage is '${stage}', past '${spec.id}', but no { "stage": "${spec.id}", "checkpoint": null } sign-off closed it` });
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const interview = isObject(collection.interview) ? collection.interview : {};
|
|
450
|
+
const post = isObject(interview.post) ? interview.post : {};
|
|
451
|
+
const groups: [Json | undefined, string, 'answer' | 'guide', boolean][] = [
|
|
452
|
+
[interview.questions, 'questions', 'answer', false],
|
|
453
|
+
[interview.framing, 'framing', 'answer', false],
|
|
454
|
+
[interview.waypoints, 'waypoints', 'guide', false],
|
|
455
|
+
[post.items, 'post.items', 'guide', true],
|
|
456
|
+
];
|
|
457
|
+
const postIdx = order.indexOf('post-interview');
|
|
458
|
+
for (const [list, name, field, isPost] of groups) {
|
|
459
|
+
for (const item of asArray(list)) {
|
|
460
|
+
if (!isObject(item) || typeof item.id !== 'string') continue;
|
|
461
|
+
const path = `interview.${name}[${item.id}].${field}`;
|
|
462
|
+
const raw = item[field];
|
|
463
|
+
if (isObject(raw) && 'deferred' in raw) {
|
|
464
|
+
const deferred = raw.deferred;
|
|
465
|
+
const deadline = isObject(deferred) ? order.indexOf(String(deferred.until_stage)) : -1;
|
|
466
|
+
const valid = name === 'questions' && item.scope === 'collection'
|
|
467
|
+
&& Object.keys(raw).length === 1 && isObject(deferred)
|
|
468
|
+
&& ['by', 'at', 'reason'].every(key => typeof deferred[key] === 'string' && String(deferred[key]).trim() !== '')
|
|
469
|
+
&& Number.isFinite(Date.parse(String(deferred.at))) && typeof deferred.until_stage === 'string' && deadline >= 0;
|
|
470
|
+
if (!valid) {
|
|
471
|
+
refusals.push({ code: 'ANSWER_MALFORMED', path, message: `${path}: a collection question deferral needs by, valid at, reason and an ordered until_stage (not paused); record it with collection answer --defer-until` });
|
|
472
|
+
} else if (cur < deadline) {
|
|
473
|
+
warnings.push({ code: 'COLLECTION_ANSWER_DEFERRED', path, message: `${path}: deferred by ${String(deferred.by)} until ${String(deferred.until_stage)} (${String(deferred.reason)}); record an answer with collection answer --value-file before that stage` });
|
|
474
|
+
} else {
|
|
475
|
+
refusals.push({ code: cur === deadline ? 'INTERVIEW_INCOMPLETE' : 'COLLECTION_ITEM_SILENT', path, message: `${path}: deferral reached ${String(deferred.until_stage)}; record an answer with collection answer --value-file` });
|
|
476
|
+
}
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const state = classify(raw, { collection: null });
|
|
480
|
+
if (state.state === 'malformed') {
|
|
481
|
+
refusals.push({ code: 'ANSWER_MALFORMED', path, message: `${path}: ${state.why}` });
|
|
482
|
+
} else if (state.state === 'silent') {
|
|
483
|
+
const gate = isPost ? postIdx : 0;
|
|
484
|
+
if (cur < gate) continue;
|
|
485
|
+
if (cur === gate) refusals.push({ code: 'INTERVIEW_INCOMPLETE', path, message: `${path}: not answered yet; the collection interview is incomplete (answer "per-member" to leave it to each member)` });
|
|
486
|
+
else refusals.push({ code: 'COLLECTION_ITEM_SILENT', path, message: `${path}: no answer, no delegation and not "per-member", yet the collection stage is past ${isPost ? 'post-interview' : 'interview'}` });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return effective;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function checkCollection(dirArg: string): CollectionReport {
|
|
494
|
+
const dir = resolve(dirArg);
|
|
495
|
+
const refusals: Finding[] = [];
|
|
496
|
+
const warnings: Finding[] = [];
|
|
497
|
+
const fail = (): CollectionReport => ({ kind: 'collection', ok: false, collection_dir: dir, stage: null, effective_stage: null, members: [], pins_checked: null, refusals, warnings });
|
|
498
|
+
const file = join(dir, 'collection.json');
|
|
499
|
+
if (!existsSync(file)) {
|
|
500
|
+
refusals.push({ code: 'COLLECTION_JSON_MISSING', path: file, message: `${file} does not exist; run in a collection folder or name one` });
|
|
501
|
+
return fail();
|
|
502
|
+
}
|
|
503
|
+
const read = readJsonObject(file);
|
|
504
|
+
if (!read.ok) {
|
|
505
|
+
refusals.push({ code: 'COLLECTION_JSON_INVALID', path: file, message: `${file}: ${read.error}` });
|
|
506
|
+
return fail();
|
|
507
|
+
}
|
|
508
|
+
const collection = read.value;
|
|
509
|
+
if (collection.schema !== COLLECTION_SCHEMA) {
|
|
510
|
+
refusals.push({ code: 'SCHEMA_UNKNOWN', path: 'schema', message: `schema is ${JSON.stringify(collection.schema ?? null)}; this check reads ${COLLECTION_SCHEMA}` });
|
|
511
|
+
return fail();
|
|
512
|
+
}
|
|
513
|
+
const stage = asString(collection.stage);
|
|
514
|
+
if (stage === null || !COLLECTION_STAGE_VALUES.includes(stage)) {
|
|
515
|
+
refusals.push({ code: 'STAGE_UNKNOWN', path: 'stage', message: `stage is ${JSON.stringify(collection.stage ?? null)}; the collection stages are ${COLLECTION_STAGE_VALUES.join(', ')}` });
|
|
516
|
+
return fail();
|
|
517
|
+
}
|
|
518
|
+
const recordValues = asArray(collection.stage_values).filter((x): x is string => typeof x === 'string');
|
|
519
|
+
if (recordValues.join(',') !== COLLECTION_STAGE_VALUES.join(',')) {
|
|
520
|
+
warnings.push({ code: 'STAGE_VALUES_STALE', path: 'stage_values', message: `stage_values is [${recordValues.join(', ')}]; the collection order is [${COLLECTION_STAGE_VALUES.join(', ')}]` });
|
|
521
|
+
}
|
|
522
|
+
const effective = checkCollectionRecord(collection, stage, refusals, warnings);
|
|
523
|
+
const members = readMembers(dir, collection, refusals);
|
|
524
|
+
for (const m of members) {
|
|
525
|
+
if (m.project === null) refusals.push({ code: 'MEMBER_PROJECT_MISSING', path: m.path, message: `${m.path}: ${m.why}` });
|
|
526
|
+
}
|
|
527
|
+
const membersDir = join(dir, 'members');
|
|
528
|
+
if (existsSync(membersDir)) {
|
|
529
|
+
for (const entry of readdirSync(membersDir, { withFileTypes: true })) {
|
|
530
|
+
if (entry.isDirectory() && !members.some((m) => basename(m.dir) === entry.name)) {
|
|
531
|
+
warnings.push({ code: 'MEMBER_UNREGISTERED', path: `members/${entry.name}`, message: `members/${entry.name}/ is not in collection.json members[]; register it (rig-create init --member makes and registers a member)` });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const pinsChecked = checkPins(dir, collection, members, refusals, warnings);
|
|
536
|
+
return { kind: 'collection', ok: refusals.length === 0, collection_dir: dir, stage, effective_stage: effective, members: members.map((m) => m.name), pins_checked: pinsChecked, refusals, warnings };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export function formatCollectionReport(report: CollectionReport): string {
|
|
540
|
+
const lines = [`collection --check: ${report.collection_dir}`];
|
|
541
|
+
if (report.stage !== null) lines.push(` stage ${report.stage}${report.effective_stage !== report.stage ? ` (checked as ${report.effective_stage})` : ''}`);
|
|
542
|
+
lines.push(` members (${report.members.length}): ${report.members.join(', ') || 'none'}`);
|
|
543
|
+
lines.push(report.pins_checked === null ? ' pins: no collection version has outputs yet; nothing to check against' : ` pins: checked against collection v${report.pins_checked} (the latest version with outputs)`);
|
|
544
|
+
for (const r of report.refusals) lines.push(` REFUSED ${r.code}: ${r.message}`);
|
|
545
|
+
for (const w of report.warnings) lines.push(` warning ${w.code}: ${w.message}`);
|
|
546
|
+
const n = (count: number, word: string): string => `${count} ${word}${count === 1 ? '' : 's'}`;
|
|
547
|
+
lines.push(report.ok ? `clean: ${n(report.warnings.length, 'warning')}` : `not clear: ${n(report.refusals.length, 'refusal')}, ${n(report.warnings.length, 'warning')}`);
|
|
548
|
+
return lines.join('\n');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export const COLLECTION_HELP = `collection:
|
|
552
|
+
answer <question> [<dir>] --by <who>
|
|
553
|
+
(--value-file <answer JSON file> | --defer-until <stage> --reason-file <file>)
|
|
554
|
+
record a collection-scope answer or defer it until a later lifecycle stage;
|
|
555
|
+
retain the prior answer in history when present, without changing other fields
|
|
556
|
+
pin [<dir>] [--changed <sentence>] [--member <name>=<version>]...
|
|
557
|
+
record a collection version: --member selects a rig version (repeatable); others use latest; hash each rig
|
|
558
|
+
file it lists (model, atlas, config per variant), and hash the collection's outputs/v<N>/
|
|
559
|
+
--check [<dir>] [--json]
|
|
560
|
+
refuse by member name a member rebuilt since the pins of the latest collection version with
|
|
561
|
+
outputs, and check the collection record (stage, sign-offs, answers, members[]) (docs/CHECKS.md)`;
|
|
562
|
+
|
|
563
|
+
export function collectionCommand(args: string[], now: number): CommandResult {
|
|
564
|
+
const [first, ...rest] = args;
|
|
565
|
+
if (first === 'answer') return collectionAnswerCommand(rest, now);
|
|
566
|
+
if (first === 'pin') {
|
|
567
|
+
let changed: string | null = null;
|
|
568
|
+
const selectors: string[] = [];
|
|
569
|
+
const positional: string[] = [];
|
|
570
|
+
for (let i = 0; i < rest.length; i++) {
|
|
571
|
+
const arg = rest[i] as string;
|
|
572
|
+
if (arg === '--changed') {
|
|
573
|
+
const value = rest[++i];
|
|
574
|
+
if (value === undefined) refuse('COLLECTION_USAGE', '--changed takes a sentence', USAGE_EXIT);
|
|
575
|
+
changed = value;
|
|
576
|
+
} else if (arg === '--member') {
|
|
577
|
+
const value = rest[++i];
|
|
578
|
+
if (value === undefined || value.startsWith('--')) refuse('PIN_SELECTION_INVALID', 'members: --member needs name=version; supply a registered member and existing rig version', USAGE_EXIT);
|
|
579
|
+
selectors.push(value);
|
|
580
|
+
} else if (arg.startsWith('-')) refuse('UNKNOWN_OPTION', `collection pin does not take ${arg}`, USAGE_EXIT);
|
|
581
|
+
else positional.push(arg);
|
|
582
|
+
}
|
|
583
|
+
if (positional.length > 1) refuse('COLLECTION_USAGE', `collection pin takes one folder, got ${positional.join(' ')}`, USAGE_EXIT);
|
|
584
|
+
const { result, refusals, dir } = pinCollection(positional[0] ?? process.cwd(), changed, new Date(now), selectors);
|
|
585
|
+
if (result === null) {
|
|
586
|
+
const lines = [`collection pin: ${dir}`, ...refusals.map((r) => ` REFUSED ${r.code}: ${r.message}`), `not pinned: ${refusals.length} refusal${refusals.length === 1 ? '' : 's'}`];
|
|
587
|
+
return { stdout: lines.join('\n'), exit: 1 };
|
|
588
|
+
}
|
|
589
|
+
const lines = [
|
|
590
|
+
`collection pin: ${dir}`,
|
|
591
|
+
` collection v${result.version}: ${result.pins.map((p) => `${p.member} v${p.version}`).join(', ')}`,
|
|
592
|
+
` outputs/v${result.version}/: ${result.outputs.length} file${result.outputs.length === 1 ? '' : 's'} hashed`,
|
|
593
|
+
...(result.outputs.length === 0 ? [` no outputs/v${result.version}/ yet: this version is not checked until it has outputs (make the output, then pin)`] : []),
|
|
594
|
+
'collection.json is written, not committed.',
|
|
595
|
+
];
|
|
596
|
+
return { stdout: lines.join('\n'), exit: 0 };
|
|
597
|
+
}
|
|
598
|
+
let check = false;
|
|
599
|
+
let json = false;
|
|
600
|
+
const positional: string[] = [];
|
|
601
|
+
for (const arg of args) {
|
|
602
|
+
if (arg === '--check') check = true;
|
|
603
|
+
else if (arg === '--json') json = true;
|
|
604
|
+
else if (arg.startsWith('-')) refuse('UNKNOWN_OPTION', `collection does not take ${arg}`, USAGE_EXIT);
|
|
605
|
+
else positional.push(arg);
|
|
606
|
+
}
|
|
607
|
+
if (!check) refuse('COLLECTION_USAGE', 'collection needs pin or --check: rig-create collection pin [<dir>] | rig-create collection --check [<dir>]', USAGE_EXIT);
|
|
608
|
+
if (positional.length > 1) refuse('COLLECTION_USAGE', `collection --check takes one folder, got ${positional.join(' ')}`, USAGE_EXIT);
|
|
609
|
+
const report = checkCollection(positional[0] ?? process.cwd());
|
|
610
|
+
return { stdout: json ? JSON.stringify(report, null, 2) : formatCollectionReport(report), exit: report.ok ? 0 : 1 };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Where `interview --check` was pointed, when that is a collection: a folder with collection.json and no project.json, or the collection.json itself. */
|
|
614
|
+
export function collectionDirOf(target: string): string | null {
|
|
615
|
+
const abs = resolve(target);
|
|
616
|
+
if (basename(abs) === 'collection.json' && existsSync(abs) && statSync(abs).isFile()) return resolve(abs, '..');
|
|
617
|
+
if (existsSync(join(abs, 'collection.json')) && !existsSync(join(abs, 'project.json'))) return abs;
|
|
618
|
+
return null;
|
|
619
|
+
}
|
package/src/pins.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rig-c packages a new project uses, at exact versions. This is the only
|
|
3
|
+
* place they are written: init copies them into the project's package.json,
|
|
4
|
+
* and a test checks they stay exact (no ranges).
|
|
5
|
+
*
|
|
6
|
+
* Core is pinned as `@rig-c/core`, its canonical name (rig-c/core README) and
|
|
7
|
+
* the name `@rig-c/parts` depends on. `rig-c` is the same package under a
|
|
8
|
+
* convenience name; pinning it as well installs core twice, with two `rigc` bins.
|
|
9
|
+
*
|
|
10
|
+
* `interview.question_set` in templates/project.json names the @rig-c/parts
|
|
11
|
+
* version its waypoints were read from; a test keeps it equal to the pin, so
|
|
12
|
+
* bumping parts here without re-reading the waypoints fails.
|
|
13
|
+
*/
|
|
14
|
+
export const PINNED_PACKAGES = {
|
|
15
|
+
'@rig-c/core': '3.7.0',
|
|
16
|
+
'@rig-c/parts': '2.37.0',
|
|
17
|
+
} as const satisfies Record<string, string>;
|