@rig-c/review 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -7
- package/docs/DESIGN.md +2 -1
- package/docs/REVIEW_PACKAGE.md +17 -7
- package/package.json +1 -1
- package/src/applied-context.ts +35 -7
- package/src/cli.ts +1 -1
- package/src/cloth-recipe.ts +199 -0
- package/src/compare-server.ts +1 -1
- package/src/environment-check.ts +149 -0
- package/src/environment-schedule.ts +183 -0
- package/src/feedback-export.ts +8 -1
- package/src/frame-clock.ts +30 -0
- package/src/front.ts +4 -1
- package/src/handoff.ts +20 -13
- package/src/open-protocol.ts +4 -3
- package/src/open.ts +1 -1
- package/src/readings-run.ts +2 -1
- package/src/readings.ts +12 -6
- package/src/rebuild.ts +198 -34
- package/src/retune-check.ts +100 -0
- package/src/server.ts +12 -2
- package/src/session.ts +3 -0
- package/web/cloth-panel.ts +168 -0
- package/web/compare.ts +7 -5
- package/web/environment-world.ts +42 -0
- package/web/index.html +1 -1
- package/web/label-placement.ts +46 -0
- package/web/main.ts +127 -17
- package/web/readings-panel.ts +1 -1
- package/web/rebuild-client.ts +14 -9
- package/web/rig-overlays.ts +26 -10
- package/web/styles.css +2 -0
package/src/front.ts
CHANGED
|
@@ -61,11 +61,14 @@ export function frontRoute(mode:'single'|'compare',method:string,path:string):Ro
|
|
|
61
61
|
// Bones check (#109, single only): one typed request {ops, configHash}, answered in the response.
|
|
62
62
|
if(mode==='single'&&path==='/api/bones-check')return method==='POST'?{mutation:true,limit:64*1024,timeoutMs:60000,control:false}:null;
|
|
63
63
|
// Readings (#108, single only): one typed request, then its job's status.
|
|
64
|
-
if(mode==='single'&&path==='/api/readings')return method==='POST'?{mutation:true,limit:
|
|
64
|
+
if(mode==='single'&&path==='/api/readings')return method==='POST'?{mutation:true,limit:64*1024,timeoutMs:30000,control:false}:null;
|
|
65
65
|
if(mode==='single'&&/^\/api\/readings\/[a-f0-9]{64}$/.test(path))return read?get:null;
|
|
66
66
|
// Declared requirements (#113, single only): the stage's catalog, one typed request, then its job's status.
|
|
67
67
|
if(mode==='single'&&path==='/api/requirements')return read?get:method==='POST'?{mutation:true,limit:64*1024,timeoutMs:30000,control:false}:null;
|
|
68
68
|
if(mode==='single'&&/^\/api\/requirements\/[a-f0-9]{64}$/.test(path))return read?get:null;
|
|
69
|
+
// Check under this world (#112, single only): one typed request {build, schedule}, then its job's status.
|
|
70
|
+
if(mode==='single'&&path==='/api/environment-check')return method==='POST'?{mutation:true,limit:64*1024,timeoutMs:30000,control:false}:null;
|
|
71
|
+
if(mode==='single'&&/^\/api\/environment-check\/[a-f0-9]{64}$/.test(path))return read?get:null;
|
|
69
72
|
return null;
|
|
70
73
|
}
|
|
71
74
|
async function boundedBody(req:Request,limit:number,timeoutMs=10000):Promise<ArrayBuffer>{
|
package/src/handoff.ts
CHANGED
|
@@ -25,9 +25,9 @@ export const CONFIG_NAMED_FILES = [['motion', 'animations_from'], ['generation',
|
|
|
25
25
|
export const RIG_FILES = ['rig.json', 'motion.json', 'mesh_report.json'] as const;
|
|
26
26
|
const MAX_LISTED = 4096;
|
|
27
27
|
|
|
28
|
-
export interface HandoffDeclaration { config: string; parts?: string; source?: string; full?: string; head?: string }
|
|
28
|
+
export interface HandoffDeclaration { config: string; parts?: string; source?: string; full?: string; head?: string; keypoints?: string }
|
|
29
29
|
export type HandoffFlags = HandoffDeclaration;
|
|
30
|
-
export const HANDOFF_FLAGS = ['config', 'parts', 'source', 'full', 'head'] as const;
|
|
30
|
+
export const HANDOFF_FLAGS = ['config', 'parts', 'source', 'full', 'head', 'keypoints'] as const;
|
|
31
31
|
export interface HandoffFile { path: string; sha256: string }
|
|
32
32
|
/** The identity record: paths relative to the package root (`../` only for the discovered parent config), every hashed file. */
|
|
33
33
|
export interface HandoffRecord {
|
|
@@ -37,13 +37,16 @@ export interface HandoffRecord {
|
|
|
37
37
|
outside: boolean;
|
|
38
38
|
config: HandoffFile;
|
|
39
39
|
parts: string | null; rig: string | null; source: HandoffFile | null; full: string | null; head: string | null;
|
|
40
|
+
/** #111: the declared body-18 keypoint file `rig-parts propose --keypoints` reads; the key is absent when none is declared, so an undeclared handoff's identity is unchanged. */
|
|
41
|
+
keypoints?: HandoffFile;
|
|
40
42
|
files: HandoffFile[];
|
|
41
43
|
missing: string[];
|
|
42
44
|
}
|
|
43
45
|
/** The record plus the absolute paths the producer is run with (never served, never in the identity). */
|
|
44
46
|
/** `source` (#109): the declared source painting's absolute path, which `propose --from-config` reads; null when none was declared. */
|
|
45
47
|
/** `full` / `head` (#115): the declared See-through runs' absolute paths, which `rig-parts build` reads; null when none was declared. */
|
|
46
|
-
|
|
48
|
+
/** `keypoints` (#111): the declared keypoint file's absolute path, which a cloth recipe Apply's `propose --keypoints` reads; null when none was declared. */
|
|
49
|
+
export interface Handoff { record: HandoffRecord; config: string; parts: string | null; rig: string | null; root: string; flags?: HandoffFlags; source?: string | null; full?: string | null; head?: string | null; keypoints?: string | null }
|
|
47
50
|
|
|
48
51
|
export class HandoffError extends Error {
|
|
49
52
|
constructor(readonly code: string, readonly field: string, readonly detail: string) { super(`${code} ${field}: ${detail}`); }
|
|
@@ -87,15 +90,15 @@ async function blockPath(root: string, value: unknown, field: string, kind: 'fil
|
|
|
87
90
|
return contained(root, join(root, value), field, kind, true);
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
/** `producer: {config, parts?, source?, full?, head?}` — strings only, no other key. Paths are checked at load. */
|
|
93
|
+
/** `producer: {config, parts?, source?, full?, head?, keypoints?}` — strings only, no other key. Paths are checked at load. */
|
|
91
94
|
export function parseHandoffDeclaration(value: unknown): HandoffDeclaration | undefined {
|
|
92
95
|
if (value === undefined) return undefined;
|
|
93
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new HandoffError('HANDOFF_PATH_INVALID', 'producer', 'expected an object {config, parts?, source?, full?, head?}');
|
|
96
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new HandoffError('HANDOFF_PATH_INVALID', 'producer', 'expected an object {config, parts?, source?, full?, head?, keypoints?}');
|
|
94
97
|
const v = value as Record<string, unknown>;
|
|
95
|
-
for (const key of Object.keys(v)) if (!(HANDOFF_FLAGS as readonly string[]).includes(key)) throw new HandoffError('HANDOFF_PATH_INVALID', `producer.${key}`, 'unknown key; expected config, parts, source, full, head');
|
|
98
|
+
for (const key of Object.keys(v)) if (!(HANDOFF_FLAGS as readonly string[]).includes(key)) throw new HandoffError('HANDOFF_PATH_INVALID', `producer.${key}`, 'unknown key; expected config, parts, source, full, head, keypoints');
|
|
96
99
|
if (typeof v.config !== 'string') throw new HandoffError('HANDOFF_PATH_INVALID', 'producer.config', 'required: the producer config path');
|
|
97
100
|
const out: HandoffDeclaration = { config: v.config };
|
|
98
|
-
for (const key of ['parts', 'source', 'full', 'head'] as const) if (v[key] !== undefined) { if (typeof v[key] !== 'string') throw new HandoffError('HANDOFF_PATH_INVALID', `producer.${key}`, 'expected a string path'); out[key] = v[key] as string; }
|
|
101
|
+
for (const key of ['parts', 'source', 'full', 'head', 'keypoints'] as const) if (v[key] !== undefined) { if (typeof v[key] !== 'string') throw new HandoffError('HANDOFF_PATH_INVALID', `producer.${key}`, 'expected a string path'); out[key] = v[key] as string; }
|
|
99
102
|
return out;
|
|
100
103
|
}
|
|
101
104
|
/** `--source` and `--full` travel together, `--head` only with them, and everything else only with `--config`. */
|
|
@@ -119,7 +122,7 @@ async function listed(base: string, dir: string, field: string): Promise<string[
|
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
/** Hash every handoff file and build the record. `root` is the package root (display paths are relative to it). */
|
|
122
|
-
async function record(root: string, base: string, how: HandoffRecord['how'], via: HandoffRecord['via'], config: string, partsPath: string | null, extra: { source?: string; full?: string; head?: string }, partsDeclared: boolean): Promise<Handoff> {
|
|
125
|
+
async function record(root: string, base: string, how: HandoffRecord['how'], via: HandoffRecord['via'], config: string, partsPath: string | null, extra: { source?: string; full?: string; head?: string; keypoints?: string }, partsDeclared: boolean): Promise<Handoff> {
|
|
123
126
|
const show = (path: string) => relative(root, path).split(sep).join('/') || '.';
|
|
124
127
|
const files = new Map<string, string>(), missing: string[] = [];
|
|
125
128
|
const add = async (path: string) => { files.set(show(path), await hashFile(path)); };
|
|
@@ -154,10 +157,12 @@ async function record(root: string, base: string, how: HandoffRecord['how'], via
|
|
|
154
157
|
let source: HandoffFile | null = null;
|
|
155
158
|
if (extra.source) { await add(extra.source); source = { path: show(extra.source), sha256: files.get(show(extra.source))! }; }
|
|
156
159
|
for (const dir of [extra.full, extra.head]) if (dir) for (const file of await listed(base, dir, show(dir))) await add(file);
|
|
160
|
+
// #111: hashed only when declared — an undeclared handoff's record (and so its identity) holds no keypoints key at all.
|
|
161
|
+
if (extra.keypoints) await add(extra.keypoints);
|
|
157
162
|
const sorted = [...files].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)).map(([path, hash]) => ({ path, sha256: hash }));
|
|
158
163
|
return {
|
|
159
|
-
record: { schema: HANDOFF_SCHEMA, how, via, outside: !inside(root, config), config: { path: show(config), sha256: files.get(show(config))! }, parts: parts ? show(parts) : null, rig: rig ? show(rig) : null, source, full: extra.full ? show(extra.full) : null, head: extra.head ? show(extra.head) : null, files: sorted, missing },
|
|
160
|
-
config, parts, rig, root: base, source: extra.source ?? null, full: extra.full ?? null, head: extra.head ?? null,
|
|
164
|
+
record: { schema: HANDOFF_SCHEMA, how, via, outside: !inside(root, config), config: { path: show(config), sha256: files.get(show(config))! }, parts: parts ? show(parts) : null, rig: rig ? show(rig) : null, source, full: extra.full ? show(extra.full) : null, head: extra.head ? show(extra.head) : null, ...(extra.keypoints ? { keypoints: { path: show(extra.keypoints), sha256: files.get(show(extra.keypoints))! } } : {}), files: sorted, missing },
|
|
165
|
+
config, parts, rig, root: base, source: extra.source ?? null, full: extra.full ?? null, head: extra.head ?? null, keypoints: extra.keypoints ?? null,
|
|
161
166
|
};
|
|
162
167
|
}
|
|
163
168
|
|
|
@@ -175,9 +180,10 @@ export async function resolveHandoff(root: string, declared: HandoffDeclaration
|
|
|
175
180
|
const source = flags.source !== undefined ? await flagPath(root, flags.source, '--source', 'file') : undefined;
|
|
176
181
|
const full = flags.full !== undefined ? await flagPath(root, flags.full, '--full', 'dir') : undefined;
|
|
177
182
|
const head = flags.head !== undefined ? await flagPath(root, flags.head, '--head', 'dir') : undefined;
|
|
178
|
-
const
|
|
183
|
+
const keypoints = flags.keypoints !== undefined ? await flagPath(root, flags.keypoints, '--keypoints', 'file') : undefined;
|
|
184
|
+
const handoff = await record(root, root, 'declared', 'open --config', config, parts, { source, full, head, keypoints }, flags.parts !== undefined);
|
|
179
185
|
// Canonical absolute flags: the background worker and later reloads read exactly these.
|
|
180
|
-
handoff.flags = { config, ...(flags.parts !== undefined ? { parts } : {}), ...(source ? { source } : {}), ...(full ? { full } : {}), ...(head ? { head } : {}) };
|
|
186
|
+
handoff.flags = { config, ...(flags.parts !== undefined ? { parts } : {}), ...(source ? { source } : {}), ...(full ? { full } : {}), ...(head ? { head } : {}), ...(keypoints ? { keypoints } : {}) };
|
|
181
187
|
return { handoff, reason: null };
|
|
182
188
|
}
|
|
183
189
|
if (declared) {
|
|
@@ -186,8 +192,9 @@ export async function resolveHandoff(root: string, declared: HandoffDeclaration
|
|
|
186
192
|
const source = declared.source !== undefined ? await blockPath(root, declared.source, 'producer.source', 'file') : undefined;
|
|
187
193
|
const full = declared.full !== undefined ? await blockPath(root, declared.full, 'producer.full', 'dir') : undefined;
|
|
188
194
|
const head = declared.head !== undefined ? await blockPath(root, declared.head, 'producer.head', 'dir') : undefined;
|
|
195
|
+
const keypoints = declared.keypoints !== undefined ? await blockPath(root, declared.keypoints, 'producer.keypoints', 'file') : undefined;
|
|
189
196
|
if ((source === undefined) !== (full === undefined) || (head !== undefined && full === undefined)) throw new HandoffError('HANDOFF_FLAGS_INVALID', 'producer', 'source and full are given together, head only with them');
|
|
190
|
-
return { handoff: await record(root, root, 'declared', 'review.json producer', config, parts, { source, full, head }, declared.parts !== undefined), reason: null };
|
|
197
|
+
return { handoff: await record(root, root, 'declared', 'review.json producer', config, parts, { source, full, head, keypoints }, declared.parts !== undefined), reason: null };
|
|
191
198
|
}
|
|
192
199
|
for (const name of ['config.json', 'rig/config.json']) {
|
|
193
200
|
const path = join(root, name);
|
package/src/open-protocol.ts
CHANGED
|
@@ -18,10 +18,11 @@ export const capabilityHeader='X-Review-Capability',openNonceHeader='X-Review-Op
|
|
|
18
18
|
export const capabilityPattern=/^[a-f0-9]{64}$/;
|
|
19
19
|
export function sameSecret(actual:string|null,expected:string):boolean{const a=Buffer.from(actual??''),b=Buffer.from(expected);return a.length===b.length&&timingSafeEqual(a,b);}
|
|
20
20
|
/** Every non-GET request needs this instance's page capability, compared in constant time before any
|
|
21
|
-
* Origin check. GET /api/access only says whether one is required; no endpoint ever returns it.
|
|
22
|
-
|
|
21
|
+
* Origin check. GET /api/access only says whether one is required; no endpoint ever returns it. It also says
|
|
22
|
+
* whether this instance serves agent control, so a page without it never probes `/api/control/config` (#164). */
|
|
23
|
+
export function accessGate(req:Request,capability:string|undefined,control=false):Response|null{
|
|
23
24
|
const url=new URL(req.url),headers={'Cache-Control':'no-store','X-Content-Type-Options':'nosniff'};
|
|
24
|
-
if(url.pathname==='/api/access')return req.method==='GET'||req.method==='HEAD'?Response.json({schema:'rig-review-access/1',capabilityRequired:!!capability},{headers}):Response.json({error:'Method not allowed'},{status:405,headers});
|
|
25
|
+
if(url.pathname==='/api/access')return req.method==='GET'||req.method==='HEAD'?Response.json({schema:'rig-review-access/1',capabilityRequired:!!capability,control},{headers}):Response.json({error:'Method not allowed'},{status:405,headers});
|
|
25
26
|
if(!capability||req.method==='GET'||req.method==='HEAD')return null;
|
|
26
27
|
return sameSecret(req.headers.get(capabilityHeader),capability)?null:Response.json({error:'REVIEW_CAPABILITY_REQUIRED: reopen with rig-review open to edit'},{status:403,headers});
|
|
27
28
|
}
|
package/src/open.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {ensureFront,frontPort} from './front.ts';
|
|
|
18
18
|
import {playPackagePath} from './versions.ts';
|
|
19
19
|
import {HandoffError,HANDOFF_FLAGS,type HandoffFlags} from './handoff.ts';
|
|
20
20
|
/** `direct` chooses how the page is reached (own port instead of the managed front); it is not part of the instance identity. */
|
|
21
|
-
export interface OpenInputs {input:string;compare?:string;reference?:string;animation?:string;control?:boolean;direct?:boolean;config?:string;parts?:string;source?:string;full?:string;head?:string}
|
|
21
|
+
export interface OpenInputs {input:string;compare?:string;reference?:string;animation?:string;control?:boolean;direct?:boolean;config?:string;parts?:string;source?:string;full?:string;head?:string;keypoints?:string}
|
|
22
22
|
/** The producer handoff flags (#101) among the inputs, or undefined when none was given. */
|
|
23
23
|
export function handoffFlags(inputs:OpenInputs):HandoffFlags|undefined{const present=HANDOFF_FLAGS.filter(f=>inputs[f]!==undefined);if(!present.length)return undefined;return Object.fromEntries(present.map(f=>[f,inputs[f]])) as unknown as HandoffFlags;}
|
|
24
24
|
export type OpenPackage=({mode:'single';p:ReviewPackage;inputs:OpenInputs;contentHash:string}|{mode:'compare';p:ComparePackage;inputs:OpenInputs;contentHash:string})&{selection:AnimationSelection|null};
|
package/src/readings-run.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { copyFile, lstat, mkdir, readFile, rm, writeFile } from 'node:fs/promise
|
|
|
10
10
|
import { dirname, isAbsolute, join, relative } from 'node:path';
|
|
11
11
|
import { safeFile, type ReviewPackage } from './manifest.ts';
|
|
12
12
|
import { resolveCompiler, runTool, type InstalledTool } from './producer.ts';
|
|
13
|
+
import { scheduleText, type EnvironmentSchedule } from './environment-schedule.ts';
|
|
13
14
|
import { environmentSchedule, helpHasEnvironment, parseReadingsRequest, READING_FILE, readingLabel, readingResult, READINGS_SCHEMA, READINGS_SINCE, ReadingsError, renderArgs, type ReadingBuild, type ReadingRequest, type ReadingResult, type ReadingsRequest } from './readings.ts';
|
|
14
15
|
|
|
15
16
|
const sha256 = (bytes: string | Uint8Array) => createHash('sha256').update(bytes).digest('hex');
|
|
@@ -39,7 +40,7 @@ export async function runReading(r: ReadingRequest, animation: string, candidate
|
|
|
39
40
|
try { const stored = JSON.parse(await readFile(resultFile, 'utf8')) as { key: string; result: ReadingResult }; if (stored.key === key) return stored.result; } catch { /* not run yet */ }
|
|
40
41
|
await rm(dir, { recursive: true, force: true });
|
|
41
42
|
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
42
|
-
if (schedule) await writeFile(join(dir, 'environment.json'),
|
|
43
|
+
if (schedule) await writeFile(join(dir, 'environment.json'), scheduleText(schedule as EnvironmentSchedule), { mode: 0o600 });
|
|
43
44
|
const run = await runTool(compiler.bin, renderArgs(r, animation, { candidate, out: 'render', environment: schedule ? 'environment.json' : null }), { cwd: dir });
|
|
44
45
|
let json: unknown = null, fps: number | null = null;
|
|
45
46
|
if (run.exitCode === 0) {
|
package/src/readings.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Review judges nothing here: a reading is not a gate, and an unknown slot or bone is rigc's to refuse.
|
|
8
8
|
*/
|
|
9
9
|
import { firstRefusal } from './producer.ts';
|
|
10
|
+
import { parseSchedule, ScheduleError, standingSchedule, type EnvironmentSchedule } from './environment-schedule.ts';
|
|
10
11
|
|
|
11
12
|
export const READINGS_SCHEMA = 'rig-review-readings/1';
|
|
12
13
|
/** The first `rig-c` release with all five readings (its CHANGELOG: 2.63.0, "render: --overlaps names which slot covers which"). */
|
|
@@ -24,7 +25,8 @@ export type ReadingKind = ReadingRequest['kind'];
|
|
|
24
25
|
export const READING_KINDS: readonly ReadingKind[] = ['folds', 'pass-through', 'seam', 'overlaps', 'angle-range'];
|
|
25
26
|
/** The world the physics answers while the readings are posed: the Environment block's wind and gravity as they stand (x right, y down). */
|
|
26
27
|
export interface ReadingEnvironment { wind: [number, number]; gravity: [number, number] }
|
|
27
|
-
|
|
28
|
+
/** #112: with "Use for readings" on, the Environment block's exported `rigc-environment/1` schedule goes in its place. */
|
|
29
|
+
export interface ReadingsRequest { animation: string; readings: ReadingRequest[]; environment: ReadingEnvironment | EnvironmentSchedule | null }
|
|
28
30
|
|
|
29
31
|
export class ReadingsError extends Error { constructor(readonly code: string, detail: string) { super(`${code}: ${detail}`); } }
|
|
30
32
|
const bad = (detail: string): never => { throw new ReadingsError('READINGS_REQUEST_INVALID', detail); };
|
|
@@ -57,8 +59,12 @@ export function parseReadingsRequest(input: unknown): ReadingsRequest {
|
|
|
57
59
|
const readings = (v.readings as unknown[]).map((r, i) => parseReading(r, `readings[${i}]`));
|
|
58
60
|
const labels = readings.map(readingLabel);
|
|
59
61
|
if (new Set(labels).size !== labels.length) bad('readings: one entry named twice');
|
|
60
|
-
let environment:
|
|
61
|
-
if (v.environment !== null) {
|
|
62
|
+
let environment: ReadingsRequest['environment'] = null;
|
|
63
|
+
if (v.environment !== null) {
|
|
64
|
+
const e = record(v.environment, 'environment');
|
|
65
|
+
if (Object.hasOwn(e, 'spec')) { try { environment = parseSchedule(e); } catch (error) { if (error instanceof ScheduleError) bad(`environment: ${error.message}`); throw error; } }
|
|
66
|
+
else { exactly(e, ['wind', 'gravity'], 'environment'); environment = { wind: pair(e.wind, 'environment.wind'), gravity: pair(e.gravity, 'environment.gravity') }; }
|
|
67
|
+
}
|
|
62
68
|
return { animation, readings, environment };
|
|
63
69
|
}
|
|
64
70
|
|
|
@@ -88,8 +94,8 @@ export interface RenderPaths { candidate: string; out: string; environment: stri
|
|
|
88
94
|
export function renderArgs(r: ReadingRequest, animation: string, paths: RenderPaths): string[] {
|
|
89
95
|
return ['render', '--candidate', paths.candidate, '--animation', animation, ...(paths.environment ? ['--environment', paths.environment] : []), ...readingFlag(r), '--out', paths.out];
|
|
90
96
|
}
|
|
91
|
-
/** The schedule file `--environment` reads: the world as it stands, from t = 0. */
|
|
92
|
-
export const environmentSchedule = (e: ReadingEnvironment) => (
|
|
97
|
+
/** The schedule file `--environment` reads: the world as it stands, from t = 0 — or, #112, the exported schedule as sent. */
|
|
98
|
+
export const environmentSchedule = (e: ReadingEnvironment | EnvironmentSchedule): EnvironmentSchedule => ('spec' in e ? e : standingSchedule(e));
|
|
93
99
|
/** Whether this rigc's `render --help` lists `--environment` (the schedule is passed only then). */
|
|
94
100
|
export const helpHasEnvironment = (lines: readonly string[]) => lines.some(l => /^\s*--environment\s/.test(l));
|
|
95
101
|
|
|
@@ -216,7 +222,7 @@ export function ticksOnClip(ticks: readonly ReadingTick[], jobAnimation: string,
|
|
|
216
222
|
|
|
217
223
|
/** `context.readings`: the pairs chosen and where they ran (the builds' identities), never the results' bytes. */
|
|
218
224
|
export interface ReadingsContext {
|
|
219
|
-
schema: typeof READINGS_SCHEMA; animation: string; requests: ReadingRequest[]; environment:
|
|
225
|
+
schema: typeof READINGS_SCHEMA; animation: string; requests: ReadingRequest[]; environment: ReadingsRequest['environment']; environmentPassed: boolean;
|
|
220
226
|
builds: { inspected: string; scratch: string | null }; compiler: { package: string; version: string };
|
|
221
227
|
ticks: { inspected: number; scratch: number | null };
|
|
222
228
|
}
|