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.
- package/README.md +5 -5
- package/docs/AGENT-REFERENCE.md +62 -0
- package/docs/CHECKS.md +45 -9
- package/docs/INTERVIEW.md +19 -7
- package/package.json +2 -1
- package/src/agent-guide.ts +7 -4
- package/src/check.ts +7 -4
- package/src/choice-writers.ts +3 -14
- package/src/cli.ts +1 -0
- package/src/feedback.ts +3 -10
- package/src/fix-ledger.ts +1 -1
- package/src/job-acts.ts +4 -0
- package/src/job-host-state.ts +154 -0
- package/src/job-host.ts +37 -14
- package/src/job-outbox.ts +5 -3
- package/src/job-publication.ts +17 -13
- package/src/job-runtime.ts +17 -4
- package/src/next.ts +21 -13
- package/src/parts-review.ts +4 -11
- package/src/reopen.ts +72 -9
- package/src/review-act.ts +49 -14
- package/src/review-package.ts +3 -1
- package/src/rig-version.ts +219 -8
- package/src/scene-writers.ts +5 -3
- package/src/scene.ts +14 -4
- package/src/signable.ts +25 -3
- package/src/signoff-checkpoint-act.ts +2 -16
- package/src/signoff.ts +3 -15
- package/src/waypoints.ts +20 -9
- package/templates/agent-guide.md +2 -24
- package/templates/collection.json +10 -2
- package/templates/example-chain/AGENT-REFERENCE.md +62 -0
- package/templates/example-chain/AGENTS.md +4 -26
- package/templates/example-chain/README.md +1 -1
- package/templates/example-chain/chain.json +385 -319
- package/templates/example-chain/checkpoints/0a-interview-options/AGENT-REFERENCE.md +62 -0
- package/templates/example-chain/checkpoints/0a-interview-options/AGENTS.md +4 -26
- package/templates/example-chain/checkpoints/0a-interview-options/project.json +22 -5
- package/templates/example-chain/checkpoints/0a-interview-options.json +7 -3
- package/templates/example-chain/checkpoints/0b-plan-pending/AGENT-REFERENCE.md +62 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/AGENTS.md +4 -26
- package/templates/example-chain/checkpoints/0b-plan-pending/project.json +22 -5
- package/templates/example-chain/checkpoints/0b-plan-pending.json +7 -3
- package/templates/example-chain/checkpoints/1-parts-verdict/AGENT-REFERENCE.md +62 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/AGENTS.md +4 -26
- package/templates/example-chain/checkpoints/1-parts-verdict/derived/drafts/chain/out/check/build/run.json +2 -2
- package/templates/example-chain/checkpoints/1-parts-verdict/derived/drafts/chain/out/run.json +2 -2
- package/templates/example-chain/checkpoints/1-parts-verdict/project.json +24 -7
- package/templates/example-chain/checkpoints/1-parts-verdict.json +9 -5
- package/templates/example-chain/checkpoints/2-rigging-answers/AGENT-REFERENCE.md +62 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/AGENTS.md +4 -26
- package/templates/example-chain/checkpoints/2-rigging-answers/derived/drafts/chain/out/check/build/run.json +2 -2
- package/templates/example-chain/checkpoints/2-rigging-answers/derived/drafts/chain/out/run.json +2 -2
- package/templates/example-chain/checkpoints/2-rigging-answers/project.json +24 -7
- package/templates/example-chain/checkpoints/2-rigging-answers.json +21 -5
- package/templates/example-chain/checkpoints/3-rig-review/AGENT-REFERENCE.md +62 -0
- package/templates/example-chain/checkpoints/3-rig-review/AGENTS.md +4 -26
- package/templates/example-chain/checkpoints/3-rig-review/derived/drafts/chain/out/check/build/run.json +2 -2
- package/templates/example-chain/checkpoints/3-rig-review/derived/drafts/chain/out/run.json +2 -2
- package/templates/example-chain/checkpoints/3-rig-review/derived/review/v1/out/check/build/run.json +2 -2
- package/templates/example-chain/checkpoints/3-rig-review/derived/review/v1/out/rig/run.json +2 -2
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/motion.json +1 -1
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/turn.json +108 -0
- package/templates/example-chain/checkpoints/3-rig-review/outputs/v1/out/check/build/run.json +2 -2
- package/templates/example-chain/checkpoints/3-rig-review/outputs/v1/out/rig/run.json +2 -2
- package/templates/example-chain/checkpoints/3-rig-review/project.json +77 -28
- package/templates/example-chain/checkpoints/3-rig-review.json +19 -11
- package/templates/example-chain/checkpoints/4-direction-post/AGENT-REFERENCE.md +62 -0
- package/templates/example-chain/checkpoints/4-direction-post/AGENTS.md +4 -26
- package/templates/example-chain/checkpoints/4-direction-post/derived/drafts/chain/out/check/build/run.json +2 -2
- package/templates/example-chain/checkpoints/4-direction-post/derived/drafts/chain/out/run.json +2 -2
- package/templates/example-chain/checkpoints/4-direction-post/derived/review/v1/out/check/build/run.json +2 -2
- package/templates/example-chain/checkpoints/4-direction-post/derived/review/v1/out/rig/run.json +2 -2
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/camera_depth.json +2 -2
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/motion.json +1 -1
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/post_composition.json +2 -2
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/post_timing.json +2 -2
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/turn.json +108 -0
- package/templates/example-chain/checkpoints/4-direction-post/outputs/v1/out/check/build/run.json +2 -2
- package/templates/example-chain/checkpoints/4-direction-post/outputs/v1/out/rig/run.json +2 -2
- package/templates/example-chain/checkpoints/4-direction-post/project.json +106 -57
- package/templates/example-chain/checkpoints/4-direction-post.json +22 -14
- package/templates/example-chain/derived/drafts/chain/out/check/build/run.json +2 -2
- package/templates/example-chain/derived/drafts/chain/out/run.json +2 -2
- package/templates/example-chain/derived/review/v1/out/check/build/run.json +2 -2
- package/templates/example-chain/derived/review/v1/out/rig/run.json +2 -2
- package/templates/example-chain/evidence/captions.json +1 -1
- package/templates/example-chain/evidence/direction/v2.json +1 -1
- package/templates/example-chain/evidence/direction-input.json +1 -1
- package/templates/example-chain/evidence/interview/camera_depth.json +2 -2
- package/templates/example-chain/evidence/interview/motion.json +1 -1
- package/templates/example-chain/evidence/interview/post_composition.json +2 -2
- package/templates/example-chain/evidence/interview/post_timing.json +2 -2
- package/templates/example-chain/evidence/interview/turn.json +108 -0
- package/templates/example-chain/evidence/scene/s1.json +2 -2
- package/templates/example-chain/outputs/v1/out/check/build/run.json +2 -2
- package/templates/example-chain/outputs/v1/out/rig/run.json +2 -2
- package/templates/example-chain/project.json +127 -78
- package/templates/example-direction/derived/review/v1/out/check/build/run.json +2 -2
- package/templates/example-draft/derived/drafts/lantern/out/check/build/run.json +2 -2
- package/templates/example-draft/derived/drafts/lantern/out/run.json +2 -2
- package/templates/example-draft/derived/review/v1/out/check/build/run.json +2 -2
- package/templates/example-draft/derived/review/v1/out/rig/run.json +2 -2
- package/templates/example-draft/derived/review/v2/out/check/build/run.json +2 -2
- package/templates/example-draft/derived/review/v2/out/rig/run.json +2 -2
- package/templates/example-draft/evidence/direction/v4.json +3 -3
- package/templates/example-draft/evidence/direction-input-v4.json +3 -3
- package/templates/example-draft/evidence/parts-review.json +1 -1
- package/templates/example-draft/evidence/review-reads/{52673b635b9cbc92da1a9e3d4c023a249bbacf79cd21b59724f03f18f8caf09b.json → 8866887a00012b5d5944b7525a478aea70931935bff35ddfcb186ffc515ef5af.json} +2 -2
- package/templates/example-draft/evidence/scene/s1.json +3 -3
- package/templates/example-draft/evidence/scene-citations.json +1 -1
- package/templates/example-draft/out/check/build/run.json +2 -2
- package/templates/example-draft/out/run.json +2 -2
- package/templates/example-draft/outputs/v1/out/check/build/run.json +2 -2
- package/templates/example-draft/outputs/v1/out/rig/run.json +2 -2
- package/templates/example-draft/outputs/v2/out/check/build/run.json +2 -2
- package/templates/example-draft/outputs/v2/out/rig/run.json +2 -2
- package/templates/example-interview-choice.json +113 -68
- package/templates/example-interview-options/constraint.json +2 -2
- package/templates/example-interview-options/example-assemble.json +2 -2
- package/templates/example-interview-options/example-bones.json +5 -5
- package/templates/example-interview-options/example-camera-depth.json +3 -3
- package/templates/example-interview-options/example-constraints.json +5 -5
- package/templates/example-interview-options/example-meshes.json +5 -5
- package/templates/example-interview-options/example-motion.json +5 -5
- package/templates/example-interview-options/example-picks.json +2 -2
- package/templates/example-interview-options/example-post-composition.json +3 -3
- package/templates/example-interview-options/example-post-timing.json +3 -3
- package/templates/example-interview-options/example-reference.json +2 -2
- package/templates/example-interview-options/example-rig-type.json +2 -2
- package/templates/example-interview-options/example-seethrough.json +2 -2
- package/templates/example-interview-options/example-turn.json +108 -0
- package/templates/example-interview-options/fullscreen.json +2 -2
- package/templates/example-interview-options/other-explicit.json +2 -2
- package/templates/example-interview-options/other-pending.json +2 -2
- package/templates/example-interview-options/other-picks.json +2 -2
- package/templates/example-invariants/invariants.json +223 -164
- package/templates/example-project.json +301 -249
- package/templates/example-remesh/AGENT-REFERENCE.md +62 -0
- package/templates/example-remesh/AGENTS.md +4 -26
- package/templates/example-remesh/derived/drafts/remesh/out/check/build/run.json +2 -2
- package/templates/example-remesh/derived/drafts/remesh/out/run.json +2 -2
- package/templates/example-remesh/derived/review/v1/out/check/build/run.json +2 -2
- package/templates/example-remesh/derived/review/v1/out/rig/run.json +2 -2
- package/templates/example-remesh/derived/review/v2/out/check/build/run.json +2 -2
- package/templates/example-remesh/derived/review/v2/out/rig/run.json +2 -2
- package/templates/example-remesh/derived/review/v3/out/check/build/run.json +2 -2
- package/templates/example-remesh/derived/review/v3/out/rig/run.json +2 -2
- package/templates/example-remesh/evidence/captions.json +1 -1
- package/templates/example-remesh/evidence/direction/v4.json +1 -1
- package/templates/example-remesh/evidence/direction/v5.json +1 -1
- package/templates/example-remesh/evidence/direction-input-v4.json +1 -1
- package/templates/example-remesh/evidence/direction-input-v5.json +1 -1
- package/templates/example-remesh/evidence/interview/assemble.json +1 -1
- package/templates/example-remesh/evidence/interview/bones.json +4 -4
- package/templates/example-remesh/evidence/interview/camera_depth.json +2 -2
- package/templates/example-remesh/evidence/interview/constraints.json +4 -4
- package/templates/example-remesh/evidence/interview/meshes.json +4 -4
- package/templates/example-remesh/evidence/interview/motion.json +4 -4
- package/templates/example-remesh/evidence/interview/post_composition.json +2 -2
- package/templates/example-remesh/evidence/interview/post_timing.json +2 -2
- package/templates/example-remesh/evidence/interview/reference.json +1 -1
- package/templates/example-remesh/evidence/interview/rig_type.json +1 -1
- package/templates/example-remesh/evidence/interview/seethrough.json +1 -1
- package/templates/example-remesh/evidence/interview/turn.json +108 -0
- package/templates/example-remesh/evidence/parts-review.json +1 -1
- package/templates/example-remesh/evidence/review-reads/{afe2522be4087f7f2fa7e97746d5f8574b0aedb474245ce2dbce0455e662f041.json → 99ade3eb19d70bd5f727138b3b4876ce86f1e1c22a59b516639bf85593996c32.json} +2 -2
- package/templates/example-remesh/evidence/scene/s1.json +2 -2
- package/templates/example-remesh/evidence/v1/registration/at-source.json +2 -2
- package/templates/example-remesh/evidence/v1/registration/rigc-run-1.json +2 -2
- package/templates/example-remesh/evidence/v2/registration/at-source.json +2 -2
- package/templates/example-remesh/evidence/v2/registration/rigc-run-1.json +2 -2
- package/templates/example-remesh/evidence/v3/registration/at-source.json +2 -2
- package/templates/example-remesh/evidence/v3/registration/rigc-run-1.json +2 -2
- package/templates/example-remesh/outputs/v1/out/check/build/run.json +2 -2
- package/templates/example-remesh/outputs/v1/out/rig/run.json +2 -2
- package/templates/example-remesh/outputs/v2/out/check/build/run.json +2 -2
- package/templates/example-remesh/outputs/v2/out/rig/run.json +2 -2
- package/templates/example-remesh/outputs/v3/out/check/build/run.json +2 -2
- package/templates/example-remesh/outputs/v3/out/rig/run.json +2 -2
- package/templates/example-remesh/project.json +279 -230
- package/templates/example-reopened/project.json +341 -289
- package/templates/example-split/captions.json +1 -1
- 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:
|
|
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: '
|
|
155
|
-
export function decodeReviewAct(bytes: string, kind:
|
|
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 (
|
|
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:
|
|
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?: '
|
|
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:
|
|
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:
|
|
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' ?
|
|
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;
|
package/src/review-package.ts
CHANGED
|
@@ -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;
|
package/src/rig-version.ts
CHANGED
|
@@ -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
|
-
/**
|
|
393
|
-
|
|
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)
|
|
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
|
+
}
|
package/src/scene-writers.ts
CHANGED
|
@@ -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
|
-
|
|
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'
|
|
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
|
-
|
|
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' &&
|
|
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)
|