@rig-c/create 1.0.4 → 1.2.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/docs/LIVE-REVIEW.md +4 -4
- package/package.json +3 -2
- 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/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)
|
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
5
|
import { isDeepStrictEqual } from 'node:util';
|
|
7
6
|
import { asArray, isObject } from './check.ts';
|
|
8
7
|
import { jobHash } from './job-outbox.ts';
|
|
9
|
-
import { jobRuntimeExecArgv } from './job-writer.ts';
|
|
10
8
|
import { refuse } from './refusal.ts';
|
|
11
|
-
import { assertActSubjectDeclared, checkpointActSignoff, decodeReviewAct, reviewActFindings, reviewActTransport, type CheckpointAct, type ReviewActTransport } from './review-act.ts';
|
|
9
|
+
import { assertActSubjectDeclared, checkpointActSignoff, decodeReviewAct, fetchReviewActBytes, reviewActFindings, reviewActTransport, type CheckpointAct, type ReviewActTransport } from './review-act.ts';
|
|
12
10
|
import { stepHash } from './step-hash.ts';
|
|
13
11
|
import { readCheckpointEvidence } from './checkpoint-evidence.ts';
|
|
14
12
|
import type { JsonObject } from './templates.ts';
|
|
@@ -29,24 +27,12 @@ export interface CheckpointIngest {
|
|
|
29
27
|
replay?: JsonObject;
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
function fetchBytes(transport: ReviewActTransport, hooks: SignoffHooks): string {
|
|
33
|
-
if (hooks.fetch) return hooks.fetch(transport).bytes;
|
|
34
|
-
const child = Bun.spawnSync([process.execPath, ...jobRuntimeExecArgv(), fileURLToPath(new URL('./review-act-runtime.ts', import.meta.url))], {
|
|
35
|
-
stdin: Buffer.from(JSON.stringify(transport)), stdout: 'pipe', stderr: 'pipe',
|
|
36
|
-
});
|
|
37
|
-
let response: { ok: boolean; bytes: string; code: string; message: string };
|
|
38
|
-
try { response = JSON.parse(child.stdout.toString()); }
|
|
39
|
-
catch { return refuse('REVIEW_ACT_FETCH_REFUSED', 'project.json.review_acts: fetch adapter failed; restore the live Review job'); }
|
|
40
|
-
if (!response.ok || child.exitCode) refuse(response.code, response.message);
|
|
41
|
-
return response.bytes;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
30
|
/** Fetches, decodes and binds the act to the `--checkpoint` step the command already accepted. */
|
|
45
31
|
export function ingestCheckpointAct(dir: string, project: JsonObject, actId: string, checkpointId: string, now: number, hooks: SignoffHooks): CheckpointIngest {
|
|
46
32
|
const invalid = reviewActFindings(project)[0];
|
|
47
33
|
if (invalid) refuse(invalid.code, invalid.message);
|
|
48
34
|
const transport: ReviewActTransport = { ...reviewActTransport(dir, actId), kind: 'checkpoint' };
|
|
49
|
-
const bytes =
|
|
35
|
+
const bytes = hooks.fetch ? hooks.fetch(transport).bytes : fetchReviewActBytes(transport);
|
|
50
36
|
const act = decodeReviewAct(bytes, 'checkpoint');
|
|
51
37
|
if (act.jobId !== transport.jobId || act.actId !== transport.actId || Date.parse(act.at) > now) {
|
|
52
38
|
refuse('REVIEW_ACT_INVALID', `signoffs.checkpoint ${checkpointId}: fetched act identity or date differs from the request; fetch the checkpoint act again`);
|
package/src/signoff.ts
CHANGED
|
@@ -6,13 +6,12 @@ import { assertDirectionApproval } from './direction.ts';
|
|
|
6
6
|
import { assertCorrectionApproval } from './correction-writers.ts';
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
8
|
import { readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
9
|
-
import { acquireBoundJobWrite
|
|
9
|
+
import { acquireBoundJobWrite } from './job-writer.ts';
|
|
10
10
|
import { assertInterviewRecordSignable, checkpointProblem } from './signable.ts';
|
|
11
11
|
import { checkpointReceipt, commitIngestedSignoff, ingestCheckpointAct, type CheckpointIngest, type SignoffHooks } from './signoff-checkpoint-act.ts';
|
|
12
12
|
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
13
|
-
import { fileURLToPath } from 'node:url';
|
|
14
13
|
import { jobHash } from './job-outbox.ts';
|
|
15
|
-
import { assertActSubjectDeclared, decodeReviewAct, isRecordDirectionSignoff, isRecordInterviewSignoff, recordSignoffActVersion, PLAN_BOUND_SIGNOFF_STAGES, reviewActFindings, reviewActSignoff, reviewActTransport, SIGNOFF_TARGET_KINDS, signoffActIdentity, signoffActVersion, type ReviewActTransport, type SignoffAct } from './review-act.ts';
|
|
14
|
+
import { assertActSubjectDeclared, decodeReviewAct, fetchReviewActBytes, isRecordDirectionSignoff, isRecordInterviewSignoff, recordSignoffActVersion, PLAN_BOUND_SIGNOFF_STAGES, reviewActFindings, reviewActSignoff, reviewActTransport, SIGNOFF_TARGET_KINDS, signoffActIdentity, signoffActVersion, type ReviewActTransport, type SignoffAct } from './review-act.ts';
|
|
16
15
|
|
|
17
16
|
import { asArray, checkProject, classify, collectItems, collectSignoffs, isObject, loadCollection, postNotPassed, type Finding } from './check.ts';
|
|
18
17
|
import { checkDiffMap } from './diff-map.ts';
|
|
@@ -52,18 +51,7 @@ function fetchSignoffAct(dir: string, stage: string, actId: string, now: number,
|
|
|
52
51
|
const invalid = reviewActFindings(readProject(dir).project)[0];
|
|
53
52
|
if (invalid) refuse(invalid.code, invalid.message);
|
|
54
53
|
const transport: ReviewActTransport = { ...reviewActTransport(dir, actId), kind: 'signoff' };
|
|
55
|
-
|
|
56
|
-
if (hooks.fetch) bytes = hooks.fetch(transport).bytes;
|
|
57
|
-
else {
|
|
58
|
-
const child = Bun.spawnSync([process.execPath, ...jobRuntimeExecArgv(), fileURLToPath(new URL('./review-act-runtime.ts', import.meta.url))], {
|
|
59
|
-
stdin: Buffer.from(JSON.stringify(transport)), stdout: 'pipe', stderr: 'pipe',
|
|
60
|
-
});
|
|
61
|
-
let response: { ok: boolean; bytes: string; code: string; message: string };
|
|
62
|
-
try { response = JSON.parse(child.stdout.toString()); }
|
|
63
|
-
catch { return refuse('REVIEW_ACT_FETCH_REFUSED', 'project.json.review_acts: fetch adapter failed; restore the live Review job'); }
|
|
64
|
-
if (!response.ok || child.exitCode) refuse(response.code, response.message);
|
|
65
|
-
bytes = response.bytes;
|
|
66
|
-
}
|
|
54
|
+
const bytes = hooks.fetch ? hooks.fetch(transport).bytes : fetchReviewActBytes(transport);
|
|
67
55
|
const act = decodeReviewAct(bytes, 'signoff');
|
|
68
56
|
if (act.jobId !== transport.jobId || act.actId !== transport.actId || Date.parse(act.at) > now) {
|
|
69
57
|
refuse('REVIEW_ACT_INVALID', 'project.json.review_acts: fetched signoff identity or date differs from the request; fetch the correct signoff act');
|
package/src/waypoints.ts
CHANGED
|
@@ -17,7 +17,6 @@ export type Placement =
|
|
|
17
17
|
| { readonly kind: 'none'; readonly why: string };
|
|
18
18
|
|
|
19
19
|
const wp = (waypoint: string): Placement => ({ kind: 'waypoint', waypoint });
|
|
20
|
-
const post = (item: string): Placement => ({ kind: 'post', item });
|
|
21
20
|
const none = (why: string): Placement => ({ kind: 'none', why });
|
|
22
21
|
|
|
23
22
|
export const SECTION_PLACEMENT: Readonly<Record<string, Placement>> = {
|
|
@@ -33,8 +32,8 @@ export const SECTION_PLACEMENT: Readonly<Record<string, Placement>> = {
|
|
|
33
32
|
stops: wp('constraints'),
|
|
34
33
|
cloth: wp('constraints'),
|
|
35
34
|
motion: wp('motion'),
|
|
36
|
-
turn:
|
|
37
|
-
depth:
|
|
35
|
+
turn: wp('turn'),
|
|
36
|
+
depth: wp('turn'),
|
|
38
37
|
key: none('a name'),
|
|
39
38
|
generation: none('a record of a run'),
|
|
40
39
|
proposed_rig_scale: none('a value the tool proposes'),
|
|
@@ -49,17 +48,16 @@ export const SECTION_PLACEMENT: Readonly<Record<string, Placement>> = {
|
|
|
49
48
|
};
|
|
50
49
|
|
|
51
50
|
/** Pipeline order of the rig waypoints; a waypoint not listed here sorts last, by name. */
|
|
52
|
-
export const WAYPOINT_ORDER: readonly string[] = ['seethrough', 'assemble', 'bones', 'meshes', 'constraints', 'motion'];
|
|
51
|
+
export const WAYPOINT_ORDER: readonly string[] = ['seethrough', 'assemble', 'bones', 'meshes', 'constraints', 'turn', 'motion'];
|
|
53
52
|
|
|
54
53
|
/** The framing questions asked before the waypoints (INTERVIEW.md F1, F2). */
|
|
55
54
|
export const FRAMING_IDS: readonly string[] = ['rig_type', 'reference'];
|
|
56
55
|
|
|
57
56
|
/**
|
|
58
57
|
* Post items with no config section of their own (INTERVIEW.md § Post-production).
|
|
59
|
-
*
|
|
60
|
-
* sections through the table above.
|
|
58
|
+
* Camera framing remains post; rig depth and turn are asked before rigging.
|
|
61
59
|
*/
|
|
62
|
-
export const SECTIONLESS_POST_ITEMS: readonly string[] = ['post_timing', 'post_composition'];
|
|
60
|
+
export const SECTIONLESS_POST_ITEMS: readonly string[] = ['camera_depth', 'post_timing', 'post_composition'];
|
|
63
61
|
|
|
64
62
|
export interface DerivedWaypoint {
|
|
65
63
|
readonly id: string;
|
|
@@ -86,12 +84,13 @@ function byOrder(a: string, b: string): number {
|
|
|
86
84
|
}
|
|
87
85
|
|
|
88
86
|
/** Places every section parts has; the result is what a project's interview is checked against. */
|
|
89
|
-
export function deriveWaypoints(sections: readonly string[]): DerivedSet {
|
|
87
|
+
export function deriveWaypoints(sections: readonly string[], turnInterview = true): DerivedSet {
|
|
90
88
|
const waypoints = new Map<string, string[]>();
|
|
91
89
|
const items = new Map<string, string[]>();
|
|
92
90
|
const unplaced: string[] = [];
|
|
93
91
|
for (const section of sections) {
|
|
94
|
-
const placement =
|
|
92
|
+
const placement = !turnInterview && (section === 'turn' || section === 'depth')
|
|
93
|
+
? { kind: 'post' as const, item: 'camera_depth' } : SECTION_PLACEMENT[section];
|
|
95
94
|
if (placement === undefined) {
|
|
96
95
|
unplaced.push(section);
|
|
97
96
|
} else if (placement.kind === 'waypoint') {
|
|
@@ -117,6 +116,17 @@ export interface WaypointAsk {
|
|
|
117
116
|
readonly required_for?: 'fullscreen.game_logic' | 'other';
|
|
118
117
|
}
|
|
119
118
|
|
|
119
|
+
/** New depth obligations are dated by the rig build; old fixed-view records retain their mapping. */
|
|
120
|
+
export const TURN_QUESTION_SPEC = 'rig-create/turn-question@1';
|
|
121
|
+
export const HEAD_TURN_SINCE = '2026-11-14T00:00:00Z';
|
|
122
|
+
export const TURN_QUESTION = 'Should this rig keep the painted view, or turn its head with depth?';
|
|
123
|
+
export const TURN_OPTIONS = [
|
|
124
|
+
{ id: 'fixed', kind: 'value', label: 'Fixed view', effect: 'Keep the painted view.', fragment: { mode: 'fixed' } },
|
|
125
|
+
{ id: 'turn', kind: 'value', label: 'Head turn', effect: 'The head turns with depth, like the head-turn demo.', fragment: { mode: 'depth' } },
|
|
126
|
+
{ id: 'other', kind: 'other', label: 'Other', effect: 'Describe the view you want.' },
|
|
127
|
+
{ id: 'auto', kind: 'auto', label: 'Let the agent decide', effect: 'The agent records its decision before approval.' },
|
|
128
|
+
] as const;
|
|
129
|
+
|
|
120
130
|
export const WAYPOINT_ASKS: Readonly<Record<string, readonly WaypointAsk[]>> = {
|
|
121
131
|
seethrough: [
|
|
122
132
|
{ id: 'layers', text: 'What must the layer split keep together or apart?' },
|
|
@@ -134,6 +144,7 @@ export const WAYPOINT_ASKS: Readonly<Record<string, readonly WaypointAsk[]>> = {
|
|
|
134
144
|
{ id: 'occluders', text: 'What is kept under occluders?' },
|
|
135
145
|
{ id: 'outfits', text: 'Which outfit sets exist?' },
|
|
136
146
|
],
|
|
147
|
+
turn: [{ id: 'view', text: TURN_QUESTION }],
|
|
137
148
|
bones: [
|
|
138
149
|
{ id: 'chains', text: 'Which parts get chains or stay rigid?' },
|
|
139
150
|
{ id: 'placement', text: 'Where do bones sit?' },
|