@rig-c/review 0.10.3 → 0.10.4
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/docs/LIVE_FIXTURE.md +1 -1
- package/docs/REVIEW_PACKAGE.md +2 -1
- package/package.json +1 -1
- package/src/rebuild.ts +62 -8
package/docs/LIVE_FIXTURE.md
CHANGED
|
@@ -89,5 +89,5 @@ baseline: reproduced — rig.json d19892a32cec… = inspected d19892a32cec…
|
|
|
89
89
|
|
|
90
90
|
- **Whole rig.** On a reproduced baseline, a physics patch runs `rig --config` (#136).
|
|
91
91
|
- **Retune.** To exercise retune (#136, #139), change one number in `$P/out/rig/rig.json` (a +0.001 nudge is enough), so the baseline is not reproduced. Then a patch inside retune's scope routes to `rig-parts retune`. Keep the file the writer's text (edit the number in place, or re-serialise with `JSON.stringify(value, null, 1)` and a closing newline): any other text refuses `RETUNE_INPUT_WRITTEN` (#148). A nudge in an image, not in rig.json, refuses every retune `RETUNE_REQUEST_HELD` instead.
|
|
92
|
-
- **Density strip (#173).** Select `bottomwear` (a `grid` 31 entry) in the Mesh panel and build grid 40 → 120 with N 5: grid 120 refuses `REMESH_CHECK_HELD` (TIP_OVER_ROOT), the other four build.
|
|
92
|
+
- **Density strip (#173).** Select `bottomwear` (a `grid` 31 entry) in the Mesh panel and build grid 40 → 120 with N 5: grid 120 refuses `REMESH_CHECK_HELD` (TIP_OVER_ROOT), the other four build. With parts 1.55.0 or later (#179), the first successful remesh records the input check and the remaining candidates reuse it over the same inspected directories; every candidate still runs its own remesh and revision check. At most four candidates run at once, including the seed. Older producers measure the input independently for each candidate. Re-measure the five-value strip against one fresh grid-40 Apply immediately before it, with wall time, CPU and machine load recorded; the acceptance bar is at most 1.3 times that Apply, or a measured blocker report. Sharing the input check alone does not establish that bar.
|
|
93
93
|
- **Build.** A patch under `/assemble`, `/limbs`, or `/bones` on a config with `limbs` runs `rig-parts build` (#115). So does a cloth Apply (#111).
|
package/docs/REVIEW_PACKAGE.md
CHANGED
|
@@ -227,7 +227,8 @@ The issue export adds an `Applied:` line for a valid record (`patch <hash>… (r
|
|
|
227
227
|
**Undo across Applies (#166).** The session's applied records are a stack, in order, each with its build id; the last is on the stage. `GET /api/rebuild/history` (single review) answers `{schema:"rig-review-applied-history/1", depth, records:[{id, kind:"regions"|"patch"|"delta"|"retarget", op, what, modelHash, kept}]}` (`kept`: the record's build is still on disk). `POST /api/rebuild/undo` (same-origin, capability, 1 KiB) takes exactly `{from}`, the build id the page shows on the stage; it removes the last record and returns the stage to its predecessor's build, or to the inspected build when none is left: `{status:"built", undone, job, history}` (the predecessor's job, its model served at `/rebuild/<id>/` again), `{status:"running", undone, job}` (an evicted predecessor re-running; poll `/api/rebuild/<id>`), or `{status:"inspected", undone, history}`. Refusals (HTTP 200 `{status:"refused", error}`): `UNDO_NOTHING_APPLIED`, `UNDO_STALE` (`from` is not the last record), `REBUILD_BUSY`, and `UNDO_PREDECESSOR_CHANGED: <what>`; malformed: HTTP 400 `REBUILD_REQUEST_INVALID`. The scratch root `<state>/rebuild/<package hash>/` still holds only the build on the stage; the other records' builds wait in `<state>/rebuild-kept/<package hash>/<id>/`, at most `MAX_KEPT_BUILDS` (8) builds per package on disk counting the stage's, the oldest record's evicted first, and a build no record names is removed. A kept predecessor is moved back and re-hashed (every file the model names) against both its result.json and its record; an evicted one re-runs its recorded request through the same runner, refused before anything runs when the handoff config's sha256 or the producer or compiler version is not the record's (`UNDO_PREDECESSOR_CHANGED: <config path> sha256 a… → b…`, `UNDO_PREDECESSOR_CHANGED: @rig-c/parts 1.49.1 → @rig-c/parts 1.50.0; …`), or when its build id hashes differently (`UNDO_PREDECESSOR_CHANGED: build id a… → b… — …`), and refused after the run when the build is not byte-identical, by the first differing file (`UNDO_PREDECESSOR_CHANGED: skeleton.model.json — the record's build has sha256 a…, the re-run wrote b…; …`) — never guessed; a refusal leaves the stage and the stack on the last Apply. The undone record's build is removed (no redo). Undo writes nothing into the package. Feedback written after an undo carries the post-undo record as `context.applied` (or `null` on the inspected build), never the reverted one. The Cuts and Cloth blocks read the same record (#172): a `rig-review-applied/2` record with op `build` is the Cuts block's, with op `cloth` the Cloth block's, and a pending op is marked applied only while that record holds it (same op, path, value and name); their readouts show the restored build's job when it is theirs, else nothing. The stack lives in the review process; a restarted review starts with none.
|
|
228
228
|
|
|
229
229
|
**Density strip (#173).** Next to the selected part's density field — `grid` on a lattice entry, `contour.spacing` on a contour entry — the Mesh panel takes "N values from A to B" (2 ≤ N ≤ 5; a lattice's grid is rounded to whole numbers) and **Build strip** sends one Mesh-panel patch per value (the pending entry with that field at that value, `replace /meshes/<part>`, `remesh --part <part>`) to `POST /api/density-strip` (single review; same-origin, capability, 320 KiB = five 64 KiB patches) as exactly `{patches}`. Malformed (HTTP 400): `DENSITY_STRIP_REQUEST_INVALID` (not 2–5 patches, a repeated value, another config hash, producer or flag set), `MESH_PATCH_SCOPE` (a patch that is not the panel's one op) and `DENSITY_STRIP_FIELD` (entries that differ anywhere but the density field, or a mode without one); each patch is then refused as an Apply refuses it (`EDIT_PATCH_STALE`, `EDIT_PATCH_NAME_MISMATCH`, `EDIT_PATCH_PATH_HELD`, `EDIT_PRODUCER_MISMATCH`, the producer-resolution lines), before anything runs. The strip id hashes the package, the config bytes, every patch's bytes in order and both tool versions; each candidate's build id is the patch's own Apply id (*Run*, above).
|
|
230
|
-
- **Runner.** Every candidate runs the patch runner a remesh Apply runs — remesh into `rig/`, the compile with remesh.json's pack flags, #174's carried check (no third check; an older producer runs check, as an Apply does), the build hashed and `result.json` written — into its own folder `<state>/density-strip/<package hash>/<strip id>/<build id>/`, at most `max(1, floor(cores / 2))` at once (the strip's `concurrency
|
|
230
|
+
- **Runner.** Every candidate runs the patch runner a remesh Apply runs — remesh into `rig/`, the compile with remesh.json's pack flags, #174's carried check (no third check; an older producer runs check, as an Apply does), the build hashed and `result.json` written — into its own folder `<state>/density-strip/<package hash>/<strip id>/<build id>/`, at most `min(4, max(1, floor(cores / 2)))` at once (the strip's `concurrency`; explicit limits are also capped at four). A candidate does not land: no applied record, no `latest` build, nothing in the Apply scratch root or `rebuild-kept`, no undo entry (#166's stack is untouched). One producer workload runs at a time per review: a strip is refused while an Apply (or undo's re-run) runs, and an Apply, an undo and another strip are refused `REBUILD_BUSY: density strip <id> is still running` while a strip runs; a single Apply is otherwise unchanged (its records, ids, scratch and lines).
|
|
231
|
+
- **Shared input check (#179).** On parts 1.55.0 or later, the first candidate asks remesh for `--record-input-check`. When remesh succeeds, Review moves its `input-check.json` out of `rig/` into the strip's folder, before the ordinary revision hash binding: the auxiliary record is not a revision file, and the revision binding still rejects every unexpected or changed file. The remaining candidates pass that record as `--input-check`, over the same absolute inspected rig and parts directories, unchanged bytes, tools and pack flags. The producer validates the record and refuses malformed or mismatched input by name; Review never retries without it. A refused seed has no record: its refusal stays on its row and the next candidate tries to record. A successful producer that leaves no available regular record refuses `DENSITY_STRIP_INPUT_CHECK_MISSING`, and the remaining rows refuse without starting a producer. A valid record releases the other slots while the seed finishes compiling; the seed counts toward the four-candidate limit. Older producers receive neither flag and still measure each input. The record stays private to this strip: it is never served, adopted with a candidate, or reused by a later strip; replacement/close removes it. The revision is always checked, and a strip still approves nothing.
|
|
231
232
|
- **Rows.** `GET /api/density-strip/<id>` (or `GET /api/density-strip` → `{strip}`, the one on screen or null) answers `{schema:"rig-review-density-strip/1", id, status:"running"|"done", part, field, concurrency, configHash, producer, compiler, wallMs, rows[], labels:{fewest, widest}}`, one row per patch in the request's order: `{value, id, status, counts:{vertices, triangles}|null (remesh.json target.after), modelTriangles, folds (ANIMATIONS_DO_NOT_FOLD's folding mesh count), stretch:{severity, frame}|null (the part's TEXTURE_STRETCH per-mesh severity and the frame of the side it came from), bars:[{bar, status, key, value, bound, kind:"floor"|"ceiling", ratio, margin}] (every bar line that measures the part — "part <name>," or "slot <name>," not unmeasured — its figure against the line's <key>_floor or <key>_ceiling), verdict ("PASS", check's line when it did not pass, or the refusal verbatim, e.g. "FAIL REMESH_CHECK_HELD: …"), modelUrl, modelHash, applied, entry}`. The figures are the producer's own printed check of the revision (`[check-revision] …` lines; the lines of review's check when it ran one). `GET /api/density-strip/<id>/<build id>` is the candidate's job in full. `labels.fewest` is the passing row with the fewest triangles; `labels.widest`, when passing rows tie on that count, the one whose narrowest margin over its bars is widest (margin = value/floor, or ceiling/value). Labels only: the table has no Apply.
|
|
232
233
|
- **Load is not Apply.** A built row's model is served read-only at `/density-strip/<id>/<build id>/<file>` (its hashed files only; a refused row has none and cannot be loaded). Load puts it on the stage as **pending** through the same in-place reload an Apply uses (camera, animation, time, world kept) and sets the Mesh panel's pending entry to the row's entry; the counts line shows `MESH_COUNTS_AGREE` / `MESH_COUNTS_DIFFER` for the row. Nothing is recorded: while a pending row is on the stage the feedback context's applied state is null (nothing applied is on the stage), Undo is unavailable (the stage is not the last record), and **Back** returns the stage to what it showed. The Mesh panel's Apply then sends the same patch; its build id is the candidate's, so the server moves the candidate's folder into the Apply scratch root and the ordinary reuse re-hashes and lands it — one applied record, exactly as a re-Applied patch, with no producer run. The row then reads `applied` and is no longer served from the strip. Opening, building or loading a strip approves nothing.
|
|
233
234
|
- **Refine between** two adjacent rows builds the next strip with N values strictly inside their interval.
|
package/package.json
CHANGED
package/src/rebuild.ts
CHANGED
|
@@ -407,12 +407,18 @@ const flagValue = (args: readonly string[], flag: string, fallback: string) => {
|
|
|
407
407
|
|
|
408
408
|
export interface RebuildOptions {
|
|
409
409
|
stateDir: string; starts: readonly string[]; unchanged: () => Promise<void>; bones: readonly string[] | null;
|
|
410
|
-
/** #
|
|
410
|
+
/** #179: how many strip candidates run at once, at most four including the recording candidate. */
|
|
411
411
|
stripConcurrency?: number;
|
|
412
412
|
}
|
|
413
413
|
/** #173: a density strip's scratch — one folder per package, holding at most the one strip on screen. */
|
|
414
414
|
export const stripRoot = (stateDir: string, sourceHash: string) => join(stateDir, 'density-strip', sourceHash);
|
|
415
|
-
export const defaultStripConcurrency = () => Math.max(1, Math.floor(availableParallelism() / 2));
|
|
415
|
+
export const defaultStripConcurrency = () => Math.min(4, Math.max(1, Math.floor(availableParallelism() / 2)));
|
|
416
|
+
export const boundedStripConcurrency = (value = defaultStripConcurrency()) => Number.isFinite(value) ? Math.min(4, Math.max(1, Math.floor(value))) : defaultStripConcurrency();
|
|
417
|
+
/** parts#617: the input check is reusable only over the same inspected directories, bytes, options and tools. */
|
|
418
|
+
const INPUT_CHECK_SINCE = '1.55.0';
|
|
419
|
+
interface StripInputCheck {
|
|
420
|
+
path: string; record: boolean; recorded?: () => void; problem?: string;
|
|
421
|
+
}
|
|
416
422
|
/** Deterministic: the package, the config bytes, every candidate's patch bytes in order and both tool versions. */
|
|
417
423
|
export function stripId(input: { sourceHash: string; configHash: string; patches: readonly ConfigPatch[]; producer: string; compiler: string }): string {
|
|
418
424
|
return sha256(JSON.stringify([DENSITY_STRIP_SCHEMA, input.sourceHash, input.configHash, input.patches.map(patchText), input.producer, input.compiler]));
|
|
@@ -925,7 +931,7 @@ export function createRebuilds(p: ReviewPackage, options: RebuildOptions) {
|
|
|
925
931
|
* #102: the patched config → `rig-parts rig` (whole rig), `retune` (#136: the constraints and the idle spliced onto the
|
|
926
932
|
* inspected rig) or `remesh` (one part's patched entry) → `<dir>/rig`; the tail adds the diff and check.
|
|
927
933
|
*/
|
|
928
|
-
function executePatch(job: PatchJob, dir: string, inputs: ProducerInputs, resolved: Resolved, patched: JsonValue, part: string | null, plan: BuildPlan | null = null, world: ApplyWorld | null = null, cloth: ClothPlan | null = null, candidate = false): Promise<void> {
|
|
934
|
+
function executePatch(job: PatchJob, dir: string, inputs: ProducerInputs, resolved: Resolved, patched: JsonValue, part: string | null, plan: BuildPlan | null = null, world: ApplyWorld | null = null, cloth: ClothPlan | null = null, candidate = false, inputCheck?: StripInputCheck): Promise<void> {
|
|
929
935
|
const args = job.request.patch.op.args, producer = resolved.producer;
|
|
930
936
|
let counts: RemeshCounts | null = null;
|
|
931
937
|
let built: ProcessResult | null = null;
|
|
@@ -1005,7 +1011,24 @@ export function createRebuilds(p: ReviewPackage, options: RebuildOptions) {
|
|
|
1005
1011
|
await writeFile(join(dir, 'mesh.json'), JSON.stringify(entry, null, 1) + '\n', { mode: 0o600 });
|
|
1006
1012
|
const rest = args.filter((_, i) => args[i] !== '--part' && args[i - 1] !== '--part');
|
|
1007
1013
|
push(`review: ${producer.package} ${producer.version} remesh ${part} (its patched meshes entry) → scratch rig`);
|
|
1008
|
-
|
|
1014
|
+
const checkArgs = inputCheck ? inputCheck.record ? ['--record-input-check'] : ['--input-check', inputCheck.path] : [];
|
|
1015
|
+
const run = await runTool(producer.bin, [...remeshArgs({ rig: inputs.rig, config: inputs.config, parts: inputs.parts, part, mesh: 'mesh.json', out: 'rig' }), ...rest, ...checkArgs], { cwd: dir, onLine: push });
|
|
1016
|
+
if (run.exitCode === 0 && inputCheck?.record) {
|
|
1017
|
+
// The record is beside remesh.json but is not in its revision hash rows. Move only this auxiliary file;
|
|
1018
|
+
// revisionBinding must still reject every extra or changed revision file. Keep it outside any candidate,
|
|
1019
|
+
// so a failed compile's cleanup or a later Apply's adoption cannot remove the shared input check.
|
|
1020
|
+
try {
|
|
1021
|
+
const record = join(dir, 'rig', 'input-check.json');
|
|
1022
|
+
if (!(await lstat(record)).isFile()) throw new Error('not a regular file');
|
|
1023
|
+
await rename(record, inputCheck.path);
|
|
1024
|
+
} catch {
|
|
1025
|
+
const line = 'DENSITY_STRIP_INPUT_CHECK_MISSING: the producer succeeded without an available regular input-check.json; no unrecorded retry was run';
|
|
1026
|
+
inputCheck.problem = line; push(line);
|
|
1027
|
+
return { ...run, exitCode: 1, lines: [...run.lines, line] };
|
|
1028
|
+
}
|
|
1029
|
+
inputCheck.recorded?.();
|
|
1030
|
+
}
|
|
1031
|
+
return run;
|
|
1009
1032
|
},
|
|
1010
1033
|
wallMs: ms => { job.wallMs.op = ms; },
|
|
1011
1034
|
async flags(push) {
|
|
@@ -1165,7 +1188,7 @@ export function createRebuilds(p: ReviewPackage, options: RebuildOptions) {
|
|
|
1165
1188
|
// Bounded disk: the strip on screen is the only one this package keeps; its predecessor's folder goes first.
|
|
1166
1189
|
const root = stripRoot(options.stateDir, p.sourceHash);
|
|
1167
1190
|
await rm(root, { recursive: true, force: true });
|
|
1168
|
-
const concurrency =
|
|
1191
|
+
const concurrency = boundedStripConcurrency(options.stripConcurrency);
|
|
1169
1192
|
const candidates: StripCandidate[] = planned.map(({ patch }, i) => {
|
|
1170
1193
|
const cid = patchId({ sourceHash: p.sourceHash, configHash: inputs.configHash, patch, producer, compiler, op: 'remesh' });
|
|
1171
1194
|
const job: PatchJob = { schema: REBUILD_SCHEMA, kind: 'patch', id: cid, status: 'running', reused: false, op: 'remesh', request: { patch, patchSha256: patchSha256(patch), configHash: inputs.configHash }, producer: tool(resolved.producer), compiler: tool(resolved.compiler), lines: [], producerLine: null, exitCode: null, wallMs: { op: null, compile: null, check: null }, check: null, diff: null, files: null, remesh: null, build: null };
|
|
@@ -1175,13 +1198,44 @@ export function createRebuilds(p: ReviewPackage, options: RebuildOptions) {
|
|
|
1175
1198
|
strip = current;
|
|
1176
1199
|
// A bounded pool: each worker takes the next candidate in order; the strip is done when every candidate settled.
|
|
1177
1200
|
let next = 0;
|
|
1201
|
+
const shared = versionAtLeast(resolved.producer.version, INPUT_CHECK_SINCE) ? join(root, id, 'input-check.json') : null;
|
|
1202
|
+
const runCandidate = (i: number, check?: StripInputCheck) => {
|
|
1203
|
+
const c = candidates[i]!;
|
|
1204
|
+
return executePatch(c.job, c.dir, inputs, resolved, planned[i]!.patched, shape.part, null, null, null, true, check);
|
|
1205
|
+
};
|
|
1178
1206
|
const worker = async () => {
|
|
1179
1207
|
for (let i = next++; i < candidates.length; i = next++) {
|
|
1180
|
-
|
|
1181
|
-
await executePatch(c.job, c.dir, inputs, resolved, planned[i]!.patched, shape.part, null, null, null, true).catch(() => {});
|
|
1208
|
+
await runCandidate(i, shared ? { path: shared, record: false } : undefined);
|
|
1182
1209
|
}
|
|
1183
1210
|
};
|
|
1184
|
-
|
|
1211
|
+
const runStrip = async () => {
|
|
1212
|
+
if (!shared) { await Promise.all(Array.from({ length: Math.min(concurrency, candidates.length) }, worker)); return; }
|
|
1213
|
+
let ready!: (recorded: boolean) => void;
|
|
1214
|
+
const recorded = new Promise<boolean>(resolve => { ready = resolve; });
|
|
1215
|
+
// A refused revision writes no record. Preserve its refusal and let the next candidate try to record.
|
|
1216
|
+
// Once a producer succeeds, release the other slots before its Review compile finishes (still at most four).
|
|
1217
|
+
const seed = (async () => {
|
|
1218
|
+
for (let i = next++; i < candidates.length; i = next++) {
|
|
1219
|
+
let hasRecord = false;
|
|
1220
|
+
const check: StripInputCheck = { path: shared, record: true, recorded: () => { hasRecord = true; ready(true); } };
|
|
1221
|
+
await runCandidate(i, check);
|
|
1222
|
+
if (hasRecord) return;
|
|
1223
|
+
if (check.problem) {
|
|
1224
|
+
for (; next < candidates.length; next++) {
|
|
1225
|
+
const job = candidates[next]!.job;
|
|
1226
|
+
job.status = 'refused'; job.producerLine = check.problem; job.lines.push(check.problem);
|
|
1227
|
+
}
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
})().finally(() => ready(false));
|
|
1232
|
+
if (await recorded) {
|
|
1233
|
+
const others = Array.from({ length: Math.min(concurrency - 1, candidates.length - next) }, worker);
|
|
1234
|
+
await seed;
|
|
1235
|
+
await Promise.all([...others, worker()]);
|
|
1236
|
+
} else await seed;
|
|
1237
|
+
};
|
|
1238
|
+
void runStrip().finally(() => {
|
|
1185
1239
|
if (strip === current) { (current as { status: 'running' | 'done' }).status = 'done'; (current as { wallMs: number | null }).wallMs = Math.round(performance.now() - current.started); }
|
|
1186
1240
|
});
|
|
1187
1241
|
return { status: 202, body: stripView() };
|