@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/reopen.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { readFileSync } from './check-reads.ts';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
4
|
import { resolve } from 'node:path';
|
|
3
5
|
|
|
4
|
-
import { asArray, asString, isObject, type Finding } from './check.ts';
|
|
6
|
+
import { asArray, asString, collectSignoffs, isObject, type Finding } from './check.ts';
|
|
5
7
|
import { collectAmendments } from './amend.ts';
|
|
6
8
|
import { readProject } from './feedback.ts';
|
|
9
|
+
import { jobHash } from './job-outbox.ts';
|
|
10
|
+
import { acquireBoundJobWrite } from './job-writer.ts';
|
|
11
|
+
import { assertActSubjectDeclared, decodeReviewAct, fetchReviewActBytes, reviewActFindings, reviewActTransport, type ReviewActTransport, type ReopenAct } from './review-act.ts';
|
|
12
|
+
import { stepHash } from './step-hash.ts';
|
|
7
13
|
import { readVerbatim } from './record-input.ts';
|
|
8
14
|
import { refuse, USAGE_EXIT } from './refusal.ts';
|
|
9
15
|
import { newRefusals } from './refusal-baseline.ts';
|
|
@@ -77,6 +83,16 @@ function reachedIndex(project: JsonObject, at: number, fromStage: string): numbe
|
|
|
77
83
|
return reached;
|
|
78
84
|
}
|
|
79
85
|
|
|
86
|
+
/** An acceptance remains authoring authority until an explicit reopen supersedes it. */
|
|
87
|
+
export function directionReviewClosed(project: JsonObject): boolean {
|
|
88
|
+
const reopens = collectReopens(project, []);
|
|
89
|
+
return collectSignoffs(project, []).some(signoff => signoff.stage === 'direction-review'
|
|
90
|
+
&& signoff.checkpoint === null && liveSignoff(reopens, signoff.stage, signoff.at))
|
|
91
|
+
|| asArray(project.versions).filter(isObject).some(version => version.kind === 'post'
|
|
92
|
+
&& asArray(version.verdicts).filter(isObject).some(verdict => verdict.review === 'direction-review'
|
|
93
|
+
&& verdict.verdict === 'accepted' && liveSignoff(reopens, 'direction-review', Date.parse(String(verdict.at)))));
|
|
94
|
+
}
|
|
95
|
+
|
|
80
96
|
/** Each reopen invalidates its suffix; a later, narrower reopen cannot resurrect an earlier suffix. */
|
|
81
97
|
export function reopenTime(reopens: readonly Reopen[], stage: string): number {
|
|
82
98
|
return Math.max(-Infinity, ...reopens.filter(r => stageIndex(r.stage) <= stageIndex(stage)).map(r => r.at));
|
|
@@ -94,38 +110,77 @@ export function liveSignoff(reopens: readonly Reopen[], stage: string, at: numbe
|
|
|
94
110
|
|
|
95
111
|
export const REOPEN_HELP = `reopen:
|
|
96
112
|
<stage> [<project dir>] record a human backward move and set project.stage
|
|
113
|
+
--from-review <actId> ingest the exported Review reopen (excludes --by, --verbatim-file and --source)
|
|
97
114
|
--by <who> human signer (agent is refused)
|
|
98
115
|
--verbatim-file <file> unchanged UTF-8 words; relative to the current working directory, not the project; or - for stdin (default)
|
|
99
116
|
--source <text> optional origin of the words, separate from verbatim
|
|
100
117
|
--json print the reopen record
|
|
101
118
|
The clock supplies at; sign-offs and versions remain history.`;
|
|
102
119
|
|
|
103
|
-
export function reopenCommand(args: string[], now: number): { stdout: string; exit: number } {
|
|
120
|
+
export function reopenCommand(args: string[], now: number, hooks: { fetch?: (transport: ReviewActTransport) => { bytes: string } } = {}): { stdout: string; exit: number } {
|
|
104
121
|
const opts: Record<string, string> = {};
|
|
105
122
|
const positional: string[] = [];
|
|
106
123
|
let json = false;
|
|
107
124
|
for (let i = 0; i < args.length; i++) {
|
|
108
125
|
const arg = args[i] as string;
|
|
109
126
|
if (arg === '--json') json = true;
|
|
110
|
-
else if (['--by', '--verbatim-file', '--source'].includes(arg)) {
|
|
127
|
+
else if (['--by', '--verbatim-file', '--source', '--from-review'].includes(arg)) {
|
|
111
128
|
const value = args[++i];
|
|
112
129
|
if (arg in opts || value === undefined || value === '') refuse('REOPEN_OPTION_INVALID', `reopens: ${arg} needs one non-empty value; supply it once`, USAGE_EXIT);
|
|
113
130
|
opts[arg] = value;
|
|
114
131
|
} else if (arg.startsWith('-')) refuse('UNKNOWN_OPTION', `reopens: unsupported ${arg}; see reopen help`, USAGE_EXIT);
|
|
115
132
|
else positional.push(arg);
|
|
116
133
|
}
|
|
117
|
-
|
|
134
|
+
const actId = opts['--from-review'];
|
|
135
|
+
if (positional.length < 1 || positional.length > 2 || actId === undefined && opts['--by'] === undefined) refuse('REOPEN_OPTION_INVALID', 'reopens: supply a stage, --by or --from-review, and one optional project directory', USAGE_EXIT);
|
|
136
|
+
if (actId !== undefined && ['--by', '--verbatim-file', '--source'].some(flag => opts[flag] !== undefined)) {
|
|
137
|
+
refuse('REOPEN_OPTION_INVALID', 'project.json.reopens: --from-review excludes --by, --verbatim-file and --source; use the exported human values', USAGE_EXIT);
|
|
138
|
+
}
|
|
118
139
|
const dir = resolve(positional[1] ?? process.cwd());
|
|
140
|
+
const release = actId === undefined ? () => {} : acquireBoundJobWrite(dir);
|
|
141
|
+
try { return recordReopen(dir, positional[0]!, opts, json, now, hooks); }
|
|
142
|
+
finally { release(); }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function recordReopen(dir: string, stage: string, opts: Record<string, string>, json: boolean, now: number,
|
|
146
|
+
hooks: { fetch?: (transport: ReviewActTransport) => { bytes: string } }): { stdout: string; exit: number } {
|
|
119
147
|
const { file, project } = readProject(dir);
|
|
148
|
+
const original = readFileSync(file);
|
|
149
|
+
let review: { act: ReopenAct; bytes: string } | undefined;
|
|
150
|
+
const result = (entry: JsonObject): { stdout: string; exit: number } => ({ stdout: json ? JSON.stringify(entry, null, 2) : `reopen: ${String(entry.stage)} by ${String(entry.by)} at ${String(entry.at)}`, exit: 0 });
|
|
151
|
+
const actId = opts['--from-review'];
|
|
152
|
+
if (actId !== undefined) {
|
|
153
|
+
const invalid = reviewActFindings(project)[0];
|
|
154
|
+
if (invalid) refuse(invalid.code, invalid.message);
|
|
155
|
+
const transport: ReviewActTransport = { ...reviewActTransport(dir, actId), kind: 'reopen' };
|
|
156
|
+
const bytes = hooks.fetch ? hooks.fetch(transport).bytes : fetchReviewActBytes(transport);
|
|
157
|
+
const act = decodeReviewAct(bytes, 'reopen');
|
|
158
|
+
if (act.jobId !== transport.jobId || act.actId !== actId || Date.parse(act.at) > now) {
|
|
159
|
+
refuse('REVIEW_ACT_INVALID', 'project.json.reopens: fetched act identity or date differs from the request; request the correct reopen act');
|
|
160
|
+
}
|
|
161
|
+
if (act.value.stage !== stage) refuse('REVIEW_ACT_KIND_MISMATCH', `project.json.reopens: act reopens ${act.value.stage}, not ${stage}; invoke reopen for the exported stage`);
|
|
162
|
+
const existing = asArray(project.review_acts).filter(isObject).find(entry => entry.jobId === act.jobId && entry.actId === actId);
|
|
163
|
+
if (existing) {
|
|
164
|
+
if (existing.kind !== 'reopen' || existing.bytes !== bytes) refuse('REVIEW_ACT_REPLAY_CONFLICT', 'project.json.review_acts: immutable reopen act changed; restore the original receipt');
|
|
165
|
+
return result(asArray(project.reopens)[Number(existing.reopen)] as JsonObject);
|
|
166
|
+
}
|
|
167
|
+
if (act.source.projectHash !== jobHash(original) || !isDeepStrictEqual(act.source.plan.steps,
|
|
168
|
+
asArray(isObject(project.plan) ? project.plan.steps : undefined).map(raw => ({ id: isObject(raw) ? raw.id : null, hash: isObject(raw) ? stepHash(raw) : null })))) {
|
|
169
|
+
refuse('REVIEW_ACT_SOURCE_STALE', 'project.json.reopens: displayed record or plan changed; request a reopen on the current record');
|
|
170
|
+
}
|
|
171
|
+
assertActSubjectDeclared(dir, act, 'project.json.reopens');
|
|
172
|
+
review = { act, bytes };
|
|
173
|
+
}
|
|
120
174
|
const before = structuredClone(project);
|
|
121
175
|
const steps = asArray(isObject(project.plan) ? project.plan.steps : undefined);
|
|
122
176
|
if (steps.some(s => !isObject(s) || typeof s.id !== 'string' || (s.phase !== 'rig' && s.phase !== 'post'))) {
|
|
123
177
|
refuse('REOPEN_MALFORMED', 'plan.steps: each step needs id and phase; restore the plan before reopening');
|
|
124
178
|
}
|
|
125
179
|
const affected = steps.filter((s): s is JsonObject => isObject(s)
|
|
126
|
-
&& stageIndex(stageOfStep(s.phase as 'rig' | 'post', s.id as string)) >= stageIndex(
|
|
127
|
-
const entry: JsonObject = { from_stage: project.stage ?? null, steps_before: affected.map(s => ({ id: s.id as string, ...(s.status === undefined ? {} : { status: s.status }) })), stage
|
|
128
|
-
if (
|
|
180
|
+
&& stageIndex(stageOfStep(s.phase as 'rig' | 'post', s.id as string)) >= stageIndex(stage));
|
|
181
|
+
const entry: JsonObject = { from_stage: project.stage ?? null, steps_before: affected.map(s => ({ id: s.id as string, ...(s.status === undefined ? {} : { status: s.status }) })), stage, by: review?.act.authority.reviewer.id ?? opts['--by']!, at: review?.act.at ?? new Date(now).toISOString(), verbatim: '' };
|
|
182
|
+
if (review) entry.source = `review act ${review.act.jobId}/${review.act.actId}`;
|
|
183
|
+
else if (opts['--source'] !== undefined) entry.source = opts['--source'];
|
|
129
184
|
const refusals: Finding[] = [];
|
|
130
185
|
// Validate against the original reached stage before moving it backwards.
|
|
131
186
|
const candidate = { ...project, reopens: [...asArray(project.reopens), entry] };
|
|
@@ -133,12 +188,20 @@ export function reopenCommand(args: string[], now: number): { stdout: string; ex
|
|
|
133
188
|
collectReopens(candidate, refusals);
|
|
134
189
|
const first = refusals[0];
|
|
135
190
|
if (first !== undefined) refuse(first.code, first.message);
|
|
136
|
-
entry.verbatim = readVerbatim(opts['--verbatim-file'] ?? '-');
|
|
191
|
+
entry.verbatim = review ? review.act.value.words : readVerbatim(opts['--verbatim-file'] ?? '-');
|
|
137
192
|
for (const step of affected) step.status = 'pending';
|
|
138
193
|
project.reopens = candidate.reopens;
|
|
139
194
|
project.stage = entry.stage;
|
|
195
|
+
if (review) {
|
|
196
|
+
project.review_acts = [...asArray(project.review_acts), { schema: 'rig-create/review-act@1', kind: 'reopen',
|
|
197
|
+
jobId: review.act.jobId, actId: review.act.actId, reopen: asArray(project.reopens).length - 1,
|
|
198
|
+
bytes: review.bytes, sha256: jobHash(review.bytes), version_hash: jobHash(JSON.stringify(entry)) }];
|
|
199
|
+
const invalid = reviewActFindings(project)[0];
|
|
200
|
+
if (invalid) refuse(invalid.code, invalid.message);
|
|
201
|
+
}
|
|
140
202
|
const rejected = newRefusals(dir, before, project, { now })[0];
|
|
141
203
|
if (rejected !== undefined) refuse(rejected.code, rejected.message);
|
|
204
|
+
if (review && !readFileSync(file).equals(original)) refuse('REVIEW_ACT_SOURCE_STALE', 'project.json.reopens: record changed during fetch; retry on the current record');
|
|
142
205
|
writeFileSync(file, `${JSON.stringify(project, null, 2)}\n`);
|
|
143
|
-
return
|
|
206
|
+
return result(entry);
|
|
144
207
|
}
|
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;
|