rig-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,154 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, join, relative, resolve } from 'node:path';
5
+
6
+ import { isObject } from './check.ts';
7
+ import { locateParts } from './parts-sections.ts';
8
+ import { refuse, USAGE_EXIT } from './refusal.ts';
9
+ import type { Json, JsonObject } from './templates.ts';
10
+
11
+ export const SPLIT_CAPTIONS_HELP = `split-captions [<project dir|project.json>] [--config <file>]
12
+ Print generated caption data as JSON. Config defaults to config.json beside the
13
+ project record; relative --config paths are resolved beside that record.`;
14
+
15
+ interface Caption {
16
+ path: string;
17
+ value: Json;
18
+ text: string;
19
+ }
20
+
21
+ interface Input {
22
+ path: string;
23
+ sha256: string;
24
+ }
25
+
26
+ export interface SplitCaptions {
27
+ schema: 'rig-create/split-captions@0';
28
+ inputs: { project: Input; config: Input };
29
+ config: Caption[];
30
+ parts: { name: string; label: string; text: string }[];
31
+ }
32
+
33
+ function readInput(file: string, base: string, kind: string): { input: Input; data: JsonObject } {
34
+ let bytes: Buffer;
35
+ try {
36
+ bytes = readFileSync(file);
37
+ } catch {
38
+ refuse('SPLIT_CAPTIONS_INPUT_UNREADABLE', `${kind} at ${file} cannot be read`);
39
+ }
40
+ let data: Json;
41
+ try {
42
+ data = JSON.parse(bytes.toString('utf8')) as Json;
43
+ } catch {
44
+ refuse('SPLIT_CAPTIONS_INPUT_INVALID', `${kind} at ${file} must be a JSON object`);
45
+ }
46
+ if (!isObject(data)) refuse('SPLIT_CAPTIONS_INPUT_INVALID', `${kind} at ${file} must be a JSON object`);
47
+ return {
48
+ input: { path: relative(base, file).split('\\').join('/'), sha256: createHash('sha256').update(bytes).digest('hex') },
49
+ data,
50
+ };
51
+ }
52
+
53
+ const load = createRequire(import.meta.url);
54
+
55
+ /** Use the project's installed parts contract, including its early assembly door. */
56
+ function validateConfig(config: JsonObject, dir: string): void {
57
+ const parts = locateParts(dir);
58
+ if (parts === null) refuse('PARTS_NOT_INSTALLED', `@rig-c/parts is needed to read the config for ${dir}`);
59
+ try {
60
+ const parser = load(join(parts, 'src', 'config.ts')) as {
61
+ parseConfig: (raw: JsonObject) => unknown;
62
+ parseEarlyConfig: (raw: JsonObject, door: 'assemble') => unknown;
63
+ };
64
+ if (['bones', 'meshes', 'regions', 'motion'].every((key) => key in config)) parser.parseConfig(config);
65
+ else parser.parseEarlyConfig(config, 'assemble');
66
+ } catch (error) {
67
+ refuse('SPLIT_CAPTIONS_CONFIG_INVALID', `${parts}: ${error instanceof Error ? error.message : String(error)}`);
68
+ }
69
+ }
70
+
71
+ /** These parts maps consume exact names, including keys that resemble annotations. */
72
+ const EXACT_NAME_MAPS = new Set([
73
+ '["meshes"]',
74
+ '["regions"]',
75
+ '["limbs"]',
76
+ '["motion","blink","still"]',
77
+ '["turn","depths"]',
78
+ '["depth","full","layers"]',
79
+ '["depth","head","layers"]',
80
+ ]);
81
+
82
+ function annotation(key: string): boolean {
83
+ return key === 'note' || key.endsWith('_note') || (key.startsWith('x-') && key.length > 2);
84
+ }
85
+
86
+ /** Drop annotations before serialising, including those nested in array entries. */
87
+ function captionValue(value: Json, path: string[] = []): Json {
88
+ if (Array.isArray(value)) return value.map((entry, i) => captionValue(entry, [...path, String(i)]));
89
+ if (!isObject(value)) return value;
90
+ const exactNames = EXACT_NAME_MAPS.has(JSON.stringify(path));
91
+ return Object.fromEntries(Object.entries(value)
92
+ .filter(([key]) => exactNames || !annotation(key))
93
+ .map(([key, entry]) => [key, captionValue(entry, [...path, key])]));
94
+ }
95
+
96
+ /** Read both files once. Captions describe declarations, with exact input-byte hashes. */
97
+ export function splitCaptions(projectFile: string, configFile = join(dirname(projectFile), 'config.json')): SplitCaptions {
98
+ const projectPath = resolve(projectFile);
99
+ const dir = dirname(projectPath);
100
+ const project = readInput(projectPath, dir, 'project');
101
+ const config = readInput(resolve(configFile), dir, 'config');
102
+ if (project.data.schema !== 'rig-create/project@0') {
103
+ refuse('SCHEMA_UNKNOWN', `${projectPath} must have schema rig-create/project@0`);
104
+ }
105
+ const split = project.data.split;
106
+ if (!isObject(split) || !Array.isArray(split.parts) || split.parts.length === 0) {
107
+ refuse('SPLIT_CAPTIONS_PARTS_INVALID', 'split.parts must be a non-empty list');
108
+ }
109
+ const seen = new Set<string>();
110
+ const labels = new Set(['physics', 'wind', 'jiggle', 'rigid', 'still']);
111
+ const parts = split.parts.map((part, index) => {
112
+ if (!isObject(part) || typeof part.name !== 'string' || part.name.trim() === '' ||
113
+ typeof part.label !== 'string' || !labels.has(part.label) || seen.has(part.name)) {
114
+ refuse('SPLIT_CAPTIONS_PARTS_INVALID', `split.parts[${index}] needs a unique non-empty name and a physics, wind, jiggle, rigid or still label`);
115
+ }
116
+ seen.add(part.name);
117
+ return { name: part.name, label: part.label, text: `${JSON.stringify(part.name)} = ${JSON.stringify(part.label)}` };
118
+ });
119
+ validateConfig(config.data, dir);
120
+ const values = captionValue(config.data) as JsonObject;
121
+ const captions = Object.entries(values).map(([key, value]) => {
122
+ const path = `config[${JSON.stringify(key)}]`;
123
+ return { path, value, text: `${path} = ${JSON.stringify(value)}` };
124
+ });
125
+ return {
126
+ schema: 'rig-create/split-captions@0',
127
+ inputs: { project: project.input, config: config.input },
128
+ config: captions,
129
+ parts,
130
+ };
131
+ }
132
+
133
+ export function splitCaptionsCommand(args: string[]): string {
134
+ let target: string | undefined;
135
+ let config: string | undefined;
136
+ for (let i = 0; i < args.length; i++) {
137
+ const arg = args[i] as string;
138
+ if (arg === '--config') {
139
+ const next = args[++i];
140
+ if (config !== undefined || next === undefined || next.trim() === '' || next.startsWith('-')) {
141
+ refuse('SPLIT_CAPTIONS_USAGE', '--config needs one file and may appear once', USAGE_EXIT);
142
+ }
143
+ config = next;
144
+ } else if (arg.startsWith('-')) {
145
+ refuse('UNKNOWN_OPTION', `split-captions does not take ${arg}`, USAGE_EXIT);
146
+ } else {
147
+ if (target !== undefined) refuse('SPLIT_CAPTIONS_USAGE', 'split-captions takes one project folder or project.json', USAGE_EXIT);
148
+ target = arg;
149
+ }
150
+ }
151
+ const path = resolve(target ?? process.cwd());
152
+ const project = path.endsWith('.json') ? path : join(path, 'project.json');
153
+ return JSON.stringify(splitCaptions(project, resolve(dirname(project), config ?? 'config.json')), null, 2);
154
+ }
@@ -0,0 +1,379 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { asArray, asString, collectSignoffs, isObject, postNotPassed, type Ctx, type Finding } from './check.ts';
3
+ import { LEGACY_STAGES, PARTS_REVIEW_SINCE, PAUSED_STAGE, SKIPPED_WHEN_NO_POST, STAGES, hasStageOrder, stageIndex, stageOfStep, type StageSpec } from './stages.ts';
4
+ import { collectAmendments } from './amend.ts';
5
+ import { collectReopens, liveSignoff } from './reopen.ts';
6
+ import type { Json, JsonObject } from './templates.ts';
7
+
8
+ /** Dates the authorizing stage-level signature, including a signature earlier on landing day. */
9
+ export const STAGE_PROGRESS_SINCE = '2026-11-08T00:00:00Z';
10
+
11
+ /** Dates a history link break by the earliest recorded event the unrecorded move must follow. */
12
+ export const PROGRESS_HISTORY_SINCE = '2026-11-08T00:00:00Z';
13
+
14
+ /** Select the latest signature at the furthest still-live closing stage. */
15
+ export function furthestStageSignoff(signoffs: Ctx['signoffs']): Ctx['signoffs'][number] | undefined {
16
+ return signoffs.filter(s => s.checkpoint === null && STAGES.some(stage => stage.id === s.stage && stage.closed_by === 'signoff'))
17
+ .sort((a, b) => stageIndex(b.stage) - stageIndex(a.stage)
18
+ || (Number.isFinite(b.at) ? b.at : -Infinity) - (Number.isFinite(a.at) ? a.at : -Infinity) || b.index - a.index)[0];
19
+ }
20
+
21
+ /** Stable approval identity survives insertion and reordering of signoffs[]. */
22
+ export function signoffIdentity(record: JsonObject): JsonObject {
23
+ return { stage: record.stage!, checkpoint: record.checkpoint ?? null, at: record.at! };
24
+ }
25
+
26
+ export function isSignoffIdentity(value: Json | undefined): value is JsonObject {
27
+ return isObject(value) && Object.keys(value).length === 3 && typeof value.stage === 'string'
28
+ && (value.checkpoint === null || typeof value.checkpoint === 'string')
29
+ && typeof value.at === 'string' && Number.isFinite(Date.parse(value.at));
30
+ }
31
+
32
+ export function matchesSignoff(identity: Json | undefined, record: JsonObject): boolean {
33
+ return isSignoffIdentity(identity) && identity.stage === record.stage
34
+ && identity.checkpoint === (record.checkpoint ?? null) && identity.at === record.at;
35
+ }
36
+
37
+ function resolveSignoff(project: JsonObject, identity: Json | undefined): JsonObject | undefined {
38
+ const matches = asArray(project.signoffs).filter((raw): raw is JsonObject => isObject(raw) && matchesSignoff(identity, raw));
39
+ return matches.length === 1 ? matches[0] : undefined;
40
+ }
41
+
42
+ /** Re-signing an earlier plan does not revoke later live approvals. Reopen/amend owns revocation. */
43
+ export function stageWindow(ctx: Ctx): { minimum: string; maximum: string; signoff: string | null; at: number } {
44
+ const order = (hasStageOrder(ctx.project) ? STAGES : LEGACY_STAGES).filter(s =>
45
+ !(ctx.predatesPartsReview !== null && ['parts', 'parts-review'].includes(s.id))
46
+ && !(postNotPassed(ctx) && SKIPPED_WHEN_NO_POST.includes(s.id)));
47
+ const latest = furthestStageSignoff(ctx.signoffs.filter(s => order.some(stage => stage.id === s.stage)));
48
+ const start = latest === undefined ? 0 : Math.min(order.findIndex(s => s.id === latest.stage) + 1, order.length - 1);
49
+ let end = start;
50
+ while (order[end]?.closed_by === 'bar') {
51
+ const spec = order[end]!;
52
+ const steps = ctx.steps.filter(s => ctx.predatesPartsReview !== null
53
+ ? s.phase === spec.phase : stageOfStep(s.phase, s.id) === spec.id);
54
+ if (steps.length === 0 || steps.some(s => s.record.status !== 'done')) break;
55
+ end++;
56
+ }
57
+ return { minimum: order[start]!.id, maximum: order[Math.min(end, order.length - 1)]!.id,
58
+ signoff: latest === undefined ? null : `signoffs[${latest.index}]`, at: latest?.at ?? NaN };
59
+ }
60
+
61
+ export function checkStageProgress(ctx: Ctx, forwardWrite = false): void {
62
+ if (ctx.stage === PAUSED_STAGE) return;
63
+ // Amend retains the stored stage while its approvals lapse, so only the recorded-move check applies (PROJECT.md).
64
+ const amended = ctx.effectiveStage !== ctx.stage;
65
+ const current = stageIndex(ctx.stage);
66
+ const frontier = furthestStageSignoff(ctx.signoffs);
67
+ const unreadable = ctx.signoffs.find(s => s.stage === frontier?.stage && s.checkpoint === null && !Number.isFinite(s.at));
68
+ if (!amended && unreadable !== undefined) {
69
+ ctx.refusals.push({ code: 'STAGE_SIGNOFF_DATE_INVALID', path: `signoffs[${unreadable.index}].at`, message: `signoffs[${unreadable.index}].at: frontier approval needs a readable date; restore its original date before advancing` });
70
+ return;
71
+ }
72
+ const since = Date.parse(STAGE_PROGRESS_SINCE);
73
+ const moves = [
74
+ ...asArray(ctx.project.advances).filter(isObject).map((record, index) => ({ record, advance: true, index })),
75
+ ...asArray(ctx.project.reopens).filter(isObject).map((record, index) => ({ record, advance: false, index })),
76
+ ].filter(move => move.record.from_stage !== move.record.stage && Number.isFinite(Date.parse(String(move.record.at))))
77
+ .sort((a, b) => Date.parse(String(b.record.at)) - Date.parse(String(a.record.at)) || Number(a.advance) - Number(b.advance) || b.index - a.index);
78
+ const latestMove = moves[0];
79
+ if (latestMove === undefined ? ctx.stage !== 'interview' : latestMove.record.stage !== ctx.stage) {
80
+ // The unrecorded move is no older than the latest recorded move and the stage's arrival authority:
81
+ // the latest stage-level signature at the furthest stage before the current one. It is live, or on an
82
+ // amended project retained, since amend voids the approvals that dated the arrival.
83
+ const earlier = (amended ? collectSignoffs(ctx.project, []) : ctx.signoffs).filter(s => s.checkpoint === null && stageIndex(s.stage) >= 0 && stageIndex(s.stage) < current);
84
+ const arrivalStage = Math.max(...earlier.map(s => stageIndex(s.stage)));
85
+ const arrival = earlier.filter(s => stageIndex(s.stage) === arrivalStage);
86
+ const dates = [
87
+ ...(latestMove === undefined ? [] : [Date.parse(String(latestMove.record.at))]),
88
+ ...(arrival.length === 0 ? [] : [arrival.every(s => Number.isFinite(s.at)) ? Math.max(...arrival.map(s => s.at)) : NaN]),
89
+ ];
90
+ const movedAt = dates.length === 0 ? NaN : Math.max(...dates);
91
+ const finding = { code: 'STAGE_SKIPPED', path: 'stage', message: `stage: ${ctx.stage} differs from its latest recorded move (${latestMove === undefined ? 'none' : String(latestMove.record.stage)}); use advance for the next authorized stage or reopen the unfinished stage` };
92
+ (forwardWrite || !(movedAt < since) ? ctx.refusals : ctx.warnings).push(finding);
93
+ }
94
+ if (amended) return;
95
+ const window = stageWindow(ctx);
96
+ const code = current < stageIndex(window.minimum) ? 'STAGE_LAGGING'
97
+ : current > stageIndex(window.maximum) ? 'STAGE_SKIPPED' : null;
98
+ if (code === null) return;
99
+ const finding = { code, path: 'stage', message: `stage: ${ctx.stage} is outside ${window.minimum} through ${window.maximum} authorized by ${window.signoff ?? 'the unsigned interview'}; use advance for the next authorized stage or reopen the unfinished stage` };
100
+ (forwardWrite || window.at >= since || !Number.isFinite(window.at) ? ctx.refusals : ctx.warnings).push(finding);
101
+ }
102
+
103
+ /** The stage order an advance entry's `order` names. */
104
+ export function advanceOrder(order: Json | undefined): readonly StageSpec[] {
105
+ return order === 'pre-parts' ? LEGACY_STAGES.filter(s => !['parts', 'parts-review'].includes(s.id))
106
+ : order === 'legacy' ? LEGACY_STAGES : STAGES;
107
+ }
108
+
109
+ /** The signed stage whose stage-level signoff authorizes a move from `from`: itself, or the last signed stage before a bar stage. */
110
+ export function moveOwner(order: readonly StageSpec[], from: Json | undefined): string | undefined {
111
+ const index = order.findIndex(s => s.id === from);
112
+ return index < 0 ? undefined : order.slice(0, index + 1).filter(s => s.closed_by === 'signoff').at(-1)?.id;
113
+ }
114
+
115
+ /** Advances are history, separate from reopen cutoffs. Their reference must witness the recorded move. */
116
+ export function checkAdvances(ctx: Ctx): void {
117
+ const push = (path: string, why: string): void => {
118
+ ctx.refusals.push({ code: 'ADVANCE_MALFORMED', path, message: `${path}: ${why}; preserve the original forward move and its authorizing signoff` });
119
+ };
120
+ if (ctx.project.advances !== undefined && !Array.isArray(ctx.project.advances)) push('advances', 'must be a list');
121
+ for (const [index, entry] of asArray(ctx.project.advances).entries()) {
122
+ const path = `advances[${index}]`;
123
+ // An advance records the signoff it rests on. Entries written before that carry the mover's by and verbatim, both or neither.
124
+ if (!isObject(entry) || ['stage', 'from_stage', 'at'].some(k => asString(entry[k]) === null)
125
+ || ('by' in entry || 'verbatim' in entry) && ['by', 'verbatim'].some(k => asString(entry[k]) === null)
126
+ || !Number.isFinite(Date.parse(String(entry.at))) || (entry.source !== undefined && typeof entry.source !== 'string')
127
+ || typeof entry.post_not_passed !== 'boolean'
128
+ || !isSignoffIdentity(entry.signoff) || !Array.isArray(entry.signoffs_before)
129
+ || !entry.signoffs_before.every(isSignoffIdentity)
130
+ || !Array.isArray(entry.steps_before)) { push(path, 'needs reopen-shaped fields and stable signoff identities'); continue; }
131
+ const order = advanceOrder(entry.order);
132
+ const from = order.findIndex(s => s.id === entry.from_stage);
133
+ const target = order.findIndex(s => s.id === entry.stage);
134
+ const noPost = entry.from_stage === 'post-interview' && entry.stage === 'accepted' && entry.post_not_passed === true;
135
+ const at = Date.parse(String(entry.at));
136
+ const cutoffs = collectReopens(ctx.project, []).filter(r => r.at <= at);
137
+ const authority = furthestStageSignoff(collectSignoffs(ctx.project, []).filter(s => asArray(entry.signoffs_before).some(identity => matchesSignoff(identity, s.record)) && s.at <= at && liveSignoff(cutoffs, s.stage, s.at)));
138
+ const firstPlan = collectSignoffs(ctx.project, []).filter(s => s.stage === 'plan-approved' && s.checkpoint === null).map(s => s.at);
139
+ const historicalOrder = entry.order !== 'pre-parts' || Math.min(...firstPlan) < Date.parse(PARTS_REVIEW_SINCE);
140
+ const signature = resolveSignoff(ctx.project, entry.signoff);
141
+ const owner = moveOwner(order, entry.from_stage);
142
+ if (entry.steps_before.some(s => !isObject(s) || typeof s.id !== 'string' || !['rig', 'post'].includes(String(s.phase)) || (s.status !== undefined && !STEP_STATUSES.includes(s.status as typeof STEP_STATUSES[number])))) push(path, 'steps_before must retain step ids, phases and original statuses');
143
+ if (typeof entry.by === 'string' && (entry.by.trim() === '' || entry.by.toLowerCase() === 'agent') || from < 0 || target <= from
144
+ || !['legacy', 'split', 'pre-parts'].includes(String(entry.order)) || !historicalOrder
145
+ || (target !== from + 1 && !noPost) || !isObject(signature) || signature.checkpoint !== null
146
+ || typeof signature.by !== 'string' || signature.by.toLowerCase() === 'agent'
147
+ || !(Date.parse(String(signature.at)) <= Date.parse(String(entry.at)))
148
+ || signature.stage !== owner || authority === undefined || !matchesSignoff(entry.signoff, authority.record)) push(path, 'invalid human move or authorizing signature');
149
+ }
150
+ }
151
+
152
+ /**
153
+ * pauses[] is the pause and resume writers' own history, kept apart from advances[] and reopens[], so the
154
+ * move linkage and the latest-move rule read exactly what they read before it existed: a resume back to the
155
+ * paused-from stage leaves the latest recorded move matching stage, and a resume anywhere else is STAGE_SKIPPED.
156
+ * Only the shape is checked here; the field did not exist before, so no earlier record is refused.
157
+ */
158
+ export function checkPauses(ctx: Ctx): void {
159
+ const push = (path: string, why: string): void => {
160
+ ctx.refusals.push({ code: 'PAUSE_MALFORMED', path, message: `${path}: ${why}; preserve the history and record pauses through pause and resume` });
161
+ };
162
+ if (ctx.project.pauses === undefined) return;
163
+ if (!Array.isArray(ctx.project.pauses)) { push('pauses', 'must be a list'); return; }
164
+ let previous = -Infinity;
165
+ for (const [index, entry] of ctx.project.pauses.entries()) {
166
+ const path = `pauses[${index}]`;
167
+ if (!isObject(entry) || ['stage', 'from_stage', 'by', 'at', 'verbatim'].some(k => typeof entry[k] !== 'string')
168
+ || !Number.isFinite(Date.parse(String(entry.at))) || (entry.source !== undefined && typeof entry.source !== 'string')
169
+ || (entry.by as string).trim() === '' || (entry.by as string).toLowerCase() === 'agent') {
170
+ push(path, 'needs stage, from_stage, a human by, a readable at, verbatim and an optional text source'); continue;
171
+ }
172
+ const at = Date.parse(String(entry.at));
173
+ if (at < previous) push(path, 'is dated before the entry before it');
174
+ previous = Math.max(previous, at);
175
+ const from = String(entry.from_stage);
176
+ const signature = resolveSignoff(ctx.project, entry.signoff);
177
+ if (entry.stage === PAUSED_STAGE) {
178
+ if (stageIndex(from) < 0 || from === 'accepted') push(path, `pauses from ${from}, which is not a stage in the order before accepted`);
179
+ else if (!isObject(signature) || signature.stage !== PAUSED_STAGE || signature.checkpoint !== null
180
+ || typeof signature.by !== 'string' || signature.by.toLowerCase() === 'agent' || !(Date.parse(String(signature.at)) <= at)) {
181
+ push(path, 'signoff must be the { stage, checkpoint, at } identity of a human paused sign-off dated at or before it');
182
+ }
183
+ } else if (from !== PAUSED_STAGE || stageIndex(String(entry.stage)) < 0) push(path, 'must enter paused from a stage, or leave paused for a stage in the order');
184
+ }
185
+ }
186
+
187
+ /** Snapshot bars remain historical facts even when the current plan is reopened or replaced. */
188
+ export function checkAdvanceBars(ctx: Ctx): void {
189
+ for (const [index, entry] of asArray(ctx.project.advances).entries()) {
190
+ if (!isObject(entry) || !Array.isArray(entry.steps_before)) continue;
191
+ const order = entry.order === 'split' ? STAGES : LEGACY_STAGES;
192
+ const bar = order.find(s => s.id === entry.from_stage);
193
+ if (bar?.closed_by !== 'bar') continue;
194
+ const unfinished = entry.steps_before.some(raw => isObject(raw)
195
+ && (entry.order === 'pre-parts' ? raw.phase === bar.phase
196
+ : (raw.phase === 'rig' || raw.phase === 'post') && stageOfStep(raw.phase, String(raw.id)) === bar.id)
197
+ && raw.status !== 'done');
198
+ if (unfinished) ctx.refusals.push({ code: 'ADVANCE_MALFORMED', path: `advances[${index}].steps_before`,
199
+ message: `advances[${index}].steps_before: the execution stage had unfinished steps at the move; restore its original completed bar and use advance` });
200
+ }
201
+ }
202
+
203
+ export const STEP_STATUSES = ['pending', 'in-progress', 'at-checkpoint', 'done'] as const;
204
+
205
+ /** Shared transition validation for the public writer and retained status history. */
206
+ export function statusTransitionFinding(entry: JsonObject, path: string): Finding | null {
207
+ if ('note' in entry || 'note_sha256' in entry) {
208
+ if (typeof entry.note !== 'string' || Buffer.byteLength(entry.note, 'utf8') === 0
209
+ || Buffer.byteLength(entry.note, 'utf8') > 1000 || /[\p{Cc}\p{Cs}]/u.test(entry.note.replace(/\n/g, ''))
210
+ || typeof entry.note_sha256 !== 'string' || entry.note_sha256 !== createHash('sha256').update(entry.note, 'utf8').digest('hex')) {
211
+ return { code: 'PROGRESS_OPTION_INVALID', path, message: `${path}: retain both note and its matching note_sha256; record a non-empty UTF-8 note of at most 1,000 bytes with no controls except newline through step-status --note-file` };
212
+ }
213
+ }
214
+ const from = STEP_STATUSES.indexOf(entry.from_status as typeof STEP_STATUSES[number]);
215
+ const next = STEP_STATUSES.indexOf(entry.status as typeof STEP_STATUSES[number]);
216
+ if (from < 0 || next !== from + 1 || typeof entry.checkpoint_required !== 'boolean' || typeof entry.by !== 'string' || entry.by.trim() === ''
217
+ || !Number.isFinite(Date.parse(String(entry.at))) || (entry.signoff !== null && !isSignoffIdentity(entry.signoff))) {
218
+ return { code: 'STEP_STATUS_TRANSITION', path, message: `${path}: record by, at, boolean checkpoint_required and one forward pending → in-progress → at-checkpoint → done transition with a checkpoint signoff identity or null; use step-status` };
219
+ }
220
+ return null;
221
+ }
222
+
223
+ export function checkStepStatusHistory(ctx: Ctx): void {
224
+ for (const step of ctx.steps) {
225
+ if (step.record.status_history === undefined) continue;
226
+ const path = `${step.path}.status_history`;
227
+ if (!Array.isArray(step.record.status_history)) {
228
+ ctx.refusals.push({ code: 'STEP_STATUS_TRANSITION', path, message: `${path}: must be a list; retain step-status writer history` });
229
+ continue;
230
+ }
231
+ for (const [index, raw] of step.record.status_history.entries()) {
232
+ const entryPath = `${path}[${index}]`;
233
+ const finding = statusTransitionFinding(isObject(raw) ? raw : {}, entryPath);
234
+ if (finding !== null) { ctx.refusals.push(finding); continue; }
235
+ const entry = raw as JsonObject;
236
+ if (entry.status === 'done' && entry.checkpoint_required === true && entry.signoff === null) {
237
+ ctx.refusals.push({ code: 'STEP_STATUS_SIGNOFF', path: entryPath, message: `${entryPath}: checkpointed done needs its authorizing signoff identity; record the human checkpoint with step-status` });
238
+ }
239
+ if (entry.signoff === null) continue;
240
+ const signature = resolveSignoff(ctx.project, entry.signoff);
241
+ const allowed = [stageOfStep(step.phase, step.id)];
242
+ const legacyAtEvent = collectSignoffs(ctx.project, []).some(s => s.stage === 'plan-approved' && s.checkpoint === null
243
+ && s.at < Date.parse(PARTS_REVIEW_SINCE) && s.at <= Date.parse(String(entry.at)));
244
+ if (legacyAtEvent) allowed.push(stageOfStep(step.phase, ''));
245
+ if (!isObject(signature) || signature.checkpoint !== step.id || !allowed.includes(String(signature.stage))
246
+ || typeof signature.by !== 'string' || signature.by.toLowerCase() === 'agent'
247
+ || !(Date.parse(String(signature.at)) <= Date.parse(String(entry.at)))) {
248
+ ctx.refusals.push({ code: 'STEP_STATUS_SIGNOFF', path: entryPath, message: `${entryPath}: checkpoint reference does not authorize this step at this time; retain the matching human checkpoint signature` });
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ const eventTime = (value: Json | undefined): number => Date.parse(String(value));
255
+
256
+ /**
257
+ * Advances and reopens must chain from the initial stage, and each step's status_history must chain from
258
+ * pending through reopen and amendment resets to its current status. A reopen's steps_before must hold the
259
+ * chained status at the reopen. A break is dated by the latest recorded event the unrecorded move must follow,
260
+ * so a later writer entry does not turn an older hand move into a refusal.
261
+ */
262
+ export function checkProgressLinkage(ctx: Ctx): void {
263
+ const since = Date.parse(PROGRESS_HISTORY_SINCE);
264
+ const remedies: Record<string, string> = {
265
+ STAGE_MOVE_UNLINKED: 'preserve the history and record the move through advance or reopen',
266
+ STEP_STATUS_UNLINKED: 'preserve the history and record the move through step-status',
267
+ // A reopen copies the current status, so a hand-set status must be restored before the stage is reopened.
268
+ REOPEN_STEP_UNLINKED: 'restore the status the reopen recorded; before reopening, return a hand-set status to its last recorded one and record moves through step-status',
269
+ };
270
+ const push = (code: string, path: string, why: string, after: number): void => {
271
+ (after >= since ? ctx.refusals : ctx.warnings).push({ code, path, message: `${path}: ${why}; ${remedies[code]!}` });
272
+ };
273
+ const signatures = collectSignoffs(ctx.project, []).filter(s => s.checkpoint === null && Number.isFinite(s.at) && stageIndex(s.stage) >= 0);
274
+ type Move = { record: JsonObject; path: string; advance: boolean; index: number };
275
+ const moves = [
276
+ ...asArray(ctx.project.advances).map((record, index) => ({ record, path: `advances[${index}]`, advance: true, index })),
277
+ ...asArray(ctx.project.reopens).map((record, index) => ({ record, path: `reopens[${index}]`, advance: false, index })),
278
+ ].filter((move): move is Move => isObject(move.record) && typeof move.record.stage === 'string'
279
+ && typeof move.record.from_stage === 'string' && Number.isFinite(eventTime(move.record.at)));
280
+
281
+ let latestAdvance = -Infinity;
282
+ for (const move of moves.filter(m => m.advance)) {
283
+ const at = eventTime(move.record.at);
284
+ if (at < latestAdvance) push('STAGE_MOVE_UNLINKED', move.path, 'is dated before an earlier advances[] entry', latestAdvance);
285
+ latestAdvance = Math.max(latestAdvance, at);
286
+ }
287
+ // Time order; on a tie, prefer the move that links, then advances before reopens as checkStageProgress does.
288
+ const queue = [...moves].sort((a, b) => eventTime(a.record.at) - eventTime(b.record.at) || Number(b.advance) - Number(a.advance) || a.index - b.index);
289
+ let stage = 'interview';
290
+ let previous = -Infinity;
291
+ const links = (move: Move): boolean => move.record.from_stage === stage || move.record.from_stage === PAUSED_STAGE;
292
+ while (queue.length > 0) {
293
+ const at = eventTime(queue[0]!.record.at);
294
+ const tied = queue.filter(m => eventTime(m.record.at) === at);
295
+ const move = tied.find(links) ?? tied[0]!;
296
+ queue.splice(queue.indexOf(move), 1);
297
+ if (!links(move)) {
298
+ const from = stageIndex(String(move.record.from_stage));
299
+ const before = signatures.filter(s => stageIndex(s.stage) < from && s.at <= at);
300
+ const furthest = Math.max(...before.map(s => stageIndex(s.stage)));
301
+ const arrival = Math.max(-Infinity, ...before.filter(s => stageIndex(s.stage) === furthest).map(s => s.at));
302
+ push('STAGE_MOVE_UNLINKED', move.path, `from_stage ${String(move.record.from_stage)} does not follow ${stage}, the stage its previous recorded move left`, Math.max(previous, arrival));
303
+ }
304
+ stage = String(move.record.stage);
305
+ previous = at;
306
+ }
307
+
308
+ const legacy = collectSignoffs(ctx.project, []).some(s => s.stage === 'plan-approved' && s.checkpoint === null && s.at < Date.parse(PARTS_REVIEW_SINCE));
309
+ const amendments = collectAmendments(ctx.project, []);
310
+ for (const step of ctx.steps) {
311
+ const history = step.record.status_history;
312
+ if (!Array.isArray(history) || history.length === 0
313
+ || history.some(raw => statusTransitionFinding(isObject(raw) ? raw : {}, '') !== null)) continue;
314
+ const executes = [stageOfStep(step.phase, step.id), ...(legacy ? [stageOfStep(step.phase, '')] : [])];
315
+ const reopenResets = asArray(ctx.project.reopens).map((r, index) => ({ r, index })).filter(({ r }) => isObject(r)
316
+ && (Array.isArray(r.steps_before) ? r.steps_before.some(s => isObject(s) && s.id === step.id)
317
+ : stageIndex(stageOfStep(step.phase, step.id)) >= stageIndex(String(r.stage))))
318
+ .map(({ r, index }) => ({ index, at: eventTime((r as JsonObject).at) }));
319
+ const amendResets = amendments.filter(a => stageIndex(stageOfStep(step.phase, step.id)) >= stageIndex(a.stage)).map(a => a.at);
320
+ const resetsExcept = (reopen: number): number[] =>
321
+ [...reopenResets.filter(r => r.index !== reopen).map(r => r.at), ...amendResets].filter(Number.isFinite);
322
+ const resets = resetsExcept(-1);
323
+ const arrivals = moves.filter(m => m.advance && executes.includes(String(m.record.stage))).map(m => eventTime(m.record.at));
324
+ const after = (until: number, previous: number, events = resets): number => Math.max(previous, ...[...events, ...arrivals].filter(t => t <= until));
325
+ // A reset strictly between two entries forces pending; one tied with an entry may fall on either side.
326
+ const reset = (possible: Set<string>, from: number, to: number, events = resets): Set<string> => {
327
+ const between = events.filter(r => r >= from && r <= to);
328
+ if (between.some(r => r > from && r < to)) return new Set(['pending']);
329
+ return between.length > 0 ? new Set([...possible, 'pending']) : possible;
330
+ };
331
+ let possible = new Set(['pending']);
332
+ let previous = -Infinity;
333
+ for (const [index, raw] of history.entries()) {
334
+ const entry = raw as JsonObject;
335
+ const path = `${step.path}.status_history[${index}]`;
336
+ const at = eventTime(entry.at);
337
+ if (at < previous) push('STEP_STATUS_UNLINKED', path, `is dated before status_history[${index - 1}]`, previous);
338
+ else {
339
+ possible = reset(possible, previous, at);
340
+ if (!possible.has(String(entry.from_status))) push('STEP_STATUS_UNLINKED', path, `from_status ${String(entry.from_status)} does not follow ${[...possible].join(' or ')}, the status before it`, after(at, previous));
341
+ }
342
+ possible = new Set([String(entry.status)]);
343
+ previous = Math.max(previous, at);
344
+ }
345
+ possible = reset(possible, previous, Infinity);
346
+ const current = step.record.status === undefined ? 'pending' : step.record.status;
347
+ if (typeof current === 'string' && STEP_STATUSES.includes(current as typeof STEP_STATUSES[number]) && !possible.has(current)) {
348
+ push('STEP_STATUS_UNLINKED', `${step.path}.status`, `${current} is not the status its last status_history entry recorded (${[...possible].join(' or ')})`, after(Infinity, previous));
349
+ }
350
+ // A reordered history has no single status at a given time; its break is already reported above.
351
+ if (history.some((raw, index) => index > 0 && eventTime((raw as JsonObject).at) < eventTime((history[index - 1] as JsonObject).at))) continue;
352
+ for (const { index: reopen, at } of reopenResets) {
353
+ const steps = (asArray(ctx.project.reopens)[reopen] as JsonObject).steps_before;
354
+ if (!Number.isFinite(at) || !Array.isArray(steps)) continue;
355
+ // The reopen writer copies the current, possibly hand-set status, so the reopen itself is not a dating event.
356
+ const events = resetsExcept(reopen);
357
+ for (const [index, raw] of steps.entries()) {
358
+ if (!isObject(raw) || raw.id !== step.id) continue;
359
+ const claimed = raw.status === undefined ? 'pending' : raw.status;
360
+ if (typeof claimed !== 'string' || !STEP_STATUSES.includes(claimed as typeof STEP_STATUSES[number])) continue;
361
+ // A step with no history before the reopen had no recorded chain then, as #186 leaves a step with no history unjudged.
362
+ // Judging it once a later redo adds history would block that redo with no writer remedy.
363
+ const earlier = (history as JsonObject[]).filter(entry => eventTime(entry.at) < at).at(-1);
364
+ if (earlier === undefined) continue;
365
+ // The chain at the reopen is the last earlier entry's status, then any reset after it.
366
+ const last = eventTime(earlier.at);
367
+ const chained = reset(new Set([String(earlier.status)]), last, at, events);
368
+ // An entry tied in time with the reopen may fall on either side of it.
369
+ const tied = (history as JsonObject[]).filter(entry => eventTime(entry.at) === at);
370
+ for (const entry of tied) chained.add(String(entry.status));
371
+ if (!chained.has(claimed)) {
372
+ push('REOPEN_STEP_UNLINKED', `reopens[${reopen}].steps_before[${index}]`,
373
+ `${step.id} status ${claimed} is not the status its history held at the reopen (${[...chained].join(' or ')})`,
374
+ after(at, last, events));
375
+ }
376
+ }
377
+ }
378
+ }
379
+ }