rig-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.
Files changed (555) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +163 -2
  3. package/docs/CHECKS.md +1665 -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,54 @@
1
+ import { asArray, isObject, type Finding } from './check.ts';
2
+ import { checkpointGeneration } from './plan-history.ts';
3
+ import { stepHash } from './step-hash.ts';
4
+ import type { JsonObject } from './templates.ts';
5
+
6
+ /** Policy amendments never change the authored step or its approval hash. */
7
+ export function collectBarAmendments(project: JsonObject, refusals: Finding[], now = Date.now()): JsonObject[] {
8
+ const plan = isObject(project.plan) ? project.plan : {};
9
+ if (plan.bar_amendments === undefined) return [];
10
+ const start = refusals.length;
11
+ const entries: JsonObject[] = [];
12
+ const identities = new Set<string>();
13
+ let previousAt = -Infinity;
14
+ const fail = (path: string): void => {
15
+ refusals.push({ code: 'BAR_AMENDMENT_INVALID', path,
16
+ message: `${path}: restore the original step, hash, generation, amended bar, operator, date, reason and exact words; use plan bar-amend` });
17
+ };
18
+ if (!Array.isArray(plan.bar_amendments)) { fail('plan.bar_amendments'); return []; }
19
+ for (const [index, raw] of plan.bar_amendments.entries()) {
20
+ const path = `plan.bar_amendments[${index}]`;
21
+ if (!isObject(raw) || Object.keys(raw).sort().join(',') !== 'amended_bar,at,by,generation,reason,schema,sha256,step,verbatim'
22
+ || raw.schema !== 'rig-create/bar-amendment@1' || !isObject(raw.step)
23
+ || typeof raw.step.id !== 'string' || !raw.step.id.trim() || !['rig', 'post'].includes(String(raw.step.phase))
24
+ || typeof raw.step.bar !== 'string' || !raw.step.bar.trim() || raw.sha256 !== stepHash(raw.step)
25
+ || !Number.isSafeInteger(raw.generation) || Number(raw.generation) < 0
26
+ || Number(raw.generation) > checkpointGeneration(project, raw.step.id)
27
+ || ['amended_bar', 'by', 'reason', 'verbatim'].some(key => typeof raw[key] !== 'string' || !String(raw[key]).trim())
28
+ || typeof raw.at !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(raw.at)
29
+ || !Number.isFinite(Date.parse(raw.at)) || Date.parse(raw.at) > now) {
30
+ fail(path); continue;
31
+ }
32
+ const id = raw.step.id;
33
+ const versions = asArray(plan.replaced).filter(entry => isObject(entry) && isObject(entry.step) && entry.step.id === id);
34
+ const historical = versions[Number(raw.generation)];
35
+ const bound = isObject(historical) ? historical.step : asArray(plan.steps).find(step => isObject(step) && step.id === id);
36
+ const identity = JSON.stringify([id, raw.sha256, raw.generation]);
37
+ const at = Date.parse(raw.at);
38
+ if (!isObject(bound) || stepHash(bound) !== raw.sha256 || at < previousAt || identities.has(identity)) {
39
+ fail(path); continue;
40
+ }
41
+ previousAt = at;
42
+ identities.add(identity);
43
+ entries.push(raw);
44
+ }
45
+ return refusals.length === start ? entries : [];
46
+ }
47
+
48
+ /** A later revision, even one restoring identical text, does not inherit policy. */
49
+ export function effectiveBar(project: JsonObject, step: JsonObject, now = Date.now()): string {
50
+ const entry = collectBarAmendments(project, [], now).find(entry => isObject(entry.step)
51
+ && entry.step.id === step.id && entry.sha256 === stepHash(step)
52
+ && entry.generation === checkpointGeneration(project, String(step.id)));
53
+ return entry ? String(entry.amended_bar) : String(step.bar ?? '');
54
+ }
@@ -0,0 +1,147 @@
1
+ import { asArray, classify, collectSignoffs, isObject, type Finding } from './check.ts';
2
+ import { collectReopens, liveSignoff } from './reopen.ts';
3
+ import { approvalStageOfStep, hasStageOrder } from './stages.ts';
4
+ import { stepHash } from './step-hash.ts';
5
+ import type { JsonObject } from './templates.ts';
6
+
7
+ /** Invalid history is reported and never used to resolve a historical signature. */
8
+ export function collectPlanReplaced(project: JsonObject, refusals: Finding[]): JsonObject[] {
9
+ const plan = isObject(project.plan) ? project.plan : {};
10
+ const entries: JsonObject[] = [];
11
+ const seen = new Set<string>();
12
+ let previousAt = -Infinity;
13
+ const push = (code: string, path: string, why: string): void => {
14
+ refusals.push({ code, path, message: `${path}: ${why}; restore the original plan.replaced entry and use plan revise or remove` });
15
+ };
16
+ if (plan.replaced !== undefined && !Array.isArray(plan.replaced)) {
17
+ push('PLAN_REPLACED_INVALID', 'plan.replaced', 'must be an append-only list');
18
+ return entries;
19
+ }
20
+ for (const [index, raw] of asArray(plan.replaced).entries()) {
21
+ const path = `plan.replaced[${index}]`;
22
+ if (!isObject(raw) || !isObject(raw.step) || typeof raw.step.id !== 'string' || !raw.step.id.trim()
23
+ || (raw.step.phase !== 'rig' && raw.step.phase !== 'post')
24
+ || typeof raw.by !== 'string' || !raw.by.trim() || typeof raw.at !== 'string'
25
+ || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(raw.at)
26
+ || !Number.isFinite(Date.parse(raw.at))) {
27
+ push('PLAN_REPLACED_INVALID', path, 'needs the full previous step with id and phase, an ISO at and a nonempty by');
28
+ continue;
29
+ }
30
+ const start = refusals.length;
31
+ if (raw.sha256 !== stepHash(raw.step)) push('PLAN_REPLACED_HASH', `${path}.sha256`, 'does not equal stepHash(step)');
32
+ if (raw.action !== 'revised' && raw.action !== 'removed') push('PLAN_REPLACED_ACTION', `${path}.action`, 'must be revised or removed');
33
+ const at = Date.parse(raw.at);
34
+ if (at < previousAt) push('PLAN_REPLACED_ORDER', `${path}.at`, 'must not precede the previous history entry');
35
+ previousAt = Math.max(previousAt, at);
36
+ const identity = JSON.stringify([raw.step.id, raw.at]);
37
+ if (seen.has(identity)) push('PLAN_REPLACED_DUPLICATE', path, 'step.id and at must identify one history entry');
38
+ seen.add(identity);
39
+ if (refusals.length === start) entries.push(raw);
40
+ }
41
+ return entries;
42
+ }
43
+
44
+ /** Only ids with replacement history gain historical hash resolution. Old records are unchanged. */
45
+ export function checkReplacedSignoffs(project: JsonObject, entries: JsonObject[], refusals: Finding[]): void {
46
+ const reopens = collectReopens(project, []);
47
+ const signatures = collectSignoffs(project, []).filter(s => s.checkpoint === null);
48
+ const steps = asArray(isObject(project.plan) ? project.plan.steps : undefined).filter(isObject);
49
+ const previousSteps = entries.map(entry => entry.step as JsonObject);
50
+ const ids = new Set(previousSteps.map(step => String(step.id)));
51
+ for (const signature of signatures) {
52
+ if (signature.record.step_hashes === undefined) continue;
53
+ const live = liveSignoff(reopens, signature.stage, signature.at);
54
+ for (const id of ids) {
55
+ if (signature.steps !== null && !signature.steps.includes(id)) continue;
56
+ const hashes = signature.record.step_hashes;
57
+ // An implicit historical approval cannot bind an id added after that signature.
58
+ if (!live && signature.steps === null && isObject(hashes) && !Object.hasOwn(hashes, id)) continue;
59
+ const candidates = [...steps, ...previousSteps].filter(step => step.id === id
60
+ && (step.phase === 'rig' || step.phase === 'post')
61
+ && approvalStageOfStep(step.phase, id, hasStageOrder(project)) === signature.stage);
62
+ if (candidates.length === 0) continue;
63
+ // Preserve latest-covering approval semantics for live re-signatures.
64
+ if (live && signatures.some(other => {
65
+ const at = Number.isNaN(signature.at) ? -Infinity : signature.at;
66
+ const otherAt = Number.isNaN(other.at) ? -Infinity : other.at;
67
+ return other.stage === signature.stage && liveSignoff(reopens, other.stage, other.at)
68
+ && (other.steps === null || other.steps.includes(id))
69
+ && (otherAt > at || (otherAt === at && other.index > signature.index));
70
+ })) continue;
71
+ const allowed = live ? candidates.filter(step => steps.includes(step)) : candidates;
72
+ if (isObject(hashes) && allowed.some(step => stepHash(step) === hashes[id])) continue;
73
+ const path = `signoffs[${signature.index}].step_hashes[${id}]`;
74
+ refusals.push({ code: 'PLAN_REPLACED_SIGNOFF', path,
75
+ message: `${path}: does not match ${live ? 'the current step' : 'a current or retained previous step'}; restore the signed step and its replacement history, or reopen the live approval before replanning` });
76
+ }
77
+ }
78
+ }
79
+
80
+
81
+ /** Replacement generations keep a withdrawal from applying to a later revision under the same id. */
82
+ export function checkpointGeneration(project: JsonObject, id: string): number {
83
+ return asArray(isObject(project.plan) ? project.plan.replaced : undefined)
84
+ .filter(entry => isObject(entry) && isObject(entry.step) && entry.step.id === id).length;
85
+ }
86
+
87
+ export function checkpointEnabled(step: JsonObject): boolean {
88
+ const checkpoint = classify(step.checkpoint, { collection: null });
89
+ return checkpoint.state === 'user' ? checkpoint.answer.value === true
90
+ : checkpoint.state === 'delegated' && checkpoint.decided && isObject(checkpoint.answer.agent_decision)
91
+ && checkpoint.answer.agent_decision.value === true;
92
+ }
93
+
94
+ /** Withdrawals preserve the signed step. Their authority is limited to its exact hash and replacement generation. */
95
+ export function collectCheckpointWithdrawals(project: JsonObject, refusals: Finding[], now = Infinity): JsonObject[] {
96
+ const plan = isObject(project.plan) ? project.plan : {};
97
+ if (plan.checkpoint_withdrawals === undefined) return [];
98
+ const start = refusals.length;
99
+ const entries: JsonObject[] = [];
100
+ const identities = new Set<string>();
101
+ let previousAt = -Infinity;
102
+ const fail = (path: string): void => {
103
+ refusals.push({ code: 'CHECKPOINT_WITHDRAWAL_INVALID', path,
104
+ message: `${path}: retain a withdrawal with its original enabled step, hash, generation, operator, date, reason and exact words; restore the journal and use plan checkpoint-withdraw` });
105
+ };
106
+ if (!Array.isArray(plan.checkpoint_withdrawals)) { fail('plan.checkpoint_withdrawals'); return []; }
107
+ for (const [index, raw] of plan.checkpoint_withdrawals.entries()) {
108
+ const path = `plan.checkpoint_withdrawals[${index}]`;
109
+ if (!isObject(raw) || Object.keys(raw).sort().join(',') !== 'at,by,generation,reason,schema,sha256,step,verbatim'
110
+ || raw.schema !== 'rig-create/checkpoint-withdrawal@1' || !isObject(raw.step)
111
+ || typeof raw.step.id !== 'string' || !raw.step.id.trim() || !['rig', 'post'].includes(String(raw.step.phase))
112
+ || !checkpointEnabled(raw.step) || raw.sha256 !== stepHash(raw.step)
113
+ || !Number.isSafeInteger(raw.generation) || Number(raw.generation) < 0
114
+ || Number(raw.generation) > checkpointGeneration(project, raw.step.id)
115
+ || ['by', 'reason', 'verbatim'].some(key => typeof raw[key] !== 'string' || !String(raw[key]).trim())
116
+ || typeof raw.at !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(raw.at)
117
+ || !Number.isFinite(Date.parse(raw.at)) || Date.parse(raw.at) > now) {
118
+ fail(path); continue;
119
+ }
120
+ const original = raw.step;
121
+ const id = String(original.id);
122
+ const at = Date.parse(raw.at);
123
+ const checkpoint = raw.step.checkpoint as JsonObject;
124
+ const decision = checkpoint.delegated === true && isObject(checkpoint.agent_decision) ? checkpoint.agent_decision : checkpoint;
125
+ const versions = asArray(plan.replaced).filter(entry => isObject(entry) && isObject(entry.step) && entry.step.id === id);
126
+ const historical = versions[Number(raw.generation)];
127
+ const current = asArray(plan.steps).find(step => isObject(step) && step.id === id);
128
+ const bound = isObject(historical) ? historical.step : current;
129
+ const identity = JSON.stringify([raw.step.id, raw.sha256, raw.generation]);
130
+ if (!isObject(bound) || stepHash(bound) !== raw.sha256) { fail(path); continue; }
131
+ if (at < previousAt || !Number.isFinite(Date.parse(String(decision.at ?? checkpoint.at))) || at < Date.parse(String(decision.at ?? checkpoint.at)) || identities.has(identity)) {
132
+ fail(path); continue;
133
+ }
134
+ previousAt = at;
135
+ identities.add(identity);
136
+ entries.push(raw);
137
+ }
138
+ // A broken journal grants no partial exemption.
139
+ return refusals.length === start ? entries : [];
140
+ }
141
+
142
+ export function checkpointRequired(project: JsonObject, step: JsonObject, now = Infinity): boolean {
143
+ if (!checkpointEnabled(step)) return false;
144
+ return !collectCheckpointWithdrawals(project, [], now).some(entry => isObject(entry.step)
145
+ && entry.step.id === step.id && entry.sha256 === stepHash(step)
146
+ && entry.generation === checkpointGeneration(project, String(step.id)));
147
+ }
@@ -0,0 +1,202 @@
1
+ import { collectBarAmendments } from './plan-bar-amendments.ts';
2
+ import { checkpointEnabled, checkpointGeneration, checkpointRequired, collectCheckpointWithdrawals } from './plan-history.ts';
3
+ import { readVerbatim } from './record-input.ts';
4
+ import { acquireBoundJobWrite } from './job-writer.ts';
5
+ import { closeSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
6
+ import { dirname, join, resolve } from 'node:path';
7
+
8
+ import { asArray, checkProject, collectSignoffs, isObject } from './check.ts';
9
+ import { readProject } from './feedback.ts';
10
+ import { collectReopens, liveSignoff } from './reopen.ts';
11
+ import { refuse, USAGE_EXIT } from './refusal.ts';
12
+ import { baselineOf, newFindings } from './refusal-baseline.ts';
13
+ import { approvalStageOfStep, hasStageOrder, stageOfStep } from './stages.ts';
14
+ import { stepHash } from './step-hash.ts';
15
+ import type { JsonObject } from './templates.ts';
16
+
17
+ export const PLAN_HELP = `plan add|revise|remove <id> [<project dir>] --by <drafter> [--json]
18
+ --phase rig|post --order <number> --does <text> --produces <text>
19
+ --rests-on <id,id,...> --bar <text> --checkpoint-proposed true|false
20
+ checkpoint-withdraw <id> [<project dir>] --by <operator> --reason-file <file> --quote-file <file>
21
+ bar-amend <id> [<project dir>] --by <operator> --reason-file <file> --quote-file <file> --bar-file <file>
22
+ Amend one execution bar without changing the signed step.
23
+ Withdraw one enabled execution checkpoint without changing the signed step.
24
+ Add requires all fields. Revise accepts any of these fields and preserves the id.
25
+ Revise/remove retain the full previous step in plan.replaced[].
26
+ Reopen live covering plan signatures first. Completed steps require removal and a new id.
27
+ Dates follow approval ownership: parts/legacy rig update drafted_at and drafted_by,
28
+ production rigging updates rigging_drafted_at, and post preserves plan dates.
29
+ Each candidate passes check's refusal delta before writing.`;
30
+
31
+ const FIELDS = ['phase', 'order', 'does', 'produces', 'rests_on', 'bar', 'checkpoint_proposed'];
32
+
33
+ /** Reopen resets status, but cannot make completed work a new draft under the same id. */
34
+ function completed(project: JsonObject, step: JsonObject): boolean {
35
+ return step.status === 'done' || asArray(step.status_history).some(event => isObject(event) && event.status === 'done')
36
+ || asArray(project.reopens).some(entry => isObject(entry) && asArray(entry.steps_before).some(before => isObject(before) && before.id === step.id && before.status === 'done'));
37
+ }
38
+
39
+ export function planCommand(args: string[], now: number): { stdout: string; exit: number } {
40
+ const action = args[0];
41
+ if (action === 'help' || args.includes('--help')) return { stdout: PLAN_HELP, exit: 0 };
42
+ const opts: Record<string, string> = {};
43
+ const positional: string[] = [];
44
+ let json = false;
45
+ const usage = (why: string): never => refuse('PLAN_OPTION_INVALID', `plan.steps: ${why}; see plan help`, USAGE_EXIT);
46
+ if (!['add', 'revise', 'remove', 'checkpoint-withdraw', 'bar-amend'].includes(action ?? '')) usage('expected add, revise, remove, checkpoint-withdraw or bar-amend');
47
+ const flags = ['--by', '--reason-file', '--quote-file', '--bar-file', ...FIELDS.map(field => `--${field.replaceAll('_', '-')}`)];
48
+ for (let index = 1; index < args.length; index++) {
49
+ const arg = args[index]!;
50
+ if (arg === '--json') json = true;
51
+ else if (flags.includes(arg)) {
52
+ const value = args[++index];
53
+ if (opts[arg] !== undefined || value === undefined || !value.trim() || value.startsWith('--')) usage(`${arg} needs one nonempty value`);
54
+ opts[arg] = value;
55
+ } else if (arg.startsWith('-')) usage(`unsupported option ${arg}`);
56
+ else positional.push(arg);
57
+ }
58
+ const id = positional[0];
59
+ if (!id?.trim() || positional.length > 2 || !opts['--by']) usage('supply an id, --by and one optional project directory');
60
+ const patch: JsonObject = {};
61
+ for (const field of FIELDS) {
62
+ const value = opts[`--${field.replaceAll('_', '-')}`];
63
+ if (value !== undefined) patch[field] = value;
64
+ }
65
+ const withdrawing = action === 'checkpoint-withdraw';
66
+ const amendingBar = action === 'bar-amend';
67
+ const journalAction = withdrawing || amendingBar;
68
+ if (journalAction && (Object.keys(patch).length || !opts['--reason-file'] || !opts['--quote-file'])) usage('journal actions need reason-file and quote-file and take no authored step fields');
69
+ if (amendingBar !== (opts['--bar-file'] !== undefined)) usage('bar-amend requires bar-file; other actions do not accept it');
70
+ if (!journalAction && (opts['--reason-file'] || opts['--quote-file'])) usage('reason-file and quote-file belong only to journal actions');
71
+ const inputFlags = journalAction ? ['--reason-file', '--quote-file', ...(amendingBar ? ['--bar-file'] : [])] : [];
72
+ if (inputFlags.some(flag => opts[flag] === '-')) usage('journal actions need retained files, not shared stdin');
73
+ const inputs = inputFlags.map(flag => ({ file: opts[flag]!, text: readVerbatim(opts[flag]!) }));
74
+ if (inputs.some(input => !input.text.trim())) usage('journal input files must contain nonempty text');
75
+ const reason = inputs[0]?.text ?? '';
76
+ const verbatim = inputs[1]?.text ?? '';
77
+ const amendedBar = inputs[2]?.text ?? '';
78
+ if (action === 'remove' && Object.keys(patch).length) usage('remove takes no step fields');
79
+ if (action === 'add' && FIELDS.some(field => patch[field] === undefined)) usage('add needs every step field');
80
+ if (action === 'revise' && Object.keys(patch).length === 0) usage('revise needs at least one step field');
81
+ if (patch.phase !== undefined && patch.phase !== 'rig' && patch.phase !== 'post') usage('--phase is rig or post');
82
+ if (patch.order !== undefined) {
83
+ const order = Number(patch.order);
84
+ if (!Number.isFinite(order)) usage('--order needs a finite number');
85
+ patch.order = order;
86
+ }
87
+ if (patch.rests_on !== undefined) {
88
+ const ids = String(patch.rests_on).split(',');
89
+ if (ids.some(value => !value.trim()) || new Set(ids).size !== ids.length) usage('--rests-on needs distinct nonempty answer ids');
90
+ patch.rests_on = ids;
91
+ }
92
+ if (patch.checkpoint_proposed !== undefined) {
93
+ if (!['true', 'false'].includes(String(patch.checkpoint_proposed))) usage('--checkpoint-proposed is true or false');
94
+ patch.checkpoint_proposed = patch.checkpoint_proposed === 'true';
95
+ }
96
+
97
+ const dir = resolve(positional[1] ?? process.cwd());
98
+ // Validate the target before creating a lock, then reload under the lock.
99
+ readProject(dir);
100
+ const releaseJob = acquireBoundJobWrite(dir);
101
+ const lock = join(dir, '.plan.lock');
102
+ let descriptor: number;
103
+ try { descriptor = openSync(lock, 'wx'); }
104
+ catch { releaseJob(); refuse('PLAN_WRITE_CONFLICT', 'project.json: cannot acquire its plan writer lock; finish or recover the active plan write before retrying'); }
105
+ try {
106
+ const { file, project } = readProject(dir);
107
+ const original = readFileSync(file);
108
+ if (amendingBar && inputs.some(input => readVerbatim(input.file) !== input.text)) refuse('PLAN_WRITE_CONFLICT', 'plan.bar_amendments: input files changed while acquiring locks; restore the intended files and retry');
109
+ if (!isObject(project.plan) || !Array.isArray(project.plan.steps)) refuse('PLAN_STEP_INVALID', 'plan.steps: must be a list in a plan object; restore the plan before drafting');
110
+ if (project.plan.replaced !== undefined && !Array.isArray(project.plan.replaced)) refuse('PLAN_REPLACED_INVALID', 'plan.replaced: must be an append-only list; restore its original history');
111
+ const next = structuredClone(project);
112
+ const plan = next.plan as JsonObject;
113
+ const steps = plan.steps as JsonObject[];
114
+ const matches = steps.filter(step => isObject(step) && step.id === id);
115
+ const previous = matches[0];
116
+ if (action === 'add' ? matches.length !== 0 : matches.length !== 1) refuse('PLAN_STEP_INVALID', `plan.steps[${id}]: ${action === 'add' ? 'id already exists' : 'no unique current step'}; use a distinct id for add or an existing id for revise/remove`);
117
+ if (action === 'add' && asArray(plan.replaced).some(entry => isObject(entry) && isObject(entry.step) && entry.step.id === id && completed(project, entry.step))) {
118
+ refuse('PLAN_STEP_DONE', `plan.steps[${id}]: completed work is retained in history; add the replacement under a new id`);
119
+ }
120
+ const at = new Date(now).toISOString();
121
+ if (amendingBar) {
122
+ const findings: import('./check.ts').Finding[] = [];
123
+ const journal = collectBarAmendments(project, findings, now);
124
+ if (findings[0]) refuse(findings[0].code, findings[0].message);
125
+ const generation = checkpointGeneration(project, id!);
126
+ const replay = journal.find(entry => isObject(entry.step) && entry.step.id === id
127
+ && entry.sha256 === stepHash(previous!) && entry.generation === generation);
128
+ if (replay) {
129
+ if (replay.by !== opts['--by'] || replay.reason !== reason || replay.verbatim !== verbatim || replay.amended_bar !== amendedBar) refuse('BAR_AMENDMENT_CONFLICT', `plan.bar_amendments: ${id} already has a different amendment; retain its original policy entry`);
130
+ return { stdout: json ? JSON.stringify(replay, null, 2) : `plan: bar ${id} already amended`, exit: 0 };
131
+ }
132
+ if (!previous || !['rig', 'post'].includes(String(previous.phase))
133
+ || stageOfStep(previous.phase as 'rig' | 'post', id!) !== project.stage
134
+ || completed(project, previous) || typeof previous.bar !== 'string' || !previous.bar.trim()) {
135
+ refuse('BAR_AMENDMENT_SCOPE', `plan.steps[${id}]: amendment needs one unfinished step with a bar in the current execution stage; preserve other stages and use the matching step`);
136
+ }
137
+ plan.bar_amendments = [...journal, { schema: 'rig-create/bar-amendment@1', step: structuredClone(previous),
138
+ sha256: stepHash(previous), generation, amended_bar: amendedBar, by: opts['--by']!, at, reason, verbatim }];
139
+ } else if (withdrawing) {
140
+ const findings: import('./check.ts').Finding[] = [];
141
+ const journal = collectCheckpointWithdrawals(project, findings, now);
142
+ if (findings[0]) refuse(findings[0].code, findings[0].message);
143
+ const generation = checkpointGeneration(project, id!);
144
+ const replay = journal.find(entry => isObject(entry.step) && entry.step.id === id
145
+ && entry.sha256 === stepHash(previous!) && entry.generation === generation);
146
+ if (replay) {
147
+ if (replay.by !== opts['--by'] || replay.reason !== reason || replay.verbatim !== verbatim) refuse('CHECKPOINT_WITHDRAWAL_CONFLICT', `plan.checkpoint_withdrawals: ${id} already has different withdrawal words or actor; retain its original journal entry`);
148
+ return { stdout: json ? JSON.stringify(replay, null, 2) : `plan: checkpoint ${id} already withdrawn`, exit: 0 };
149
+ }
150
+ if (!previous || !['rig', 'post'].includes(String(previous.phase))
151
+ || stageOfStep(previous.phase as 'rig' | 'post', id!) !== project.stage
152
+ || completed(project, previous) || !checkpointEnabled(previous) || !checkpointRequired(project, previous, now)) {
153
+ refuse('CHECKPOINT_WITHDRAWAL_SCOPE', `plan.steps[${id}]: withdrawal needs one enabled unfinished checkpoint in the current execution stage; preserve other stages and use the matching step`);
154
+ }
155
+ plan.checkpoint_withdrawals = [...journal, { schema: 'rig-create/checkpoint-withdrawal@1', step: structuredClone(previous),
156
+ sha256: stepHash(previous), generation, by: opts['--by']!, at: new Date(now).toISOString(), reason, verbatim }];
157
+ } else {
158
+ if (previous !== undefined) {
159
+ if (previous.phase !== 'rig' && previous.phase !== 'post') refuse('PLAN_STEP_INVALID', `plan.steps[${id}].phase: restore rig or post before drafting`);
160
+ const stage = approvalStageOfStep(previous.phase, id!, hasStageOrder(project));
161
+ const reopens = collectReopens(project, []);
162
+ if (collectSignoffs(project, []).some(signature => signature.stage === stage && signature.checkpoint === null
163
+ && (signature.steps === null || signature.steps.includes(id!)) && liveSignoff(reopens, signature.stage, signature.at))) {
164
+ refuse('PLAN_STEP_SIGNED', `plan.steps[${id}]: a live ${stage} signature binds this step; reopen ${stage} before revising or removing it`);
165
+ }
166
+ if (action === 'revise' && completed(project, previous)) refuse('PLAN_STEP_DONE', `plan.steps[${id}]: completed work cannot be revised; remove it and add the replacement under a new id`);
167
+ }
168
+ if (previous !== undefined) {
169
+ plan.replaced = [...asArray(plan.replaced), { step: structuredClone(previous), sha256: stepHash(previous), action: action === 'revise' ? 'revised' : 'removed', at, by: opts['--by']! }];
170
+ const index = steps.indexOf(previous);
171
+ if (action === 'remove') steps.splice(index, 1);
172
+ else steps[index] = { ...previous, ...patch };
173
+ } else steps.push({ id: id!, ...patch, checkpoint: null, status: 'pending' });
174
+ const changedSteps = [previous, ...steps.filter(step => isObject(step) && step.id === id)].filter(isObject);
175
+ const ownerships = new Set(changedSteps.map(step => approvalStageOfStep(step.phase as 'rig' | 'post', id!, hasStageOrder(project))));
176
+ if (ownerships.has('plan-approved')) {
177
+ plan.drafted_by = opts['--by']!;
178
+ plan.drafted_at = at;
179
+ }
180
+ if (ownerships.has('rigging-interview')) plan.rigging_drafted_at = at;
181
+ }
182
+
183
+ const amendmentFindings: import('./check.ts').Finding[] = [];
184
+ collectBarAmendments(next, amendmentFindings, now);
185
+ if (amendmentFindings[0]) refuse(amendmentFindings[0].code, amendmentFindings[0].message);
186
+ // Refusals the record already carries stay; a new code/path pair, or one more copy of a carried one, refuses.
187
+ const fresh = newFindings(baselineOf(checkProject(dir, project, { now })), checkProject(dir, next, { now }).refusals)[0];
188
+ if (fresh !== undefined) refuse(fresh.code, fresh.message);
189
+ const temporary = join(dirname(file), `.plan-${process.pid}-${crypto.randomUUID()}.tmp`);
190
+ try {
191
+ writeFileSync(temporary, `${JSON.stringify(next, null, 2)}\n`, { flag: 'wx' });
192
+ if (!readFileSync(file).equals(original)) refuse('PLAN_WRITE_CONFLICT', 'project.json: changed while drafting; reload it and retry the plan command');
193
+ if (amendingBar && inputs.some(input => readVerbatim(input.file) !== input.text)) refuse('PLAN_WRITE_CONFLICT', 'plan.bar_amendments: input files changed before commit; restore the intended files and retry');
194
+ renameSync(temporary, file);
195
+ } finally { rmSync(temporary, { force: true }); }
196
+ return { stdout: json ? JSON.stringify(plan, null, 2) : `plan: ${action} ${id} by ${opts['--by']} at ${at}`, exit: 0 };
197
+ } finally {
198
+ closeSync(descriptor);
199
+ rmSync(lock);
200
+ releaseJob();
201
+ }
202
+ }
@@ -0,0 +1,110 @@
1
+ /** Bind the producer's published receipt bytes, without repeating its measurements. */
2
+ import { createHash } from 'node:crypto';
3
+ import { lstatSync, readFileSync } from './check-reads.ts';
4
+ import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
5
+ import { asArray, isObject, type Ctx } from './check.ts';
6
+ import type { Json, JsonObject } from './templates.ts';
7
+
8
+ export const PLATE_RECEIPT_SINCE = '2026-11-08T00:00:00Z';
9
+ const SPEC = 'rig-parts-plate-receipt/1';
10
+ const KEYS = ['spec', 'mode', 'painting', 'mask', 'model', 'params', 'graph', 'gpu'];
11
+ const hash = (bytes: Uint8Array): string => createHash('sha256').update(bytes).digest('hex');
12
+ const hex = (value: unknown): value is string => typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value);
13
+
14
+ /** Reject symlinks at every component, including directory aliases within the project. */
15
+ function receiptBytes(dir: string, path: unknown): Buffer {
16
+ if (typeof path !== 'string' || !path || isAbsolute(path) || path.includes('\\')) throw Error('project-relative receipt path required');
17
+ const rel = relative(dir, resolve(dir, path));
18
+ if (!rel || rel === '..' || rel.startsWith(`..${sep}`) || rel === 'project.json') throw Error('receipt must stay inside the project');
19
+ let current = dir;
20
+ for (const part of path.split('/').filter(part => part && part !== '.')) {
21
+ current = resolve(current, part);
22
+ const component = relative(dir, current);
23
+ if (component === '..' || component.startsWith(`..${sep}`)) throw Error('receipt must stay inside the project');
24
+ if (lstatSync(current).isSymbolicLink()) throw Error('receipt must have no symlink components');
25
+ }
26
+ return readFileSync(current);
27
+ }
28
+
29
+ /** Also used before the derived writer commits its new entry. */
30
+ export function readPlateReceipt(dir: string, path: string): { binding: JsonObject; report: JsonObject } {
31
+ const bytes = receiptBytes(dir, path);
32
+ const value = JSON.parse(bytes.toString('utf8')) as Json;
33
+ if (!isObject(value) || value.spec !== SPEC) throw Error('known rig-parts-plate-receipt/1 spec required');
34
+ const keys = [...KEYS, ...(value.grid_transform === undefined ? [] : ['grid_transform']), 'output'];
35
+ if (Object.keys(value).join(',') !== keys.join(',')) throw Error('published receipt key order required');
36
+ const fileFields = [
37
+ ['painting', ['path', 'sha256']],
38
+ ['mask', ['source', 'from', 'path', 'sha256']],
39
+ ['output', ['path', 'sha256']],
40
+ ] as const;
41
+ for (const [field, fields] of fileFields) {
42
+ const record = value[field];
43
+ if (!isObject(record) || Object.keys(record).join(',') !== fields.join(',')
44
+ || typeof record.path !== 'string' || !record.path || isAbsolute(record.path)
45
+ || record.path.includes('\\') || !hex(record.sha256)) {
46
+ throw Error(`${field} needs the published path and sha256 fields`);
47
+ }
48
+ }
49
+ return { binding: { path, sha256: hash(bytes), spec: SPEC }, report: value };
50
+ }
51
+
52
+ export function checkPlateReceipt(ctx: Pick<Ctx, 'dir' | 'refusals'>, entry: JsonObject, at: string): void {
53
+ if (entry.receipt === undefined) return;
54
+ try {
55
+ if (!isObject(entry.receipt) || typeof entry.receipt.path !== 'string'
56
+ || !hex(entry.receipt.sha256) || entry.receipt.spec !== SPEC) {
57
+ throw Error('receipt needs path, sha256 and a known spec');
58
+ }
59
+ const { binding, report } = readPlateReceipt(ctx.dir, entry.receipt.path);
60
+ if (binding.sha256 !== entry.receipt.sha256.toLowerCase()) throw Error('receipt bytes differ from the recorded sha256');
61
+ const base = dirname(resolve(ctx.dir, entry.receipt.path));
62
+ const matches = (declared: JsonObject, recorded: JsonObject): boolean =>
63
+ typeof recorded.path === 'string' && hex(recorded.sha256)
64
+ && resolve(base, String(declared.path)) === resolve(ctx.dir, recorded.path)
65
+ && String(declared.sha256).toLowerCase() === recorded.sha256.toLowerCase();
66
+ const sources = entry.sources === undefined ? [entry.source] : asArray(entry.sources);
67
+ const painting = report.painting as JsonObject;
68
+ const mask = report.mask as JsonObject;
69
+ if (sources.length !== 2 || !sources.every(isObject)
70
+ || !(matches(painting, sources[0] as JsonObject) && matches(mask, sources[1] as JsonObject)
71
+ || matches(painting, sources[1] as JsonObject) && matches(mask, sources[0] as JsonObject))
72
+ || !matches(report.output as JsonObject, entry)) {
73
+ throw Error('painting, uploaded mask and output must match the recorded source and result paths and hashes');
74
+ }
75
+ } catch (error) {
76
+ ctx.refusals.push({ code: 'PLATE_RECEIPT_INVALID', path: `${at}.receipt`, message: `${at}.receipt: ${(error as Error).message}; restore the producer receipt and record its painting, mask and output with derived add --receipt` });
77
+ }
78
+ }
79
+
80
+ /** Scene plate registrations are the recorded discriminator. Earliest registration dates the rule. */
81
+ export function checkPlateRegistrations(ctx: Pick<Ctx, 'dir' | 'project' | 'refusals' | 'warnings'>): void {
82
+ const earliest = new Map<number, number>();
83
+ for (const scene of asArray(ctx.project.scenes)) {
84
+ if (!isObject(scene) || typeof scene.path !== 'string' || !hex(scene.sha256) || typeof scene.at !== 'string' || !Number.isFinite(Date.parse(scene.at))) continue;
85
+ let doc: JsonObject;
86
+ try {
87
+ const bytes = receiptBytes(ctx.dir, scene.path);
88
+ if (hash(bytes) !== scene.sha256.toLowerCase()) continue;
89
+ const value = JSON.parse(bytes.toString('utf8')) as Json;
90
+ if (!isObject(value)) continue;
91
+ doc = value;
92
+ } catch { continue; } // The scene checker reports missing or changed scene documents.
93
+ for (const plate of asArray(doc.plates)) {
94
+ if (!isObject(plate) || !isObject(plate.registered) || plate.registered.kind !== 'derived' || typeof plate.registered.ref !== 'string') continue;
95
+ const match = /^derived\[(\d+)\]$/.exec(plate.registered.ref);
96
+ if (!match) continue;
97
+ const index = Number(match[1]);
98
+ const entry = asArray(ctx.project.derived)[index];
99
+ if (!isObject(entry) || plate.path !== entry.path || plate.sha256 !== entry.sha256) continue;
100
+ earliest.set(index, Math.min(earliest.get(index) ?? Infinity, Date.parse(scene.at)));
101
+ }
102
+ }
103
+ for (const [index, first] of earliest) {
104
+ const entry = asArray(ctx.project.derived)[index] as JsonObject;
105
+ if (entry.receipt !== undefined) continue;
106
+ const path = `derived[${index}].receipt`;
107
+ const findings = first < Date.parse(PLATE_RECEIPT_SINCE) ? ctx.warnings : ctx.refusals;
108
+ findings.push({ code: 'PLATE_RECEIPT_MISSING', path, message: `${path}: scene-registered derived plate has no producer receipt; record the plate with derived add --receipt` });
109
+ }
110
+ }