@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,143 @@
1
+ /** Bind externally produced figure support without decoding or measuring it. */
2
+ import { createHash } from 'node:crypto';
3
+ import { existsSync, readFileSync } from './check-reads.ts';
4
+ import { dirname, join, relative, resolve, sep } from 'node:path';
5
+
6
+ import { asArray, asString, isObject, type Ctx } from './check.ts';
7
+ import { readObject } from './cg-gates.ts';
8
+ import { checkReviewPackages, reviewConfigInputs } from './review-package.ts';
9
+ import { refuse } from './refusal.ts';
10
+ import type { JsonObject } from './templates.ts';
11
+
12
+ export const FIGURE_MASK_SINCE = '2026-11-06T00:00:00Z';
13
+
14
+ export const FIGURE_MASK_INPUTS_SINCE = '2026-11-06T00:00:00Z';
15
+
16
+ type MaskContext = Pick<Ctx, 'dir' | 'project' | 'refusals' | 'warnings'>;
17
+
18
+ export function checkFigureMask(ctx: MaskContext): void {
19
+ asArray(ctx.project.versions).forEach((version, index) => {
20
+ if (!isObject(version) || version.kind !== 'rig' || version.pre_interview === true) return;
21
+ const record = version.review_package;
22
+ if (!isObject(record)) return;
23
+ const root = join(ctx.dir, 'derived/review', `v${String(version.n)}`);
24
+ if (!existsSync(root)) return;
25
+ const packagePath = `versions[v${String(version.n)}].review_package`;
26
+ if (ctx.refusals.some(f => f.path.startsWith(packagePath)
27
+ && ['REVIEW_PACKAGE_CHANGED', 'REVIEW_PACKAGE_MALFORMED', 'REVIEW_PACKAGE_INPUT_INVALID'].includes(f.code))) return;
28
+ const files = asArray(record.files).filter(isObject);
29
+ const configEntry = files.find(f => f.path === 'config.json');
30
+ if (typeof configEntry?.sha256 !== 'string') return;
31
+ const provenance = isObject(version.provenance) ? version.provenance : {};
32
+ const config = isObject(provenance.config) ? provenance.config : {};
33
+ const configPath = asString(config.path);
34
+ if (!configPath) return;
35
+ const cfg = readObject(ctx.dir, join(root, 'config.json'), configEntry.sha256);
36
+ if (!cfg.ok) return;
37
+ const assemble = isObject(cfg.value.assemble) ? cfg.value.assemble : {};
38
+ if (assemble.project !== 'visible' || assemble.ground !== 'busy') return;
39
+ const label = typeof version.n === 'number' ? `versions[v${version.n}]` : `versions[${index}]`;
40
+ const path = `${label}.provenance.config requirements setup_painting.figure_mask`;
41
+ const at = typeof version.at === 'string' ? Date.parse(version.at) : Number.NaN;
42
+ const legacy = !Number.isFinite(at) || at < Date.parse(FIGURE_MASK_SINCE);
43
+ const finding = (code: string, detail: string): void => {
44
+ (legacy ? ctx.warnings : ctx.refusals).push({
45
+ code: legacy ? 'FIGURE_MASK_LEGACY' : code, path,
46
+ message: `${path}: ${detail}${legacy ? ` (${code}; record predates ${FIGURE_MASK_SINCE} or has no readable date).` : ''}`,
47
+ });
48
+ };
49
+ const unbound = (detail: string): void => {
50
+ const old = !Number.isFinite(at) || at < Date.parse(FIGURE_MASK_INPUTS_SINCE);
51
+ (old ? ctx.warnings : ctx.refusals).push({
52
+ code: old ? 'FIGURE_MASK_INPUTS_LEGACY' : 'FIGURE_MASK_INPUT_UNBOUND', path,
53
+ message: `${path}: ${detail}; preserve this package and record a new build with review-package to bind the requirements and figure mask`,
54
+ });
55
+ };
56
+ const requirements = asString(cfg.value.requirements);
57
+ if (!requirements) {
58
+ finding('FIGURE_MASK_MISSING', `${configPath}: declare requirements with setup_painting.figure_mask`);
59
+ return;
60
+ }
61
+ let named: JsonObject[];
62
+ let unavailable = false;
63
+ try { named = reviewConfigInputs(record, ctx.dir, Number(version.n), () => {}, () => { unavailable = true; }); }
64
+ catch { return; } // The package checker owns loader and config diagnostics.
65
+ if (unavailable) return;
66
+ const reqPath = relative(root, resolve(root, requirements)).split(sep).join('/');
67
+ const reqEntry = named.find(f => f.path === reqPath);
68
+ if (typeof reqEntry?.sha256 !== 'string') {
69
+ unbound(`requirements ${requirements} has no frozen input binding`);
70
+ return;
71
+ }
72
+ const req = readObject(ctx.dir, resolve(root, requirements), reqEntry.sha256);
73
+ if (!req.ok) {
74
+ finding('FIGURE_MASK_MISSING', `${requirements}: ${req.why}; retain readable requirements JSON with setup_painting.figure_mask`);
75
+ return;
76
+ }
77
+ const setup = isObject(req.value.setup_painting) ? req.value.setup_painting : {};
78
+ const mask = isObject(setup.figure_mask) ? setup.figure_mask : {};
79
+ if (typeof mask.path !== 'string' || !mask.path.trim() || typeof mask.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(mask.sha256)) {
80
+ finding('FIGURE_MASK_MISSING', `${configPath}: declare setup_painting.figure_mask {path, sha256} in its requirements file, naming the externally produced figure PNG`);
81
+ return;
82
+ }
83
+ const file = resolve(dirname(req.file), mask.path);
84
+ const maskPath = relative(root, file).split(sep).join('/');
85
+ const maskEntry = files.find(f => f.path === maskPath);
86
+ if (typeof maskEntry?.sha256 !== 'string') {
87
+ unbound(`figure mask ${mask.path} has no frozen input binding`);
88
+ return;
89
+ }
90
+ let actual: string;
91
+ try { actual = createHash('sha256').update(readFileSync(file)).digest('hex'); }
92
+ catch {
93
+ finding('FIGURE_MASK_CHANGED', `${mask.path}: cannot read the declared PNG; restore the file relative to ${requirements}`);
94
+ return;
95
+ }
96
+ if (actual !== maskEntry.sha256) {
97
+ ctx.refusals.push({ code: 'REVIEW_PACKAGE_CHANGED', path: packagePath, message: `${packagePath}: ${maskPath} differs from its frozen inventory hash; restore the recorded package` });
98
+ return;
99
+ }
100
+ if (actual !== mask.sha256) {
101
+ finding('FIGURE_MASK_CHANGED', `${mask.path}: bytes differ from the declaration; restore the recorded PNG or update its declaration and derived record`);
102
+ return;
103
+ }
104
+ let original = resolve(dirname(resolve(ctx.dir, configPath)), requirements);
105
+ let maskRel = mask.path;
106
+ if (isObject(version.registration) && version.registration.kind === 'frozen-rig/1'
107
+ && isObject(version.registration.path_adapter) && typeof version.registration.path_adapter.path === 'string') {
108
+ const reqSource = version.registration.requirements_source;
109
+ if (isObject(reqSource) && typeof reqSource.source_path === 'string') {
110
+ original = resolve(ctx.dir, reqSource.source_path);
111
+ }
112
+ try {
113
+ const rows = JSON.parse(readFileSync(resolve(ctx.dir, version.registration.path_adapter.path), 'utf8'));
114
+ if (Array.isArray(rows)) {
115
+ const maskRow = rows.find(r => isObject(r) && r.doc === 'requirements' && r.pointer === '/setup_painting/figure_mask/path' && typeof r.before === 'string' && r.after === mask.path);
116
+ if (maskRow) maskRel = maskRow.before as string;
117
+ }
118
+ } catch { /* Handled by registration checks. */ }
119
+ }
120
+ const originalMask = resolve(dirname(original), maskRel);
121
+ const recorded = asArray(ctx.project.derived).some((entry) => {
122
+ if (!isObject(entry) || entry.kind !== 'prepared-input' || typeof entry.path !== 'string'
123
+ || resolve(ctx.dir, entry.path) !== originalMask || typeof entry.sha256 !== 'string' || entry.sha256.toLowerCase() !== mask.sha256
124
+ || typeof entry.operation !== 'string' || !entry.operation.trim()) return false;
125
+ const sources = entry.sources === undefined ? [entry.source] : asArray(entry.sources);
126
+ return sources.length > 0 && sources.every((source) => isObject(source)
127
+ && typeof source.path === 'string' && source.path.trim() !== ''
128
+ && typeof source.sha256 === 'string' && /^[a-f0-9]{64}$/i.test(source.sha256));
129
+ });
130
+ if (!recorded) finding('FIGURE_MASK_UNRECORDED', `${relative(ctx.dir, originalMask)}: record this PNG and hash with derived add and at least one source; operation names the matte model, alpha >= 128 threshold and foreground plane subtracted (or none)`);
131
+ });
132
+ }
133
+
134
+ /** Apply the same packaged gates at review writer and handoff boundaries. */
135
+ export function assertFigureMaskApproval(dir: string, project: JsonObject, version: number): void {
136
+ const selected = { ...project, versions: asArray(project.versions).filter(v => isObject(v) && v.n === version) };
137
+ const ctx: MaskContext = { dir, project: selected, refusals: [], warnings: [] };
138
+ checkReviewPackages(ctx);
139
+ checkFigureMask(ctx);
140
+ const first = ctx.refusals.find(f => f.code.startsWith('FIGURE_MASK_')
141
+ || ['RIG_VERSION_AT_INVALID', 'REVIEW_PACKAGE_PART_FILE_MISMATCH', 'REVIEW_PACKAGE_CHANGED', 'REVIEW_PACKAGE_PARTS_MISMATCH'].includes(f.code));
142
+ if (first) refuse(first.code, first.message);
143
+ }
@@ -0,0 +1,355 @@
1
+ /**
2
+ * The project's fix ledger (create#285): the owner fixes every version inherits, and the producer
3
+ * ledger reports a draft is recorded with. create never runs a ledger check. It binds the report a
4
+ * project's own check script wrote by its sha256 and spec, and compares only the report's rows with
5
+ * the ledger: coverage, build identity, the rows the predecessor draft passed, and what a draft fixes.
6
+ */
7
+ import { createHash, randomUUID } from 'node:crypto';
8
+ import { renameSync, rmSync, writeFileSync } from 'node:fs';
9
+ import { readFileSync, realpathSync, statSync } from './check-reads.ts';
10
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
11
+ import { asArray, isObject, type Finding } from './check.ts';
12
+ import { readProject } from './feedback.ts';
13
+ import { IGNORED_DIRS } from './init.ts';
14
+ import { readVerbatim } from './record-input.ts';
15
+ import { refuse, USAGE_EXIT } from './refusal.ts';
16
+ import { newRefusals } from './refusal-baseline.ts';
17
+ import type { Json, JsonObject } from './templates.ts';
18
+
19
+ /** Drafts recorded from this date bind a ledger report whenever the ledger has a live row; earlier drafts WARN. */
20
+ export const FIX_LEDGER_SINCE = '2026-11-09T00:00:00Z';
21
+ export const FIX_LEDGER_REPORT_SPEC = 'rig-create/fix-ledger-report@1';
22
+
23
+ const RESULTS = ['pass', 'fail', 'unmeasurable'] as const;
24
+ const ROW_KEYS = ['id', 'note', 'at', 'by', 'check', 'bar', 'retired'];
25
+ const idPattern = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
26
+ /** A correction by id, or a verdict by version number and index, as the record paths name them. */
27
+ const NOTE_REF = /^(?:corrections\[([a-zA-Z0-9][a-zA-Z0-9_-]{0,63})\]|versions\[v(\d+)\]\.verdicts\[(\d+)\])$/;
28
+ const hex = (v: Json | undefined): v is string => typeof v === 'string' && /^[a-f0-9]{64}$/.test(v);
29
+ const text = (v: Json | undefined): v is string => typeof v === 'string' && v.trim() !== '';
30
+ const time = (v: Json | undefined): number => typeof v === 'string' ? Date.parse(v) : NaN;
31
+ const sha = (bytes: Uint8Array): string => createHash('sha256').update(bytes).digest('hex');
32
+ const literal = (value: Json | undefined): value is string => typeof value === 'string' && value !== ''
33
+ && !isAbsolute(value) && !value.includes('\\') && value.split('/').every(segment => segment !== '' && segment !== '.' && segment !== '..');
34
+ const finding = (code: string, path: string, message: string): Finding => ({ code, path, message: `${path}: ${message}` });
35
+
36
+ export interface LedgerRow { readonly id: string; readonly at: number; readonly retiredAt: number | null }
37
+
38
+ /** Rows a draft recorded at `at` must cover: written by then and not retired by then. */
39
+ export const liveRows = (rows: readonly LedgerRow[], at: number): LedgerRow[] => rows.filter(r => r.at <= at && (r.retiredAt === null || r.retiredAt > at));
40
+
41
+ /** The owner words the note's reference points at, or undefined when the record no longer holds it. */
42
+ function noteWords(project: JsonObject, ref: string): string | undefined {
43
+ const match = NOTE_REF.exec(ref);
44
+ if (!match) return undefined;
45
+ if (match[1] !== undefined) {
46
+ const correction = asArray(project.corrections).filter(isObject).find(c => c.id === match[1]);
47
+ return typeof correction?.verbatim === 'string' ? correction.verbatim : undefined;
48
+ }
49
+ const version = asArray(project.versions).filter(isObject).find(v => v.n === Number(match[2]));
50
+ const verdict = version === undefined ? undefined : asArray(version.verdicts)[Number(match[3])];
51
+ return isObject(verdict) && typeof verdict.verbatim === 'string' ? verdict.verbatim : undefined;
52
+ }
53
+
54
+ /**
55
+ * Validate `fix_ledger` rows; the ledger writer and check share this. Returns null when the ledger is absent.
56
+ * The row being written (`writing`) must quote a note the record holds. Check does not re-read older rows'
57
+ * notes: a restart retires the record they pointed into, and the row keeps the words it quoted.
58
+ */
59
+ export function ledgerRows(project: JsonObject, refusals: Finding[], now: number, writing?: number): LedgerRow[] | null {
60
+ if (project.fix_ledger === undefined) return null;
61
+ if (!Array.isArray(project.fix_ledger)) { refusals.push(finding('FIX_LEDGER_MALFORMED', 'fix_ledger', 'expected a list of rows; restore the rows written by ledger add')); return []; }
62
+ const rows: LedgerRow[] = [];
63
+ const ids = new Set<string>();
64
+ project.fix_ledger.forEach((raw, index) => {
65
+ const path = `fix_ledger[${index}]`;
66
+ const bad = (why: string): void => { refusals.push(finding('FIX_LEDGER_MALFORMED', path, `${why}; restore the row ledger add wrote, or retire it with ledger retire`)); };
67
+ if (!isObject(raw) || Object.keys(raw).some(key => !ROW_KEYS.includes(key))) return bad(`expected only ${ROW_KEYS.join(', ')}`);
68
+ if (typeof raw.id !== 'string' || !idPattern.test(raw.id) || ids.has(raw.id)) return bad('needs a unique id');
69
+ ids.add(raw.id);
70
+ const at = time(raw.at);
71
+ if (!Number.isFinite(at) || at > now) return bad('needs an at that is not in the future');
72
+ if (!text(raw.by) || !text(raw.check) || !text(raw.bar)) return bad('needs by, check and bar text');
73
+ const note = raw.note;
74
+ if (!isObject(note) || typeof note.ref !== 'string' || !NOTE_REF.test(note.ref) || !text(note.verbatim)) {
75
+ return bad('needs note { ref: corrections[<id>] or versions[v<n>].verdicts[<i>], verbatim }');
76
+ }
77
+ let retiredAt: number | null = null;
78
+ if (raw.retired !== undefined) {
79
+ const retired = raw.retired;
80
+ retiredAt = isObject(retired) ? time(retired.at) : NaN;
81
+ if (!isObject(retired) || !Number.isFinite(retiredAt) || retiredAt < at || retiredAt > now || !text(retired.by) || !text(retired.reason)) {
82
+ return bad('retired needs { at, by, reason } dated after the row and not in the future');
83
+ }
84
+ }
85
+ if (index === writing) {
86
+ const words = noteWords(project, note.ref);
87
+ if (words === undefined) refusals.push(finding('FIX_LEDGER_NOTE_UNKNOWN', `${path}.note.ref`, `${note.ref} names no recorded correction or verdict; quote an owner note the record holds`));
88
+ else if (!words.includes(note.verbatim)) refusals.push(finding('FIX_LEDGER_NOTE_MISMATCH', `${path}.note.verbatim`, `the words are not in ${note.ref}; quote the owner's words verbatim`));
89
+ }
90
+ rows.push({ id: raw.id, at, retiredAt });
91
+ });
92
+ return rows;
93
+ }
94
+
95
+ /** The report bytes at a project-relative path outside ignored roots, read the way the writer and check both do. */
96
+ function reportBytes(dir: string, path: Json | undefined, at: string): Buffer | Finding {
97
+ const invalid = (why: string): Finding => finding('DRAFT_LEDGER_REPORT_INVALID', at, `${why}; retain the report the project's check script wrote under the project (for example evidence/) and record the draft with --ledger-report`);
98
+ if (!literal(path)) return invalid('the report needs a literal project-relative path');
99
+ if ([...IGNORED_DIRS, 'node_modules', '.git'].includes(path.split('/')[0]!)) return invalid(`${path} is under an ignored root and would not survive a clone`);
100
+ try {
101
+ const root = realpathSync(dir);
102
+ const file = realpathSync(resolve(dir, path));
103
+ const rel = relative(root, file);
104
+ if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) return invalid(`${path} leaves the project through a link`);
105
+ if (!statSync(file).isFile()) return invalid(`${path} is not a regular file`);
106
+ return readFileSync(file);
107
+ } catch { return invalid(`${path} is not there or cannot be read`); }
108
+ }
109
+
110
+ interface LedgerReport { readonly manifest: string; readonly results: Map<string, string> }
111
+
112
+ /** The report's own fields, read and never recomputed. */
113
+ function decodeReport(bytes: Buffer, at: string): LedgerReport | Finding {
114
+ const invalid = (why: string): Finding => finding('DRAFT_LEDGER_REPORT_INVALID', at, `${why}; record the draft with the ${FIX_LEDGER_REPORT_SPEC} JSON the project's check script wrote`);
115
+ let report: Json;
116
+ try { report = JSON.parse(bytes.toString('utf8')) as Json; } catch { return invalid('the report is not JSON'); }
117
+ if (!isObject(report) || report.schema !== FIX_LEDGER_REPORT_SPEC) return invalid(`schema is not ${FIX_LEDGER_REPORT_SPEC}`);
118
+ if (!isObject(report.build) || !hex(report.build.manifest_sha256)) return invalid('build.manifest_sha256 must be the sha256 of the build\'s parts.json');
119
+ if (!hex(report.script_sha256)) return invalid('script_sha256 must be the sha256 of the check script');
120
+ if (!Array.isArray(report.rows)) return invalid('rows must be a list');
121
+ const results = new Map<string, string>();
122
+ for (const [index, row] of report.rows.entries()) {
123
+ if (!isObject(row) || typeof row.id !== 'string' || !idPattern.test(row.id) || results.has(row.id) || !RESULTS.includes(row.result as typeof RESULTS[number])) {
124
+ return invalid(`rows[${index}] needs a unique id and a result of ${RESULTS.join(', ')}`);
125
+ }
126
+ results.set(row.id, String(row.result));
127
+ }
128
+ return { manifest: report.build.manifest_sha256, results };
129
+ }
130
+
131
+ /** The report a draft binds, checked against its binding; null when the draft binds none. */
132
+ function draftReport(dir: string, draft: JsonObject, path: string): LedgerReport | Finding | null {
133
+ const binding = draft.ledger_report;
134
+ if (binding === undefined) return null;
135
+ const at = `${path}.ledger_report`;
136
+ if (!isObject(binding) || binding.spec !== FIX_LEDGER_REPORT_SPEC || !hex(binding.sha256) || !literal(binding.path)) {
137
+ return finding('DRAFT_LEDGER_REPORT_INVALID', at, `needs { path, sha256, spec: ${FIX_LEDGER_REPORT_SPEC} }; record the draft with --ledger-report`);
138
+ }
139
+ const bytes = reportBytes(dir, binding.path, at);
140
+ if (!Buffer.isBuffer(bytes)) return bytes;
141
+ if (sha(bytes) !== binding.sha256) return finding('DRAFT_LEDGER_REPORT_INVALID', at, `${binding.path} differs from its recorded sha256; restore the report the draft was recorded with`);
142
+ return decodeReport(bytes, at);
143
+ }
144
+
145
+ const manifestOf = (draft: JsonObject): string | undefined => {
146
+ const parts = isObject(draft.parts) ? draft.parts : {};
147
+ const entry = asArray(parts.files).filter(isObject).find(f => f.path === `${String(parts.path)}/parts.json`);
148
+ return typeof entry?.sha256 === 'string' ? entry.sha256 : undefined;
149
+ };
150
+
151
+ const hasPartsReceipt = (project: JsonObject, id: string): boolean => asArray(project.review_acts)
152
+ .some(entry => isObject(entry) && entry.kind === 'parts-verdict' && entry.draft_id === id);
153
+
154
+ /**
155
+ * "The last draft a person accepted or saw" for drafts[index]: the latest earlier draft with a recorded
156
+ * parts-review receipt (any verdict), else the previous draft, else none.
157
+ */
158
+ export function fixesAgainst(project: JsonObject, index: number): string | null {
159
+ const earlier = asArray(project.drafts).slice(0, index).filter(isObject);
160
+ const seen = [...earlier].reverse().find(d => typeof d.id === 'string' && hasPartsReceipt(project, d.id));
161
+ const chosen = seen ?? earlier.at(-1);
162
+ return typeof chosen?.id === 'string' ? chosen.id : null;
163
+ }
164
+
165
+ /** Live rows the report passes that the reference draft's report did not pass, in ledger order. */
166
+ const fixedRows = (live: LedgerRow[], results: Map<string, string>, reference: LedgerReport | null): string[] =>
167
+ live.filter(r => results.get(r.id) === 'pass' && reference?.results.get(r.id) !== 'pass').map(r => r.id);
168
+
169
+ /** The fixes the writer records for drafts[index] against the named reference; reports are read, never produced. */
170
+ export function draftFixes(dir: string, project: JsonObject, index: number, against: string | null, now: number): string[] {
171
+ const rows = ledgerRows(project, [], now) ?? [];
172
+ const drafts = asArray(project.drafts);
173
+ const draft = drafts[index] as JsonObject;
174
+ const report = draftReport(dir, draft, `drafts[${index}]`);
175
+ if (report === null || 'code' in report) return [];
176
+ const refIndex = drafts.findIndex(d => isObject(d) && d.id === against);
177
+ const reference = refIndex < 0 ? null : draftReport(dir, drafts[refIndex] as JsonObject, `drafts[${refIndex}]`);
178
+ return fixedRows(liveRows(rows, time(draft.at)), report.results, reference === null || 'code' in reference ? null : reference);
179
+ }
180
+
181
+ export interface LedgerSummary { readonly draft: string; readonly fixes: string[]; readonly known_regressions: string[] }
182
+
183
+ /**
184
+ * check and the writers: ledger rows, restarts[], and every draft's ledger binding. Returns the
185
+ * per-draft fixes check reports.
186
+ */
187
+ export function checkFixLedger(dir: string, project: JsonObject, refusals: Finding[], warnings: Finding[], now: number): LedgerSummary[] {
188
+ checkRestarts(project, refusals);
189
+ const rows = ledgerRows(project, refusals, now);
190
+ const drafts = asArray(project.drafts);
191
+ if (rows === null && drafts.length > 0) {
192
+ warnings.push(finding('FIX_LEDGER_MISSING', 'fix_ledger', 'the project has drafts but no fix ledger, so no draft is checked against earlier accepted fixes; record each accepted owner fix with ledger add'));
193
+ }
194
+ const reports: (LedgerReport | null)[] = [];
195
+ const owed: string[][] = [];
196
+ const summaries: LedgerSummary[] = [];
197
+ drafts.forEach((raw, index) => {
198
+ const path = `drafts[${index}]`;
199
+ reports[index] = null;
200
+ if (!isObject(raw) || typeof raw.id !== 'string') return; // DRAFT_RIG_MALFORMED reports it.
201
+ const at = time(raw.at);
202
+ const live = liveRows(rows ?? [], at);
203
+ const malformed = (why: string): void => { refusals.push(finding('DRAFT_LEDGER_MALFORMED', path, `${why}; record the draft again with draft-rig under a new id; the old record is kept`)); };
204
+ const report = draftReport(dir, raw, path);
205
+ if (report === null) {
206
+ if (raw.fixes !== undefined || raw.fixes_against !== undefined || raw.known_regressions !== undefined) malformed('fixes, fixes_against and known_regressions need a ledger_report');
207
+ if (live.length > 0) {
208
+ const legacy = !Number.isFinite(at) || at < Date.parse(FIX_LEDGER_SINCE);
209
+ (legacy ? warnings : refusals).push(finding('DRAFT_LEDGER_REPORT_MISSING', `${path}.ledger_report`, `the ledger had ${live.length} live row(s) (${live.map(r => r.id).join(', ')}) when the draft was recorded, and it binds no ledger report${legacy ? '; legacy record kept' : ''}; run the project's check script on the build and record the draft with draft-rig --ledger-report <file>`));
210
+ }
211
+ return;
212
+ }
213
+ if ('code' in report) { refusals.push(report); return; }
214
+ reports[index] = report;
215
+ const manifest = manifestOf(raw);
216
+ if (manifest !== report.manifest) {
217
+ refusals.push(finding('DRAFT_LEDGER_BUILD_MISMATCH', `${path}.ledger_report`, `the report's build.manifest_sha256 ${report.manifest} is not the draft's parts.json (${manifest ?? 'unbound'}); run the check script on this draft's build and record the draft with that report`));
218
+ }
219
+ const missing = live.filter(r => !report.results.has(r.id)).map(r => r.id);
220
+ if (missing.length) {
221
+ refusals.push(finding('DRAFT_LEDGER_INCOMPLETE', `${path}.ledger_report`, `the report has no row for ${missing.join(', ')}; run the check script over every non-retired fix_ledger row and record the draft with that report`));
222
+ }
223
+ const known = asArray(raw.known_regressions);
224
+ if (raw.known_regressions !== undefined && (!Array.isArray(raw.known_regressions) || known.length === 0
225
+ || known.some(k => !isObject(k) || typeof k.id !== 'string' || !text(k.reason)) || new Set(known.map(k => (k as JsonObject).id)).size !== known.length)) {
226
+ malformed('known_regressions needs a list of { id, reason } with unique ids');
227
+ return;
228
+ }
229
+ const knownIds = known.map(k => String((k as JsonObject).id));
230
+ owed[index] = knownIds;
231
+ // The predecessor is the previous recorded draft; a restart clears drafts, so its first draft has none.
232
+ // What it owes is every row it passed and every row it recorded as a known regression: a known regression stays owed.
233
+ const predecessor = index > 0 ? reports[index - 1] : null;
234
+ const debt = new Set(index > 0 ? owed[index - 1] ?? [] : []);
235
+ const regressed = predecessor ? live.filter(r => (predecessor.results.get(r.id) === 'pass' || debt.has(r.id)) && report.results.get(r.id) !== 'pass').map(r => r.id) : [];
236
+ const unnamed = regressed.filter(id => !knownIds.includes(id));
237
+ if (unnamed.length) {
238
+ refusals.push(finding('DRAFT_LEDGER_REGRESSION', `${path}.ledger_report`, `${unnamed.map(id => `${id} (${report.results.get(id) ?? 'absent'})`).join(', ')} passed in ${String((drafts[index - 1] as JsonObject).id)}, or are known regressions it still owes, and do not pass here; fix the build, or record it with --known-regression <row id>=<reason file> for each row`));
239
+ }
240
+ const stray = knownIds.filter(id => !regressed.includes(id));
241
+ if (stray.length) malformed(`known_regressions names ${stray.join(', ')}, which the predecessor draft neither passed nor owed`);
242
+ // The writer chose the reference when it recorded the draft; a receipt recorded later for another draft does not move it.
243
+ const against = raw.fixes_against;
244
+ const previous = index > 0 && isObject(drafts[index - 1]) ? (drafts[index - 1] as JsonObject).id : undefined;
245
+ const allowed = index === 0 ? against === null : typeof against === 'string' && (against === previous
246
+ || drafts.slice(0, index).some(d => isObject(d) && d.id === against && hasPartsReceipt(project, against)));
247
+ if (!allowed) {
248
+ malformed(`fixes_against must name an earlier draft with a parts-review receipt, or the previous draft (${fixesAgainst(project, index) ?? 'none'})`);
249
+ return;
250
+ }
251
+ const refIndex = drafts.findIndex(d => isObject(d) && d.id === against);
252
+ const reference = refIndex >= 0 && refIndex < index ? reports[refIndex] ?? null : null;
253
+ const fixes = fixedRows(live, report.results, reference);
254
+ if (!Array.isArray(raw.fixes) || JSON.stringify(raw.fixes) !== JSON.stringify(fixes)) malformed(`fixes must be [${fixes.join(', ')}], the live rows this report passes that ${against ?? 'no earlier draft'} did not`);
255
+ summaries.push({ draft: raw.id, fixes, known_regressions: knownIds });
256
+ });
257
+ return summaries;
258
+ }
259
+
260
+ function checkRestarts(project: JsonObject, refusals: Finding[]): void {
261
+ if (project.restarts === undefined) return;
262
+ if (!Array.isArray(project.restarts)) { refusals.push(finding('RESTART_MALFORMED', 'restarts', 'expected a list written by restart')); return; }
263
+ project.restarts.forEach((raw, index) => {
264
+ if (!isObject(raw) || !Number.isFinite(time(raw.at)) || !text(raw.by) || !text(raw.reason) || !hex(raw.previous_record_sha256)) {
265
+ refusals.push(finding('RESTART_MALFORMED', `restarts[${index}]`, 'needs { at, by, reason, previous_record_sha256 }; restore the entry restart wrote'));
266
+ }
267
+ });
268
+ }
269
+
270
+ /** The draft-rig fields for a ledger report, read from the report and the earlier drafts' reports. */
271
+ export function draftLedgerFields(dir: string, project: JsonObject, index: number, reportPath: string, known: [string, string][], now: number): JsonObject {
272
+ const at = `drafts[${index}].ledger_report`;
273
+ const bytes = reportBytes(dir, reportPath, at);
274
+ if (!Buffer.isBuffer(bytes)) refuse(bytes.code, bytes.message);
275
+ const fields: JsonObject = { ledger_report: { path: reportPath, sha256: sha(bytes), spec: FIX_LEDGER_REPORT_SPEC } };
276
+ if (known.length) fields.known_regressions = known.map(([id, reason]) => ({ id, reason }));
277
+ const candidate = structuredClone(project);
278
+ const drafts = asArray(candidate.drafts);
279
+ Object.assign(drafts[index] as JsonObject, fields);
280
+ const against = fixesAgainst(candidate, index);
281
+ fields.fixes_against = against;
282
+ fields.fixes = draftFixes(dir, candidate, index, against, now);
283
+ return fields;
284
+ }
285
+
286
+ export const LEDGER_HELP = `ledger:
287
+ add <id> [<dir>] --by <who> --check <check id> --bar <text> --note <ref> --verbatim-file <file> [--json]
288
+ retire <id> [<dir>] --by <who> --reason-file <file> [--json]
289
+ Append a fix_ledger row { id, note { ref, verbatim }, at, by, check, bar }, or retire one with a reason.
290
+ --note is corrections[<id>] or versions[v<n>].verdicts[<i>]; the words must be quoted from that note.
291
+ Every version inherits every row; draft-rig --ledger-report binds the project's own check report.`;
292
+
293
+ export function ledgerCommand(args: string[], now: number): { stdout: string; exit: number } {
294
+ const action = args[0];
295
+ if (action !== 'add' && action !== 'retire') refuse('LEDGER_USAGE', LEDGER_HELP, USAGE_EXIT);
296
+ const allowed = action === 'add' ? ['--by', '--check', '--bar', '--note', '--verbatim-file'] : ['--by', '--reason-file'];
297
+ const opts: Record<string, string> = {};
298
+ const positional: string[] = [];
299
+ let json = false;
300
+ for (let i = 1; i < args.length; i++) {
301
+ const arg = args[i]!;
302
+ if (arg === '--json') json = true;
303
+ else if (allowed.includes(arg)) {
304
+ const value = args[++i];
305
+ if (value === undefined || value.startsWith('--') || value.trim() === '' || opts[arg] !== undefined) refuse('LEDGER_USAGE', `ledger ${action}: ${arg} needs one value; see ledger help`, USAGE_EXIT);
306
+ opts[arg] = value;
307
+ } else if (arg.startsWith('-')) refuse('LEDGER_USAGE', `ledger ${action}: unsupported ${arg}; see ledger help`, USAGE_EXIT);
308
+ else positional.push(arg);
309
+ }
310
+ const id = positional[0];
311
+ if (!id || !idPattern.test(id) || positional.length > 2 || allowed.some(flag => opts[flag] === undefined)) refuse('LEDGER_USAGE', LEDGER_HELP, USAGE_EXIT);
312
+ const dir = resolve(positional[1] ?? process.cwd());
313
+ const { file, project } = readProject(dir);
314
+ const original = readFileSync(file);
315
+ const before = structuredClone(project);
316
+ const rows = project.fix_ledger === undefined ? [] : project.fix_ledger;
317
+ if (!Array.isArray(rows)) refuse('FIX_LEDGER_MALFORMED', 'fix_ledger: expected a list of rows; restore the rows written by ledger add');
318
+ const stamp = new Date(now).toISOString();
319
+ let index: number;
320
+ let row: JsonObject;
321
+ if (action === 'add') {
322
+ const ref = opts['--note']!;
323
+ if (!NOTE_REF.test(ref)) refuse('LEDGER_USAGE', `fix_ledger: --note ${ref} must be corrections[<id>] or versions[v<n>].verdicts[<i>]`, USAGE_EXIT);
324
+ row = { id, note: { ref, verbatim: readVerbatim(opts['--verbatim-file']!).trim() }, at: stamp, by: opts['--by']!, check: opts['--check']!, bar: opts['--bar']! };
325
+ index = rows.length;
326
+ project.fix_ledger = [...rows, row];
327
+ } else {
328
+ index = rows.findIndex(r => isObject(r) && r.id === id);
329
+ if (index < 0) refuse('LEDGER_ROW_UNKNOWN', `fix_ledger: no row has id ${id}; name a row ledger add wrote`);
330
+ const current = rows[index] as JsonObject;
331
+ if (current.retired !== undefined) refuse('LEDGER_ROW_RETIRED', `fix_ledger[${index}]: ${id} is already retired; the retirement is kept`);
332
+ const reason = readVerbatim(opts['--reason-file']!).trim();
333
+ if (!reason) refuse('LEDGER_USAGE', `fix_ledger[${index}].retired.reason: the reason file is empty; say why the fix no longer applies`, USAGE_EXIT);
334
+ row = { ...current, retired: { at: stamp, by: opts['--by']!, reason } };
335
+ project.fix_ledger = rows.map((r, i) => i === index ? row : r);
336
+ }
337
+ const errors: Finding[] = [];
338
+ ledgerRows(project, errors, now, action === 'add' ? index : undefined);
339
+ const own = errors.find(f => f.path.startsWith(`fix_ledger[${index}]`)) ?? errors.find(f => f.path === 'fix_ledger');
340
+ if (own !== undefined) refuse(own.code, own.message);
341
+ const rejected = newRefusals(dir, before, project, { now })[0];
342
+ if (rejected !== undefined) refuse(rejected.code, rejected.message);
343
+ writeRecord(file, original, project, 'ledger');
344
+ return { stdout: json ? JSON.stringify(row, null, 2) : `ledger: ${action === 'add' ? 'added' : 'retired'} ${id}`, exit: 0 };
345
+ }
346
+
347
+ /** Replace project.json in one rename, refusing if it changed since it was read; a refusal writes nothing. */
348
+ export function writeRecord(file: string, original: Buffer, project: JsonObject, label: 'ledger' | 'restart'): void {
349
+ const tmp = join(dirname(file), `.${label}-${process.pid}-${randomUUID()}.tmp`);
350
+ try {
351
+ writeFileSync(tmp, `${JSON.stringify(project, null, 2)}\n`, { flag: 'wx' });
352
+ if (!readFileSync(file).equals(original)) refuse(label === 'ledger' ? 'LEDGER_SOURCE_CHANGED' : 'RESTART_SOURCE_CHANGED', `project.json: changed while ${label} was validating; retry against the current record`);
353
+ renameSync(tmp, file);
354
+ } finally { rmSync(tmp, { force: true }); }
355
+ }