rig-create 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +5 -5
  2. package/docs/AGENT-REFERENCE.md +62 -0
  3. package/docs/CHECKS.md +45 -9
  4. package/docs/INTERVIEW.md +19 -7
  5. package/package.json +2 -1
  6. package/src/agent-guide.ts +7 -4
  7. package/src/check.ts +7 -4
  8. package/src/choice-writers.ts +3 -14
  9. package/src/cli.ts +1 -0
  10. package/src/feedback.ts +3 -10
  11. package/src/fix-ledger.ts +1 -1
  12. package/src/job-acts.ts +4 -0
  13. package/src/job-host-state.ts +154 -0
  14. package/src/job-host.ts +37 -14
  15. package/src/job-outbox.ts +5 -3
  16. package/src/job-publication.ts +17 -13
  17. package/src/job-runtime.ts +17 -4
  18. package/src/next.ts +21 -13
  19. package/src/parts-review.ts +4 -11
  20. package/src/reopen.ts +72 -9
  21. package/src/review-act.ts +49 -14
  22. package/src/review-package.ts +3 -1
  23. package/src/rig-version.ts +219 -8
  24. package/src/scene-writers.ts +5 -3
  25. package/src/scene.ts +14 -4
  26. package/src/signable.ts +25 -3
  27. package/src/signoff-checkpoint-act.ts +2 -16
  28. package/src/signoff.ts +3 -15
  29. package/src/waypoints.ts +20 -9
  30. package/templates/agent-guide.md +2 -24
  31. package/templates/collection.json +10 -2
  32. package/templates/example-chain/AGENT-REFERENCE.md +62 -0
  33. package/templates/example-chain/AGENTS.md +4 -26
  34. package/templates/example-chain/README.md +1 -1
  35. package/templates/example-chain/chain.json +385 -319
  36. package/templates/example-chain/checkpoints/0a-interview-options/AGENT-REFERENCE.md +62 -0
  37. package/templates/example-chain/checkpoints/0a-interview-options/AGENTS.md +4 -26
  38. package/templates/example-chain/checkpoints/0a-interview-options/project.json +22 -5
  39. package/templates/example-chain/checkpoints/0a-interview-options.json +7 -3
  40. package/templates/example-chain/checkpoints/0b-plan-pending/AGENT-REFERENCE.md +62 -0
  41. package/templates/example-chain/checkpoints/0b-plan-pending/AGENTS.md +4 -26
  42. package/templates/example-chain/checkpoints/0b-plan-pending/project.json +22 -5
  43. package/templates/example-chain/checkpoints/0b-plan-pending.json +7 -3
  44. package/templates/example-chain/checkpoints/1-parts-verdict/AGENT-REFERENCE.md +62 -0
  45. package/templates/example-chain/checkpoints/1-parts-verdict/AGENTS.md +4 -26
  46. package/templates/example-chain/checkpoints/1-parts-verdict/derived/drafts/chain/out/check/build/run.json +2 -2
  47. package/templates/example-chain/checkpoints/1-parts-verdict/derived/drafts/chain/out/run.json +2 -2
  48. package/templates/example-chain/checkpoints/1-parts-verdict/project.json +24 -7
  49. package/templates/example-chain/checkpoints/1-parts-verdict.json +9 -5
  50. package/templates/example-chain/checkpoints/2-rigging-answers/AGENT-REFERENCE.md +62 -0
  51. package/templates/example-chain/checkpoints/2-rigging-answers/AGENTS.md +4 -26
  52. package/templates/example-chain/checkpoints/2-rigging-answers/derived/drafts/chain/out/check/build/run.json +2 -2
  53. package/templates/example-chain/checkpoints/2-rigging-answers/derived/drafts/chain/out/run.json +2 -2
  54. package/templates/example-chain/checkpoints/2-rigging-answers/project.json +24 -7
  55. package/templates/example-chain/checkpoints/2-rigging-answers.json +21 -5
  56. package/templates/example-chain/checkpoints/3-rig-review/AGENT-REFERENCE.md +62 -0
  57. package/templates/example-chain/checkpoints/3-rig-review/AGENTS.md +4 -26
  58. package/templates/example-chain/checkpoints/3-rig-review/derived/drafts/chain/out/check/build/run.json +2 -2
  59. package/templates/example-chain/checkpoints/3-rig-review/derived/drafts/chain/out/run.json +2 -2
  60. package/templates/example-chain/checkpoints/3-rig-review/derived/review/v1/out/check/build/run.json +2 -2
  61. package/templates/example-chain/checkpoints/3-rig-review/derived/review/v1/out/rig/run.json +2 -2
  62. package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/motion.json +1 -1
  63. package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/turn.json +108 -0
  64. package/templates/example-chain/checkpoints/3-rig-review/outputs/v1/out/check/build/run.json +2 -2
  65. package/templates/example-chain/checkpoints/3-rig-review/outputs/v1/out/rig/run.json +2 -2
  66. package/templates/example-chain/checkpoints/3-rig-review/project.json +77 -28
  67. package/templates/example-chain/checkpoints/3-rig-review.json +19 -11
  68. package/templates/example-chain/checkpoints/4-direction-post/AGENT-REFERENCE.md +62 -0
  69. package/templates/example-chain/checkpoints/4-direction-post/AGENTS.md +4 -26
  70. package/templates/example-chain/checkpoints/4-direction-post/derived/drafts/chain/out/check/build/run.json +2 -2
  71. package/templates/example-chain/checkpoints/4-direction-post/derived/drafts/chain/out/run.json +2 -2
  72. package/templates/example-chain/checkpoints/4-direction-post/derived/review/v1/out/check/build/run.json +2 -2
  73. package/templates/example-chain/checkpoints/4-direction-post/derived/review/v1/out/rig/run.json +2 -2
  74. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/camera_depth.json +2 -2
  75. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/motion.json +1 -1
  76. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/post_composition.json +2 -2
  77. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/post_timing.json +2 -2
  78. package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/turn.json +108 -0
  79. package/templates/example-chain/checkpoints/4-direction-post/outputs/v1/out/check/build/run.json +2 -2
  80. package/templates/example-chain/checkpoints/4-direction-post/outputs/v1/out/rig/run.json +2 -2
  81. package/templates/example-chain/checkpoints/4-direction-post/project.json +106 -57
  82. package/templates/example-chain/checkpoints/4-direction-post.json +22 -14
  83. package/templates/example-chain/derived/drafts/chain/out/check/build/run.json +2 -2
  84. package/templates/example-chain/derived/drafts/chain/out/run.json +2 -2
  85. package/templates/example-chain/derived/review/v1/out/check/build/run.json +2 -2
  86. package/templates/example-chain/derived/review/v1/out/rig/run.json +2 -2
  87. package/templates/example-chain/evidence/captions.json +1 -1
  88. package/templates/example-chain/evidence/direction/v2.json +1 -1
  89. package/templates/example-chain/evidence/direction-input.json +1 -1
  90. package/templates/example-chain/evidence/interview/camera_depth.json +2 -2
  91. package/templates/example-chain/evidence/interview/motion.json +1 -1
  92. package/templates/example-chain/evidence/interview/post_composition.json +2 -2
  93. package/templates/example-chain/evidence/interview/post_timing.json +2 -2
  94. package/templates/example-chain/evidence/interview/turn.json +108 -0
  95. package/templates/example-chain/evidence/scene/s1.json +2 -2
  96. package/templates/example-chain/outputs/v1/out/check/build/run.json +2 -2
  97. package/templates/example-chain/outputs/v1/out/rig/run.json +2 -2
  98. package/templates/example-chain/project.json +127 -78
  99. package/templates/example-direction/derived/review/v1/out/check/build/run.json +2 -2
  100. package/templates/example-draft/derived/drafts/lantern/out/check/build/run.json +2 -2
  101. package/templates/example-draft/derived/drafts/lantern/out/run.json +2 -2
  102. package/templates/example-draft/derived/review/v1/out/check/build/run.json +2 -2
  103. package/templates/example-draft/derived/review/v1/out/rig/run.json +2 -2
  104. package/templates/example-draft/derived/review/v2/out/check/build/run.json +2 -2
  105. package/templates/example-draft/derived/review/v2/out/rig/run.json +2 -2
  106. package/templates/example-draft/evidence/direction/v4.json +3 -3
  107. package/templates/example-draft/evidence/direction-input-v4.json +3 -3
  108. package/templates/example-draft/evidence/parts-review.json +1 -1
  109. package/templates/example-draft/evidence/review-reads/{52673b635b9cbc92da1a9e3d4c023a249bbacf79cd21b59724f03f18f8caf09b.json → 8866887a00012b5d5944b7525a478aea70931935bff35ddfcb186ffc515ef5af.json} +2 -2
  110. package/templates/example-draft/evidence/scene/s1.json +3 -3
  111. package/templates/example-draft/evidence/scene-citations.json +1 -1
  112. package/templates/example-draft/out/check/build/run.json +2 -2
  113. package/templates/example-draft/out/run.json +2 -2
  114. package/templates/example-draft/outputs/v1/out/check/build/run.json +2 -2
  115. package/templates/example-draft/outputs/v1/out/rig/run.json +2 -2
  116. package/templates/example-draft/outputs/v2/out/check/build/run.json +2 -2
  117. package/templates/example-draft/outputs/v2/out/rig/run.json +2 -2
  118. package/templates/example-interview-choice.json +113 -68
  119. package/templates/example-interview-options/constraint.json +2 -2
  120. package/templates/example-interview-options/example-assemble.json +2 -2
  121. package/templates/example-interview-options/example-bones.json +5 -5
  122. package/templates/example-interview-options/example-camera-depth.json +3 -3
  123. package/templates/example-interview-options/example-constraints.json +5 -5
  124. package/templates/example-interview-options/example-meshes.json +5 -5
  125. package/templates/example-interview-options/example-motion.json +5 -5
  126. package/templates/example-interview-options/example-picks.json +2 -2
  127. package/templates/example-interview-options/example-post-composition.json +3 -3
  128. package/templates/example-interview-options/example-post-timing.json +3 -3
  129. package/templates/example-interview-options/example-reference.json +2 -2
  130. package/templates/example-interview-options/example-rig-type.json +2 -2
  131. package/templates/example-interview-options/example-seethrough.json +2 -2
  132. package/templates/example-interview-options/example-turn.json +108 -0
  133. package/templates/example-interview-options/fullscreen.json +2 -2
  134. package/templates/example-interview-options/other-explicit.json +2 -2
  135. package/templates/example-interview-options/other-pending.json +2 -2
  136. package/templates/example-interview-options/other-picks.json +2 -2
  137. package/templates/example-invariants/invariants.json +223 -164
  138. package/templates/example-project.json +301 -249
  139. package/templates/example-remesh/AGENT-REFERENCE.md +62 -0
  140. package/templates/example-remesh/AGENTS.md +4 -26
  141. package/templates/example-remesh/derived/drafts/remesh/out/check/build/run.json +2 -2
  142. package/templates/example-remesh/derived/drafts/remesh/out/run.json +2 -2
  143. package/templates/example-remesh/derived/review/v1/out/check/build/run.json +2 -2
  144. package/templates/example-remesh/derived/review/v1/out/rig/run.json +2 -2
  145. package/templates/example-remesh/derived/review/v2/out/check/build/run.json +2 -2
  146. package/templates/example-remesh/derived/review/v2/out/rig/run.json +2 -2
  147. package/templates/example-remesh/derived/review/v3/out/check/build/run.json +2 -2
  148. package/templates/example-remesh/derived/review/v3/out/rig/run.json +2 -2
  149. package/templates/example-remesh/evidence/captions.json +1 -1
  150. package/templates/example-remesh/evidence/direction/v4.json +1 -1
  151. package/templates/example-remesh/evidence/direction/v5.json +1 -1
  152. package/templates/example-remesh/evidence/direction-input-v4.json +1 -1
  153. package/templates/example-remesh/evidence/direction-input-v5.json +1 -1
  154. package/templates/example-remesh/evidence/interview/assemble.json +1 -1
  155. package/templates/example-remesh/evidence/interview/bones.json +4 -4
  156. package/templates/example-remesh/evidence/interview/camera_depth.json +2 -2
  157. package/templates/example-remesh/evidence/interview/constraints.json +4 -4
  158. package/templates/example-remesh/evidence/interview/meshes.json +4 -4
  159. package/templates/example-remesh/evidence/interview/motion.json +4 -4
  160. package/templates/example-remesh/evidence/interview/post_composition.json +2 -2
  161. package/templates/example-remesh/evidence/interview/post_timing.json +2 -2
  162. package/templates/example-remesh/evidence/interview/reference.json +1 -1
  163. package/templates/example-remesh/evidence/interview/rig_type.json +1 -1
  164. package/templates/example-remesh/evidence/interview/seethrough.json +1 -1
  165. package/templates/example-remesh/evidence/interview/turn.json +108 -0
  166. package/templates/example-remesh/evidence/parts-review.json +1 -1
  167. package/templates/example-remesh/evidence/review-reads/{afe2522be4087f7f2fa7e97746d5f8574b0aedb474245ce2dbce0455e662f041.json → 99ade3eb19d70bd5f727138b3b4876ce86f1e1c22a59b516639bf85593996c32.json} +2 -2
  168. package/templates/example-remesh/evidence/scene/s1.json +2 -2
  169. package/templates/example-remesh/evidence/v1/registration/at-source.json +2 -2
  170. package/templates/example-remesh/evidence/v1/registration/rigc-run-1.json +2 -2
  171. package/templates/example-remesh/evidence/v2/registration/at-source.json +2 -2
  172. package/templates/example-remesh/evidence/v2/registration/rigc-run-1.json +2 -2
  173. package/templates/example-remesh/evidence/v3/registration/at-source.json +2 -2
  174. package/templates/example-remesh/evidence/v3/registration/rigc-run-1.json +2 -2
  175. package/templates/example-remesh/outputs/v1/out/check/build/run.json +2 -2
  176. package/templates/example-remesh/outputs/v1/out/rig/run.json +2 -2
  177. package/templates/example-remesh/outputs/v2/out/check/build/run.json +2 -2
  178. package/templates/example-remesh/outputs/v2/out/rig/run.json +2 -2
  179. package/templates/example-remesh/outputs/v3/out/check/build/run.json +2 -2
  180. package/templates/example-remesh/outputs/v3/out/rig/run.json +2 -2
  181. package/templates/example-remesh/project.json +279 -230
  182. package/templates/example-reopened/project.json +341 -289
  183. package/templates/example-split/captions.json +1 -1
  184. package/templates/project.json +10 -2
package/src/review-act.ts CHANGED
@@ -3,6 +3,8 @@ import { PARTS_CLI_RECEIPT } from './parts-verdict-record.ts';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { readFileSync, realpathSync } from './check-reads.ts';
5
5
  import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { jobRuntimeExecArgv } from './job-writer.ts';
6
8
  import { isDeepStrictEqual } from 'node:util';
7
9
  import { JOB_COMMENTS_ACT_SCHEMA, parseCommentsAct, parseRecordAct, projectComments,
8
10
  type JobAct, type JobCommentPartsAnchor, type JobCommentsAct, type JobModelSourceBinding, type JobRigStill, type RecordAct } from '@rig-c/review/job-host';
@@ -28,6 +30,9 @@ export type PartsIssue = {
28
30
  };
29
31
  /** Review's JobAct now includes comments; legacy adapters keep their original source guarantees. */
30
32
  type LegacyJobAct = Exclude<JobAct, { schema: typeof JOB_COMMENTS_ACT_SCHEMA }>;
33
+ export type ReopenAct = Extract<RecordAct, { kind: 'reopen' }>;
34
+ export type ReviewActKind = 'review-verdict' | 'parts-verdict' | 'interview-picks' | 'checkpoint' | 'signoff' | 'reopen';
35
+ export type DecodedReviewAct = ReviewVerdictAct | PartsVerdictAct | InterviewPicksAct | CheckpointAct | SignoffAct | ReopenAct;
31
36
  export type InterviewPicksAct = Extract<LegacyJobAct | RecordAct, { kind: 'interview-picks' }>;
32
37
  export type PartsVerdictActV2 = Extract<LegacyJobAct, { kind: 'parts-verdict' }>;
33
38
  export type PartsVerdictActV5 = Omit<PartsVerdictActV2, 'schema' | 'value'> & {
@@ -88,7 +93,7 @@ function failure(code: string, detail: string): never {
88
93
  }
89
94
 
90
95
  /** The shared decoder validates grammar; Create separately admits its existing source adapters. */
91
- function decodeCommentsReviewAct(raw: JsonObject, kind: 'review-verdict' | 'parts-verdict' | 'interview-picks' | 'checkpoint' | 'signoff'): ReviewVerdictAct | PartsVerdictAct | CheckpointAct | SignoffAct {
96
+ function decodeCommentsReviewAct(raw: JsonObject, kind: ReviewActKind): ReviewVerdictAct | PartsVerdictAct | CheckpointAct | SignoffAct {
92
97
  if (kind === 'interview-picks' || raw.kind === 'interview-picks') failure('REVIEW_ACT_KIND_MISMATCH', 'comments acts do not carry interview answers');
93
98
  let act: JobCommentsAct;
94
99
  try { act = parseCommentsAct(raw); }
@@ -151,18 +156,22 @@ export function decodeReviewAct(bytes: string, kind: 'interview-picks'): Intervi
151
156
  export function decodeReviewAct(bytes: string, kind: 'parts-verdict'): PartsVerdictAct;
152
157
  export function decodeReviewAct(bytes: string, kind: 'checkpoint'): CheckpointAct;
153
158
  export function decodeReviewAct(bytes: string, kind: 'signoff'): SignoffAct;
154
- export function decodeReviewAct(bytes: string, kind: 'review-verdict' | 'parts-verdict' | 'interview-picks' | 'checkpoint' | 'signoff'): ReviewVerdictAct | PartsVerdictAct | InterviewPicksAct | CheckpointAct | SignoffAct;
155
- export function decodeReviewAct(bytes: string, kind: 'review-verdict' | 'parts-verdict' | 'interview-picks' | 'checkpoint' | 'signoff' = 'review-verdict'): ReviewVerdictAct | PartsVerdictAct | InterviewPicksAct | CheckpointAct | SignoffAct {
159
+ export function decodeReviewAct(bytes: string, kind: 'reopen'): ReopenAct;
160
+ export function decodeReviewAct(bytes: string, kind: ReviewActKind): DecodedReviewAct;
161
+ export function decodeReviewAct(bytes: string, kind: ReviewActKind = 'review-verdict'): DecodedReviewAct {
156
162
  let act: JsonObject;
157
163
  try { const parsed: unknown = JSON.parse(bytes); if (!object(parsed)) throw Error(); act = parsed; }
158
164
  catch { return failure('REVIEW_ACT_INVALID', 'act is not a JSON object'); }
165
+ if (kind === 'reopen' && act.schema !== 'rig-review/act@4') failure('REVIEW_ACT_SCHEMA_UNSUPPORTED', 'reopen requires a record act@4');
166
+ if (act.schema === 'rig-review/act@4' && act.kind === 'reopen' && kind !== 'reopen') {
167
+ failure('REVIEW_ACT_KIND_MISMATCH', 'record reopen acts require the reopen writer');
168
+ }
159
169
  if (act.schema === JOB_COMMENTS_ACT_SCHEMA) return decodeCommentsReviewAct(act, kind);
160
- if (act.schema === 'rig-review/act@4' && act.kind === 'reopen' && kind !== 'checkpoint') failure('REVIEW_ACT_SCHEMA_UNSUPPORTED', 'record reopen acts have no Create writer mapping');
161
- if (['checkpoint', 'interview-picks', 'signoff'].includes(kind) && act.schema === 'rig-review/act@4') {
170
+ if (['checkpoint', 'interview-picks', 'signoff', 'reopen'].includes(kind) && act.schema === 'rig-review/act@4') {
162
171
  try {
163
172
  const recordAct = parseRecordAct(act);
164
173
  if (recordAct.kind !== kind) return failure('REVIEW_ACT_KIND_MISMATCH', 'record act kind differs from requested kind');
165
- return recordAct as CheckpointAct | InterviewPicksAct | SignoffAct;
174
+ return recordAct as CheckpointAct | InterviewPicksAct | SignoffAct | ReopenAct;
166
175
  } catch { return failure('REVIEW_ACT_INVALID', 'record act or its sealed source is malformed'); }
167
176
  }
168
177
  if (act.schema !== 'rig-review/act@2' && !(kind === 'review-verdict' && act.schema === 'rig-review/act@3')
@@ -292,7 +301,7 @@ export function decodeReviewAct(bytes: string, kind: 'review-verdict' | 'parts-v
292
301
  return act as unknown as ReviewVerdictAct;
293
302
  }
294
303
 
295
- export function decodeReviewResponse(text: string, jobId: string, actId: string, requestId: string, kind: 'review-verdict' | 'parts-verdict' | 'interview-picks' | 'checkpoint' | 'signoff' = 'review-verdict'): { act: ReviewVerdictAct | PartsVerdictAct | InterviewPicksAct | CheckpointAct | SignoffAct; bytes: string } {
304
+ export function decodeReviewResponse(text: string, jobId: string, actId: string, requestId: string, kind: ReviewActKind = 'review-verdict'): { act: DecodedReviewAct; bytes: string } {
296
305
  let response: JsonObject;
297
306
  try { const parsed: unknown = JSON.parse(text); if (!object(parsed)) throw Error(); response = parsed; }
298
307
  catch { return failure('REVIEW_ACT_INVALID', 'Review response is not JSON'); }
@@ -312,7 +321,23 @@ export interface ReviewActTransport {
312
321
  jobId: string;
313
322
  actId: string;
314
323
  fetchCapability: string;
315
- kind?: 'parts-verdict' | 'interview-picks' | 'checkpoint' | 'signoff';
324
+ kind?: Exclude<ReviewActKind, 'review-verdict'>;
325
+ }
326
+
327
+ /** Synchronous writers use the same authenticated fetch/decoder through the private runtime. */
328
+ export function fetchReviewActRuntime(transport: ReviewActTransport): { bytes: string; stillBytes?: string } {
329
+ const child = Bun.spawnSync([process.execPath, ...jobRuntimeExecArgv(), fileURLToPath(new URL('./review-act-runtime.ts', import.meta.url))], {
330
+ stdin: Buffer.from(JSON.stringify(transport)), stdout: 'pipe', stderr: 'pipe',
331
+ });
332
+ let response: { ok: boolean; bytes: string; stillBytes?: string; code: string; message: string };
333
+ try { response = JSON.parse(child.stdout.toString()); }
334
+ catch { return failure('REVIEW_ACT_FETCH_REFUSED', 'fetch adapter failed'); }
335
+ if (!response.ok || child.exitCode !== 0) refuse(response.code, response.message);
336
+ return { bytes: response.bytes, stillBytes: response.stillBytes };
337
+ }
338
+
339
+ export function fetchReviewActBytes(transport: ReviewActTransport): string {
340
+ return fetchReviewActRuntime(transport).bytes;
316
341
  }
317
342
 
318
343
  /** Only the route and fetch grant leave the binding reader. Never attach a host. */
@@ -329,7 +354,7 @@ export function reviewActTransport(root: string, actId: string): ReviewActTransp
329
354
  return { origin: origin.origin, jobId: binding.jobId, actId, fetchCapability: binding.fetchCapability };
330
355
  }
331
356
 
332
- export async function fetchReviewAct(transport: ReviewActTransport, fetchAdapter: typeof fetch = fetch): Promise<{ act: ReviewVerdictAct | PartsVerdictAct | InterviewPicksAct | CheckpointAct | SignoffAct; bytes: string; stillBytes?: string }> {
357
+ export async function fetchReviewAct(transport: ReviewActTransport, fetchAdapter: typeof fetch = fetch): Promise<{ act: DecodedReviewAct; bytes: string; stillBytes?: string }> {
333
358
  const requestId = randomUUID();
334
359
  const request = JSON.stringify({ schema: 'rig-review/job-api@2', verb: 'fetch-act', jobId: transport.jobId, actId: transport.actId, requestId });
335
360
  async function read(accept: string): Promise<Response> {
@@ -342,7 +367,7 @@ export async function fetchReviewAct(transport: ReviewActTransport, fetchAdapter
342
367
  return response;
343
368
  } catch { return failure('REVIEW_ACT_FETCH_REFUSED', 'live Review fetch failed'); }
344
369
  }
345
- if (transport.kind === 'parts-verdict' || transport.kind === 'interview-picks' || transport.kind === 'checkpoint' || transport.kind === 'signoff') return decodeReviewResponse(await (await read('application/json')).text(), transport.jobId, transport.actId, requestId, transport.kind);
370
+ if (transport.kind === 'parts-verdict' || transport.kind === 'interview-picks' || transport.kind === 'checkpoint' || transport.kind === 'signoff' || transport.kind === 'reopen') return decodeReviewResponse(await (await read('application/json')).text(), transport.jobId, transport.actId, requestId, transport.kind);
346
371
  const bytes = await (await read('application/vnd.rig-review.act+json')).text();
347
372
  const act = decodeReviewAct(bytes);
348
373
  if (act.jobId !== transport.jobId || act.actId !== transport.actId) failure('REVIEW_ACT_INVALID', 'fetched act identity differs from the request');
@@ -459,12 +484,12 @@ export function reviewActSignoff(act: SignoffAct): JsonObject {
459
484
  * acknowledged receipt keeps the subjects create published; a receipt that predates
460
485
  * them, or none (a clone), leaves only Review's own ask binding to vouch for it.
461
486
  */
462
- export function assertActSubjectDeclared(root: string, act: SignoffAct | CheckpointAct | InterviewPicksAct, path: string): void {
487
+ export function assertActSubjectDeclared(root: string, act: SignoffAct | CheckpointAct | InterviewPicksAct | ReopenAct, path: string): void {
463
488
  const receipt = readAcknowledgedPublication(root, act.jobId, act.publicationId);
464
489
  if (receipt?.subjects === undefined) return;
465
- const subject = act.kind === 'interview-picks' ? { kind: 'interview-picks', questionId: act.value.questionId, setId: act.source.optionSet.id } : act.kind === 'signoff' ? { kind: 'signoff', stage: act.value.stage } : { kind: 'checkpoint', checkpointId: act.value.checkpointId };
490
+ const subject = act.kind === 'interview-picks' ? { kind: 'interview-picks', questionId: act.value.questionId, setId: act.source.optionSet.id } : act.kind === 'signoff' || act.kind === 'reopen' ? { kind: act.kind, stage: act.value.stage } : { kind: 'checkpoint', checkpointId: act.value.checkpointId };
466
491
  if (receipt.subjects.some(declared => isDeepStrictEqual(declared, subject))) return;
467
- const named = act.kind === 'interview-picks' ? `answers ${act.value.questionId}` : act.kind === 'signoff' ? `signs ${act.value.stage}` : `passes checkpoint ${act.value.checkpointId}`;
492
+ const named = act.kind === 'interview-picks' ? `answers ${act.value.questionId}` : act.kind === 'signoff' || act.kind === 'reopen' ? `${act.kind} ${act.value.stage}` : `passes checkpoint ${act.value.checkpointId}`;
468
493
  refuse('REVIEW_ACT_SUBJECT_UNDECLARED', `${path}: act ${act.jobId}/${act.actId} ${named}, which its publication ${act.publicationId} did not declare; ask for it in Review on a publication made while the record can sign it`);
469
494
  }
470
495
 
@@ -512,7 +537,7 @@ export function reviewActFindings(project: JsonObject): { code: string; path: st
512
537
  try {
513
538
  if (!object(entry) || entry.schema !== 'rig-create/review-act@1' || typeof entry.bytes !== 'string'
514
539
  || !sha(entry.sha256) || jobHash(entry.bytes) !== entry.sha256 || !sha(entry.version_hash)) failure('REVIEW_ACT_LEDGER_INVALID', 'receipt shape or bytes hash is invalid');
515
- const act = decodeReviewAct(entry.bytes, entry.kind === 'parts-verdict' ? 'parts-verdict' : entry.kind === 'interview-picks' ? 'interview-picks' : entry.kind === 'checkpoint' ? 'checkpoint' : entry.kind === 'signoff' ? 'signoff' : 'review-verdict');
540
+ const act = decodeReviewAct(entry.bytes, entry.kind === 'parts-verdict' ? 'parts-verdict' : entry.kind === 'interview-picks' ? 'interview-picks' : entry.kind === 'checkpoint' ? 'checkpoint' : entry.kind === 'signoff' ? 'signoff' : entry.kind === 'reopen' ? 'reopen' : 'review-verdict');
516
541
  if (entry.jobId !== act.jobId || entry.actId !== act.actId) failure('REVIEW_ACT_LEDGER_INVALID', 'receipt identity differs from its act');
517
542
  const key = JSON.stringify([act.jobId, act.actId]);
518
543
  if (seen.has(key)) failure('REVIEW_ACT_REPLAY_CONFLICT', 'duplicate act receipt');
@@ -537,6 +562,16 @@ export function reviewActFindings(project: JsonObject): { code: string; path: st
537
562
  }
538
563
  return;
539
564
  }
565
+ if (act.kind === 'reopen') {
566
+ const recorded = Number.isSafeInteger(entry.reopen) && Number(entry.reopen) >= 0 ? asArray(project.reopens)[Number(entry.reopen)] : null;
567
+ if (!object(recorded) || entry.version_hash !== jobHash(JSON.stringify(recorded))
568
+ || recorded.stage !== act.value.stage || recorded.by !== act.authority.reviewer.id
569
+ || recorded.at !== act.at || recorded.verbatim !== act.value.words
570
+ || recorded.source !== `review act ${act.jobId}/${act.actId}`) {
571
+ failure('REVIEW_ACT_LEDGER_INVALID', 'recorded reopen differs from the immutable act');
572
+ }
573
+ return;
574
+ }
540
575
  if (act.kind === 'checkpoint') {
541
576
  const signoffs = Array.isArray(project.signoffs) ? project.signoffs : [];
542
577
  const signoff = Number.isSafeInteger(entry.signoff) && Number(entry.signoff) >= 0 ? signoffs[Number(entry.signoff)] : undefined;
@@ -13,6 +13,7 @@ import { asArray, isObject, readJsonObject, type Ctx } from './check.ts';
13
13
  import { draftFileUnavailable, draftVersionFindings } from './drafts.ts';
14
14
  import { Refusal, refuse, USAGE_EXIT } from './refusal.ts';
15
15
  import { assertRigVersionDate, rigVersionDateFinding } from './rig-version-date.ts';
16
+ import { registeredDepthPartsMatch } from './rig-version.ts';
16
17
  import { latestPartsApproval, retainedJson } from './structure.ts';
17
18
  import type { Json, JsonObject } from './templates.ts';
18
19
 
@@ -50,7 +51,8 @@ function checkBuildIdentity(ctx: Pick<Ctx, 'dir' | 'project' | 'refusals' | 'war
50
51
  const files = asArray(parts.files).filter(isObject);
51
52
  const manifest = files.find(file => file.path === `${parts.path}/parts.json`);
52
53
  const packaged = asArray(record.files).find(f => isObject(f) && f.path === 'out/parts.json');
53
- if (isObject(manifest) && typeof manifest.sha256 === 'string' && isObject(packaged) && typeof packaged.sha256 === 'string' && manifest.sha256 !== packaged.sha256) {
54
+ if (isObject(manifest) && typeof manifest.sha256 === 'string' && isObject(packaged) && typeof packaged.sha256 === 'string' && manifest.sha256 !== packaged.sha256
55
+ && !registeredDepthPartsMatch(ctx.dir, version, manifest.sha256, packaged.sha256)) {
54
56
  mismatch('REVIEW_PACKAGE_PARTS_MISMATCH', 'files[out/parts.json]', manifest.sha256, packaged.sha256);
55
57
  }
56
58
  if (!isObject(manifest) || typeof manifest.path !== 'string') return;
@@ -8,7 +8,7 @@ import { partsFileReferences } from '@rig-c/parts/src/parts.ts';
8
8
  import { tmpdir } from 'node:os';
9
9
  import { dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } from 'node:path';
10
10
  import { isDeepStrictEqual } from 'node:util';
11
- import { asArray, checkProject, isObject, type Finding } from './check.ts';
11
+ import { asArray, checkProject, collectItems, isObject, type Finding } from './check.ts';
12
12
  import { literalFile } from './choices.ts';
13
13
  import { configInputReferences } from './config-inputs.ts';
14
14
  import { parseFeedbackAnswers, readProject } from './feedback.ts';
@@ -18,7 +18,9 @@ import { collectReopens, reopenTime } from './reopen.ts';
18
18
  import { prepareRigReviewPackage } from './review-package.ts';
19
19
  import { rigVersionDateFinding } from './rig-version-date.ts';
20
20
  import { checkRegisteredRuns, RUN_RECORD_SINCE, RunRecordError, validateRunRecord, type RunSpec } from './run-record.ts';
21
- import { approvedStructureDelta, structurePathIsIgnored } from './structure.ts';
21
+ import { approvedStructureDelta, latestPartsApproval, structurePathIsIgnored } from './structure.ts';
22
+ import { stageOfStep } from './stages.ts';
23
+ import { HEAD_TURN_SINCE } from './waypoints.ts';
22
24
  import type { Json, JsonObject } from './templates.ts';
23
25
 
24
26
  const sha = (bytes: Buffer): string => createHash('sha256').update(bytes).digest('hex');
@@ -38,6 +40,9 @@ const RESERVED_REGISTRATION_NAMES = new Set([
38
40
  'package-lock.json',
39
41
  'path-adapter.json',
40
42
  'citations.json',
43
+ 'depth-manifest.json',
44
+ 'depth-report.json',
45
+ 'depth-source-parts.json',
41
46
  ]);
42
47
  function isValidAdaptedTarget(target: string): boolean {
43
48
  return literal(target)
@@ -389,13 +394,137 @@ function collectRequirementsAssetRefs(requirements: JsonObject, portableRequirem
389
394
  return refs;
390
395
  }
391
396
 
392
- /** Only explicitly registered versions opt in. Older version contracts are unchanged. */
393
- export function rigRegistrationFindings(dir: string, project: JsonObject, warnings: Finding[] = []): Finding[] {
397
+ /** A declared depth/turn config is the existing registration's depth input branch. */
398
+ function usesDepth(config: JsonObject): boolean {
399
+ const turn = isObject(config.turn) ? config.turn : {};
400
+ const body = isObject(turn.body) ? turn.body : {};
401
+ return isObject(config.depth) || [turn.roles, body.roles].some(roles => isObject(roles) && Object.keys(roles).length > 0);
402
+ }
403
+
404
+ /** Reads only producer declarations and bytes, never producer measurements. */
405
+ function depthBindingFindings(dir: string, version: JsonObject, config: JsonObject, record: JsonObject | undefined, warnings: Finding[]): Finding[] {
406
+ if (!usesDepth(config) && record?.depth === undefined) return [];
407
+ const path = `versions[v${String(version.n)}].registration.depth`;
394
408
  const findings: Finding[] = [];
409
+ const missing = (why: string): void => {
410
+ const finding = { code: 'RIG_DEPTH_BINDING_MISSING', path, message: `${path}: ${why}; retain rig's depth_sheets.json and, for per-part maps, --depth-manifest and parts' depth-report.json when registering a new rig version` };
411
+ (typeof version.at === 'string' && Date.parse(version.at) < Date.parse(HEAD_TURN_SINCE) ? warnings : findings).push(finding);
412
+ };
413
+ const depth = record?.depth;
414
+ if (!isObject(depth)) { missing('no producer depth binding'); return findings; }
415
+ if (!isObject(depth.sheets) || depth.route === 'per-part' && (!isObject(depth.manifest) || !isObject(depth.report))) {
416
+ missing('incomplete producer depth binding'); return findings;
417
+ }
418
+ try {
419
+ if (depth.route !== 'per-part' && depth.route !== 'per-layer') throw Error('expected per-part or per-layer route');
420
+ if (depth.route === 'per-layer' && (depth.manifest !== undefined || depth.report !== undefined)) throw Error('per-part reports require the per-part route');
421
+ const read = (binding: Json | undefined, spec: string, output = false): JsonObject => {
422
+ const inventory = asArray(output ? version.outputs : record?.files).filter(isObject);
423
+ if (!isObject(binding) || !inventory.some(row => row.path === binding.path && row.sha256 === binding.sha256)) throw Error('depth file is absent from the retained inventory');
424
+ const doc: Json = JSON.parse(bytesAt(dir, binding, output).toString('utf8'));
425
+ if (!isObject(doc) || doc.spec !== spec || !Array.isArray(doc.parts)) throw Error(`expected ${spec} with parts[]`);
426
+ return doc;
427
+ };
428
+ const digest = (value: Json | undefined, field: string): string => {
429
+ if (typeof value !== 'string' || !/^[a-f0-9]{64}$/.test(value)) throw Error(`${field}: expected producer SHA-256`);
430
+ return value;
431
+ };
432
+ const namedRows = (value: Json | undefined, label: string): JsonObject[] => {
433
+ if (!Array.isArray(value) || value.length === 0 || value.some(row => !isObject(row) || typeof row.part !== 'string' || !row.part)) throw Error(`${label}: expected nonempty named parts[]`);
434
+ const rows = value as JsonObject[];
435
+ if (new Set(rows.map(row => row.part)).size !== rows.length) throw Error(`${label}: duplicate part`);
436
+ return rows;
437
+ };
438
+ const outputRoot = `outputs/v${String(version.n)}/out`;
439
+ const outputs = asArray(version.outputs).filter(isObject);
440
+ const outputBytes = (path: string): Buffer => {
441
+ const binding = outputs.find(row => row.path === path);
442
+ if (!binding) throw Error(`${path}: absent from retained outputs`);
443
+ return bytesAt(dir, binding, true);
444
+ };
445
+ const parts: Json = JSON.parse(outputBytes(`${outputRoot}/parts.json`).toString('utf8'));
446
+ if (!isObject(parts) || !Array.isArray(parts.parts)) throw Error('retained assembled parts.json has no parts[]');
447
+ const mappedParts = parts.parts.filter(isObject);
448
+ const sourceList = (raw: Json | undefined, label: string): JsonObject[] => {
449
+ if (!Array.isArray(raw) || raw.length === 0 || raw.some(row => !isObject(row))) throw Error(`${label}: expected depth sources[]`);
450
+ const rows = raw as JsonObject[];
451
+ for (const row of rows) {
452
+ digest(row.sha256, `${label}.sha256`);
453
+ if (typeof row.from !== 'string' || !row.from || typeof row.image !== 'string' || !literal(row.image)
454
+ || !Array.isArray(row.crop) || row.crop.length !== 4 || row.crop.some(v => typeof v !== 'number' || !Number.isFinite(v))) throw Error(`${label}: invalid declared depth source`);
455
+ }
456
+ return rows;
457
+ };
458
+ const sheets = namedRows(read(depth.sheets, 'rig-parts-depth-sheets/1', true).parts, 'rig sheets');
459
+ for (const row of sheets) {
460
+ const ref = row.depth;
461
+ if (!isObject(ref) || typeof ref.image !== 'string' || !literal(ref.image) || ref.image.includes('/')
462
+ || !['white', 'black'].includes(String(ref.near)) || typeof ref.zScale !== 'number' || !(ref.zScale > 0) || !Number.isFinite(ref.zScale)
463
+ || !Array.isArray(row.size) || row.size.length !== 2 || row.size.some(v => typeof v !== 'number' || !Number.isSafeInteger(v) || v < 1)) throw Error('invalid consumed sheet metadata');
464
+ const sources = sourceList(ref.sources, 'rig sheet sources');
465
+ // Rig may pad/crop a sheet. Its declared source provenance must still match
466
+ // an assembled part; comparing the padded sheet hash to apply's would be wrong.
467
+ if (!mappedParts.some(part => isObject(part.depth) && part.depth.near === ref.near && part.depth.zScale === ref.zScale
468
+ && isDeepStrictEqual(sourceList(part.depth.sources, 'assembled sheet sources'), sources))) throw Error('rig sheet sources do not bind the retained assembled parts');
469
+ outputBytes(`${outputRoot}/rig/images/${ref.image}`);
470
+ }
471
+ if (depth.route === 'per-part') {
472
+ const manifest = read(depth.manifest, 'rig-parts-depth-inputs/1');
473
+ const report = read(depth.report, 'rig-parts-depth-report/1');
474
+ for (const field of ['partsSha256', 'recipeSha256'] as const) {
475
+ if (digest(report[field], `report.${field}`) !== digest(manifest[field], `manifest.${field}`)) throw Error(`producer ${field} declarations disagree`);
476
+ }
477
+ digest(report.rigConfigSha256, 'report.rigConfigSha256');
478
+ if (digest(report.manifestSha256, 'report.manifestSha256') !== (depth.manifest as JsonObject).sha256) throw Error('producer report does not bind the retained prepare manifest');
479
+ if (depth.source_parts !== undefined) {
480
+ const binding = depth.source_parts;
481
+ if (!isObject(binding) || !asArray(record?.files).filter(isObject).some(f => f.path === binding.path && f.sha256 === binding.sha256)
482
+ || digest(binding.sha256, 'source parts') !== manifest.partsSha256) throw Error('prepare manifest does not bind the retained source parts');
483
+ bytesAt(dir, binding);
484
+ }
485
+ const prepared = namedRows(manifest.parts, 'prepared parts');
486
+ const applied = namedRows(report.parts, 'applied parts');
487
+ if (!isDeepStrictEqual(prepared.map(row => row.part).sort(), applied.map(row => row.part).sort())) throw Error('prepare/apply part sets disagree');
488
+ for (const row of applied) {
489
+ const input = prepared.find(p => p.part === row.part)!;
490
+ const part = mappedParts.find(p => p.name === row.part);
491
+ if (!part || !isObject(part.depth) || typeof part.name !== 'string' || !literal(part.name) || part.name.includes('/')
492
+ || typeof part.depth.image !== 'string' || !literal(part.depth.image) || part.depth.image.includes('/')) throw Error('report part has no retained assembled depth declaration');
493
+ const partDepth = part.depth;
494
+ for (const field of ['sourceSha256', 'inputSha256'] as const) {
495
+ if (digest(row[field], `report.${field}`) !== digest(input[field], `manifest.${field}`)) throw Error(`producer ${field} declarations disagree`);
496
+ }
497
+ if (digest(row.sourceSha256, 'report.sourceSha256') !== sha(outputBytes(`${outputRoot}/parts/${part.name}.png`))) throw Error('applied part art differs from the retained assembled part');
498
+ if (digest(row.sheetSha256, 'report.sheetSha256') !== sha(outputBytes(`${outputRoot}/parts/${partDepth.image}`))) throw Error('applied sheet differs from the retained assembled sheet');
499
+ const map = digest(row.mapSha256, 'report.mapSha256');
500
+ const sources = sourceList(partDepth.sources, 'applied sheet sources');
501
+ if (!sources.some(source => source.sha256 === map && source.from === part.from)) throw Error('applied map hash is absent from the assembled depth sources');
502
+ if (row.near !== partDepth.near || row.zScale !== partDepth.zScale) throw Error('applied depth units disagree with the assembled declaration');
503
+ if (!sheets.some(sheet => isObject(sheet.depth) && isDeepStrictEqual(sheet.depth.sources, sources)
504
+ && sheet.depth.near === partDepth.near && sheet.depth.zScale === partDepth.zScale)) throw Error('rig consumed no sheet from the reported applied part');
505
+ }
506
+ }
507
+ } catch (error) {
508
+ findings.push({ code: 'RIG_DEPTH_BINDING_INVALID', path, message: `${path}: ${String(error)}; restore the exact producer files or register a new rig version` });
509
+ }
510
+ return findings;
511
+ }
512
+
513
+ /** Only explicitly registered versions opt in. Older version contracts are unchanged. */
514
+ export function rigRegistrationFindings(dir: string, project: JsonObject, warnings: Finding[] = [], collection: JsonObject | null = null): Finding[] {
515
+ const findings: Finding[] = depthPlanFindings(project, warnings, collection);
395
516
  const allVersions = asArray(project.versions).filter(isObject);
396
517
  const registeredNumbers = new Set(allVersions.map(v => v.n).filter((n): n is number => typeof n === 'number' && Number.isSafeInteger(n)));
397
518
  for (const [index, version] of allVersions.entries()) {
398
- if (version.registration === undefined) continue;
519
+ if (version.registration === undefined) {
520
+ if (version.kind === 'rig' && version.pre_interview !== true && isObject(version.provenance)) {
521
+ try {
522
+ const config: Json = JSON.parse(bytesAt(dir, version.provenance.config).toString('utf8'));
523
+ if (isObject(config)) findings.push(...depthBindingFindings(dir, version, config, undefined, warnings));
524
+ } catch { /* Existing provenance checks own unreadable config evidence. */ }
525
+ }
526
+ continue;
527
+ }
399
528
  const path = `versions[v${String(version.n)}]`;
400
529
  const add = (code: string, field: string, why: string): void => { findings.push({ code, path: `${path}.${field}`, message: `${path}.${field}: ${why}; restore the retained registration inputs or register a new rig version` }); };
401
530
  const record = version.registration;
@@ -457,6 +586,7 @@ export function rigRegistrationFindings(dir: string, project: JsonObject, warnin
457
586
  const configSourceRaw = JSON.parse(bytesAt(dir, record.config_source).toString()) as Json;
458
587
  const portable = JSON.parse(bytesAt(dir, config).toString()) as Json;
459
588
  if (!isObject(configSourceRaw) || !isObject(portable)) throw Error('config-source and portable config must be JSON objects');
589
+ findings.push(...depthBindingFindings(dir, version, configSourceRaw, record, warnings));
460
590
  const checkPortable = (value: Json, document: string, requirementsDocument = false, location: string[] = []): void => {
461
591
  if (typeof value === 'string' && absolute(value)) {
462
592
  const field = `${document}#/${location.map(escapePointerToken).join('/')}`;
@@ -635,7 +765,7 @@ export function rigRegistrationFindings(dir: string, project: JsonObject, warnin
635
765
 
636
766
  export const RIG_VERSION_HELP = `rig-version <n> [<dir>] --parts <dir> --rig <dir> --build <dir> --idle <render dir>
637
767
  --config <file> --path-adapter <file> --compile-report <file> --toolchain-lock <package-lock.json>
638
- --at-source <dated JSON file> --at-pointer </pointer> --by <registrant> [--run-record <run.json>]... [--citations <sections JSON>] [--json]
768
+ --at-source <dated JSON file> --at-pointer </pointer> --by <registrant> [--run-record <run.json>]... [--citations <sections JSON>] [--depth-manifest <prepare manifest>] [--json]
639
769
  Copy and hash frozen producer outputs. Retain an explicitly declared npm lock, producer run records and UTC not-after date.
640
770
  Idle requires rigc render's frames.json. No build, acceptance or stage advance runs.
641
771
  Repeat --answers <version>:<verdict> to answer earlier human bounced feedback.
@@ -648,7 +778,7 @@ export function rigVersionCommand(args: string[], now: number): { stdout: string
648
778
  const options: Record<string, string> = {};
649
779
  const runRecords: string[] = [];
650
780
  const answerValues: string[] = [];
651
- const flags = ['--parts', '--rig', '--build', '--idle', '--config', '--path-adapter', '--compile-report', '--toolchain-lock', '--at-source', '--at-pointer', '--by', '--citations'];
781
+ const flags = ['--parts', '--rig', '--build', '--idle', '--config', '--path-adapter', '--compile-report', '--toolchain-lock', '--at-source', '--at-pointer', '--by', '--citations', '--depth-manifest'];
652
782
  let json = false;
653
783
  for (let i = 0; i < args.length; i++) {
654
784
  const arg = args[i]!;
@@ -669,7 +799,7 @@ export function rigVersionCommand(args: string[], now: number): { stdout: string
669
799
  else positional.push(arg);
670
800
  }
671
801
  const n = Number(positional[0]?.replace(/^v/, ''));
672
- if (!Number.isSafeInteger(n) || n < 1 || positional.length > 2 || flags.filter(f => f !== '--citations').some(f => !options[f])) refuse('RIG_REGISTRATION_USAGE', RIG_VERSION_HELP, USAGE_EXIT);
802
+ if (!Number.isSafeInteger(n) || n < 1 || positional.length > 2 || flags.filter(f => f !== '--citations' && f !== '--depth-manifest').some(f => !options[f])) refuse('RIG_REGISTRATION_USAGE', RIG_VERSION_HELP, USAGE_EXIT);
673
803
  const dir = realpathSync(resolve(positional[1] ?? process.cwd()));
674
804
  let { file, project } = readProject(dir);
675
805
  let original = readFileSync(file);
@@ -973,6 +1103,27 @@ export function rigVersionCommand(args: string[], now: number): { stdout: string
973
1103
  const partsDir = resolve(dir, options['--parts']!);
974
1104
  copyTree(join(partsDir, 'parts.json'), `${out}/out/parts.json`, true);
975
1105
  copyTree(join(partsDir, 'parts'), `${out}/out/parts`, true);
1106
+ // Keep the producer's complete depth reports in this same frozen registration.
1107
+ // The config.depth image references above still use the ordinary path adapter.
1108
+ let depthBinding: JsonObject | undefined;
1109
+ const depthReport = join(partsDir, 'depth-report.json');
1110
+ if (usesDepth(config) || options['--depth-manifest'] || existsSync(depthReport)) {
1111
+ const perPart = options['--depth-manifest'] !== undefined || existsSync(depthReport) || !isObject(config.depth);
1112
+ const sheets = join(dir, out, 'out/rig/depth_sheets.json');
1113
+ // Rig files are copied below; bind the exact frozen source now.
1114
+ const sourceSheets = resolve(dir, options['--rig']!, 'depth_sheets.json');
1115
+ depthBinding = { route: perPart ? 'per-part' : 'per-layer' };
1116
+ const approval = latestPartsApproval(project, Date.parse(String(at)));
1117
+ const draft = asArray(project.drafts).filter(isObject).find(d => d.id === approval?.draft_id);
1118
+ if (perPart && approval && isObject(draft?.parts) && typeof draft.parts.path === 'string') {
1119
+ const originalParts = resolve(dir, draft.parts.path, 'parts.json');
1120
+ if (!existsSync(originalParts)) fail('RIG_DEPTH_BINDING_MISSING', `versions[v${n}].registration.depth.source_parts`, 'restore the accepted assembled parts manifest before registering the depth-enriched version');
1121
+ depthBinding.source_parts = retain(`${evidence}/depth-source-parts.json`, sourceBytes(originalParts));
1122
+ }
1123
+ if (existsSync(sourceSheets)) depthBinding.sheets = { path: relative(dir, sheets).split(sep).join('/'), sha256: sha(sourceBytes(sourceSheets)) };
1124
+ if (options['--depth-manifest']) depthBinding.manifest = input('--depth-manifest', 'depth-manifest.json');
1125
+ if (existsSync(depthReport)) depthBinding.report = retain(`${evidence}/depth-report.json`, sourceBytes(depthReport));
1126
+ }
976
1127
  const partReferences = partsFileReferences(partsDir);
977
1128
  for (const reference of partReferences) {
978
1129
  const target = `${out}/out/${reference.path}`;
@@ -1005,6 +1156,7 @@ export function rigVersionCommand(args: string[], now: number): { stdout: string
1005
1156
  changed: 'Registered frozen producer outputs', provenance: { config: configBinding, sections, ...(structureChanges.length ? { structure_delta: structureChanges } : {}) },
1006
1157
  registration: { kind: 'frozen-rig/1', at: registrationAt, toolchain: { kind: 'npm-lock-v3', source: toolchain },
1007
1158
  ...(runs.length ? { runs } : {}),
1159
+ ...(depthBinding ? { depth: depthBinding } : {}),
1008
1160
  ...(skipped.length ? { skipped } : {}),
1009
1161
  files: retained, config_source: configSource,
1010
1162
  ...(requirementsSourceBinding ? { requirements_source: requirementsSourceBinding } : {}),
@@ -1012,6 +1164,8 @@ export function rigVersionCommand(args: string[], now: number): { stdout: string
1012
1164
  path_adapter: { ...adapterBinding, ...(pointerAdapted ? { format: 'json-pointer/1' } : {}) }, idle, compile,
1013
1165
  copied: { files: outputFiles.length, bytes: outputFiles.reduce((total, f) => total + Number(f.bytes), 0) } } };
1014
1166
  const candidate = { ...project, versions: [...project.versions, version] };
1167
+ const depthProblem = depthBindingFindings(dir, { ...version, at: new Date(now).toISOString() }, config, version.registration as JsonObject, [])[0];
1168
+ if (depthProblem) refuse(depthProblem.code, depthProblem.message);
1015
1169
  const problem = rigRegistrationFindings(dir, candidate)[0];
1016
1170
  if (problem) refuse(problem.code, problem.message);
1017
1171
  created.push(join(dir, pkg));
@@ -1045,3 +1199,60 @@ export function rigVersionCommand(args: string[], now: number): { stdout: string
1045
1199
  releasePublication();
1046
1200
  }
1047
1201
  }
1202
+
1203
+ /** The optional plan step consumes approved parts before the ordinary rig step. */
1204
+ export function depthPlanMoves(project: JsonObject, collection: JsonObject | null = null): { role: 'agent'; field: string; awaits: string; command: null; act: string }[] {
1205
+ const { answer, mode } = depthAnswer(project, collection);
1206
+ if (mode !== 'depth') return [];
1207
+ const answerAt = typeof answer.at === 'string' ? Date.parse(answer.at) : -Infinity;
1208
+ const rig = asArray(project.versions).filter(isObject).find(v => v.kind === 'rig' && typeof v.at === 'string' && Date.parse(v.at) >= answerAt && isObject(v.registration) && isObject(v.registration.depth));
1209
+ if (rig) return [];
1210
+ const steps = ["Record the chosen depth route and reason in the turn plan step text. Default per-part sequence: rig-parts depth-prepare --parts <accepted assembled parts> --config <depth recipe> --out <inference directory>",
1211
+ "Run the supplied depth model on each prepared image; retain raw rig-parts-map/1 maps (face normals, scalar depth for other parts).",
1212
+ "rig-parts depth-apply --parts <accepted assembled parts> --manifest <inference/manifest.json> --config <depth recipe> --rig-config <config.json> --out <mapped directory>; then rig-parts rig --parts <mapped directory> --config <mapped/config.json> --out <rig directory>. Register with rig-version --parts <mapped directory> --rig <rig directory> --config <mapped/config.json> --depth-manifest <inference/manifest.json>."];
1213
+ return steps.map((act, i) => ({ role: 'agent', field: `plan.steps[turn].moves[${i}]`, awaits: 'versions[].registration.depth', command: null, act }));
1214
+ }
1215
+
1216
+ function depthAnswer(project: JsonObject, collection: JsonObject | null = null): { answer: JsonObject; mode: Json | undefined } {
1217
+ const item = collectItems(project, { collection }).find(item => item.id === 'turn');
1218
+ const answer = item?.state.state === 'user' || item?.state.state === 'delegated' ? item.state.answer : {};
1219
+ const decision = answer.delegated === true && isObject(answer.agent_decision) ? answer.agent_decision.value : answer.value;
1220
+ return { answer, mode: isObject(decision) ? decision.mode : undefined };
1221
+ }
1222
+
1223
+ /** The optional depth step is drafted before its rigging-interview approval, just like the other rig steps. */
1224
+ export function depthPlanFindings(project: JsonObject, warnings: Finding[], collection: JsonObject | null = null): Finding[] {
1225
+ const { answer, mode } = depthAnswer(project, collection);
1226
+ if (mode !== 'depth') return [];
1227
+ const answerAt = typeof answer.at === 'string' ? Date.parse(answer.at) : NaN;
1228
+ const approved = asArray(project.signoffs).filter(isObject).some(s => s.stage === 'rigging-interview' && (s.checkpoint ?? null) === null && Date.parse(String(s.at)) >= answerAt);
1229
+ if (!approved && !['rigging', 'rig-review', 'post-interview', 'post', 'direction-review', 'accepted'].includes(String(project.stage))) return [];
1230
+ const plan = isObject(project.plan) ? project.plan : {};
1231
+ const steps = asArray(plan.steps).filter(isObject);
1232
+ const depth = steps.find(s => s.id === 'turn' && s.phase === 'rig' && asArray(s.rests_on).includes('turn'));
1233
+ if (depth && steps.filter(s => s !== depth && s.phase === 'rig' && stageOfStep('rig', String(s.id)) === 'rigging').every(s => Number(depth.order) < Number(s.order))) return [];
1234
+ const path = 'plan.steps[turn]';
1235
+ const finding = { code: 'RIG_DEPTH_PLAN_MISSING', path, message: `${path}: a depth turn needs a rig-phase depth step resting on turn, ordered before the rig steps; draft it with plan add turn and approve it at rigging-interview before running rig` };
1236
+ if (answerAt < Date.parse(HEAD_TURN_SINCE)) { warnings.push(finding); return []; }
1237
+ return [finding];
1238
+ }
1239
+
1240
+ /** Depth-apply may enrich only depth declarations; accepted art/geometry/measurements stay identical. */
1241
+ export function registeredDepthPartsMatch(dir: string, version: JsonObject, acceptedSha256: string, packagedSha256: string): boolean {
1242
+ try {
1243
+ const record = version.registration;
1244
+ if (!isObject(record) || !isObject(record.depth) || record.depth.route !== 'per-part' || !isObject(record.depth.source_parts)
1245
+ || record.depth.source_parts.sha256 !== acceptedSha256) return false;
1246
+ const binding = asArray(version.outputs).filter(isObject).find(f => f.path === `outputs/v${String(version.n)}/out/parts.json` && f.sha256 === packagedSha256);
1247
+ if (!binding) return false;
1248
+ const config: Json = JSON.parse(bytesAt(dir, record.config_source).toString('utf8'));
1249
+ if (!isObject(config) || depthBindingFindings(dir, version, config, record, []).length > 0) return false;
1250
+ const withoutDepth = (bytes: Buffer): JsonObject => {
1251
+ const doc: Json = JSON.parse(bytes.toString('utf8'));
1252
+ if (!isObject(doc) || !Array.isArray(doc.parts) || doc.parts.some(p => !isObject(p))) throw Error('invalid parts manifest');
1253
+ for (const part of doc.parts as JsonObject[]) delete part.depth;
1254
+ return doc;
1255
+ };
1256
+ return isDeepStrictEqual(withoutDepth(bytesAt(dir, record.depth.source_parts)), withoutDepth(bytesAt(dir, binding, true)));
1257
+ } catch { return false; }
1258
+ }
@@ -10,13 +10,15 @@ import { SCENE_CHOICE_MISMATCH, resolveAgentCitationSpec } from './scene-authori
10
10
  import { SCENE_SPEC, SceneError, checkScenes, plateRegistration, pngDimensions, sceneBytes, sceneEntries, sceneFail, scenePath, sceneRig, validateScene } from './scene.ts';
11
11
  import type { JsonObject } from './templates.ts';
12
12
 
13
- export const SCENE_HELP = `scene write --rig <R> --background <path> [--foreground <path>]
13
+ export const SCENE_HELP = `scene write --rig <R> --background <path> [--foreground <path>] [--animation <name>]
14
14
  [--background-rect x,y,w,h] [--foreground-rect x,y,w,h]
15
15
  --citations-file <retained project-relative JSON> [<dir>]
16
16
  Retain citations inside the project, for example evidence/scene-src/citations.json; absolute paths are refused.
17
17
  Citations are a role map {background: citation, foreground?: citation}.
18
18
  Each citation names post_composition, asks:["post_composition"], and quote or delegated:true.
19
19
  Paths and rects must match the current ingested post_composition choice.
20
+ Animation defaults to idle and must name a clip in the accepted bound model.
21
+ Only idle binds registration.idle frames; other clips omit optional frames.
20
22
  Records by:agent; typed --by is refused. Earlier human scenes remain readable.
21
23
  Appends evidence/scene/s<N>.json and scenes[]; stage stays unchanged.`;
22
24
  export interface SceneWriteHooks { beforeCommit?: () => void; }
@@ -31,7 +33,7 @@ function writeScene(args: string[], now: number, hooks: SceneWriteHooks): { stdo
31
33
  if (args[0] !== 'write') usage();
32
34
  for (let index = 1; index < args.length; index++) {
33
35
  const arg = args[index]!;
34
- if (['--rig', '--background', '--foreground', '--background-rect', '--foreground-rect', '--citations-file', '--by'].includes(arg)) {
36
+ if (['--rig', '--background', '--foreground', '--background-rect', '--foreground-rect', '--citations-file', '--animation', '--by'].includes(arg)) {
35
37
  const value = args[++index];
36
38
  if (!value || value.startsWith('--') || opts[arg] !== undefined) usage();
37
39
  opts[arg] = value;
@@ -62,7 +64,7 @@ function writeScene(args: string[], now: number, hooks: SceneWriteHooks): { stdo
62
64
  try {
63
65
  const entries = sceneEntries(project);
64
66
  const n = entries.length + 1;
65
- const binding = sceneRig(dir, project, Number(opts['--rig']), snapshots);
67
+ const binding = sceneRig(dir, project, Number(opts['--rig']), snapshots, opts['--animation']);
66
68
  const citationsBytes = sceneBytes(dir, opts['--citations-file']!, 'SCENE_CITATION_MISSING', snapshots);
67
69
  let citations: JsonObject;
68
70
  try { citations = JSON.parse(citationsBytes.toString('utf8')) as JsonObject; }
package/src/scene.ts CHANGED
@@ -175,7 +175,17 @@ function modelForRig(dir: string, version: JsonObject, snapshots?: Map<string, B
175
175
  const binding = { path, sha256: row.sha256 };
176
176
  return { binding, model: json(boundBytes(dir, binding, 'SCENE_MODEL_MISMATCH', snapshots), path) };
177
177
  }
178
- export function sceneRig(dir: string, project: JsonObject, n: number, snapshots?: Map<string, Buffer>): { rig: JsonObject; stage: JsonObject } {
178
+ /** New non-idle scenes omit frames. Retain the old reader's exact idle binding as legacy metadata. */
179
+ function validateSceneAnimation(model: JsonObject, animation: JsonObject, registration: JsonObject): void {
180
+ if (!objects(model.animations).some(value => value.name === animation.name)) sceneFail('SCENE_ANIMATION_MISSING', 'scene.rig.animation.name', 'choose an animation named by the bound model', 'pass --animation with a name from the accepted model and run scene write');
181
+ if (registration.idle !== undefined && !sceneBinding(registration.idle)) sceneFail('SCENE_MALFORMED', 'scene.rig.animation.frames', 'registration.idle must be a frames binding');
182
+ if (animation.name === 'idle') {
183
+ if (!equal(animation.frames, registration.idle)) sceneFail('SCENE_MALFORMED', 'scene.rig.animation.frames', 'idle frames must be present exactly when registration.idle exists and equal that binding');
184
+ } else if (animation.frames !== undefined && !equal(animation.frames, registration.idle)) {
185
+ sceneFail('SCENE_ANIMATION_FRAMES_UNBOUND', 'scene.rig.animation.frames', 'frames differ from the only legacy binding, registration.idle', 'omit frames for a non-idle animation and record a new scene with scene write --animation <name>');
186
+ }
187
+ }
188
+ export function sceneRig(dir: string, project: JsonObject, n: number, snapshots?: Map<string, Buffer>, animation = 'idle'): { rig: JsonObject; stage: JsonObject } {
179
189
  const { version } = rigVersion(project, n);
180
190
  const reopens = collectReopens(project, []);
181
191
  const signature = collectSignoffs(project, []).filter(value => value.stage === 'rig-review' && value.checkpoint === null && liveSignoff(reopens, value.stage, value.at)
@@ -185,7 +195,8 @@ export function sceneRig(dir: string, project: JsonObject, n: number, snapshots?
185
195
  const { binding, model } = modelForRig(dir, version, snapshots);
186
196
  const registration = isObject(version.registration) ? version.registration : {};
187
197
  const rig: JsonObject = { version: n, acceptance: { signoff: { stage: 'rig-review', checkpoint: null, at: signature.record.at! }, verdict: target.verdict!, review_package: target.review_package! },
188
- model: binding, animation: { name: 'idle', ...(registration.idle === undefined ? {} : { frames: registration.idle }) } };
198
+ model: binding, animation: { name: animation, ...(animation !== 'idle' || registration.idle === undefined ? {} : { frames: registration.idle }) } };
199
+ validateSceneAnimation(model, rig.animation as JsonObject, registration);
189
200
  validateSceneCurrent(project, rig, true);
190
201
  if (model.stage === null || model.stage === undefined) sceneFail('SCENE_STAGE_MISSING', String(binding.path), 'compile a model with a stage box');
191
202
  return { rig, stage: model.stage as JsonObject };
@@ -218,9 +229,8 @@ export function validateScene(dir: string, project: JsonObject, doc: JsonObject,
218
229
  const { binding, model } = modelForRig(dir, version, snapshots);
219
230
  if (!equal(rig.model, binding)) sceneFail('SCENE_MODEL_MISMATCH', 'scene.rig.model', 'use the accepted package model path and hash');
220
231
  const animation = rig.animation as JsonObject;
221
- if (!objects(model.animations).some(value => value.name === animation.name)) sceneFail('SCENE_ANIMATION_MISSING', 'scene.rig.animation.name', 'choose an animation named by the model');
222
232
  const registration = isObject(version.registration) ? version.registration : {};
223
- if (registration.idle !== undefined && !sceneBinding(registration.idle) || !equal(animation.frames, registration.idle)) sceneFail('SCENE_MALFORMED', 'scene.rig.animation.frames', 'frames must be present exactly when registration.idle exists and equal that binding');
233
+ validateSceneAnimation(model, animation, registration);
224
234
  if (isObject(animation.frames)) boundBytes(dir, animation.frames, 'SCENE_FILE_CHANGED', snapshots);
225
235
  if (model.stage === null || model.stage === undefined) sceneFail('SCENE_STAGE_MISSING', String(binding.path), 'compile a model with a stage box');
226
236
  if (!boxShape(model.stage) || !Object.values(model.stage).every(finite) || Number(model.stage.width) <= 0 || Number(model.stage.height) <= 0) sceneFail('SCENE_MALFORMED', `${String(binding.path)}.stage`, 'the model stage must be a finite positive box');
package/src/signable.ts CHANGED
@@ -8,6 +8,7 @@ import { JOB_PLAN_BOUND_SIGNOFF_STAGES, type JobSelector, type JobSubject } from
8
8
  import { asArray, collectSignoffs, isObject, type CheckReport } from './check.ts';
9
9
  import { refuse } from './refusal.ts';
10
10
  import { collectReopens, liveSignoff } from './reopen.ts';
11
+ import { acceptedReviewVerdict } from './review.ts';
11
12
  import { SIGNOFF_TARGET_KINDS } from './review-act.ts';
12
13
  import { STAGES, stageOfStep } from './stages.ts';
13
14
  import type { JsonObject } from './templates.ts';
@@ -27,7 +28,7 @@ export function assertInterviewRecordSignable(project: JsonObject, report: Check
27
28
  }
28
29
 
29
30
  /** Why `signoff <stage> --checkpoint <id>` refuses the step, or null when it can sign it. */
30
- export function checkpointProblem(project: JsonObject, stage: string, id: string): string | null {
31
+ export function checkpointProblem(project: JsonObject, stage: string, id: string, now = Infinity): string | null {
31
32
  const spec = STAGES.find(spec => spec.id === stage);
32
33
  const matches = asArray(isObject(project.plan) ? project.plan.steps : undefined)
33
34
  .filter((step): step is JsonObject => isObject(step) && step.id === id);
@@ -35,7 +36,28 @@ export function checkpointProblem(project: JsonObject, stage: string, id: string
35
36
  if (matches.length !== 1 || spec?.phase === undefined || step.phase !== spec.phase || stageOfStep(spec.phase, id) !== stage) {
36
37
  return `${id}: no unique step runs in stage ${stage}`;
37
38
  }
38
- return checkpointRequired(project, step) ? null : `${id}: checkpoint must be enabled before it can be signed`;
39
+ return checkpointRequired(project, step, now) ? null : `${id}: checkpoint must be enabled before it can be signed`;
40
+ }
41
+
42
+ /** An enabled, unwithdrawn checkpoint in `stage` that has no live checkpoint signature yet. */
43
+ export function checkpointSignable(project: JsonObject, stage: string | null, id: string, now = Infinity): boolean {
44
+ if (stage === null || checkpointProblem(project, stage, id, now) !== null) return false;
45
+ const reopens = collectReopens(project, []);
46
+ return !collectSignoffs(project, []).some(signature => signature.stage === stage && signature.checkpoint === id
47
+ && liveSignoff(reopens, signature.stage, signature.at));
48
+ }
49
+
50
+ /** Whether the displayed version still needs a live accepted review verdict for `stage`. */
51
+ export function reviewVerdictSignable(project: JsonObject, n: number, stage: 'rig-review' | 'direction-review'): boolean {
52
+ const version = asArray(project.versions).filter(isObject).find(v => v.n === n);
53
+ if (!version) return true;
54
+ const index = acceptedReviewVerdict(version, stage);
55
+ const verdict = index >= 0 ? asArray(version.verdicts)[index] : undefined;
56
+ if (!isObject(verdict)) return true;
57
+ const reopens = collectReopens(project, []);
58
+ const versionAt = Date.parse(String(version.at));
59
+ return (Number.isFinite(versionAt) && !liveSignoff(reopens, stage === 'direction-review' ? 'post' : 'rig-review', versionAt))
60
+ || !liveSignoff(reopens, stage, Date.parse(String(verdict.at)));
39
61
  }
40
62
 
41
63
  /** A stage closed by a sign-off that has no live stage-level signature yet. */
@@ -56,7 +78,7 @@ export function stageSignable(project: JsonObject, stage: string | null): boolea
56
78
  export function packageSignSubjects(project: JsonObject, stage: string | null, selector: JobSelector, planSteps: number): JobSubject[] {
57
79
  if (stage === null) return [];
58
80
  const subjects: JobSubject[] = asArray(isObject(project.plan) ? project.plan.steps : undefined)
59
- .filter((step): step is JsonObject => isObject(step) && typeof step.id === 'string' && checkpointProblem(project, stage, step.id) === null)
81
+ .filter((step): step is JsonObject => isObject(step) && typeof step.id === 'string' && checkpointSignable(project, stage, step.id))
60
82
  .map(step => ({ kind: 'checkpoint', checkpointId: String(step.id) }));
61
83
  const target = SIGNOFF_TARGET_KINDS[stage];
62
84
  if (stageSignable(project, stage) && (target === undefined || selector.kind === target)