@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
package/src/feedback.ts
ADDED
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
import { assertDirectionApproval } from './direction.ts';
|
|
2
|
+
import { assertFigureMaskApproval } from './figure-mask.ts';
|
|
3
|
+
import { assertCorrectionApproval } from './correction-writers.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Polish feedback (rig-c/create#9, docs/INTERVIEW.md § Polish feedback):
|
|
6
|
+
* what the user says after seeing a version is kept word for word in that
|
|
7
|
+
* version's `verdicts[]`, tied to the answers it is about (`on_items[]`), and
|
|
8
|
+
* answered by a later version's `answers_feedback`.
|
|
9
|
+
*
|
|
10
|
+
* Two halves:
|
|
11
|
+
* - `rig-create review <version> …` records a verdict. It derives each item's
|
|
12
|
+
* `answer_was` from the record where the record decides it, refuses a value
|
|
13
|
+
* the record contradicts, and writes a `not-asked` item as a new sentence in
|
|
14
|
+
* that answer's `additions[]` (never a silent fix). It never moves `stage`
|
|
15
|
+
* and never signs anything: a verdict is not a sign-off.
|
|
16
|
+
* - `checkFeedback` runs inside `interview --check`: bounced feedback with a
|
|
17
|
+
* later version that does not answer it, an answering ref that points at no
|
|
18
|
+
* verdict, and a `not-asked` item whose sentence was never added are refused
|
|
19
|
+
* by name; per bounced verdict, its items are counted by cause.
|
|
20
|
+
*/
|
|
21
|
+
import { closeSync, fsyncSync, mkdirSync, openSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { existsSync, readFileSync } from './check-reads.ts';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import { randomUUID } from 'node:crypto';
|
|
25
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
26
|
+
import { acquireBoundJobWrite, jobRuntimeExecArgv } from './job-writer.ts';
|
|
27
|
+
import { jobHash } from './job-outbox.ts';
|
|
28
|
+
import { assertReviewActSource, decodeReviewAct, reviewActFindings, reviewActPublicationWarning, reviewActStill, reviewActTransport, reviewActVerbatim, reviewActVersionHash, type FetchedReviewAct } from './review-act.ts';
|
|
29
|
+
import { newRefusals } from './refusal-baseline.ts';
|
|
30
|
+
import { dirname, join, resolve } from 'node:path';
|
|
31
|
+
|
|
32
|
+
import { asArray, asString, collectItems, isObject, itemFor, loadCollection, type Ctx, type Item } from './check.ts';
|
|
33
|
+
import { draftVersionFindings } from './drafts.ts';
|
|
34
|
+
import { answersOf, askAnswers } from './provenance.ts';
|
|
35
|
+
import { readVerbatim } from './record-input.ts';
|
|
36
|
+
import { Refusal, refuse, USAGE_EXIT } from './refusal.ts';
|
|
37
|
+
import { REVIEW_ACCEPTS } from './stages.ts';
|
|
38
|
+
import { WAYPOINT_ASKS } from './waypoints.ts';
|
|
39
|
+
import type { Json, JsonObject } from './templates.ts';
|
|
40
|
+
|
|
41
|
+
export const VERDICTS = ['accepted', 'bounced', 'provisional'] as const;
|
|
42
|
+
export const ANSWER_WAS = ['user', 'delegated', 'not-asked'] as const;
|
|
43
|
+
type AnswerWas = (typeof ANSWER_WAS)[number];
|
|
44
|
+
type Review = keyof typeof REVIEW_ACCEPTS;
|
|
45
|
+
|
|
46
|
+
/** The `source` of a guide sentence added from feedback on version `n`. */
|
|
47
|
+
export function feedbackSource(n: number): string {
|
|
48
|
+
return `polish feedback, v${n}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isHuman(by: string | null): boolean {
|
|
52
|
+
return by !== null && by.toLowerCase() !== 'agent';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ------------------------------------------------------ what the record says
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Which `answer_was` values the record allows for an item, and the one it
|
|
59
|
+
* decides on its own (`derived`), or why the item cannot carry feedback.
|
|
60
|
+
*
|
|
61
|
+
* - A delegation: the agent's decision — `delegated`, nothing else.
|
|
62
|
+
* - A user answer with no `agent_decision`: `user`, unless the feedback names
|
|
63
|
+
* something the sentence did not cover (`not-asked`), which the record
|
|
64
|
+
* cannot tell; never `delegated`.
|
|
65
|
+
* - A user answer that left part open (`agent_decision` beside it): any of the
|
|
66
|
+
* three, and the record cannot choose — the reviewer names it.
|
|
67
|
+
* - No answer: `not-asked`, but the sentence has no guide to be added to;
|
|
68
|
+
* refused until the item is answered.
|
|
69
|
+
*/
|
|
70
|
+
type Allowed = { ok: true; derived: AnswerWas | null; allowed: AnswerWas[] } | { ok: false; code: string; why: string };
|
|
71
|
+
|
|
72
|
+
function allowedFor(item: Item): Allowed {
|
|
73
|
+
const s = item.state;
|
|
74
|
+
switch (s.state) {
|
|
75
|
+
case 'delegated':
|
|
76
|
+
return { ok: true, derived: 'delegated', allowed: ['delegated'] };
|
|
77
|
+
case 'user':
|
|
78
|
+
return isObject(s.answer.agent_decision)
|
|
79
|
+
? { ok: true, derived: null, allowed: ['user', 'delegated', 'not-asked'] }
|
|
80
|
+
: { ok: true, derived: 'user', allowed: ['user', 'not-asked'] };
|
|
81
|
+
case 'not_applicable':
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
code: 'FEEDBACK_ITEM_NOT_APPLICABLE',
|
|
85
|
+
why: `${item.path} is answered not applicable (${String(s.answer.because)}); feedback on it says the plan passes it after all — ask it (the new answer replaces this one, which moves to history[]), then record the verdict`,
|
|
86
|
+
};
|
|
87
|
+
case 'silent':
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
code: 'FEEDBACK_ITEM_SILENT',
|
|
91
|
+
why: `${item.path} has no answer, so a not-asked sentence has no guide to join; hold that question first (the user answers or delegates it), then record the verdict`,
|
|
92
|
+
};
|
|
93
|
+
case 'malformed':
|
|
94
|
+
case 'unresolved':
|
|
95
|
+
return { ok: false, code: 'FEEDBACK_ITEM_SILENT', why: `${item.path} cannot be read (${s.why}); fix the answer first` };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Validate an explicit delegated feedback ask against one current or historical answer. */
|
|
100
|
+
function feedbackAsk(item: Item, ctx: Pick<Ctx, 'collection'>, ask: Json | undefined): { code: string; message: string } | null {
|
|
101
|
+
if (typeof ask !== 'string' || !WAYPOINT_ASKS[item.id]?.some((entry) => entry.id === ask)) {
|
|
102
|
+
return { code: 'FEEDBACK_ASK_UNKNOWN', message: `guide '${item.id}' has no ask ${JSON.stringify(ask ?? null)}; name a canonical ask recorded by this guide` };
|
|
103
|
+
}
|
|
104
|
+
const recorded = askAnswers(item, ctx, ask);
|
|
105
|
+
if (recorded.length === 0) return { code: 'FEEDBACK_ASK_UNRECORDED', message: `guide '${item.id}' does not record ask '${ask}'; record its delegation before naming it in feedback` };
|
|
106
|
+
if (!recorded.some((answer) => asArray(answer.asks).some((entry) => isObject(entry) && entry.id === ask && entry.state === 'delegated'))) {
|
|
107
|
+
return { code: 'FEEDBACK_ASK_STATE', message: `guide '${item.id}' ask '${ask}' is not delegated; use the user's answer as the feedback cause` };
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// -------------------------------------------------------- interview --check
|
|
113
|
+
|
|
114
|
+
export interface FeedbackSummary {
|
|
115
|
+
/** `versions[v1].verdicts[0]` */
|
|
116
|
+
readonly verdict: string;
|
|
117
|
+
readonly version: number;
|
|
118
|
+
readonly index: number;
|
|
119
|
+
readonly review: string | null;
|
|
120
|
+
readonly by: string | null;
|
|
121
|
+
readonly at: string | null;
|
|
122
|
+
/** The verdict's items by cause: an agent error (`user`), a cost the user chose (`delegated`), a gap in the guides (`not_asked`). */
|
|
123
|
+
readonly items: { user: number; delegated: number; not_asked: number };
|
|
124
|
+
/** The versions whose `answers_feedback` lists this verdict. */
|
|
125
|
+
readonly answered_by: number[];
|
|
126
|
+
/** `true` once a later human acceptance of the same review on the same version withdrew it. */
|
|
127
|
+
readonly withdrawn: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface VersionRec {
|
|
131
|
+
readonly n: number;
|
|
132
|
+
readonly path: string;
|
|
133
|
+
readonly record: JsonObject;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function versionsOf(project: JsonObject): VersionRec[] {
|
|
137
|
+
const out: VersionRec[] = [];
|
|
138
|
+
for (const v of asArray(project.versions)) {
|
|
139
|
+
if (isObject(v) && typeof v.n === 'number' && Number.isInteger(v.n)) out.push({ n: v.n, path: `versions[v${v.n}]`, record: v });
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The refs in `answers_feedback`, read leniently; a malformed ref is reported with its index. */
|
|
145
|
+
function refsOf(v: VersionRec): { index: number; version: number | null; verdict: number | null }[] {
|
|
146
|
+
return asArray(v.record.answers_feedback).map((r, index) => {
|
|
147
|
+
const version = isObject(r) && typeof r.version === 'number' && Number.isInteger(r.version) ? r.version : null;
|
|
148
|
+
const verdict = isObject(r) && typeof r.verdict === 'number' && Number.isInteger(r.verdict) ? r.verdict : null;
|
|
149
|
+
return { index, version, verdict };
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Shared reference validation. Historical records retain their original target rules. */
|
|
154
|
+
export function feedbackReferenceFinding(project: JsonObject, n: number, ref: Json, path: string, humanBounce = false): { code: string; path: string; message: string } | null {
|
|
155
|
+
const bad = (why: string) => ({ code: 'FEEDBACK_REF_UNKNOWN', path, message: `${path}: ${why}` });
|
|
156
|
+
if (!isObject(ref) || typeof ref.version !== 'number' || !Number.isInteger(ref.version) || typeof ref.verdict !== 'number' || !Number.isInteger(ref.verdict)) return bad('an answering ref is { "version": <n>, "verdict": <index into its verdicts[]> }');
|
|
157
|
+
const target = versionsOf(project).find(v => v.n === ref.version);
|
|
158
|
+
const verdict = target === undefined ? undefined : asArray(target.record.verdicts)[ref.verdict];
|
|
159
|
+
if (target === undefined || !isObject(verdict)) return bad(`answers v${ref.version} verdict ${ref.verdict}, and ${target === undefined ? `there is no version ${ref.version}` : `versions[v${ref.version}].verdicts has no index ${ref.verdict}`}`);
|
|
160
|
+
if (ref.version >= n) return bad(`v${n} answers versions[v${ref.version}].verdicts[${ref.verdict}], which is not an earlier version's; a version answers feedback given before it was built`);
|
|
161
|
+
if (humanBounce && (verdict.verdict !== 'bounced' || !isHuman(asString(verdict.by)))) return bad('target must be a human bounced verdict; name an earlier human bounce in answers_feedback');
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Repeatable writer input, validated before any outputs are retained. */
|
|
166
|
+
export function parseFeedbackAnswers(project: JsonObject, n: number, values: string[]): JsonObject[] {
|
|
167
|
+
const refs: JsonObject[] = [];
|
|
168
|
+
const seen = new Set<string>();
|
|
169
|
+
for (const value of values) {
|
|
170
|
+
const match = /^(\d+):(\d+)$/.exec(value);
|
|
171
|
+
const ref = match ? { version: Number(match[1]), verdict: Number(match[2]) } : null;
|
|
172
|
+
const path = `versions[v${n}].answers_feedback[${refs.length}]`;
|
|
173
|
+
const problem = feedbackReferenceFinding(project, n, ref, path, true);
|
|
174
|
+
if (problem) refuse(problem.code, problem.message);
|
|
175
|
+
const key = `${String(ref!.version)}:${String(ref!.verdict)}`;
|
|
176
|
+
if (seen.has(key)) refuse('FEEDBACK_REF_DUPLICATE', `${path}: duplicate ${key}; name each feedback verdict once`);
|
|
177
|
+
seen.add(key);
|
|
178
|
+
refs.push(ref!);
|
|
179
|
+
}
|
|
180
|
+
return refs;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Feedback refusals and warnings, appended to the context; returns one summary per human bounced verdict. */
|
|
184
|
+
export function checkFeedback(ctx: Ctx): FeedbackSummary[] {
|
|
185
|
+
const versions = versionsOf(ctx.project);
|
|
186
|
+
const summaries: FeedbackSummary[] = [];
|
|
187
|
+
const push = (code: string, path: string, message: string): void => {
|
|
188
|
+
ctx.refusals.push({ code, path, message });
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Every answering ref points at a verdict of an earlier version.
|
|
192
|
+
const answeredBy = new Map<string, number[]>();
|
|
193
|
+
for (const v of versions) {
|
|
194
|
+
for (const ref of refsOf(v)) {
|
|
195
|
+
const path = `${v.path}.answers_feedback[${ref.index}]`;
|
|
196
|
+
const problem = feedbackReferenceFinding(ctx.project, v.n, { version: ref.version, verdict: ref.verdict }, path);
|
|
197
|
+
if (problem) {
|
|
198
|
+
push(problem.code, problem.path, problem.message);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const key = `${ref.version}:${ref.verdict}`;
|
|
202
|
+
answeredBy.set(key, [...(answeredBy.get(key) ?? []), v.n]);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const v of versions) {
|
|
207
|
+
const verdicts = asArray(v.record.verdicts);
|
|
208
|
+
verdicts.forEach((d, index) => {
|
|
209
|
+
const path = `${v.path}.verdicts[${index}]`;
|
|
210
|
+
if (!isObject(d)) {
|
|
211
|
+
push('VERDICT_MALFORMED', path, `${path}: a verdict is { by, at, review, verdict, verbatim, source, on_items[] }`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const preInterview = v.record.pre_interview === true;
|
|
215
|
+
// Imported verdict shape is checked by checkPreInterview, also at handoff.
|
|
216
|
+
if (!preInterview) {
|
|
217
|
+
const problems: string[] = [];
|
|
218
|
+
for (const k of ['by', 'at', 'verbatim']) if (asString(d[k]) === null) problems.push(`no ${k}`);
|
|
219
|
+
if (!(VERDICTS as readonly string[]).includes(d.verdict as string)) problems.push(`verdict is ${JSON.stringify(d.verdict ?? null)} (${VERDICTS.join(', ')})`);
|
|
220
|
+
if (!(typeof d.review === 'string' && d.review in REVIEW_ACCEPTS)) problems.push(`review is ${JSON.stringify(d.review ?? null)} (${Object.keys(REVIEW_ACCEPTS).join(', ')})`);
|
|
221
|
+
if (!Array.isArray(d.on_items)) problems.push('on_items is not a list');
|
|
222
|
+
if (problems.length > 0) push('VERDICT_MALFORMED', path, `${path}: ${problems.join('; ')}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const counts = { user: 0, delegated: 0, not_asked: 0 };
|
|
226
|
+
asArray(d.on_items).forEach((raw, j) => {
|
|
227
|
+
const ipath = `${path}.on_items[${j}]`;
|
|
228
|
+
const id = isObject(raw) ? asString(raw.id) : null;
|
|
229
|
+
const was = isObject(raw) ? raw.answer_was : undefined;
|
|
230
|
+
if (id === null || !(ANSWER_WAS as readonly Json[]).includes(was ?? null)) {
|
|
231
|
+
push('VERDICT_MALFORMED', ipath, `${ipath}: an item is { "id": <answer id>, "answer_was": ${ANSWER_WAS.map((a) => `"${a}"`).join(' | ')} }`);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (was === 'user') counts.user++;
|
|
235
|
+
else if (was === 'delegated') counts.delegated++;
|
|
236
|
+
else counts.not_asked++;
|
|
237
|
+
const item = itemFor(ctx, id);
|
|
238
|
+
if (item === undefined) {
|
|
239
|
+
push('FEEDBACK_ITEM_UNKNOWN', ipath, `${ipath}: '${id}' is no framing, waypoint or post item of this interview; feedback about a decision no waypoint reaches goes to the waypoint it belongs to (an x_ prompt there, INTERVIEW.md § Questions outside this set)`);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (was === 'delegated' && !preInterview) {
|
|
243
|
+
if (isObject(raw) && 'ask' in raw) {
|
|
244
|
+
const problem = feedbackAsk(item, ctx, raw.ask);
|
|
245
|
+
if (problem !== null) push(problem.code, ipath, `${ipath}: ${problem.message}`);
|
|
246
|
+
} else if (answersOf(item, ctx).some((answer) => Array.isArray(answer.asks))) {
|
|
247
|
+
ctx.warnings.push({ code: 'FEEDBACK_ASK_MISSING', path: ipath, message: `${ipath}: guide '${id}' has per-ask coverage; add ask naming the delegated ask this feedback concerns` });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Historical feedback predates the guide. Its record is retained here,
|
|
251
|
+
// never promoted into a quotation or an invented interview answer.
|
|
252
|
+
if (was !== 'not-asked' || preInterview) return;
|
|
253
|
+
// The sentence lands in the guide: an addition marked with this version, quoted from this verdict.
|
|
254
|
+
const verbatim = asString(d.verbatim) ?? '';
|
|
255
|
+
const added = answersOf(item, ctx).some((a) =>
|
|
256
|
+
asArray(a.additions).some(
|
|
257
|
+
(add) => isObject(add) && add.source === feedbackSource(v.n) && typeof add.verbatim === 'string' && add.verbatim !== '' && verbatim.includes(add.verbatim),
|
|
258
|
+
),
|
|
259
|
+
);
|
|
260
|
+
if (!added) {
|
|
261
|
+
push(
|
|
262
|
+
'FEEDBACK_NOT_ADDED',
|
|
263
|
+
ipath,
|
|
264
|
+
`${ipath}: '${id}' was not asked, and ${item.path} has no additions[] entry with source "${feedbackSource(v.n)}" quoting this verdict; the feedback becomes a guide sentence before the next version is built — never a silent fix`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (d.verdict !== 'bounced' || !isHuman(asString(d.by))) return;
|
|
270
|
+
// The last human verdict of a review decides: a later acceptance on the same version withdraws the bounce.
|
|
271
|
+
const withdrawn = verdicts
|
|
272
|
+
.slice(index + 1)
|
|
273
|
+
.some((later) => isObject(later) && later.review === d.review && later.verdict === 'accepted' && isHuman(asString(later.by)));
|
|
274
|
+
const by = answeredBy.get(`${v.n}:${index}`) ?? [];
|
|
275
|
+
summaries.push({
|
|
276
|
+
verdict: path,
|
|
277
|
+
version: v.n,
|
|
278
|
+
index,
|
|
279
|
+
review: asString(d.review),
|
|
280
|
+
by: asString(d.by),
|
|
281
|
+
at: asString(d.at),
|
|
282
|
+
items: counts,
|
|
283
|
+
answered_by: by,
|
|
284
|
+
withdrawn,
|
|
285
|
+
});
|
|
286
|
+
if (withdrawn || by.length > 0) return;
|
|
287
|
+
const later = versions.filter((x) => x.n > v.n).map((x) => `v${x.n}`);
|
|
288
|
+
if (later.length > 0) {
|
|
289
|
+
push(
|
|
290
|
+
'FEEDBACK_UNANSWERED',
|
|
291
|
+
path,
|
|
292
|
+
`${path}: v${v.n} was bounced, and ${later.join(', ')} ${later.length === 1 ? 'was' : 'were'} built since without listing it in answers_feedback ({ "version": ${v.n}, "verdict": ${index} }); the next version answers the feedback it was built from`,
|
|
293
|
+
);
|
|
294
|
+
} else {
|
|
295
|
+
ctx.warnings.push({ code: 'FEEDBACK_OPEN', path, message: `${path}: v${v.n} was bounced and no later version is built yet; the next version lists { "version": ${v.n}, "verdict": ${index} } in answers_feedback` });
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return summaries;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Lines for the human report: one per bounced verdict, items counted by cause. */
|
|
303
|
+
export function formatFeedback(summaries: readonly FeedbackSummary[]): string[] {
|
|
304
|
+
if (summaries.length === 0) return [];
|
|
305
|
+
const total = { user: 0, delegated: 0, not_asked: 0 };
|
|
306
|
+
for (const s of summaries) {
|
|
307
|
+
total.user += s.items.user;
|
|
308
|
+
total.delegated += s.items.delegated;
|
|
309
|
+
total.not_asked += s.items.not_asked;
|
|
310
|
+
}
|
|
311
|
+
const lines = [
|
|
312
|
+
` feedback: ${summaries.length} bounce${summaries.length === 1 ? '' : 's'}; items ${total.user} user (agent error), ${total.delegated} delegated (the user's chosen cost), ${total.not_asked} not-asked (a guide gap)`,
|
|
313
|
+
];
|
|
314
|
+
for (const s of summaries) {
|
|
315
|
+
const state = s.withdrawn ? 'withdrawn by a later acceptance' : s.answered_by.length > 0 ? `answered by ${s.answered_by.map((n) => `v${n}`).join(', ')}` : 'not answered';
|
|
316
|
+
lines.push(` ${s.verdict} (${s.review ?? '?'}): ${s.items.user} user, ${s.items.delegated} delegated, ${s.items.not_asked} not-asked; ${state}`);
|
|
317
|
+
}
|
|
318
|
+
return lines;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ------------------------------------------------------------ review command
|
|
322
|
+
|
|
323
|
+
export interface ItemArg {
|
|
324
|
+
readonly id: string;
|
|
325
|
+
readonly answerWas: AnswerWas | null;
|
|
326
|
+
readonly ask?: string;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export interface ReviewInput {
|
|
330
|
+
readonly version: number;
|
|
331
|
+
readonly verdict: (typeof VERDICTS)[number];
|
|
332
|
+
readonly by: string;
|
|
333
|
+
readonly review: Review;
|
|
334
|
+
readonly verbatim: string;
|
|
335
|
+
readonly source: string;
|
|
336
|
+
readonly items: ItemArg[];
|
|
337
|
+
/** Per not-asked item: the fragment of the verbatim that becomes the guide sentence (default: the whole verbatim). */
|
|
338
|
+
readonly quotes: Record<string, string>;
|
|
339
|
+
/** Per not-asked item: the normalised `value` of the new sentence (default: null — normalise it before the next version). */
|
|
340
|
+
readonly values: Record<string, Json>;
|
|
341
|
+
readonly at?: string;
|
|
342
|
+
readonly answersFeedback?: { version: number; verdict: number }[];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export interface RecordedItem {
|
|
346
|
+
readonly id: string;
|
|
347
|
+
readonly answer_was: AnswerWas;
|
|
348
|
+
readonly ask?: string;
|
|
349
|
+
/** How the value was reached: from the record alone, or named by the reviewer and allowed by the record. */
|
|
350
|
+
readonly how: 'derived' | 'named';
|
|
351
|
+
/** For not-asked: where the sentence was added. */
|
|
352
|
+
readonly addition: string | null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export interface Recorded {
|
|
356
|
+
readonly project_dir: string;
|
|
357
|
+
readonly verdict_path: string;
|
|
358
|
+
readonly verdict: JsonObject;
|
|
359
|
+
readonly items: RecordedItem[];
|
|
360
|
+
readonly stage: string | null;
|
|
361
|
+
readonly warnings?: string[];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function readProject(dir: string): { file: string; project: JsonObject } {
|
|
365
|
+
const file = join(dir, 'project.json');
|
|
366
|
+
if (!existsSync(file)) refuse('PROJECT_JSON_MISSING', `${file} does not exist; run in a project folder or name one`);
|
|
367
|
+
let parsed: unknown;
|
|
368
|
+
try {
|
|
369
|
+
parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
370
|
+
} catch (e) {
|
|
371
|
+
refuse('PROJECT_JSON_INVALID', `${file}: ${e instanceof Error ? e.message : String(e)}`);
|
|
372
|
+
}
|
|
373
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) refuse('PROJECT_JSON_INVALID', `${file}: not a JSON object`);
|
|
374
|
+
return { file, project: parsed as JsonObject };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Appends one verdict to `versions[v<n>].verdicts[]`, writes not-asked sentences into their guides, and saves project.json. Refuses before writing anything. */
|
|
378
|
+
export function recordVerdict(projectDirArg: string, input: ReviewInput, now: number): Recorded {
|
|
379
|
+
return writeVerdict(projectDirArg, input, now);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function writeVerdict(projectDirArg: string, input: ReviewInput, now: number, fetched?: FetchedReviewAct, warnings: string[] = []): Recorded {
|
|
383
|
+
if (!isHuman(input.by)) refuse('REVIEW_NOT_HUMAN', 'review must name a human, not agent');
|
|
384
|
+
if (input.at !== undefined && !Number.isFinite(Date.parse(input.at))) refuse('REVIEW_OPTION_INVALID', '--at needs a valid timestamp', USAGE_EXIT);
|
|
385
|
+
const dir = resolve(projectDirArg);
|
|
386
|
+
const { file, project } = readProject(dir);
|
|
387
|
+
const originalBytes = readFileSync(file);
|
|
388
|
+
const original = structuredClone(project);
|
|
389
|
+
const version = asArray(project.versions).find((v): v is JsonObject => isObject(v) && v.n === input.version);
|
|
390
|
+
if (version === undefined) refuse('REVIEW_VERSION_UNKNOWN', `versions[] has no version ${input.version}; a verdict is on a version that was shown`);
|
|
391
|
+
const draftProblem = draftVersionFindings(dir, { ...project, versions: [version] })[0];
|
|
392
|
+
if (draftProblem !== undefined) refuse(draftProblem.code, draftProblem.message);
|
|
393
|
+
const vpath = `versions[v${input.version}]`;
|
|
394
|
+
const accepts = REVIEW_ACCEPTS[input.review];
|
|
395
|
+
if (version.kind !== accepts) {
|
|
396
|
+
refuse(
|
|
397
|
+
'REVIEW_WRONG_KIND',
|
|
398
|
+
`${vpath} is a ${JSON.stringify(version.kind ?? null)} version, and ${input.review} reviews ${accepts} versions; rig feedback given at the direction review is recorded on the post version shown, with --review direction-review`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const collection = loadCollection(dir);
|
|
403
|
+
const items = collectItems(project, { collection });
|
|
404
|
+
const seen = new Set<string>();
|
|
405
|
+
const recorded: RecordedItem[] = [];
|
|
406
|
+
for (const arg of input.items) {
|
|
407
|
+
if (seen.has(arg.id)) refuse('FEEDBACK_ITEM_DUPLICATE', `--item ${arg.id} is given twice; one entry per answer the feedback touches`, USAGE_EXIT);
|
|
408
|
+
seen.add(arg.id);
|
|
409
|
+
const item = items.find((i) => i.id === arg.id);
|
|
410
|
+
if (item === undefined) {
|
|
411
|
+
refuse(
|
|
412
|
+
'FEEDBACK_ITEM_UNKNOWN',
|
|
413
|
+
`'${arg.id}' is no framing, waypoint or post item of this interview (${items.map((i) => i.id).join(', ')}); feedback about a decision no prompt reaches is asked as an x_ prompt at the waypoint it belongs to (INTERVIEW.md § Questions outside this set) — add the prompt there, then name that waypoint's id`,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
const allowed = allowedFor(item);
|
|
417
|
+
if (!allowed.ok) refuse(allowed.code, `--item ${arg.id}: ${allowed.why}`);
|
|
418
|
+
let was: AnswerWas;
|
|
419
|
+
let how: RecordedItem['how'];
|
|
420
|
+
if (arg.answerWas === null) {
|
|
421
|
+
if (allowed.derived === null) {
|
|
422
|
+
refuse(
|
|
423
|
+
'FEEDBACK_ITEM_AMBIGUOUS',
|
|
424
|
+
`--item ${arg.id}: ${item.path} is a user sentence that left part to the agent (agent_decision covers ${JSON.stringify((item.state.state === 'user' && isObject(item.state.answer.agent_decision) ? item.state.answer.agent_decision.covers : null) ?? 'part')}); the record cannot tell which part the feedback is about — name it: --item ${arg.id}:user | ${arg.id}:delegated | ${arg.id}:not-asked`,
|
|
425
|
+
USAGE_EXIT,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
was = allowed.derived;
|
|
429
|
+
how = 'derived';
|
|
430
|
+
} else {
|
|
431
|
+
if (!allowed.allowed.includes(arg.answerWas)) {
|
|
432
|
+
const recordSays = item.state.state === 'delegated' ? 'a delegation (the user said "auto"; the agent decided)' : 'a user sentence with no delegated part';
|
|
433
|
+
refuse(
|
|
434
|
+
'FEEDBACK_ITEM_CONTRADICTS',
|
|
435
|
+
`--item ${arg.id}:${arg.answerWas}: ${item.path} is ${recordSays}, so answer_was is ${allowed.allowed.join(' or ')}; a bounce on a delegated item is the user's chosen cost, not an agent error — if the user now wants it theirs, record their new answer first`,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
was = arg.answerWas;
|
|
439
|
+
how = allowed.derived === arg.answerWas ? 'derived' : 'named';
|
|
440
|
+
}
|
|
441
|
+
if (arg.ask !== undefined) {
|
|
442
|
+
if (was !== 'delegated') refuse('FEEDBACK_ASK_STATE', `guide '${arg.id}' ask '${arg.ask}': --ask requires delegated feedback`);
|
|
443
|
+
const problem = feedbackAsk(item, { collection }, arg.ask);
|
|
444
|
+
if (problem !== null) refuse(problem.code, problem.message);
|
|
445
|
+
}
|
|
446
|
+
let addition: string | null = null;
|
|
447
|
+
if (was === 'not-asked') {
|
|
448
|
+
if (isObject(item.raw) && item.raw.basis === 'inherited') {
|
|
449
|
+
refuse(
|
|
450
|
+
'FEEDBACK_ITEM_INHERITED',
|
|
451
|
+
`--item ${arg.id}:not-asked: ${item.path} is inherited from the collection; the new sentence belongs in the collection's answer, or this member overrides it with its own (COLLECTIONS.md) — record that first`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
const quote = input.quotes[arg.id] ?? input.verbatim;
|
|
455
|
+
if (quote === '' || !input.verbatim.includes(quote)) {
|
|
456
|
+
refuse('FEEDBACK_QUOTE_NOT_IN_VERBATIM', `--quote ${arg.id}: ${JSON.stringify(quote)} is not a fragment of the verbatim; the guide sentence is quoted from the feedback, word for word`, USAGE_EXIT);
|
|
457
|
+
}
|
|
458
|
+
addition = quote;
|
|
459
|
+
} else if (input.quotes[arg.id] !== undefined || input.values[arg.id] !== undefined) {
|
|
460
|
+
refuse('FEEDBACK_QUOTE_NOT_NOT_ASKED', `--quote/--value ${arg.id}: '${arg.id}' is ${was}, not not-asked; only a not-asked item becomes a new guide sentence`, USAGE_EXIT);
|
|
461
|
+
}
|
|
462
|
+
recorded.push({ id: arg.id, answer_was: was, ...(arg.ask === undefined ? {} : { ask: arg.ask }), how, addition });
|
|
463
|
+
}
|
|
464
|
+
for (const id of [...Object.keys(input.quotes), ...Object.keys(input.values)]) {
|
|
465
|
+
if (!seen.has(id)) refuse('FEEDBACK_QUOTE_NOT_NOT_ASKED', `--quote/--value ${id}: no --item ${id}`, USAGE_EXIT);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const refs = asArray(version.answers_feedback);
|
|
469
|
+
const seenRefs = new Set(refs.filter(isObject).map((ref) => `${String(ref.version)}:${String(ref.verdict)}`));
|
|
470
|
+
for (const [index, ref] of (input.answersFeedback ?? []).entries()) {
|
|
471
|
+
const key = `${ref.version}:${ref.verdict}`;
|
|
472
|
+
if (seenRefs.has(key)) refuse('FEEDBACK_REF_DUPLICATE', `answers_feedback already names ${key}`);
|
|
473
|
+
const problem = feedbackReferenceFinding(project, input.version, ref, `${vpath}.answers_feedback[${refs.length + index}]`);
|
|
474
|
+
if (problem) refuse(problem.code, problem.message);
|
|
475
|
+
seenRefs.add(key);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Everything checked: write.
|
|
479
|
+
const at = input.at ?? new Date(now).toISOString();
|
|
480
|
+
const verdicts = Array.isArray(version.verdicts) ? version.verdicts : [];
|
|
481
|
+
version.verdicts = verdicts;
|
|
482
|
+
const verdict: JsonObject = {
|
|
483
|
+
by: input.by,
|
|
484
|
+
at,
|
|
485
|
+
review: input.review,
|
|
486
|
+
verdict: input.verdict,
|
|
487
|
+
verbatim: input.verbatim,
|
|
488
|
+
source: input.source,
|
|
489
|
+
on_items: recorded.map((r) => ({ id: r.id, answer_was: r.answer_was, ...(r.ask === undefined ? {} : { ask: r.ask }) })),
|
|
490
|
+
};
|
|
491
|
+
verdicts.push(verdict);
|
|
492
|
+
version.answers_feedback = [...refs, ...(input.answersFeedback ?? [])];
|
|
493
|
+
const verdictPath = `${vpath}.verdicts[${verdicts.length - 1}]`;
|
|
494
|
+
const out: RecordedItem[] = [];
|
|
495
|
+
for (const r of recorded) {
|
|
496
|
+
if (r.addition === null) {
|
|
497
|
+
out.push(r);
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
const item = items.find((i) => i.id === r.id) as Item;
|
|
501
|
+
const answer = item.raw as JsonObject;
|
|
502
|
+
const additions = Array.isArray(answer.additions) ? answer.additions : [];
|
|
503
|
+
answer.additions = additions;
|
|
504
|
+
additions.push({ by: input.by, at, verbatim: r.addition, value: input.values[r.id] ?? null, source: feedbackSource(input.version) });
|
|
505
|
+
out.push({ ...r, addition: `${item.path}.additions[${additions.length - 1}]` });
|
|
506
|
+
}
|
|
507
|
+
if (verdict.verdict === 'accepted') assertDirectionApproval(dir, project, String(verdict.review), now, input.version);
|
|
508
|
+
if (verdict.verdict === 'accepted' && verdict.review === 'rig-review') assertFigureMaskApproval(dir, project, input.version);
|
|
509
|
+
if (verdict.verdict === 'accepted') assertCorrectionApproval(dir, project, String(verdict.review), now);
|
|
510
|
+
const result = { project_dir: dir, verdict_path: verdictPath, verdict, items: out, stage: asString(project.stage), ...(warnings.length ? { warnings } : {}) };
|
|
511
|
+
const created: string[] = [];
|
|
512
|
+
try {
|
|
513
|
+
const addedEvidence: JsonObject[] = [];
|
|
514
|
+
const still = fetched === undefined ? null : reviewActStill(fetched.act);
|
|
515
|
+
if (fetched !== undefined && still !== null) {
|
|
516
|
+
const png = fetched.stillBytes === undefined ? null : Buffer.from(fetched.stillBytes, 'base64');
|
|
517
|
+
if (!png || jobHash(png) !== still.sha256) refuse('REVIEW_ACT_STILL_INVALID', 'project.json.review_acts: named PNG is missing or mismatched; fetch act.json and its still together');
|
|
518
|
+
const base = `evidence/v${input.version}`;
|
|
519
|
+
const images = [
|
|
520
|
+
{ path: `${base}/${still.path}`, bytes: png },
|
|
521
|
+
{ path: `${base}/${jobHash(fetched.bytes)}.act.json`, bytes: Buffer.from(fetched.bytes) },
|
|
522
|
+
];
|
|
523
|
+
for (const image of images) {
|
|
524
|
+
const destination = join(dir, image.path);
|
|
525
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
526
|
+
if (existsSync(destination)) {
|
|
527
|
+
if (!readFileSync(destination).equals(image.bytes)) refuse('REVIEW_ACT_STILL_INVALID', `${image.path}: retained bytes differ; restore the original export`);
|
|
528
|
+
} else { writeFileSync(destination, image.bytes, { flag: 'wx' }); created.push(destination); }
|
|
529
|
+
}
|
|
530
|
+
const evidence: JsonObject = { kind: 'still', path: images[0].path, sha256: still.sha256,
|
|
531
|
+
camera: still.camera, frame: still.frame,
|
|
532
|
+
what: still.frame === 'setup' ? 'Review displayed setup pose' : `Review displayed animation ${JSON.stringify(still.animation)} at ${still.frame} seconds` };
|
|
533
|
+
const entries = asArray(version.evidence);
|
|
534
|
+
if (!isDeepStrictEqual(entries[0], evidence)) {
|
|
535
|
+
entries.unshift(evidence); addedEvidence.push(evidence);
|
|
536
|
+
}
|
|
537
|
+
version.evidence = entries;
|
|
538
|
+
}
|
|
539
|
+
if (fetched) {
|
|
540
|
+
const receipt = { schema: 'rig-create/review-act@1', jobId: fetched.act.jobId, actId: fetched.act.actId,
|
|
541
|
+
bytes: fetched.bytes, sha256: jobHash(fetched.bytes), version: input.version, verdict: verdicts.length - 1,
|
|
542
|
+
version_hash: reviewActVersionHash(original, fetched.act), added_evidence: addedEvidence };
|
|
543
|
+
project.review_acts = [...asArray(project.review_acts), receipt];
|
|
544
|
+
const invalid = reviewActFindings(project)[0] ?? newRefusals(dir, original, project, { now })[0];
|
|
545
|
+
if (invalid) refuse(invalid.code, invalid.message);
|
|
546
|
+
assertReviewActSource(dir, original, fetched.act);
|
|
547
|
+
if (!readFileSync(file).equals(originalBytes)) refuse('REVIEW_ACT_SOURCE_STALE', 'project.json: record changed during verdict validation; retry against the current live job');
|
|
548
|
+
const temporary = join(dir, `.rig-create-review-act-${randomUUID()}.tmp`);
|
|
549
|
+
const fd = openSync(temporary, 'wx', 0o600);
|
|
550
|
+
try {
|
|
551
|
+
writeFileSync(fd, `${JSON.stringify(project, null, 2)}\n`);
|
|
552
|
+
fsyncSync(fd);
|
|
553
|
+
renameSync(temporary, file);
|
|
554
|
+
const directory = openSync(dir, 'r');
|
|
555
|
+
try { fsyncSync(directory); } finally { closeSync(directory); }
|
|
556
|
+
} finally { closeSync(fd); rmSync(temporary, { force: true }); }
|
|
557
|
+
return result;
|
|
558
|
+
}
|
|
559
|
+
const invalid = newRefusals(dir, original, project, { now })[0];
|
|
560
|
+
if (invalid) refuse(invalid.code, invalid.message);
|
|
561
|
+
writeFileSync(file, `${JSON.stringify(project, null, 2)}\n`);
|
|
562
|
+
return { project_dir: dir, verdict_path: verdictPath, verdict, items: out, stage: asString(project.stage) };
|
|
563
|
+
} catch (error) { for (const path of created) rmSync(path, { force: true }); throw error; }
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** The common #184 lock covers fetch, source validation and the atomic write. */
|
|
567
|
+
export function recordReviewAct(root: string, requestedVersion: number, actId: string, now: number): Recorded {
|
|
568
|
+
root = resolve(root);
|
|
569
|
+
const release = acquireBoundJobWrite(root);
|
|
570
|
+
try {
|
|
571
|
+
const child = Bun.spawnSync([process.execPath, ...jobRuntimeExecArgv(), fileURLToPath(new URL('./review-act-runtime.ts', import.meta.url))], {
|
|
572
|
+
stdin: Buffer.from(JSON.stringify(reviewActTransport(root, actId))), stdout: 'pipe', stderr: 'pipe',
|
|
573
|
+
});
|
|
574
|
+
let response: { ok: boolean; bytes: string; stillBytes?: string; code: string; message: string };
|
|
575
|
+
try { response = JSON.parse(child.stdout.toString()); }
|
|
576
|
+
catch (error) { if (error instanceof Refusal) throw error; return refuse('REVIEW_ACT_FETCH_REFUSED', 'project.json.review_acts: fetch adapter failed; restore the live Review job'); }
|
|
577
|
+
if (!response.ok || child.exitCode !== 0) refuse(response.code, response.message);
|
|
578
|
+
const act = decodeReviewAct(response.bytes);
|
|
579
|
+
const selector = act.source.selector!;
|
|
580
|
+
if (!('version' in selector) || selector.version !== requestedVersion) refuse('REVIEW_ACT_KIND_MISMATCH', 'project.json.review_acts: act names a different version; invoke review for the displayed version');
|
|
581
|
+
if (Date.parse(act.at) > now) refuse('REVIEW_ACT_INVALID', 'project.json.review_acts: act is dated in the future; restore the clock before recording');
|
|
582
|
+
const { project } = readProject(root);
|
|
583
|
+
const invalid = reviewActFindings(project)[0];
|
|
584
|
+
if (invalid) refuse(invalid.code, invalid.message);
|
|
585
|
+
assertReviewActSource(root, project, act);
|
|
586
|
+
const existing = asArray(project.review_acts).find(entry => isObject(entry) && entry.jobId === act.jobId && entry.actId === act.actId);
|
|
587
|
+
if (isObject(existing)) {
|
|
588
|
+
const sameAct = act.schema === 'rig-review/act@6'
|
|
589
|
+
? existing.bytes === response.bytes
|
|
590
|
+
: isDeepStrictEqual(JSON.parse(String(existing.bytes)), act);
|
|
591
|
+
if (!sameAct) refuse('REVIEW_ACT_REPLAY_CONFLICT', 'project.json.review_acts: immutable act changed; restore the original receipt');
|
|
592
|
+
if (existing.version_hash !== reviewActVersionHash(project, act, Number(existing.verdict))) refuse('REVIEW_ACT_SOURCE_STALE', 'project.json.review_acts: reviewed version changed; request a new review act');
|
|
593
|
+
const version = asArray(project.versions).find(v => isObject(v) && v.n === requestedVersion) as JsonObject;
|
|
594
|
+
return { project_dir: root, verdict_path: `versions[v${requestedVersion}].verdicts[${existing.verdict}]`,
|
|
595
|
+
verdict: (version.verdicts as JsonObject[])[Number(existing.verdict)], items: [], stage: asString(project.stage) };
|
|
596
|
+
}
|
|
597
|
+
const warning = reviewActPublicationWarning(root, project, act);
|
|
598
|
+
return writeVerdict(root, { version: requestedVersion,
|
|
599
|
+
verdict: { approved: 'accepted', 'changes-requested': 'bounced', pending: 'provisional' }[act.value.verdict] as ReviewInput['verdict'],
|
|
600
|
+
by: act.authority.reviewer.role === 'owner' ? 'user' : 'delegate', review: act.value.stage as Review,
|
|
601
|
+
verbatim: reviewActVerbatim(act), at: act.at, source: `review act ${act.jobId}/${act.actId}`, items: [], quotes: {}, values: {},
|
|
602
|
+
}, now, { act, bytes: response.bytes, stillBytes: response.stillBytes }, warning ? [warning] : []);
|
|
603
|
+
} finally { release(); }
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export const REVIEW_HELP = `review:
|
|
607
|
+
<version> the version shown (3 or v3)
|
|
608
|
+
[<project dir>] the project folder (default: the current directory)
|
|
609
|
+
--from-review <actId> fetch a rig/post verdict through the live job writer binding
|
|
610
|
+
--verdict <v> accepted | bounced | provisional
|
|
611
|
+
--by <who> who gave it (the user's name or role)
|
|
612
|
+
--review <r> rig-review (a rig version) | direction-review (a post version)
|
|
613
|
+
--verbatim <s> the words, exactly as given; stored byte for byte
|
|
614
|
+
--verbatim-file <file> read UTF-8 words unchanged instead of --verbatim (- reads stdin)
|
|
615
|
+
--at <timestamp> date the verdict (default: the clock)
|
|
616
|
+
--answers-feedback <version>:<verdict-index> earlier feedback this version answers, repeatable
|
|
617
|
+
--source <s> where the words were given (chat, a call, a note)
|
|
618
|
+
--item <id>[:<answer_was>]
|
|
619
|
+
an answer the feedback is about (a waypoint, framing or post item id), repeatable;
|
|
620
|
+
answer_was (user | delegated | not-asked) is derived from the record when it decides it,
|
|
621
|
+
and a value the record contradicts is refused (FEEDBACK_ITEM_CONTRADICTS)
|
|
622
|
+
--ask <id>=<ask-id> the recorded delegated ask for an --item (optional)
|
|
623
|
+
--quote <id>=<s> for a not-asked item: the fragment of the verbatim that becomes the guide sentence (default: all of it)
|
|
624
|
+
--value <id>=<json>
|
|
625
|
+
for a not-asked item: the sentence's normalised value (default: null)
|
|
626
|
+
--json print what was recorded as JSON
|
|
627
|
+
Appends the verdict to versions[v<n>].verdicts[] dated now; a not-asked item is written to that answer's additions[]
|
|
628
|
+
with source "polish feedback, v<n>". Never moves the stage: a verdict is not a sign-off.`;
|
|
629
|
+
|
|
630
|
+
function parseVersion(s: string | undefined): number {
|
|
631
|
+
const n = s === undefined ? Number.NaN : Number(s.replace(/^v/, ''));
|
|
632
|
+
if (!Number.isInteger(n) || n < 1) refuse('REVIEW_VERSION_INVALID', `review takes a version number first (3 or v3), got ${s ?? 'nothing'}`, USAGE_EXIT);
|
|
633
|
+
return n;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function pair(flag: string, s: string | undefined): [string, string] {
|
|
637
|
+
const eq = s === undefined ? -1 : s.indexOf('=');
|
|
638
|
+
if (s === undefined || eq < 1) refuse('REVIEW_OPTION_INVALID', `${flag} takes <id>=<text>, got ${s ?? 'nothing'}`, USAGE_EXIT);
|
|
639
|
+
return [s.slice(0, eq), s.slice(eq + 1)];
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export function reviewCommand(args: string[], now: number): { stdout: string; exit: number } {
|
|
643
|
+
const opts: Record<string, string> = {};
|
|
644
|
+
const items: ItemArg[] = [];
|
|
645
|
+
const answersFeedback: { version: number; verdict: number }[] = [];
|
|
646
|
+
const asks: Record<string, string> = {};
|
|
647
|
+
const quotes: Record<string, string> = {};
|
|
648
|
+
const values: Record<string, Json> = {};
|
|
649
|
+
const positional: string[] = [];
|
|
650
|
+
let json = false;
|
|
651
|
+
for (let i = 0; i < args.length; i++) {
|
|
652
|
+
const arg = args[i] as string;
|
|
653
|
+
const value = (): string => {
|
|
654
|
+
const v = args[++i];
|
|
655
|
+
if (v === undefined) refuse('REVIEW_OPTION_INVALID', `${arg} needs a value`, USAGE_EXIT);
|
|
656
|
+
return v;
|
|
657
|
+
};
|
|
658
|
+
if (arg === '--json') json = true;
|
|
659
|
+
else if (arg === '--from-review-file' || arg.startsWith('--from-review-file=')) {
|
|
660
|
+
refuse('REVIEW_FILE_ROUTE_REMOVED', 'project.json.review_acts: saved act files are unsupported; use --from-review <actId> through the live Review job API', USAGE_EXIT);
|
|
661
|
+
} else if (['--from-review', '--verdict', '--by', '--review', '--verbatim', '--verbatim-file', '--at', '--source'].includes(arg)) {
|
|
662
|
+
if (arg in opts) refuse('REVIEW_OPTION_INVALID', `${arg} is given twice`, USAGE_EXIT);
|
|
663
|
+
opts[arg] = value();
|
|
664
|
+
} else if (arg === '--answers-feedback') {
|
|
665
|
+
const text = value();
|
|
666
|
+
if (!/^v?[1-9]\d*:\d+$/.test(text)) refuse('REVIEW_OPTION_INVALID', '--answers-feedback takes <version>:<verdict-index>', USAGE_EXIT);
|
|
667
|
+
const [version, verdict] = text.replace(/^v/, '').split(':').map(Number);
|
|
668
|
+
answersFeedback.push({ version, verdict });
|
|
669
|
+
} else if (arg === '--item') {
|
|
670
|
+
const v = value();
|
|
671
|
+
const colon = v.indexOf(':');
|
|
672
|
+
const id = colon === -1 ? v : v.slice(0, colon);
|
|
673
|
+
const was = colon === -1 ? null : v.slice(colon + 1);
|
|
674
|
+
if (id === '' || (was !== null && !(ANSWER_WAS as readonly string[]).includes(was))) {
|
|
675
|
+
refuse('REVIEW_OPTION_INVALID', `--item takes <id> or <id>:${ANSWER_WAS.join('|')}, got ${v}`, USAGE_EXIT);
|
|
676
|
+
}
|
|
677
|
+
items.push({ id, answerWas: was as AnswerWas | null });
|
|
678
|
+
} else if (arg === '--ask') {
|
|
679
|
+
const [id, ask] = pair(arg, value());
|
|
680
|
+
if (id in asks || ask === '') refuse('REVIEW_OPTION_INVALID', `--ask ${id} needs one nonempty ask id`, USAGE_EXIT);
|
|
681
|
+
asks[id] = ask;
|
|
682
|
+
} else if (arg === '--quote') {
|
|
683
|
+
const [id, text] = pair(arg, value());
|
|
684
|
+
quotes[id] = text;
|
|
685
|
+
} else if (arg === '--value') {
|
|
686
|
+
const [id, text] = pair(arg, value());
|
|
687
|
+
try {
|
|
688
|
+
values[id] = JSON.parse(text) as Json;
|
|
689
|
+
} catch {
|
|
690
|
+
refuse('REVIEW_OPTION_INVALID', `--value ${id}: not JSON: ${text}`, USAGE_EXIT);
|
|
691
|
+
}
|
|
692
|
+
} else if (arg.startsWith('-')) refuse('UNKNOWN_OPTION', `review does not take ${arg}`, USAGE_EXIT);
|
|
693
|
+
else positional.push(arg);
|
|
694
|
+
}
|
|
695
|
+
if (positional.length > 2) refuse('REVIEW_TOO_MANY_ARGUMENTS', `review takes a version and one folder, got ${positional.join(' ')}`, USAGE_EXIT);
|
|
696
|
+
const version = parseVersion(positional[0]);
|
|
697
|
+
if (opts['--from-review'] !== undefined) {
|
|
698
|
+
if (Object.keys(opts).length !== 1 || items.length || answersFeedback.length || Object.keys(asks).length || Object.keys(quotes).length || Object.keys(values).length) {
|
|
699
|
+
refuse('REVIEW_OPTION_INVALID', 'project.json.review_acts: --from-review excludes human-value and feedback flags; submit those values through Review', USAGE_EXIT);
|
|
700
|
+
}
|
|
701
|
+
const recorded = recordReviewAct(positional[1] ?? process.cwd(), version, opts['--from-review'], now);
|
|
702
|
+
return { stdout: json ? JSON.stringify(recorded, null, 2) : [...(recorded.warnings ?? []), `review: ${recorded.verdict_path} — ${String(recorded.verdict.verdict)} (recorded or replayed)`].join('\n'), exit: 0 };
|
|
703
|
+
}
|
|
704
|
+
if (opts['--verbatim'] !== undefined && opts['--verbatim-file'] !== undefined) refuse('REVIEW_OPTION_INVALID', 'choose --verbatim or --verbatim-file', USAGE_EXIT);
|
|
705
|
+
if (opts['--verbatim-file'] !== undefined) opts['--verbatim'] = readVerbatim(opts['--verbatim-file']);
|
|
706
|
+
const missing = ['--verdict', '--by', '--review', '--verbatim', '--source'].filter((k) => opts[k] === undefined || opts[k] === '');
|
|
707
|
+
if (missing.length > 0) refuse('REVIEW_OPTION_MISSING', `review needs ${missing.join(', ')}`, USAGE_EXIT);
|
|
708
|
+
const verdict = opts['--verdict'] as string;
|
|
709
|
+
if (!(VERDICTS as readonly string[]).includes(verdict)) refuse('REVIEW_OPTION_INVALID', `--verdict is ${VERDICTS.join(', ')}, got ${verdict}`, USAGE_EXIT);
|
|
710
|
+
const review = opts['--review'] as string;
|
|
711
|
+
if (!(review in REVIEW_ACCEPTS)) refuse('REVIEW_OPTION_INVALID', `--review is ${Object.keys(REVIEW_ACCEPTS).join(' or ')}, got ${review}`, USAGE_EXIT);
|
|
712
|
+
|
|
713
|
+
for (const [id, ask] of Object.entries(asks)) {
|
|
714
|
+
const item = items.find((entry) => entry.id === id);
|
|
715
|
+
if (item === undefined) refuse('REVIEW_OPTION_INVALID', `--ask ${id}=${ask}: no matching --item`, USAGE_EXIT);
|
|
716
|
+
}
|
|
717
|
+
const r = recordVerdict(positional[1] ?? process.cwd(), {
|
|
718
|
+
version,
|
|
719
|
+
verdict: verdict as ReviewInput['verdict'],
|
|
720
|
+
by: opts['--by'] as string,
|
|
721
|
+
review: review as Review,
|
|
722
|
+
verbatim: opts['--verbatim'] as string,
|
|
723
|
+
source: opts['--source'] as string,
|
|
724
|
+
items: items.map((item) => ({ ...item, ...(asks[item.id] === undefined ? {} : { ask: asks[item.id] }) })),
|
|
725
|
+
quotes,
|
|
726
|
+
values,
|
|
727
|
+
at: opts['--at'],
|
|
728
|
+
answersFeedback,
|
|
729
|
+
}, now);
|
|
730
|
+
if (json) return { stdout: JSON.stringify(r, null, 2), exit: 0 };
|
|
731
|
+
const lines = [
|
|
732
|
+
`review: recorded ${r.verdict_path} — ${String(r.verdict.verdict)} at ${String(r.verdict.review)} by ${String(r.verdict.by)}, ${String(r.verdict.at)}`,
|
|
733
|
+
` verbatim: ${JSON.stringify(r.verdict.verbatim)}`,
|
|
734
|
+
];
|
|
735
|
+
for (const it of r.items) {
|
|
736
|
+
lines.push(` ${it.id}: ${it.answer_was} (${it.how === 'derived' ? 'from the record' : 'named, allowed by the record'})${it.addition === null ? '' : `; sentence added at ${it.addition}`}`);
|
|
737
|
+
}
|
|
738
|
+
if (r.items.length === 0) lines.push(' on no items');
|
|
739
|
+
lines.push(` stage unchanged (${r.stage ?? 'none'}): a verdict is not a sign-off`);
|
|
740
|
+
if (r.items.some((it) => it.addition !== null)) {
|
|
741
|
+
lines.push(' a guide changed: a step resting on it is re-signed before it runs again (interview --check names them)');
|
|
742
|
+
}
|
|
743
|
+
return { stdout: lines.join('\n'), exit: 0 };
|
|
744
|
+
}
|