@rig-c/create 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (555) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -2
  3. package/docs/CHECKS.md +1682 -0
  4. package/docs/COLLECTIONS.md +337 -0
  5. package/docs/CRAFT.md +277 -0
  6. package/docs/INTERVIEW.md +886 -0
  7. package/docs/LESSONS.md +172 -0
  8. package/docs/PROJECT.md +2657 -0
  9. package/docs/SOURCE.md +225 -0
  10. package/package.json +44 -4
  11. package/src/agent-bounce.ts +27 -0
  12. package/src/agent-guide.ts +237 -0
  13. package/src/amend.ts +193 -0
  14. package/src/applied-review.ts +104 -0
  15. package/src/ask-coverage.ts +69 -0
  16. package/src/assemble-answers.ts +53 -0
  17. package/src/cg-gates.ts +211 -0
  18. package/src/check-reads.ts +106 -0
  19. package/src/check.ts +1059 -0
  20. package/src/checkpoint-evidence-writer.ts +125 -0
  21. package/src/checkpoint-evidence.ts +152 -0
  22. package/src/choice-writers.ts +271 -0
  23. package/src/choices.ts +378 -0
  24. package/src/cli.ts +371 -0
  25. package/src/collection-answer.ts +136 -0
  26. package/src/collection-binding-reader.ts +496 -0
  27. package/src/collection-binding.ts +478 -0
  28. package/src/collection.ts +348 -0
  29. package/src/config-inputs.ts +155 -0
  30. package/src/config-lost.ts +135 -0
  31. package/src/correction-writers.ts +207 -0
  32. package/src/corrections.ts +685 -0
  33. package/src/diff-classes.ts +219 -0
  34. package/src/diff-map.ts +248 -0
  35. package/src/direction-citation.ts +13 -0
  36. package/src/direction-writers.ts +119 -0
  37. package/src/direction.ts +266 -0
  38. package/src/drafts.ts +363 -0
  39. package/src/feedback.ts +744 -0
  40. package/src/figure-mask.ts +143 -0
  41. package/src/fix-ledger.ts +355 -0
  42. package/src/init.ts +265 -0
  43. package/src/inputs.ts +160 -0
  44. package/src/intake.ts +158 -0
  45. package/src/interview.ts +53 -0
  46. package/src/invariants.ts +132 -0
  47. package/src/job-acts.ts +80 -0
  48. package/src/job-advance.ts +65 -0
  49. package/src/job-close.ts +226 -0
  50. package/src/job-host.ts +529 -0
  51. package/src/job-outbox.ts +383 -0
  52. package/src/job-publication.ts +701 -0
  53. package/src/job-runtime.ts +99 -0
  54. package/src/job-writer.ts +123 -0
  55. package/src/meshes-bust.ts +63 -0
  56. package/src/next.ts +1506 -0
  57. package/src/overrides.ts +42 -0
  58. package/src/package-upgrades.ts +32 -0
  59. package/src/packages-upgrade-check.ts +6 -0
  60. package/src/packages-upgrade.ts +168 -0
  61. package/src/parts-act.ts +96 -0
  62. package/src/parts-check.ts +242 -0
  63. package/src/parts-review.ts +196 -0
  64. package/src/parts-sections.ts +98 -0
  65. package/src/parts-verdict-record.ts +265 -0
  66. package/src/parts-verdict.ts +33 -0
  67. package/src/pause.ts +118 -0
  68. package/src/pins-check.ts +619 -0
  69. package/src/pins.ts +17 -0
  70. package/src/plan-bar-amendments.ts +54 -0
  71. package/src/plan-history.ts +147 -0
  72. package/src/plan-writers.ts +202 -0
  73. package/src/plate-receipt.ts +110 -0
  74. package/src/pre-interview.ts +302 -0
  75. package/src/prepared-inputs.ts +113 -0
  76. package/src/progress-writers.ts +181 -0
  77. package/src/provenance.ts +301 -0
  78. package/src/record-direction-target.ts +26 -0
  79. package/src/record-input.ts +18 -0
  80. package/src/refusal-baseline.ts +55 -0
  81. package/src/refusal.ts +30 -0
  82. package/src/reopen-lapses.ts +80 -0
  83. package/src/reopen.ts +144 -0
  84. package/src/republish.ts +93 -0
  85. package/src/restart.ts +141 -0
  86. package/src/review-act-runtime.ts +16 -0
  87. package/src/review-act.ts +592 -0
  88. package/src/review-package.ts +524 -0
  89. package/src/review-read.ts +238 -0
  90. package/src/review.ts +432 -0
  91. package/src/rig-version-date.ts +22 -0
  92. package/src/rig-version.ts +1047 -0
  93. package/src/run-record.ts +232 -0
  94. package/src/scene-authority.ts +410 -0
  95. package/src/scene-writers.ts +144 -0
  96. package/src/scene.ts +284 -0
  97. package/src/setup-painting.ts +46 -0
  98. package/src/signable.ts +65 -0
  99. package/src/signoff-answers.ts +76 -0
  100. package/src/signoff-checkpoint-act.ts +95 -0
  101. package/src/signoff-target.ts +127 -0
  102. package/src/signoff.ts +331 -0
  103. package/src/split-captions.ts +154 -0
  104. package/src/stage-progress.ts +379 -0
  105. package/src/stages.ts +148 -0
  106. package/src/step-hash.ts +19 -0
  107. package/src/structure-snapshot.ts +109 -0
  108. package/src/structure.ts +166 -0
  109. package/src/templates.ts +16 -0
  110. package/src/waypoints.ts +163 -0
  111. package/templates/agent-guide.md +108 -0
  112. package/templates/collection-agent-guide.md +72 -0
  113. package/templates/collection.json +252 -0
  114. package/templates/example-chain/AGENTS.md +134 -0
  115. package/templates/example-chain/CLAUDE.md +3 -0
  116. package/templates/example-chain/README.md +13 -0
  117. package/templates/example-chain/chain.json +2869 -0
  118. package/templates/example-chain/checkpoints/0a-interview-options/AGENTS.md +134 -0
  119. package/templates/example-chain/checkpoints/0a-interview-options/CLAUDE.md +3 -0
  120. package/templates/example-chain/checkpoints/0a-interview-options/evidence/.gitkeep +0 -0
  121. package/templates/example-chain/checkpoints/0a-interview-options/evidence/approval.txt +1 -0
  122. package/templates/example-chain/checkpoints/0a-interview-options/evidence/interview/rig_type.json +59 -0
  123. package/templates/example-chain/checkpoints/0a-interview-options/evidence/split-config.json +1 -0
  124. package/templates/example-chain/checkpoints/0a-interview-options/inputs/painting.png +0 -0
  125. package/templates/example-chain/checkpoints/0a-interview-options/inputs/references/.gitkeep +0 -0
  126. package/templates/example-chain/checkpoints/0a-interview-options/package.json +9 -0
  127. package/templates/example-chain/checkpoints/0a-interview-options/project.json +295 -0
  128. package/templates/example-chain/checkpoints/0a-interview-options.json +127 -0
  129. package/templates/example-chain/checkpoints/0b-plan-pending/AGENTS.md +134 -0
  130. package/templates/example-chain/checkpoints/0b-plan-pending/CLAUDE.md +3 -0
  131. package/templates/example-chain/checkpoints/0b-plan-pending/evidence/.gitkeep +0 -0
  132. package/templates/example-chain/checkpoints/0b-plan-pending/evidence/approval.txt +1 -0
  133. package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/assemble.json +309 -0
  134. package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/reference.json +59 -0
  135. package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/rig_type.json +59 -0
  136. package/templates/example-chain/checkpoints/0b-plan-pending/evidence/interview/seethrough.json +186 -0
  137. package/templates/example-chain/checkpoints/0b-plan-pending/evidence/split-config.json +1 -0
  138. package/templates/example-chain/checkpoints/0b-plan-pending/inputs/painting.png +0 -0
  139. package/templates/example-chain/checkpoints/0b-plan-pending/inputs/references/.gitkeep +0 -0
  140. package/templates/example-chain/checkpoints/0b-plan-pending/package.json +9 -0
  141. package/templates/example-chain/checkpoints/0b-plan-pending/project.json +685 -0
  142. package/templates/example-chain/checkpoints/0b-plan-pending.json +81 -0
  143. package/templates/example-chain/checkpoints/1-parts-verdict/AGENTS.md +134 -0
  144. package/templates/example-chain/checkpoints/1-parts-verdict/CLAUDE.md +3 -0
  145. package/templates/example-chain/checkpoints/1-parts-verdict/evidence/.gitkeep +0 -0
  146. package/templates/example-chain/checkpoints/1-parts-verdict/evidence/approval.txt +1 -0
  147. package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/assemble.json +309 -0
  148. package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/reference.json +59 -0
  149. package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/rig_type.json +59 -0
  150. package/templates/example-chain/checkpoints/1-parts-verdict/evidence/interview/seethrough.json +186 -0
  151. package/templates/example-chain/checkpoints/1-parts-verdict/evidence/parts-check.json +127 -0
  152. package/templates/example-chain/checkpoints/1-parts-verdict/evidence/split-config.json +1 -0
  153. package/templates/example-chain/checkpoints/1-parts-verdict/inputs/painting.png +0 -0
  154. package/templates/example-chain/checkpoints/1-parts-verdict/inputs/references/.gitkeep +0 -0
  155. package/templates/example-chain/checkpoints/1-parts-verdict/package.json +9 -0
  156. package/templates/example-chain/checkpoints/1-parts-verdict/project.json +955 -0
  157. package/templates/example-chain/checkpoints/1-parts-verdict.json +204 -0
  158. package/templates/example-chain/checkpoints/2-rigging-answers/AGENTS.md +134 -0
  159. package/templates/example-chain/checkpoints/2-rigging-answers/CLAUDE.md +3 -0
  160. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/.gitkeep +0 -0
  161. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/approval.txt +1 -0
  162. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/diff-components.json +34 -0
  163. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/assemble.json +309 -0
  164. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/bones.json +190 -0
  165. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/reference.json +59 -0
  166. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/rig_type.json +59 -0
  167. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/interview/seethrough.json +186 -0
  168. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/parts-check.json +127 -0
  169. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/parts-review.json +88 -0
  170. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
  171. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/split-config.json +1 -0
  172. package/templates/example-chain/checkpoints/2-rigging-answers/evidence/structure.json +30 -0
  173. package/templates/example-chain/checkpoints/2-rigging-answers/inputs/painting.png +0 -0
  174. package/templates/example-chain/checkpoints/2-rigging-answers/inputs/references/.gitkeep +0 -0
  175. package/templates/example-chain/checkpoints/2-rigging-answers/package.json +9 -0
  176. package/templates/example-chain/checkpoints/2-rigging-answers/project.json +1198 -0
  177. package/templates/example-chain/checkpoints/2-rigging-answers.json +254 -0
  178. package/templates/example-chain/checkpoints/3-rig-review/AGENTS.md +134 -0
  179. package/templates/example-chain/checkpoints/3-rig-review/CLAUDE.md +3 -0
  180. package/templates/example-chain/checkpoints/3-rig-review/evidence/.gitkeep +0 -0
  181. package/templates/example-chain/checkpoints/3-rig-review/evidence/approval.txt +1 -0
  182. package/templates/example-chain/checkpoints/3-rig-review/evidence/diff-components.json +34 -0
  183. package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/assemble.json +309 -0
  184. package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/bones.json +190 -0
  185. package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/constraints.json +272 -0
  186. package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/meshes.json +190 -0
  187. package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/motion.json +272 -0
  188. package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/reference.json +59 -0
  189. package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/rig_type.json +59 -0
  190. package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/seethrough.json +186 -0
  191. package/templates/example-chain/checkpoints/3-rig-review/evidence/parts-check.json +127 -0
  192. package/templates/example-chain/checkpoints/3-rig-review/evidence/parts-review.json +88 -0
  193. package/templates/example-chain/checkpoints/3-rig-review/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
  194. package/templates/example-chain/checkpoints/3-rig-review/evidence/setup.png +0 -0
  195. package/templates/example-chain/checkpoints/3-rig-review/evidence/split-config.json +1 -0
  196. package/templates/example-chain/checkpoints/3-rig-review/evidence/structure.json +30 -0
  197. package/templates/example-chain/checkpoints/3-rig-review/evidence/v1/config.json +88 -0
  198. package/templates/example-chain/checkpoints/3-rig-review/evidence/v1/interview-requirements.json +8 -0
  199. package/templates/example-chain/checkpoints/3-rig-review/inputs/painting.png +0 -0
  200. package/templates/example-chain/checkpoints/3-rig-review/inputs/references/.gitkeep +0 -0
  201. package/templates/example-chain/checkpoints/3-rig-review/package.json +9 -0
  202. package/templates/example-chain/checkpoints/3-rig-review/project.json +1909 -0
  203. package/templates/example-chain/checkpoints/3-rig-review.json +424 -0
  204. package/templates/example-chain/checkpoints/4-direction-post/AGENTS.md +134 -0
  205. package/templates/example-chain/checkpoints/4-direction-post/CLAUDE.md +3 -0
  206. package/templates/example-chain/checkpoints/4-direction-post/evidence/.gitkeep +0 -0
  207. package/templates/example-chain/checkpoints/4-direction-post/evidence/approval.txt +1 -0
  208. package/templates/example-chain/checkpoints/4-direction-post/evidence/diff-components.json +34 -0
  209. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/assemble.json +309 -0
  210. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/bones.json +190 -0
  211. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/camera_depth.json +102 -0
  212. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/constraints.json +272 -0
  213. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/meshes.json +190 -0
  214. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/motion.json +272 -0
  215. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/post_composition.json +122 -0
  216. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/post_timing.json +102 -0
  217. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/reference.json +59 -0
  218. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/rig_type.json +59 -0
  219. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/seethrough.json +186 -0
  220. package/templates/example-chain/checkpoints/4-direction-post/evidence/parts-check.json +127 -0
  221. package/templates/example-chain/checkpoints/4-direction-post/evidence/parts-review.json +88 -0
  222. package/templates/example-chain/checkpoints/4-direction-post/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
  223. package/templates/example-chain/checkpoints/4-direction-post/evidence/setup.png +0 -0
  224. package/templates/example-chain/checkpoints/4-direction-post/evidence/split-config.json +1 -0
  225. package/templates/example-chain/checkpoints/4-direction-post/evidence/structure.json +30 -0
  226. package/templates/example-chain/checkpoints/4-direction-post/evidence/v1/config.json +88 -0
  227. package/templates/example-chain/checkpoints/4-direction-post/evidence/v1/interview-requirements.json +8 -0
  228. package/templates/example-chain/checkpoints/4-direction-post/inputs/painting.png +0 -0
  229. package/templates/example-chain/checkpoints/4-direction-post/inputs/references/.gitkeep +0 -0
  230. package/templates/example-chain/checkpoints/4-direction-post/package.json +9 -0
  231. package/templates/example-chain/checkpoints/4-direction-post/project.json +2302 -0
  232. package/templates/example-chain/checkpoints/4-direction-post.json +456 -0
  233. package/templates/example-chain/evidence/.gitkeep +0 -0
  234. package/templates/example-chain/evidence/approval.txt +1 -0
  235. package/templates/example-chain/evidence/captions.json +101 -0
  236. package/templates/example-chain/evidence/diff-components.json +34 -0
  237. package/templates/example-chain/evidence/direction/v2.json +74 -0
  238. package/templates/example-chain/evidence/direction-input.json +74 -0
  239. package/templates/example-chain/evidence/interview/assemble.json +309 -0
  240. package/templates/example-chain/evidence/interview/bones.json +190 -0
  241. package/templates/example-chain/evidence/interview/camera_depth.json +102 -0
  242. package/templates/example-chain/evidence/interview/constraints.json +272 -0
  243. package/templates/example-chain/evidence/interview/meshes.json +190 -0
  244. package/templates/example-chain/evidence/interview/motion.json +272 -0
  245. package/templates/example-chain/evidence/interview/post_composition.json +122 -0
  246. package/templates/example-chain/evidence/interview/post_timing.json +102 -0
  247. package/templates/example-chain/evidence/interview/reference.json +59 -0
  248. package/templates/example-chain/evidence/interview/rig_type.json +59 -0
  249. package/templates/example-chain/evidence/interview/seethrough.json +186 -0
  250. package/templates/example-chain/evidence/parts-check.json +127 -0
  251. package/templates/example-chain/evidence/parts-review.json +88 -0
  252. package/templates/example-chain/evidence/review-reads/a12dea02d32508acc99386f894af76a14f1c85f45f804db2aa6b4dea5297973f.json +59 -0
  253. package/templates/example-chain/evidence/scene/s1.json +65 -0
  254. package/templates/example-chain/evidence/scene-citations.json +9 -0
  255. package/templates/example-chain/evidence/setup.png +0 -0
  256. package/templates/example-chain/evidence/split-config.json +1 -0
  257. package/templates/example-chain/evidence/structure.json +30 -0
  258. package/templates/example-chain/evidence/v1/config.json +88 -0
  259. package/templates/example-chain/evidence/v1/interview-requirements.json +8 -0
  260. package/templates/example-chain/inputs/background.png +0 -0
  261. package/templates/example-chain/inputs/painting.png +0 -0
  262. package/templates/example-chain/inputs/references/.gitkeep +0 -0
  263. package/templates/example-chain/package.json +9 -0
  264. package/templates/example-chain/project.json +2593 -0
  265. package/templates/example-diff/diff-components.json +71 -0
  266. package/templates/example-diff/recomposite_rig.png +0 -0
  267. package/templates/example-direction/approval.txt +1 -0
  268. package/templates/example-direction/derived/review/v1/out/check/build/rectangle.png +0 -0
  269. package/templates/example-direction/derived/review/v1/out/check/build/run.json +40 -0
  270. package/templates/example-direction/derived/review/v1/out/check/build/skeleton.atlas +8 -0
  271. package/templates/example-direction/derived/review/v1/out/check/build/skeleton.json +73 -0
  272. package/templates/example-direction/derived/review/v1/out/check/build/skeleton.model.json +195 -0
  273. package/templates/example-direction/evidence/direction/v2.json +69 -0
  274. package/templates/example-direction/evidence/direction/v3.json +59 -0
  275. package/templates/example-direction/evidence/images/rectangle.png +0 -0
  276. package/templates/example-direction/evidence/input.json +69 -0
  277. package/templates/example-direction/evidence/motion.json +33 -0
  278. package/templates/example-direction/evidence/patch.json +23 -0
  279. package/templates/example-direction/evidence/rig-config.json +55 -0
  280. package/templates/example-direction/evidence/still.png +0 -0
  281. package/templates/example-direction/project.json +372 -0
  282. package/templates/example-direction/reasoning.txt +1 -0
  283. package/templates/example-direction/words.txt +1 -0
  284. package/templates/example-draft/README.md +28 -0
  285. package/templates/example-draft/config.json +71 -0
  286. package/templates/example-draft/derived/drafts/lantern/config.json +71 -0
  287. package/templates/example-draft/derived/drafts/lantern/input/painting_in_rig.png +0 -0
  288. package/templates/example-draft/derived/drafts/lantern/input/parts/background.png +0 -0
  289. package/templates/example-draft/derived/drafts/lantern/input/parts/eye_l.png +0 -0
  290. package/templates/example-draft/derived/drafts/lantern/input/parts/eye_r.png +0 -0
  291. package/templates/example-draft/derived/drafts/lantern/input/parts/face.png +0 -0
  292. package/templates/example-draft/derived/drafts/lantern/input/parts/lantern.png +0 -0
  293. package/templates/example-draft/derived/drafts/lantern/input/parts/torso.png +0 -0
  294. package/templates/example-draft/derived/drafts/lantern/input/parts.json +188 -0
  295. package/templates/example-draft/derived/drafts/lantern/input/recomposite_rig.png +0 -0
  296. package/templates/example-draft/derived/drafts/lantern/interview-requirements.json +8 -0
  297. package/templates/example-draft/derived/drafts/lantern/out/check/build/run.json +40 -0
  298. package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.atlas +28 -0
  299. package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.json +193 -0
  300. package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.model.json +483 -0
  301. package/templates/example-draft/derived/drafts/lantern/out/check/build/skeleton.png +0 -0
  302. package/templates/example-draft/derived/drafts/lantern/out/images/background.png +0 -0
  303. package/templates/example-draft/derived/drafts/lantern/out/images/eye_l.png +0 -0
  304. package/templates/example-draft/derived/drafts/lantern/out/images/eye_r.png +0 -0
  305. package/templates/example-draft/derived/drafts/lantern/out/images/face.png +0 -0
  306. package/templates/example-draft/derived/drafts/lantern/out/images/lantern.png +0 -0
  307. package/templates/example-draft/derived/drafts/lantern/out/images/torso.png +0 -0
  308. package/templates/example-draft/derived/drafts/lantern/out/mesh_report.json +1 -0
  309. package/templates/example-draft/derived/drafts/lantern/out/motion.json +137 -0
  310. package/templates/example-draft/derived/drafts/lantern/out/rig.json +102 -0
  311. package/templates/example-draft/derived/drafts/lantern/out/run.json +52 -0
  312. package/templates/example-draft/derived/parts/rig/painting_in_rig.png +0 -0
  313. package/templates/example-draft/derived/parts/rig/parts/background.png +0 -0
  314. package/templates/example-draft/derived/parts/rig/parts/eye_l.png +0 -0
  315. package/templates/example-draft/derived/parts/rig/parts/eye_r.png +0 -0
  316. package/templates/example-draft/derived/parts/rig/parts/face.png +0 -0
  317. package/templates/example-draft/derived/parts/rig/parts/lantern.png +0 -0
  318. package/templates/example-draft/derived/parts/rig/parts/torso.png +0 -0
  319. package/templates/example-draft/derived/parts/rig/parts.json +188 -0
  320. package/templates/example-draft/derived/parts/rig/recomposite_rig.png +0 -0
  321. package/templates/example-draft/derived/review/v1/config.json +71 -0
  322. package/templates/example-draft/derived/review/v1/interview-requirements.json +8 -0
  323. package/templates/example-draft/derived/review/v1/out/check/build/run.json +40 -0
  324. package/templates/example-draft/derived/review/v1/out/check/build/skeleton.atlas +28 -0
  325. package/templates/example-draft/derived/review/v1/out/check/build/skeleton.json +193 -0
  326. package/templates/example-draft/derived/review/v1/out/check/build/skeleton.model.json +483 -0
  327. package/templates/example-draft/derived/review/v1/out/check/build/skeleton.png +0 -0
  328. package/templates/example-draft/derived/review/v1/out/parts/background.png +0 -0
  329. package/templates/example-draft/derived/review/v1/out/parts/eye_l.png +0 -0
  330. package/templates/example-draft/derived/review/v1/out/parts/eye_r.png +0 -0
  331. package/templates/example-draft/derived/review/v1/out/parts/face.png +0 -0
  332. package/templates/example-draft/derived/review/v1/out/parts/lantern.png +0 -0
  333. package/templates/example-draft/derived/review/v1/out/parts/torso.png +0 -0
  334. package/templates/example-draft/derived/review/v1/out/parts.json +188 -0
  335. package/templates/example-draft/derived/review/v1/out/rig/images/background.png +0 -0
  336. package/templates/example-draft/derived/review/v1/out/rig/images/eye_l.png +0 -0
  337. package/templates/example-draft/derived/review/v1/out/rig/images/eye_r.png +0 -0
  338. package/templates/example-draft/derived/review/v1/out/rig/images/face.png +0 -0
  339. package/templates/example-draft/derived/review/v1/out/rig/images/lantern.png +0 -0
  340. package/templates/example-draft/derived/review/v1/out/rig/images/torso.png +0 -0
  341. package/templates/example-draft/derived/review/v1/out/rig/mesh_report.json +1 -0
  342. package/templates/example-draft/derived/review/v1/out/rig/motion.json +137 -0
  343. package/templates/example-draft/derived/review/v1/out/rig/rig.json +102 -0
  344. package/templates/example-draft/derived/review/v1/out/rig/run.json +52 -0
  345. package/templates/example-draft/derived/review/v2/config.json +71 -0
  346. package/templates/example-draft/derived/review/v2/interview-requirements.json +8 -0
  347. package/templates/example-draft/derived/review/v2/out/check/build/run.json +40 -0
  348. package/templates/example-draft/derived/review/v2/out/check/build/skeleton.atlas +28 -0
  349. package/templates/example-draft/derived/review/v2/out/check/build/skeleton.json +193 -0
  350. package/templates/example-draft/derived/review/v2/out/check/build/skeleton.model.json +483 -0
  351. package/templates/example-draft/derived/review/v2/out/check/build/skeleton.png +0 -0
  352. package/templates/example-draft/derived/review/v2/out/parts/background.png +0 -0
  353. package/templates/example-draft/derived/review/v2/out/parts/eye_l.png +0 -0
  354. package/templates/example-draft/derived/review/v2/out/parts/eye_r.png +0 -0
  355. package/templates/example-draft/derived/review/v2/out/parts/face.png +0 -0
  356. package/templates/example-draft/derived/review/v2/out/parts/lantern.png +0 -0
  357. package/templates/example-draft/derived/review/v2/out/parts/torso.png +0 -0
  358. package/templates/example-draft/derived/review/v2/out/parts.json +188 -0
  359. package/templates/example-draft/derived/review/v2/out/rig/images/background.png +0 -0
  360. package/templates/example-draft/derived/review/v2/out/rig/images/eye_l.png +0 -0
  361. package/templates/example-draft/derived/review/v2/out/rig/images/eye_r.png +0 -0
  362. package/templates/example-draft/derived/review/v2/out/rig/images/face.png +0 -0
  363. package/templates/example-draft/derived/review/v2/out/rig/images/lantern.png +0 -0
  364. package/templates/example-draft/derived/review/v2/out/rig/images/torso.png +0 -0
  365. package/templates/example-draft/derived/review/v2/out/rig/mesh_report.json +1 -0
  366. package/templates/example-draft/derived/review/v2/out/rig/motion.json +137 -0
  367. package/templates/example-draft/derived/review/v2/out/rig/rig.json +102 -0
  368. package/templates/example-draft/derived/review/v2/out/rig/run.json +52 -0
  369. package/templates/example-draft/evidence/diff-components.json +71 -0
  370. package/templates/example-draft/evidence/direction/v4.json +64 -0
  371. package/templates/example-draft/evidence/direction-input-v4.json +64 -0
  372. package/templates/example-draft/evidence/fix-ledger/lantern.json +17 -0
  373. package/templates/example-draft/evidence/parts-check.json +127 -0
  374. package/templates/example-draft/evidence/parts-review.json +88 -0
  375. package/templates/example-draft/evidence/review-reads/4ed0507381809746443021554e012032be5b5f8417da0f2e525e9b6df16e9f05.json +59 -0
  376. package/templates/example-draft/evidence/scene/s1.json +65 -0
  377. package/templates/example-draft/evidence/scene-background.png +0 -0
  378. package/templates/example-draft/evidence/scene-citations.json +9 -0
  379. package/templates/example-draft/evidence/structure-decisions.json +9 -0
  380. package/templates/example-draft/evidence/v1/interview-config.json +71 -0
  381. package/templates/example-draft/evidence/v1/interview-requirements.json +8 -0
  382. package/templates/example-draft/evidence/v1/review-config.json +71 -0
  383. package/templates/example-draft/evidence/v2/interview-config.json +71 -0
  384. package/templates/example-draft/evidence/v2/interview-requirements.json +8 -0
  385. package/templates/example-draft/evidence/v2/review-config.json +71 -0
  386. package/templates/example-draft/input/painting_in_rig.png +0 -0
  387. package/templates/example-draft/input/parts/background.png +0 -0
  388. package/templates/example-draft/input/parts/eye_l.png +0 -0
  389. package/templates/example-draft/input/parts/eye_r.png +0 -0
  390. package/templates/example-draft/input/parts/face.png +0 -0
  391. package/templates/example-draft/input/parts/lantern.png +0 -0
  392. package/templates/example-draft/input/parts/torso.png +0 -0
  393. package/templates/example-draft/input/parts.json +188 -0
  394. package/templates/example-draft/input/recomposite_rig.png +0 -0
  395. package/templates/example-draft/inputs/painting.png +0 -0
  396. package/templates/example-draft/interview-requirements.json +8 -0
  397. package/templates/example-draft/out/check/build/run.json +40 -0
  398. package/templates/example-draft/out/check/build/skeleton.atlas +28 -0
  399. package/templates/example-draft/out/check/build/skeleton.json +193 -0
  400. package/templates/example-draft/out/check/build/skeleton.model.json +483 -0
  401. package/templates/example-draft/out/check/build/skeleton.png +0 -0
  402. package/templates/example-draft/out/images/background.png +0 -0
  403. package/templates/example-draft/out/images/eye_l.png +0 -0
  404. package/templates/example-draft/out/images/eye_r.png +0 -0
  405. package/templates/example-draft/out/images/face.png +0 -0
  406. package/templates/example-draft/out/images/lantern.png +0 -0
  407. package/templates/example-draft/out/images/torso.png +0 -0
  408. package/templates/example-draft/out/mesh_report.json +1 -0
  409. package/templates/example-draft/out/motion.json +137 -0
  410. package/templates/example-draft/out/rig.json +102 -0
  411. package/templates/example-draft/out/run.json +52 -0
  412. package/templates/example-draft/outputs/v1/out/check/build/run.json +40 -0
  413. package/templates/example-draft/outputs/v1/out/check/build/skeleton.atlas +28 -0
  414. package/templates/example-draft/outputs/v1/out/check/build/skeleton.json +193 -0
  415. package/templates/example-draft/outputs/v1/out/check/build/skeleton.model.json +483 -0
  416. package/templates/example-draft/outputs/v1/out/check/build/skeleton.png +0 -0
  417. package/templates/example-draft/outputs/v1/out/painting_in_rig.png +0 -0
  418. package/templates/example-draft/outputs/v1/out/parts/background.png +0 -0
  419. package/templates/example-draft/outputs/v1/out/parts/eye_l.png +0 -0
  420. package/templates/example-draft/outputs/v1/out/parts/eye_r.png +0 -0
  421. package/templates/example-draft/outputs/v1/out/parts/face.png +0 -0
  422. package/templates/example-draft/outputs/v1/out/parts/lantern.png +0 -0
  423. package/templates/example-draft/outputs/v1/out/parts/torso.png +0 -0
  424. package/templates/example-draft/outputs/v1/out/parts.json +188 -0
  425. package/templates/example-draft/outputs/v1/out/rig/images/background.png +0 -0
  426. package/templates/example-draft/outputs/v1/out/rig/images/eye_l.png +0 -0
  427. package/templates/example-draft/outputs/v1/out/rig/images/eye_r.png +0 -0
  428. package/templates/example-draft/outputs/v1/out/rig/images/face.png +0 -0
  429. package/templates/example-draft/outputs/v1/out/rig/images/lantern.png +0 -0
  430. package/templates/example-draft/outputs/v1/out/rig/images/torso.png +0 -0
  431. package/templates/example-draft/outputs/v1/out/rig/mesh_report.json +1 -0
  432. package/templates/example-draft/outputs/v1/out/rig/motion.json +137 -0
  433. package/templates/example-draft/outputs/v1/out/rig/rig.json +102 -0
  434. package/templates/example-draft/outputs/v1/out/rig/run.json +52 -0
  435. package/templates/example-draft/outputs/v2/out/check/build/run.json +40 -0
  436. package/templates/example-draft/outputs/v2/out/check/build/skeleton.atlas +28 -0
  437. package/templates/example-draft/outputs/v2/out/check/build/skeleton.json +193 -0
  438. package/templates/example-draft/outputs/v2/out/check/build/skeleton.model.json +483 -0
  439. package/templates/example-draft/outputs/v2/out/check/build/skeleton.png +0 -0
  440. package/templates/example-draft/outputs/v2/out/painting_in_rig.png +0 -0
  441. package/templates/example-draft/outputs/v2/out/parts/background.png +0 -0
  442. package/templates/example-draft/outputs/v2/out/parts/eye_l.png +0 -0
  443. package/templates/example-draft/outputs/v2/out/parts/eye_r.png +0 -0
  444. package/templates/example-draft/outputs/v2/out/parts/face.png +0 -0
  445. package/templates/example-draft/outputs/v2/out/parts/lantern.png +0 -0
  446. package/templates/example-draft/outputs/v2/out/parts/torso.png +0 -0
  447. package/templates/example-draft/outputs/v2/out/parts.json +188 -0
  448. package/templates/example-draft/outputs/v2/out/rig/images/background.png +0 -0
  449. package/templates/example-draft/outputs/v2/out/rig/images/eye_l.png +0 -0
  450. package/templates/example-draft/outputs/v2/out/rig/images/eye_r.png +0 -0
  451. package/templates/example-draft/outputs/v2/out/rig/images/face.png +0 -0
  452. package/templates/example-draft/outputs/v2/out/rig/images/lantern.png +0 -0
  453. package/templates/example-draft/outputs/v2/out/rig/images/torso.png +0 -0
  454. package/templates/example-draft/outputs/v2/out/rig/mesh_report.json +1 -0
  455. package/templates/example-draft/outputs/v2/out/rig/motion.json +137 -0
  456. package/templates/example-draft/outputs/v2/out/rig/rig.json +102 -0
  457. package/templates/example-draft/outputs/v2/out/rig/run.json +52 -0
  458. package/templates/example-interview-choice.json +1068 -0
  459. package/templates/example-interview-options/constraint.json +376 -0
  460. package/templates/example-interview-options/example-assemble.json +376 -0
  461. package/templates/example-interview-options/example-bones.json +190 -0
  462. package/templates/example-interview-options/example-camera-depth.json +99 -0
  463. package/templates/example-interview-options/example-constraints.json +272 -0
  464. package/templates/example-interview-options/example-meshes.json +190 -0
  465. package/templates/example-interview-options/example-motion.json +272 -0
  466. package/templates/example-interview-options/example-picks.json +15 -0
  467. package/templates/example-interview-options/example-post-composition.json +110 -0
  468. package/templates/example-interview-options/example-post-timing.json +105 -0
  469. package/templates/example-interview-options/example-reference.json +63 -0
  470. package/templates/example-interview-options/example-rig-type.json +63 -0
  471. package/templates/example-interview-options/example-seethrough.json +190 -0
  472. package/templates/example-interview-options/fullscreen.json +63 -0
  473. package/templates/example-interview-options/other-explicit.json +63 -0
  474. package/templates/example-interview-options/other-pending.json +63 -0
  475. package/templates/example-interview-options/other-picks.json +16 -0
  476. package/templates/example-interview.md +213 -0
  477. package/templates/example-invariants/invariants.json +1339 -0
  478. package/templates/example-prepared/prepared.svg +1 -0
  479. package/templates/example-prepared/source.svg +1 -0
  480. package/templates/example-project.json +4259 -0
  481. package/templates/example-remesh/AGENTS.md +134 -0
  482. package/templates/example-remesh/CLAUDE.md +3 -0
  483. package/templates/example-remesh/evidence/.gitkeep +0 -0
  484. package/templates/example-remesh/evidence/approval.txt +1 -0
  485. package/templates/example-remesh/evidence/captions.json +101 -0
  486. package/templates/example-remesh/evidence/diff-components.json +34 -0
  487. package/templates/example-remesh/evidence/direction/v4.json +74 -0
  488. package/templates/example-remesh/evidence/direction/v5.json +74 -0
  489. package/templates/example-remesh/evidence/direction-input-v4.json +74 -0
  490. package/templates/example-remesh/evidence/direction-input-v5.json +74 -0
  491. package/templates/example-remesh/evidence/interview/assemble.json +309 -0
  492. package/templates/example-remesh/evidence/interview/bones.json +190 -0
  493. package/templates/example-remesh/evidence/interview/camera_depth.json +103 -0
  494. package/templates/example-remesh/evidence/interview/constraints.json +272 -0
  495. package/templates/example-remesh/evidence/interview/meshes.json +190 -0
  496. package/templates/example-remesh/evidence/interview/motion.json +272 -0
  497. package/templates/example-remesh/evidence/interview/post_composition.json +123 -0
  498. package/templates/example-remesh/evidence/interview/post_timing.json +103 -0
  499. package/templates/example-remesh/evidence/interview/reference.json +59 -0
  500. package/templates/example-remesh/evidence/interview/rig_type.json +59 -0
  501. package/templates/example-remesh/evidence/interview/seethrough.json +186 -0
  502. package/templates/example-remesh/evidence/parts-check.json +127 -0
  503. package/templates/example-remesh/evidence/parts-review.json +88 -0
  504. package/templates/example-remesh/evidence/reopen.txt +1 -0
  505. package/templates/example-remesh/evidence/review-reads/6a17db694db96f1b6049a61a017e0c8e66757b982b8851e0187e561cfcf9af68.json +59 -0
  506. package/templates/example-remesh/evidence/scene/s1.json +69 -0
  507. package/templates/example-remesh/evidence/scene-citations.json +9 -0
  508. package/templates/example-remesh/evidence/setup.png +0 -0
  509. package/templates/example-remesh/evidence/split-config.json +1 -0
  510. package/templates/example-remesh/evidence/structure.json +30 -0
  511. package/templates/example-remesh/evidence/v1/registration/at-source.json +44 -0
  512. package/templates/example-remesh/evidence/v1/registration/citations.json +68 -0
  513. package/templates/example-remesh/evidence/v1/registration/compile-report.json +368 -0
  514. package/templates/example-remesh/evidence/v1/registration/config-source.json +88 -0
  515. package/templates/example-remesh/evidence/v1/registration/config.json +88 -0
  516. package/templates/example-remesh/evidence/v1/registration/interview-requirements.json +8 -0
  517. package/templates/example-remesh/evidence/v1/registration/package-lock.json +11 -0
  518. package/templates/example-remesh/evidence/v1/registration/path-adapter.json +1 -0
  519. package/templates/example-remesh/evidence/v1/registration/rigc-run-1.json +44 -0
  520. package/templates/example-remesh/evidence/v2/registration/at-source.json +44 -0
  521. package/templates/example-remesh/evidence/v2/registration/citations.json +68 -0
  522. package/templates/example-remesh/evidence/v2/registration/compile-report.json +368 -0
  523. package/templates/example-remesh/evidence/v2/registration/config-source.json +88 -0
  524. package/templates/example-remesh/evidence/v2/registration/config.json +88 -0
  525. package/templates/example-remesh/evidence/v2/registration/interview-requirements.json +8 -0
  526. package/templates/example-remesh/evidence/v2/registration/package-lock.json +11 -0
  527. package/templates/example-remesh/evidence/v2/registration/path-adapter.json +1 -0
  528. package/templates/example-remesh/evidence/v2/registration/rigc-run-1.json +44 -0
  529. package/templates/example-remesh/evidence/v3/registration/at-source.json +44 -0
  530. package/templates/example-remesh/evidence/v3/registration/citations.json +68 -0
  531. package/templates/example-remesh/evidence/v3/registration/compile-report.json +368 -0
  532. package/templates/example-remesh/evidence/v3/registration/config-source.json +88 -0
  533. package/templates/example-remesh/evidence/v3/registration/config.json +88 -0
  534. package/templates/example-remesh/evidence/v3/registration/interview-requirements.json +8 -0
  535. package/templates/example-remesh/evidence/v3/registration/package-lock.json +11 -0
  536. package/templates/example-remesh/evidence/v3/registration/path-adapter.json +1 -0
  537. package/templates/example-remesh/evidence/v3/registration/rigc-run-1.json +44 -0
  538. package/templates/example-remesh/inputs/background.png +0 -0
  539. package/templates/example-remesh/inputs/painting.png +0 -0
  540. package/templates/example-remesh/inputs/references/.gitkeep +0 -0
  541. package/templates/example-remesh/package.json +9 -0
  542. package/templates/example-remesh/project.json +3916 -0
  543. package/templates/example-reopened/project.json +4568 -0
  544. package/templates/example-review/config.json +1 -0
  545. package/templates/example-review/out/check/build/skeleton.model.json +1 -0
  546. package/templates/example-review/out/parts/piece.svg +1 -0
  547. package/templates/example-review/out/parts.json +1 -0
  548. package/templates/example-review/out/rig/mesh_report.json +1 -0
  549. package/templates/example-review/out/rig/motion.json +1 -0
  550. package/templates/example-review/out/rig/rig.json +1 -0
  551. package/templates/example-split/captions.json +357 -0
  552. package/templates/example-split/config.json +215 -0
  553. package/templates/legacy-example-project.json +1071 -0
  554. package/templates/project.json +199 -0
  555. package/templates/shared/conventions.json +23 -0
@@ -0,0 +1,592 @@
1
+ import { PARTS_CLI_RECEIPT } from './parts-verdict-record.ts';
2
+ /** Finalized semantic acts. This reader never opens Review's private state. */
3
+ import { randomUUID } from 'node:crypto';
4
+ import { readFileSync, realpathSync } from './check-reads.ts';
5
+ import { dirname, join } from 'node:path';
6
+ import { isDeepStrictEqual } from 'node:util';
7
+ import { JOB_COMMENTS_ACT_SCHEMA, parseCommentsAct, parseRecordAct, projectComments,
8
+ type JobAct, type JobCommentPartsAnchor, type JobCommentsAct, type JobModelSourceBinding, type JobRigStill, type RecordAct } from '@rig-c/review/job-host';
9
+ import { jobHash, publicationVersionHash, readAcknowledgedPublication, readWriterBinding } from './job-outbox.ts';
10
+ import { liveManifestPath, publicationFile } from './job-publication.ts';
11
+ import { Refusal, refuse } from './refusal.ts';
12
+ import { INTAKE_QUESTION, intakeItem } from './intake.ts';
13
+ import { collectItems, asArray, isObject } from './check.ts';
14
+ import { stepHash } from './step-hash.ts';
15
+ import { recordDirectionTarget } from './record-direction-target.ts';
16
+ import type { Json, JsonObject } from './templates.ts';
17
+
18
+ export type PartsIssue = {
19
+ readonly id: string;
20
+ readonly summary: string;
21
+ readonly parts: readonly { readonly name: string; readonly imageHash: string }[];
22
+ readonly sourceHash: string;
23
+ readonly rigRegion: JsonObject | JobCommentPartsAnchor['rigRegion'];
24
+ readonly imageRegion: JsonObject | JobCommentPartsAnchor['imageRegion'] | null;
25
+ readonly view: JsonObject | JobCommentPartsAnchor['view'];
26
+ readonly crop: JsonObject | JobCommentPartsAnchor['crop'];
27
+ readonly nativeCrop: JsonObject | JobCommentPartsAnchor['nativeCrop'] | null;
28
+ };
29
+ /** Review's JobAct now includes comments; legacy adapters keep their original source guarantees. */
30
+ type LegacyJobAct = Exclude<JobAct, { schema: typeof JOB_COMMENTS_ACT_SCHEMA }>;
31
+ export type InterviewPicksAct = Extract<LegacyJobAct | RecordAct, { kind: 'interview-picks' }>;
32
+ export type PartsVerdictActV2 = Extract<LegacyJobAct, { kind: 'parts-verdict' }>;
33
+ export type PartsVerdictActV5 = Omit<PartsVerdictActV2, 'schema' | 'value'> & {
34
+ readonly schema: 'rig-review/act@5';
35
+ readonly value: {
36
+ readonly verdict: 'approved' | 'changes-requested';
37
+ readonly issues: readonly PartsIssue[];
38
+ readonly words: string;
39
+ };
40
+ };
41
+ type CommentsPartsVerdictAct = Extract<JobCommentsAct, { kind: 'parts-verdict' }> & { source: PartsVerdictActV2['source'] };
42
+ export type PartsVerdictAct = PartsVerdictActV2 | PartsVerdictActV5 | CommentsPartsVerdictAct;
43
+ type LegacyReviewVerdictAct = Extract<LegacyJobAct, { kind: 'review-verdict' }> & { source: JobModelSourceBinding };
44
+ type CommentsReviewVerdictAct = Extract<JobCommentsAct, { kind: 'review-verdict' }> & {
45
+ source: JobModelSourceBinding; value: { stage: 'rig-review' | 'direction-review' };
46
+ };
47
+ export type ReviewVerdictAct = LegacyReviewVerdictAct | CommentsReviewVerdictAct;
48
+ type LegacyCheckpointAct = Extract<LegacyJobAct | RecordAct, { kind: 'checkpoint' }>;
49
+ export type CheckpointAct = LegacyCheckpointAct | (Extract<JobCommentsAct, { kind: 'checkpoint' }> & { source: LegacyCheckpointAct['source'] });
50
+ type LegacySignoffAct = Extract<LegacyJobAct | RecordAct, { kind: 'signoff' }>;
51
+ export type SignoffAct = LegacySignoffAct | (Extract<JobCommentsAct, { kind: 'signoff' }> & { source: LegacySignoffAct['source'] });
52
+ /** Review requires a displayed plan before it creates a signoff act for these stages (job-contract JOB_PLAN_BOUND_SIGNOFF_STAGES). */
53
+ export const PLAN_BOUND_SIGNOFF_STAGES: readonly string[] = ['plan-approved', 'rigging-interview', 'post-interview'];
54
+ /** Interview is plan-bound only on the record route; legacy package acts keep their contract. */
55
+ export function isRecordInterviewSignoff(act: SignoffAct): boolean {
56
+ return act.schema === 'rig-review/act@4' && String(act.value.stage) === 'interview';
57
+ }
58
+ export interface FetchedReviewAct { act: ReviewVerdictAct; bytes: string; stillBytes?: string }
59
+
60
+ /** The scalar consumed by the existing feedback writers and immutable replay checks. */
61
+ export function reviewActVerbatim(act: ReviewVerdictAct | PartsVerdictAct | CheckpointAct | SignoffAct): string {
62
+ return act.schema === JOB_COMMENTS_ACT_SCHEMA ? projectComments(act.value.comments) : act.value.words;
63
+ }
64
+
65
+ /** A still belongs to the recorded rig verdict, independently of its transport. */
66
+ export function reviewActStill(act: ReviewVerdictAct): JobRigStill | null {
67
+ if (act.schema === 'rig-review/act@3') return act.still;
68
+ if (act.schema === JOB_COMMENTS_ACT_SCHEMA && act.value.stage === 'rig-review') {
69
+ return act.still ?? failure('REVIEW_ACT_STILL_INVALID', 'comments rig verdict has no recorded still');
70
+ }
71
+ return null;
72
+ }
73
+
74
+ /** Pointed parts evidence stays distinct from the overall verdict's words. */
75
+ export function partsActIssues(act: PartsVerdictAct): readonly PartsIssue[] | null {
76
+ if (act.schema === JOB_COMMENTS_ACT_SCHEMA) {
77
+ return act.value.comments.flatMap(comment => comment.anchor && 'schema' in comment.anchor
78
+ ? [{ ...comment.anchor, summary: comment.text }] : []);
79
+ }
80
+ return act.schema === 'rig-review/act@5' ? act.value.issues : null;
81
+ }
82
+ const object = (value: unknown): value is JsonObject => value !== null && typeof value === 'object' && !Array.isArray(value);
83
+ const sha = (value: unknown): value is string => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
84
+ const id = (value: unknown): value is string => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
85
+ const file = (value: unknown): value is JsonObject => object(value) && typeof value.path === 'string' && sha(value.sha256);
86
+ function failure(code: string, detail: string): never {
87
+ refuse(code, `project.json.review_acts: ${detail}; restore the original evidence or request a new act in the live Review job`);
88
+ }
89
+
90
+ /** The shared decoder validates grammar; Create separately admits its existing source adapters. */
91
+ function decodeCommentsReviewAct(raw: JsonObject, kind: 'review-verdict' | 'parts-verdict' | 'interview-picks' | 'checkpoint' | 'signoff'): ReviewVerdictAct | PartsVerdictAct | CheckpointAct | SignoffAct {
92
+ if (kind === 'interview-picks' || raw.kind === 'interview-picks') failure('REVIEW_ACT_KIND_MISMATCH', 'comments acts do not carry interview answers');
93
+ let act: JobCommentsAct;
94
+ try { act = parseCommentsAct(raw); }
95
+ catch { return failure('REVIEW_ACT_INVALID', 'comments act or its declared source binding is malformed'); }
96
+ if (act.kind !== kind) failure('REVIEW_ACT_KIND_MISMATCH', `kind ${act.kind} does not match ${kind}`);
97
+ const source = act.source as unknown as JsonObject;
98
+ if ('record' in source) {
99
+ if (act.kind !== 'checkpoint' || source.subject !== 'plan') failure('REVIEW_ACT_KIND_MISMATCH', 'this record-source kind has no Create writer mapping');
100
+ return act as CheckpointAct;
101
+ }
102
+ if (source.subject === 'image-set' || act.kind === 'review-verdict' && act.value.stage === 'image-review') {
103
+ failure('REVIEW_ACT_KIND_MISMATCH', 'image-review has no rig/post writer mapping');
104
+ }
105
+ const parts = source.parts;
106
+ const version = object(parts) && typeof parts.enumeratorVersion === 'string' ? parts.enumeratorVersion : '';
107
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version);
108
+ if (!match || !match.slice(1).every(piece => Number.isSafeInteger(Number(piece))) || (Number(match[1]) === 2 ? Number(match[2]) < 25 : Number(match[1]) !== 3)) {
109
+ failure('REVIEW_PARTS_ENUMERATOR_UNSUPPORTED', `parts enumerator ${version || 'absent'} is outside >=2.25.0 <4.0.0`);
110
+ }
111
+ if (!sha(source.projectHash) || !object(source.package) || !id(source.package.id)
112
+ || typeof source.package.revision !== 'string' || !/^\d{4}-\d\d-\d\dT.*(?:Z|[+-]\d\d:\d\d)$/.test(act.at)) {
113
+ failure('REVIEW_ACT_INVALID', 'comments act lacks the existing package/project identity');
114
+ }
115
+ if (act.kind === 'checkpoint') {
116
+ const plan = source.plan;
117
+ if (!object(plan) || !Array.isArray(plan.steps) || !plan.steps.length
118
+ || !plan.steps.some(step => object(step) && step.id === act.value.planStepId)) {
119
+ failure('REVIEW_ACT_INVALID', 'comments checkpoint has no displayed plan step');
120
+ }
121
+ return act as CheckpointAct;
122
+ }
123
+ if (act.kind === 'signoff') return act as SignoffAct;
124
+ const selector = source.selector;
125
+ if (!object(parts) || !file(parts.manifest) || !Array.isArray(parts.images)
126
+ || parts.images.some(image => !file(image) || typeof image.role !== 'string')) {
127
+ failure('REVIEW_ACT_INVALID', 'comments act has no admitted parts binding');
128
+ }
129
+ if (act.kind === 'parts-verdict') {
130
+ if (!object(selector) || selector.kind !== 'draft' || !id(selector.id)) failure('REVIEW_ACT_KIND_MISMATCH', 'parts comments require the existing draft source');
131
+ const issues = partsActIssues(act as CommentsPartsVerdictAct)!;
132
+ if (issues.some(issue => issue.parts.some(part => !(parts.images as Json[]).some(image => object(image)
133
+ && image.role === 'part' && image.path === `parts/${part.name}.png` && image.sha256 === part.imageHash)))) {
134
+ failure('REVIEW_ACT_INVALID', 'comments issue part is outside the bound parts manifest');
135
+ }
136
+ return act as CommentsPartsVerdictAct;
137
+ }
138
+ if (act.kind !== 'review-verdict' || !object(selector) || !['rig', 'post'].includes(String(selector.kind))
139
+ || !Number.isSafeInteger(selector.version) || Number(selector.version) < 1 || !file(source.model)
140
+ || act.value.stage !== (selector.kind === 'rig' ? 'rig-review' : 'direction-review')
141
+ || selector.kind === 'rig' && source.direction !== null
142
+ || selector.kind === 'post' && (!Number.isSafeInteger(selector.rigVersion) || Number(selector.rigVersion) < 1 || !file(source.direction))) {
143
+ failure('REVIEW_ACT_KIND_MISMATCH', 'comments verdict has no existing rig/post source mapping');
144
+ }
145
+ return act as CommentsReviewVerdictAct;
146
+ }
147
+
148
+ /** Also used by check on the retained, explicitly encoded act bytes. */
149
+ export function decodeReviewAct(bytes: string): ReviewVerdictAct;
150
+ export function decodeReviewAct(bytes: string, kind: 'interview-picks'): InterviewPicksAct;
151
+ export function decodeReviewAct(bytes: string, kind: 'parts-verdict'): PartsVerdictAct;
152
+ export function decodeReviewAct(bytes: string, kind: 'checkpoint'): CheckpointAct;
153
+ export function decodeReviewAct(bytes: string, kind: 'signoff'): SignoffAct;
154
+ export function decodeReviewAct(bytes: string, kind: 'review-verdict' | 'parts-verdict' | 'interview-picks' | 'checkpoint' | 'signoff'): ReviewVerdictAct | PartsVerdictAct | InterviewPicksAct | CheckpointAct | SignoffAct;
155
+ export function decodeReviewAct(bytes: string, kind: 'review-verdict' | 'parts-verdict' | 'interview-picks' | 'checkpoint' | 'signoff' = 'review-verdict'): ReviewVerdictAct | PartsVerdictAct | InterviewPicksAct | CheckpointAct | SignoffAct {
156
+ let act: JsonObject;
157
+ try { const parsed: unknown = JSON.parse(bytes); if (!object(parsed)) throw Error(); act = parsed; }
158
+ catch { return failure('REVIEW_ACT_INVALID', 'act is not a JSON object'); }
159
+ if (act.schema === JOB_COMMENTS_ACT_SCHEMA) return decodeCommentsReviewAct(act, kind);
160
+ if (act.schema === 'rig-review/act@4' && act.kind === 'reopen' && kind !== 'checkpoint') failure('REVIEW_ACT_SCHEMA_UNSUPPORTED', 'record reopen acts have no Create writer mapping');
161
+ if (['checkpoint', 'interview-picks', 'signoff'].includes(kind) && act.schema === 'rig-review/act@4') {
162
+ try {
163
+ const recordAct = parseRecordAct(act);
164
+ if (recordAct.kind !== kind) return failure('REVIEW_ACT_KIND_MISMATCH', 'record act kind differs from requested kind');
165
+ return recordAct as CheckpointAct | InterviewPicksAct | SignoffAct;
166
+ } catch { return failure('REVIEW_ACT_INVALID', 'record act or its sealed source is malformed'); }
167
+ }
168
+ if (act.schema !== 'rig-review/act@2' && !(kind === 'review-verdict' && act.schema === 'rig-review/act@3')
169
+ && !(kind === 'parts-verdict' && act.schema === 'rig-review/act@5')) failure('REVIEW_ACT_SCHEMA_UNSUPPORTED', `act schema ${String(act.schema)} is unsupported, including legacy rig-review/act@1`);
170
+ const source = act.source;
171
+ const parts = object(source) ? source.parts : null;
172
+ const version = object(parts) && typeof parts.enumeratorVersion === 'string' ? parts.enumeratorVersion : '';
173
+ // Only stable releases in the frozen range are accepted. Prereleases are outside it.
174
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version);
175
+ if (!match || !match.slice(1).every(piece => Number.isSafeInteger(Number(piece))) || (Number(match[1]) === 2 ? Number(match[2]) < 25 : Number(match[1]) !== 3)) failure('REVIEW_PARTS_ENUMERATOR_UNSUPPORTED', `parts enumerator ${version || 'absent'} is outside >=2.25.0 <4.0.0`);
176
+ if (act.kind !== kind) failure('REVIEW_ACT_KIND_MISMATCH', `kind ${String(act.kind)} does not match ${kind}`);
177
+ const authority = act.authority;
178
+ const reviewer = object(authority) ? authority.reviewer : null;
179
+ const value = act.value;
180
+ if (!id(act.actId) || !id(act.jobId) || !id(act.askId) || !id(act.publicationId)
181
+ || typeof act.at !== 'string' || !/^\d{4}-\d\d-\d\dT.*(?:Z|[+-]\d\d:\d\d)$/.test(act.at) || !Number.isFinite(Date.parse(act.at))
182
+ || !object(authority) || authority.actor !== 'human' || authority.channel !== 'review-ui'
183
+ || !object(reviewer) || typeof reviewer.id !== 'string' || !reviewer.id.trim() || !['owner', 'delegate'].includes(String(reviewer.role))
184
+ || !object(source) || !sha(source.sourceHash) || !sha(source.projectHash)
185
+ || !object(source.package) || !id(source.package.id) || typeof source.package.revision !== 'string'
186
+ || !object(value) || kind !== 'interview-picks' && kind !== 'checkpoint' && kind !== 'signoff' && (typeof value.words !== 'string' || !['pending', 'approved', 'changes-requested'].includes(String(value.verdict)))) {
187
+ failure('REVIEW_ACT_INVALID', 'act identity, authority, source or verdict is malformed');
188
+ }
189
+ if (kind === 'interview-picks') {
190
+ if (!object(source.optionSet) || !id(source.optionSet.id) || !sha(source.optionSet.sha256)
191
+ || !id(value.questionId) || typeof value.note !== 'string' || !Array.isArray(value.selections)
192
+ || !value.selections.length || value.selections.some(row => !object(row) || !id(row.itemId) || !id(row.optionId)
193
+ || row.otherText !== null && typeof row.otherText !== 'string')
194
+ || new Set(value.selections.map(row => (row as JsonObject).itemId)).size !== value.selections.length) {
195
+ failure('REVIEW_ACT_INVALID', 'interview question, option set or selections are malformed');
196
+ }
197
+ return act as unknown as InterviewPicksAct;
198
+ }
199
+ if (kind === 'checkpoint') {
200
+ // Review copies planStepId from the validated ask; Create derives the stage from its own plan.
201
+ const steps = object(source.plan) && Array.isArray(source.plan.steps) ? source.plan.steps : [];
202
+ if (!steps.length || steps.some(step => !object(step) || !id(step.id) || !sha(step.hash))
203
+ || !id(value.checkpointId) || !id(value.planStepId) || !['pass', 'fail'].includes(String(value.outcome))
204
+ || typeof value.words !== 'string' || !steps.some(step => (step as JsonObject).id === value.planStepId)) {
205
+ failure('REVIEW_ACT_INVALID', 'checkpoint plan, step identity, outcome or words are malformed');
206
+ }
207
+ return act as unknown as CheckpointAct;
208
+ }
209
+ if (kind === 'signoff') {
210
+ // The stage is the writer's to validate; a plan-bound stage carries the displayed step identities.
211
+ const plan = source.plan;
212
+ if (!id(value.stage) || value.decision !== 'approved' || typeof value.words !== 'string'
213
+ || Object.keys(value).some(key => !['stage', 'decision', 'words'].includes(key))
214
+ || plan !== null && plan !== undefined && (!object(plan) || !Array.isArray(plan.steps)
215
+ || plan.steps.some(step => !object(step) || !id(step.id) || !sha(step.hash)))
216
+ || PLAN_BOUND_SIGNOFF_STAGES.includes(value.stage) && (!object(plan) || !Array.isArray(plan.steps) || !plan.steps.length)) {
217
+ failure('REVIEW_ACT_INVALID', 'signoff stage, decision, words or displayed plan is malformed');
218
+ }
219
+ return act as unknown as SignoffAct;
220
+ }
221
+ if (kind === 'parts-verdict') {
222
+ const selector = source.selector;
223
+ if (!object(selector) || selector.kind !== 'draft' || !id(selector.id) || !object(parts) || !file(parts.manifest)
224
+ || !Array.isArray(parts.images) || parts.images.some(row => !file(row) || typeof row.role !== 'string')
225
+ || source.model !== null && !file(source.model) || source.direction !== null && !file(source.direction)
226
+ || source.plan !== null && (!object(source.plan) || !Array.isArray(source.plan.steps)
227
+ || source.plan.steps.some(step => !object(step) || !id(step.id) || !sha(step.hash)))) failure('REVIEW_ACT_INVALID', 'parts verdict source or rows are malformed');
228
+ if (act.schema === 'rig-review/act@5') {
229
+ if (!['approved', 'changes-requested'].includes(String(value.verdict)) || !Array.isArray(value.issues)
230
+ || value.parts !== undefined) failure('REVIEW_ACT_INVALID', 'parts verdict source or issues are malformed');
231
+ const issueIds = new Set<string>();
232
+ for (const issue of value.issues) {
233
+ if (!object(issue) || !id(issue.id) || issueIds.has(issue.id)
234
+ || typeof issue.summary !== 'string' || !issue.summary.trim()
235
+ || !Array.isArray(issue.parts) || !issue.parts.length
236
+ || !sha(issue.sourceHash) || !object(issue.rigRegion)
237
+ || !('imageRegion' in issue) || (issue.imageRegion !== null && !object(issue.imageRegion))
238
+ || !object(issue.view) || !object(issue.crop)
239
+ || !('nativeCrop' in issue) || (issue.nativeCrop !== null && !object(issue.nativeCrop))) {
240
+ failure('REVIEW_ACT_INVALID', 'parts verdict issue is malformed');
241
+ }
242
+ issueIds.add(issue.id);
243
+ const partNames = new Set<string>();
244
+ for (const part of issue.parts) {
245
+ if (!object(part) || !id(part.name) || !sha(part.imageHash) || partNames.has(part.name)
246
+ || !parts.images.some(img => object(img) && img.role === 'part' && img.path === `parts/${part.name}.png` && img.sha256 === part.imageHash)) {
247
+ failure('REVIEW_ACT_INVALID', 'parts verdict issue part is invalid, duplicated or outside the bound parts manifest');
248
+ }
249
+ partNames.add(part.name);
250
+ }
251
+ }
252
+ if ((value.verdict === 'approved') !== (value.issues.length === 0)) failure('REVIEW_ACT_INVALID', 'parts verdict contradicts its issues');
253
+ return act as unknown as PartsVerdictAct;
254
+ }
255
+ if (!Array.isArray(value.parts) || !value.parts.length) failure('REVIEW_ACT_INVALID', 'parts verdict source or rows are malformed');
256
+ const rows = value.parts as JsonObject[];
257
+ const ids = new Set<string>();
258
+ for (const row of rows) {
259
+ if (!object(row) || !id(row.partId) || ids.has(row.partId)) failure('REVIEW_ACT_INVALID', 'part identities are invalid or duplicated');
260
+ ids.add(row.partId);
261
+ for (const outcome of [row.part, row.mesh]) {
262
+ if (!object(outcome) || !['pending', 'clean', 'flagged'].includes(String(outcome.outcome)) || typeof outcome.note !== 'string'
263
+ || outcome.outcome === 'flagged' && !outcome.note.trim()) failure('REVIEW_ACT_INVALID', 'part or mesh outcome is malformed');
264
+ }
265
+ }
266
+ const outcomes = rows.flatMap(row => [(row.part as JsonObject).outcome, (row.mesh as JsonObject).outcome]);
267
+ if (value.verdict === 'approved' && outcomes.some(outcome => outcome !== 'clean')
268
+ || value.verdict === 'changes-requested' && !outcomes.includes('flagged')) failure('REVIEW_ACT_INVALID', 'parts verdict contradicts its outcomes');
269
+ return act as unknown as PartsVerdictAct;
270
+ }
271
+ if (source.subject === 'image-set' || value.stage === 'image-review') failure('REVIEW_ACT_KIND_MISMATCH', 'image-review has no rig/post writer mapping');
272
+ const selector = source.selector;
273
+ if (!object(selector) || !['rig', 'post'].includes(String(selector.kind)) || !Number.isSafeInteger(selector.version) || Number(selector.version) < 1
274
+ || selector.kind === 'post' && (!Number.isSafeInteger(selector.rigVersion) || Number(selector.rigVersion) < 1)
275
+ || value.stage !== (selector.kind === 'rig' ? 'rig-review' : 'direction-review') || !file(source.model)
276
+ || selector.kind === 'post' && !file(source.direction) || selector.kind === 'rig' && source.direction !== null
277
+ || !object(parts) || !file(parts.manifest) || !Array.isArray(parts.images)
278
+ || parts.images.some(row => !file(row) || typeof row.role !== 'string')
279
+ || source.plan !== null && (!object(source.plan) || !Array.isArray(source.plan.steps)
280
+ || source.plan.steps.some(step => !object(step) || !id(step.id) || !sha(step.hash)))) {
281
+ failure('REVIEW_ACT_INVALID', 'rig/post source binding is malformed or contradicts the review stage');
282
+ }
283
+ if (act.schema === 'rig-review/act@3') {
284
+ const still = act.still;
285
+ if (value.stage !== 'rig-review' || !file(still) || still.path !== `${still.sha256}.png`
286
+ || still.camera !== 'fixed' || !(still.frame === 'setup' && still.animation === undefined
287
+ || typeof still.frame === 'number' && Number.isFinite(still.frame) && still.frame >= 0
288
+ && typeof still.animation === 'string' && still.animation.length > 0)) {
289
+ failure('REVIEW_ACT_INVALID', 'rig-review still identity, camera, frame or animation is malformed');
290
+ }
291
+ }
292
+ return act as unknown as ReviewVerdictAct;
293
+ }
294
+
295
+ export function decodeReviewResponse(text: string, jobId: string, actId: string, requestId: string, kind: 'review-verdict' | 'parts-verdict' | 'interview-picks' | 'checkpoint' | 'signoff' = 'review-verdict'): { act: ReviewVerdictAct | PartsVerdictAct | InterviewPicksAct | CheckpointAct | SignoffAct; bytes: string } {
296
+ let response: JsonObject;
297
+ try { const parsed: unknown = JSON.parse(text); if (!object(parsed)) throw Error(); response = parsed; }
298
+ catch { return failure('REVIEW_ACT_INVALID', 'Review response is not JSON'); }
299
+ if (response.schema !== 'rig-review/job-api@2') failure('REVIEW_API_SCHEMA_UNSUPPORTED', `API schema ${String(response.schema)} is unsupported`);
300
+ if (response.verb !== 'fetch-act' || response.jobId !== jobId || response.requestId !== requestId) failure('REVIEW_ACT_INVALID', 'fetch response identity differs from the request');
301
+ if (response.ok !== true) failure('REVIEW_ACT_FETCH_REFUSED', `Review refused fetch (${object(response.error) ? String(response.error.code) : 'invalid response'})`);
302
+ // The API exposes a semantic value, not a producer storage file. Retain this
303
+ // encoding once, and hash its exact UTF-8 bytes without reconstructing reports.
304
+ const bytes = JSON.stringify(response.value);
305
+ const act = decodeReviewAct(bytes, kind);
306
+ if (act.jobId !== jobId || act.actId !== actId) failure('REVIEW_ACT_INVALID', 'fetched act identity differs from the request');
307
+ return { act, bytes };
308
+ }
309
+
310
+ export interface ReviewActTransport {
311
+ origin: string;
312
+ jobId: string;
313
+ actId: string;
314
+ fetchCapability: string;
315
+ kind?: 'parts-verdict' | 'interview-picks' | 'checkpoint' | 'signoff';
316
+ }
317
+
318
+ /** Only the route and fetch grant leave the binding reader. Never attach a host. */
319
+ export function reviewActTransport(root: string, actId: string): ReviewActTransport {
320
+ const binding = readWriterBinding(root);
321
+ if (!binding) failure('JOB_BINDING_INVALID', 'writer binding is absent, open the live job first');
322
+ const project = JSON.parse(readFileSync(join(root, 'project.json'), 'utf8')) as JsonObject;
323
+ if (binding.identity.root !== realpathSync(root) || !object(project.review_job)
324
+ || project.review_job.jobId !== binding.jobId || project.review_job.origin !== binding.origin) failure('JOB_BINDING_INVALID', 'writer binding and project job locator disagree');
325
+ let origin: URL;
326
+ try { origin = new URL(binding.origin); } catch { return failure('JOB_BINDING_INVALID', 'writer route is invalid'); }
327
+ if (origin.protocol !== 'http:' || origin.hostname !== '127.0.0.1' || origin.username || origin.password
328
+ || origin.pathname !== '/' || origin.search || origin.hash || !id(binding.jobId) || !id(actId)) failure('JOB_BINDING_INVALID', 'fetch requires an exact loopback job route');
329
+ return { origin: origin.origin, jobId: binding.jobId, actId, fetchCapability: binding.fetchCapability };
330
+ }
331
+
332
+ export async function fetchReviewAct(transport: ReviewActTransport, fetchAdapter: typeof fetch = fetch): Promise<{ act: ReviewVerdictAct | PartsVerdictAct | InterviewPicksAct | CheckpointAct | SignoffAct; bytes: string; stillBytes?: string }> {
333
+ const requestId = randomUUID();
334
+ const request = JSON.stringify({ schema: 'rig-review/job-api@2', verb: 'fetch-act', jobId: transport.jobId, actId: transport.actId, requestId });
335
+ async function read(accept: string): Promise<Response> {
336
+ try {
337
+ const response = await fetchAdapter(`${transport.origin}/jobs/${transport.jobId}/api`, {
338
+ method: 'POST', redirect: 'error', signal: AbortSignal.timeout(10_000),
339
+ headers: { authorization: `Bearer ${transport.fetchCapability}`, 'content-type': 'application/json', accept }, body: request,
340
+ });
341
+ if (!response.ok) return failure('REVIEW_ACT_FETCH_REFUSED', 'live Review refused fetch');
342
+ return response;
343
+ } catch { return failure('REVIEW_ACT_FETCH_REFUSED', 'live Review fetch failed'); }
344
+ }
345
+ if (transport.kind === 'parts-verdict' || transport.kind === 'interview-picks' || transport.kind === 'checkpoint' || transport.kind === 'signoff') return decodeReviewResponse(await (await read('application/json')).text(), transport.jobId, transport.actId, requestId, transport.kind);
346
+ const bytes = await (await read('application/vnd.rig-review.act+json')).text();
347
+ const act = decodeReviewAct(bytes);
348
+ if (act.jobId !== transport.jobId || act.actId !== transport.actId) failure('REVIEW_ACT_INVALID', 'fetched act identity differs from the request');
349
+ const still = reviewActStill(act);
350
+ if (still === null) return { act, bytes };
351
+ const png = Buffer.from(await (await read('image/png')).arrayBuffer());
352
+ if (jobHash(png) !== still.sha256) failure('REVIEW_ACT_STILL_INVALID', 'exported still sha256 does not match');
353
+ return { act, bytes, stillBytes: png.toString('base64') };
354
+ }
355
+
356
+ /** Exact #184 identity, discounting verdicts and exact evidence added after this receipt. */
357
+ export function reviewActVersionHash(project: JsonObject, act: ReviewVerdictAct, verdictsBefore?: number): string {
358
+ const selector = act.source.selector!;
359
+ if (!Array.isArray(project.versions)) return failure('REVIEW_ACT_SOURCE_STALE', 'versions are absent');
360
+ const versions = project.versions.map(raw => {
361
+ if (!object(raw) || !('version' in selector) || raw.n !== selector.version || verdictsBefore === undefined) return raw;
362
+ const added = (Array.isArray(project.review_acts) ? project.review_acts : []).filter(object)
363
+ .filter(receipt => receipt.version === raw.n && Number(receipt.verdict) >= verdictsBefore)
364
+ .flatMap(receipt => Array.isArray(receipt.added_evidence) ? receipt.added_evidence : []);
365
+ return { ...raw, evidence: Array.isArray(raw.evidence) ? raw.evidence.filter(entry => {
366
+ const index = added.findIndex(value => isDeepStrictEqual(value, entry));
367
+ if (index < 0) return true;
368
+ added.splice(index, 1); return false;
369
+ }) : raw.evidence,
370
+ verdicts: Array.isArray(raw.verdicts) ? raw.verdicts.slice(0, verdictsBefore) : [] };
371
+ });
372
+ const hash = publicationVersionHash(Buffer.from(JSON.stringify({ versions })), { path: '', selector });
373
+ if (!hash) return failure('REVIEW_ACT_SOURCE_STALE', 'reviewed version is absent');
374
+ return hash;
375
+ }
376
+
377
+ function previousPublicationVerdict(project: JsonObject, act: ReviewVerdictAct): number | undefined {
378
+ if (!Array.isArray(project.review_acts)) return undefined;
379
+ const receipts = project.review_acts.filter(object).filter(entry => {
380
+ if (entry.kind === 'parts-verdict' || entry.kind === 'interview-picks' || entry.kind === 'checkpoint' || entry.kind === 'signoff' || entry.jobId !== act.jobId || typeof entry.bytes !== 'string') return false;
381
+ return decodeReviewAct(entry.bytes).publicationId === act.publicationId;
382
+ });
383
+ return receipts.length ? Math.min(...receipts.map(entry => Number(entry.verdict))) : undefined;
384
+ }
385
+
386
+ export function reviewActPublicationWarning(root: string, project: JsonObject, act: ReviewVerdictAct): string | null {
387
+ const receipt = readAcknowledgedPublication(root, act.jobId, act.publicationId);
388
+ if (!receipt) return 'REVIEW_ACT_VERSION_UNVERIFIED: project.json.review_acts: publication receipt is absent, so the first-ingest version-entry check was skipped; retain acknowledged publications for future ingests';
389
+ if (!isDeepStrictEqual(receipt.result.selector, act.source.selector) || receipt.versionHash !== reviewActVersionHash(project, act, previousPublicationVerdict(project, act))) {
390
+ failure('REVIEW_ACT_SOURCE_STALE', 'version entry differs from its acknowledged publication');
391
+ }
392
+ return null;
393
+ }
394
+
395
+ export function assertReviewActSource(root: string, project: JsonObject, act: ReviewVerdictAct): void {
396
+ const stale = (detail: string): never => failure('REVIEW_ACT_SOURCE_STALE', detail);
397
+ const source = act.source;
398
+ const selector = source.selector!;
399
+ const version = (project.versions as JsonObject[] | undefined)?.find(v => v.n === ('version' in selector ? selector.version : -1));
400
+ if (!version || version.kind !== selector.kind || selector.kind === 'post' && version.rig_version !== selector.rigVersion) stale('reviewed version selection changed');
401
+ const rig = selector.kind === 'post' ? selector.rigVersion : selector.kind === 'rig' ? selector.version : -1;
402
+ const rigVersion = (project.versions as JsonObject[]).find(v => v.n === rig);
403
+ const retained = object(rigVersion?.review_package) ? rigVersion.review_package : null;
404
+ const boundFile = (identity: { readonly path: string; readonly sha256: string }): void => {
405
+ try { if (jobHash(publicationFile(root, identity.path)) !== identity.sha256) stale(`${identity.path} changed`); }
406
+ catch { stale(`${identity.path} is missing or changed`); }
407
+ };
408
+ boundFile(source.model!);
409
+ if (source.model!.path !== `derived/review/v${rig}/out/check/build/skeleton.model.json`) stale('displayed model is not the retained rig model');
410
+ if (selector.kind === 'post') {
411
+ if (source.direction!.path !== `evidence/direction/v${selector.version}.json`) stale('displayed direction is not the retained post direction');
412
+ boundFile(source.direction!);
413
+ const provenance = object(version!.provenance) ? version!.provenance : null;
414
+ if (!object(provenance?.direction) || !isDeepStrictEqual(provenance.direction, source.direction)) stale('post direction binding changed');
415
+ }
416
+ if (!retained || !Array.isArray(retained.files)) stale('retained review package is absent');
417
+ for (const raw of retained!.files as Json[]) {
418
+ if (!file(raw)) return stale('retained review package identity is malformed');
419
+ boundFile({ path: `derived/review/v${rig}/${raw.path}`, sha256: String(raw.sha256) });
420
+ }
421
+ const parts = source.parts!;
422
+ const partsRoot = typeof retained!.from === 'string' ? retained!.from : `derived/review/v${rig}/out`;
423
+ if (parts.manifest.path !== `${partsRoot}/parts.json`) stale('parts producer root changed');
424
+ boundFile(parts.manifest);
425
+ for (const row of parts.images) boundFile({ path: join(dirname(parts.manifest.path), row.path), sha256: row.sha256 });
426
+ const manifestPath = liveManifestPath(root, project, 'version' in selector ? selector.version : -1, source.package);
427
+ let manifest: JsonObject;
428
+ try { manifest = JSON.parse(publicationFile(root, manifestPath).toString('utf8')); }
429
+ catch { return stale('published review manifest is absent or unreadable'); }
430
+ if (manifest.id !== source.package.id || manifest.revision !== source.package.revision
431
+ || manifest.model !== source.model!.path || (manifest.direction ?? null) !== (source.direction?.path ?? null)) stale('published package identity changed');
432
+ if (source.plan) {
433
+ const plan = object(project.plan) ? project.plan : null;
434
+ const steps = Array.isArray(plan?.steps) ? plan.steps : [];
435
+ if (!isDeepStrictEqual(source.plan.steps, steps.map(raw => ({ id: object(raw) ? raw.id : null, hash: object(raw) ? stepHash(raw) : null })))) stale('displayed plan step identities changed');
436
+ }
437
+ }
438
+
439
+ export function reviewActVerdict(act: ReviewVerdictAct): JsonObject {
440
+ return { by: act.authority.reviewer.role === 'owner' ? 'user' : 'delegate', at: act.at,
441
+ review: act.value.stage, verdict: { approved: 'accepted', 'changes-requested': 'bounced', pending: 'provisional' }[act.value.verdict],
442
+ verbatim: reviewActVerbatim(act), source: `review act ${act.jobId}/${act.actId}`, on_items: [] };
443
+ }
444
+
445
+ /** The sign-off fields a passed checkpoint act supplies; the writer and check share them. */
446
+ export function checkpointActSignoff(act: CheckpointAct): { by: string; at: string; verbatim: string; source: string } {
447
+ return { by: act.authority.reviewer.role === 'owner' ? 'user' : 'delegate', at: act.at,
448
+ verbatim: reviewActVerbatim(act), source: `review act ${act.jobId}/${act.actId}` };
449
+ }
450
+
451
+ /** The human values a signoff act records. The act names itself in source; the stage is the writer's, never Review's. */
452
+ export function reviewActSignoff(act: SignoffAct): JsonObject {
453
+ return { stage: act.value.stage, checkpoint: null, by: act.authority.reviewer.role === 'owner' ? 'user' : 'delegate',
454
+ at: act.at, verbatim: reviewActVerbatim(act), source: `review act ${act.jobId}/${act.actId}` };
455
+ }
456
+
457
+ /**
458
+ * create's own check that the act answers a subject its publication declared. The
459
+ * acknowledged receipt keeps the subjects create published; a receipt that predates
460
+ * them, or none (a clone), leaves only Review's own ask binding to vouch for it.
461
+ */
462
+ export function assertActSubjectDeclared(root: string, act: SignoffAct | CheckpointAct | InterviewPicksAct, path: string): void {
463
+ const receipt = readAcknowledgedPublication(root, act.jobId, act.publicationId);
464
+ if (receipt?.subjects === undefined) return;
465
+ const subject = act.kind === 'interview-picks' ? { kind: 'interview-picks', questionId: act.value.questionId, setId: act.source.optionSet.id } : act.kind === 'signoff' ? { kind: 'signoff', stage: act.value.stage } : { kind: 'checkpoint', checkpointId: act.value.checkpointId };
466
+ if (receipt.subjects.some(declared => isDeepStrictEqual(declared, subject))) return;
467
+ const named = act.kind === 'interview-picks' ? `answers ${act.value.questionId}` : act.kind === 'signoff' ? `signs ${act.value.stage}` : `passes checkpoint ${act.value.checkpointId}`;
468
+ refuse('REVIEW_ACT_SUBJECT_UNDECLARED', `${path}: act ${act.jobId}/${act.actId} ${named}, which its publication ${act.publicationId} did not declare; ask for it in Review on a publication made while the record can sign it`);
469
+ }
470
+
471
+ /** Stages whose sign-off names a reviewed version, and the version kind each one signs. */
472
+ export const SIGNOFF_TARGET_KINDS: Readonly<Record<string, 'rig' | 'post'>> = { 'rig-review': 'rig', 'direction-review': 'post' };
473
+
474
+ /** The version a target-stage signoff act displayed, or null when its selector names no version of that stage's kind. */
475
+ export function signoffActVersion(act: SignoffAct): number | null {
476
+ const kind = SIGNOFF_TARGET_KINDS[act.value.stage];
477
+ const selector = act.source.selector;
478
+ if (kind === undefined || !object(selector) || selector.kind !== kind) return null;
479
+ return Number.isSafeInteger(selector.version) && Number(selector.version) >= 1 ? Number(selector.version) : null;
480
+ }
481
+
482
+ /** The Review wire shape stays unchanged; Create resolves record targets before ingest. */
483
+ export function isRecordDirectionSignoff(act: SignoffAct): boolean {
484
+ return act.schema === 'rig-review/act@4' && String(act.value.stage) === 'direction-review';
485
+ }
486
+
487
+ export function recordSignoffActVersion(act: SignoffAct, recordBytes: string): number | null {
488
+ if (!isRecordDirectionSignoff(act) || jobHash(recordBytes) !== act.source.projectHash) return null;
489
+ if (!('record' in act.source) || !act.source.record || act.source.record.sha256 !== act.source.projectHash) return null;
490
+ try {
491
+ const project: unknown = JSON.parse(recordBytes);
492
+ return object(project) ? recordDirectionTarget(project, Date.parse(act.at)) : null;
493
+ } catch { return null; }
494
+ }
495
+
496
+ /** The stable { stage, checkpoint, at } identity its receipt binds. */
497
+ export function signoffActIdentity(act: SignoffAct): JsonObject {
498
+ return { stage: act.value.stage, checkpoint: null, at: act.at };
499
+ }
500
+
501
+ /** Ledger validation deliberately checks retained evidence, not current sources. */
502
+ export function reviewActFindings(project: JsonObject): { code: string; path: string; message: string }[] {
503
+ if (project.review_acts === undefined) return [];
504
+ const findings: { code: string; path: string; message: string }[] = [];
505
+ const seen = new Set<string>();
506
+ const targets = new Set<string>();
507
+ const add = (code: string, path: string, detail: string): void => { findings.push({ code, path, message: `${path}: ${detail}; restore the original act receipt and its recorded verdict` }); };
508
+ if (!Array.isArray(project.review_acts)) { add('REVIEW_ACT_LEDGER_INVALID', 'review_acts', 'expected a replay ledger array'); return findings; }
509
+ project.review_acts.forEach((entry, index) => {
510
+ const path = `review_acts[${index}]`;
511
+ if (object(entry) && entry.schema === PARTS_CLI_RECEIPT) return;
512
+ try {
513
+ if (!object(entry) || entry.schema !== 'rig-create/review-act@1' || typeof entry.bytes !== 'string'
514
+ || !sha(entry.sha256) || jobHash(entry.bytes) !== entry.sha256 || !sha(entry.version_hash)) failure('REVIEW_ACT_LEDGER_INVALID', 'receipt shape or bytes hash is invalid');
515
+ const act = decodeReviewAct(entry.bytes, entry.kind === 'parts-verdict' ? 'parts-verdict' : entry.kind === 'interview-picks' ? 'interview-picks' : entry.kind === 'checkpoint' ? 'checkpoint' : entry.kind === 'signoff' ? 'signoff' : 'review-verdict');
516
+ if (entry.jobId !== act.jobId || entry.actId !== act.actId) failure('REVIEW_ACT_LEDGER_INVALID', 'receipt identity differs from its act');
517
+ const key = JSON.stringify([act.jobId, act.actId]);
518
+ if (seen.has(key)) failure('REVIEW_ACT_REPLAY_CONFLICT', 'duplicate act receipt');
519
+ seen.add(key);
520
+ if (act.kind === 'parts-verdict') {
521
+ if (entry.draft_id !== act.source.selector.id || !file(entry.evidence) || entry.evidence.sha256 !== entry.sha256) failure('REVIEW_ACT_LEDGER_INVALID', 'parts receipt target differs from its act');
522
+ return;
523
+ }
524
+ if (act.kind === 'interview-picks') {
525
+ const item = act.value.questionId === INTAKE_QUESTION ? intakeItem(project)
526
+ : collectItems(project, { collection: null }).find(item => item.id === act.value.questionId);
527
+ const answers = item ? [item.raw, ...asArray(item.record.history)].filter(isObject) : [];
528
+ const answer = answers.find(answer => answer.at === act.at && object(answer.choice)
529
+ && jobHash(JSON.stringify(answer.choice)) === entry.version_hash);
530
+ const choice = object(answer?.choice) ? answer.choice : null;
531
+ const picks = choice && Array.isArray(choice.picks) ? choice.picks : [];
532
+ if (entry.question !== act.value.questionId || !answer || answer.by !== (act.authority.reviewer.role === 'owner' ? 'user' : 'delegate')
533
+ || !choice || !object(choice.set) || choice.set.sha256 !== act.source.optionSet.sha256
534
+ || (choice.note ?? '') !== act.value.note || !isDeepStrictEqual(picks.map(raw => object(raw)
535
+ ? { itemId: raw.ask, optionId: raw.option_id, otherText: raw.other_text ?? null } : null), act.value.selections)) {
536
+ failure('REVIEW_ACT_LEDGER_INVALID', 'recorded interview answer differs from the immutable act');
537
+ }
538
+ return;
539
+ }
540
+ if (act.kind === 'checkpoint') {
541
+ const signoffs = Array.isArray(project.signoffs) ? project.signoffs : [];
542
+ const signoff = Number.isSafeInteger(entry.signoff) && Number(entry.signoff) >= 0 ? signoffs[Number(entry.signoff)] : undefined;
543
+ const displayed = act.source.plan.steps.find(step => step.id === act.value.planStepId);
544
+ const expected = checkpointActSignoff(act);
545
+ if (act.value.outcome !== 'pass' || act.value.checkpointId !== act.value.planStepId || entry.checkpoint !== act.value.planStepId
546
+ || entry.version_hash !== displayed?.hash || !object(signoff) || signoff.checkpoint !== act.value.planStepId
547
+ || signoff.by !== expected.by || signoff.at !== expected.at || signoff.verbatim !== expected.verbatim || signoff.source !== expected.source) {
548
+ failure('REVIEW_ACT_LEDGER_INVALID', 'recorded checkpoint sign-off differs from the immutable passed act');
549
+ }
550
+ // The sign-off's source names this act, so a second receipt cannot point at it.
551
+ return;
552
+ }
553
+ if (act.kind === 'signoff') {
554
+ const expected = reviewActSignoff(act);
555
+ const recorded = asArray(project.signoffs).filter(object).filter(signoff => signoff.source === expected.source);
556
+ const target = recorded.length === 1 && object(recorded[0]!.target) ? recorded[0]!.target : null;
557
+ const recordDirection = isRecordDirectionSignoff(act);
558
+ const shown = recordDirection
559
+ ? Number.isSafeInteger(entry.target_version) && Number(entry.target_version) > 0
560
+ && entry.record_sha256 === act.source.projectHash
561
+ && 'record' in act.source && act.source.record?.sha256 === entry.record_sha256 ? Number(entry.target_version) : null
562
+ : signoffActVersion(act);
563
+ if (entry.stage !== act.value.stage || entry.version_hash !== jobHash(JSON.stringify(signoffActIdentity(act))) || recorded.length !== 1
564
+ || Object.entries(expected).some(([key, value]) => recorded[0]![key] !== value)
565
+ || recordDirection && (typeof entry.verdict !== 'string' || target?.verdict !== entry.verdict)
566
+ // A target stage's signature belongs to the version the person was shown.
567
+ || act.value.stage in SIGNOFF_TARGET_KINDS && (target === null || shown === null || target.version !== shown)) {
568
+ failure('REVIEW_ACT_LEDGER_INVALID', 'recorded signoff differs from the immutable act');
569
+ }
570
+ return;
571
+ }
572
+ if (!Number.isSafeInteger(entry.verdict) || Number(entry.verdict) < 0 || entry.version !== ('version' in act.source.selector! ? act.source.selector.version : null)) failure('REVIEW_ACT_LEDGER_INVALID', 'verdict receipt target is invalid');
573
+ const target = JSON.stringify([entry.version, entry.verdict]);
574
+ if (targets.has(target)) failure('REVIEW_ACT_LEDGER_INVALID', 'multiple receipts point to one verdict');
575
+ targets.add(target);
576
+ const version = Array.isArray(project.versions) ? project.versions.find(v => object(v) && v.n === entry.version) : null;
577
+ const verdict = object(version) && Array.isArray(version.verdicts) ? version.verdicts[Number(entry.verdict)] : null;
578
+ if (!isDeepStrictEqual(verdict, reviewActVerdict(act))) failure('REVIEW_ACT_LEDGER_INVALID', 'recorded verdict differs from the immutable act');
579
+ const still = reviewActStill(act);
580
+ if (still !== null) {
581
+ const expected = { kind: 'still', path: `evidence/v${entry.version}/${still.path}`, sha256: still.sha256,
582
+ camera: still.camera, frame: still.frame,
583
+ what: still.frame === 'setup' ? 'Review displayed setup pose' : `Review displayed animation ${JSON.stringify(still.animation)} at ${still.frame} seconds` };
584
+ if (!object(version) || !Array.isArray(version.evidence) || !version.evidence.some(e => isDeepStrictEqual(e, expected))
585
+ || !Array.isArray(entry.added_evidence) || entry.added_evidence.some(e => !isDeepStrictEqual(e, expected))) {
586
+ failure('REVIEW_ACT_LEDGER_INVALID', 'recorded still differs from the immutable act');
587
+ }
588
+ }
589
+ } catch (error) { add(error instanceof Refusal ? error.code : 'REVIEW_ACT_LEDGER_INVALID', path, error instanceof Refusal ? error.message : 'invalid act receipt'); }
590
+ });
591
+ return findings;
592
+ }