@rig-c/review 0.13.1 → 0.14.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 CHANGED
@@ -34,718 +34,52 @@ the same CLI. `@rig-c/review` remains the canonical package.
34
34
 
35
35
  The package is 0.x: the CLI is the supported surface, and no importable API is promised yet (`exports` exposes only `package.json`).
36
36
 
37
- ## Existing output handoff
37
+ Example stdout from `open` (instance values vary):
38
38
 
39
- Open the current output folder directly, without changing the producing agent or inserting a manifest:
40
-
41
- ```sh
42
- bun run check -- /absolute/path/to/output
43
- bun run review -- /absolute/path/to/output
44
- ```
45
-
46
- The adapter recognizes root `skeleton.model.json` or the fixed rigc build layout `build/skeleton.model.json` (root model takes precedence), root `motion.json`, `report.json`, `skeleton.json`/`.skel`/`.atlas`, source videos (`.mp4`, `.webm`, `.mov`), image filenames containing `strip`, `contact`, `sheet`, `filmstrip` or `띠`, and canonical `*pose*.json` sequences. Model discovery checks only those two named paths; it never recursively searches directories. A `rig-parts build` output root (`out/`: regular `parts.json` and `rig/rig.json`, a real `check/` directory and `check/build/skeleton.model.json`) that has neither model path itself opens as its `check/` package — the same package, identity and session as opening `out/check` — so `rig-review open <out>` works on a fresh build without copying `check/build` anywhere. Model page images resolve relative to the selected model, while motion and references remain relative to the opened folder. It builds the normalized review manifest in memory and reads source files without changing them. Motion/report/skeleton evidence links are read only. A recognized `rig-pose-report/1` also supplies read-only event markers: frame-only events retain their producer interval and explicitly lack seek until a declared clock is present; explicit event seconds can seek. Empty reported categories show none, while missing fields remain missing. Report markers prefill known feedback coordinates and actual bone/slot names without creating a review decision or motion keys. It exposes each matching video/strip separately, without guessing synchronization or source-to-rig bindings.
47
-
48
- When the report declares `fps` plus `frame0` (seconds offset) and a frame count, or exact `frameTimes[]`, the matching animation also shows a producer-frame readout and frame seek controls. Without a bounded declared clock it shows `rate unknown`; current unchanged v19 has no clock. Sample times come only from the report, and feedback context records the recalculated frame or null.
49
-
50
- A raw `skeleton.json`/`.skel` can still be inspected as evidence, but cannot play through the compiled-model runtime. Playback requires an upstream gated rigc build's `skeleton.model.json` with portable local page images inside the opened folder. `check` validates the delivery contract and model envelope, not the upstream compiler gate or visual quality.
51
-
52
- For a curated handoff, [REVIEW_PACKAGE.md](docs/REVIEW_PACKAGE.md) documents optional `review.json` with named checkpoints, references and angle axes:
53
-
54
- ```sh
55
- bun run review -- /absolute/path/to/package/review.json --checkpoint silhouette
56
- ```
57
-
58
- The server prints the exact link, for example `http://127.0.0.1:4317/?checkpoint=silhouette#cap=…`. The `#cap=` fragment is this server's write capability (see [Managed front and write capability](#managed-front-and-write-capability)). `--port 0` selects a free port. It binds only `127.0.0.1` and never opens a browser automatically. A folder containing `review.json` uses that explicit manifest.
59
-
60
- ## Compare two outputs
61
-
62
- Open two compiled packages against one explicitly selected local reference video:
63
-
64
- ```sh
65
- bun run review -- compare /absolute/output-a /absolute/output-b --reference /absolute/reference.mp4 --animation dance
66
- bun run check -- compare /absolute/output-a /absolute/output-b --reference /absolute/reference.mp4 --animation dance
67
- bun run feedback -- compare /absolute/output-a /absolute/output-b --reference /absolute/reference.mp4 --animation dance
68
- ```
69
-
70
- Both models must contain the named animation. The video is a separate read-only CLI input, served only through a fixed endpoint; it is never inserted into either package. A separate compare page uses one video playhead to seek both rigs at the same time. Frame readouts use each side's explicit producer clock; no video FPS or missing clock is inferred.
71
-
72
- Compare feedback requires side `a` or `b` and carries both package source hashes, both exact rig/motion identities, the shared video hash and animation. A changed input on either side or in the video refuses saved feedback until restart. State stays outside all source inputs. Single-package commands and sessions retain their existing contract. See [the comparison contract](docs/REVIEW_PACKAGE.md#two-package-comparison).
73
-
74
- ## Feedback ownership
75
-
76
- Opening leaves status **pending**. The person explicitly saves **approved**, **changes-requested**, or **pending**, checkpoint outcomes/notes, and feedback containing an inclusive frame interval, a bone/slot/rig target, defect/quality category and one-line summary. Frame numbers are entered explicitly; the app does not invent a source FPS. **Write review**, at the top right, opens the review dialog: review status, reviewed source, selected checkpoint, structured feedback and the frame-strip export. Its **Submit review** saves that explicit status and feedback; closing the dialog keeps the unsubmitted draft. The page has no feedback export buttons: `rig-review feedback --format json` and `--format issue` (below) are the export path, and carry the original source binding and reproducible inspection context. Each submission records `reviewedSource`: package revision, actual viewer/runtime versions, and separate exact-byte SHA-256 plus path for the reviewed rig and motion; an absent artifact stays null. An active compiled model is the rig identity; raw skeleton JSON is evidence with playback unavailable.
77
-
78
- New submissions validate bone/slot names against the exact loaded model; the target field offers those names. A rig target names the package id. Source-only packages cannot validate a bone/slot target and say so explicitly. When a matching animation has a declared producer clock, the inclusive interval cannot end beyond the last declared sample at or before the actual clip end. Unknown clocks keep manual frame intervals without inventing a limit. `context.interval` records the chosen interval; inspection `time`/`frame` remain separate. Older invalid feedback stays readable with per-row warnings and exports its original evidence, but cannot be resubmitted until corrected or removed.
79
-
80
- Feedback lives under `~/.local/state/rig-c-review/`, separate from delivered files. Identity, revision and a streaming SHA-256 of the normalized manifest plus declared files/model pages bind each saved session. Changed content starts a new pending session, even without a revision bump. Restart the server after changing inputs. Saving or exporting feedback never modifies a model, authors correction keys, writes motion output or publishes an issue automatically.
81
-
82
- After the person submits, the producing agent retrieves the stored review without needing the hashed state filename:
83
-
84
- ```sh
85
- bun run feedback -- /absolute/path/to/output
86
- # Or the same explicit review.json originally opened:
87
- bun run feedback -- /absolute/path/to/package/review.json
88
- ```
89
-
90
- This read-only command prints the session JSON, including reviewed artifact identity and feedback. The exact current-tool session wins when present. After a viewer/runtime upgrade, if that exact session is absent, the CLI can retrieve the latest saved submission over the same content with its original recorded versions and `retrieval: {kind:"prior-tools", toolsChangedSince:true, matchingSubmissions, currentTools}`. It does not migrate approval into the current UI or change saved bytes. Changed inputs still return a fresh pending session; a prior submission is never borrowed across content, revision or reviewed-artifact changes. The same lookup applies to `feedback compare`, preserving both recorded side bindings, reference identity and animation. See [historical retrieval](docs/REVIEW_PACKAGE.md#historical-feedback-retrieval) for selection and validation rules.
91
-
92
-
93
- For grouped issue-ready text, use the same input/reference binding with `--format issue`:
94
-
95
- ```sh
96
- rig-review feedback /absolute/output --reference /absolute/original.mp4 --format issue
97
- rig-review feedback /absolute/a --compare /absolute/b --reference /absolute/original.mp4 --animation dance --format issue
98
- ```
99
-
100
- The header counts feedback records by defect/quality and by build; blocks group by category, then exact target kind/name. Overlapping intervals and A/B sides remain distinct items. Each block retains its id, inclusive interval, one-line summary and selected/full paired provenance. Historical tool identities and validation warnings remain visible; output never migrates approval, writes state or publishes an issue. Default output and `--format json` remain the existing session JSON. Only feedback accepts `--format`, with `json` or `issue` as its value. These two formats are how submitted feedback leaves the tool; the browser page offers no JSON or Markdown download.
101
-
102
- ## Development
103
-
104
- From a clone:
105
-
106
- ```sh
107
- bun install
108
- bun run demo
109
- ```
110
-
111
- Open the printed loopback URL. The bundled original lantern study has source art, an explicitly synthetic canonical pose-sequence sample and an original synthetic chime. It has no compiled model: playback remains unavailable until a real model is supplied. Inside a clone, `bun run review -- <dir>`, `bun run check -- <dir>` and `bun src/cli.ts <command>` are the same CLI as `npx rig-review`.
112
-
113
- ```sh
114
- bun run typecheck
115
- bun test
116
- bun run build
117
- bun run smoke
118
- ```
119
-
120
- `smoke` packs this tree, installs the tarball into an empty project, and runs the installed bin: `--help`, `open --help`, `check` on the bundled demo, and a served page whose browser bundle is built from the installed files. A file missing from `files` in `package.json` fails there, not after publishing.
121
-
122
- The server bundles `web/main.ts` at startup using the pinned installed runtime, with no CDN. `build` verifies that browser bundle in memory. CI checks types, focused path/state/API tests, bundling and the demo package. Follow AGENTS.md for contributor verification policy. For a live run against the installed producers, [docs/LIVE_FIXTURE.md](docs/LIVE_FIXTURE.md) gives the public package, the producer bench and the open line that reaches `baseline: reproduced`. [DESIGN.md](docs/DESIGN.md) distinguishes the runnable foundation from future modeling touch-up and preserves the illustrative screen concept.
123
-
124
- ### Releasing
125
-
126
- Releases are cut by `.github/workflows/release.yml`. On every push to main, release-please keeps a `release: vX.Y.Z` pull request whose version comes from the conventional-commit subjects since the last release. Merging it tags the version, creates the GitHub release, runs typecheck and the install smoke on the tagged tree, and publishes to npm through trusted publishing (OIDC) with provenance. No npm token is stored. Nothing is published until a release PR is merged. Before publishing, the push path resolves the cut tag's commit and requires it to equal the run's `GITHUB_SHA`; a re-run with an older SHA refuses `PROVENANCE_SHA_MISMATCH`, reports both SHAs and leaves the tag cut for recovery. Checkout uses the validated commit SHA. A manual dispatch only re-publishes an existing tag whose GitHub release release-please created, whose commit is on main, and whose version the registry does not serve yet. It must be dispatched on the tag itself (`gh workflow run release.yml --ref vX.Y.Z -f tag=vX.Y.Z`): npm provenance names the run's commit, so a run whose commit is not the tag's is refused `PROVENANCE_SHA_MISMATCH`. Only a tag cut after that check carries it.
127
-
128
- ## Frame strip export
129
-
130
- The strip panel exports the explicitly entered inclusive feedback interval, or all current in-memory feedback intervals, as a PNG or JSON download. Batch selections include unsaved annotations; they do not submit or approve a review. Choose a declared reference video in single-package mode and an explicit clock basis A/B in compare mode. Every selected feedback interval must identify the selected animation. Separate intervals remain separate strip groups, including overlaps.
131
-
132
- The same export is available without a browser:
133
-
134
- ```sh
135
- bun src/cli.ts strip /absolute/package/review.json --reference source-video --animation dance --frames 176:184 --out /absolute/external/strip.png
136
- bun src/cli.ts strip compare /absolute/a /absolute/b --reference /absolute/reference.mp4 --animation dance --basis b --frames 176:184 --out /absolute/external/compare.png
137
- bun src/cli.ts strip /absolute/package/review.json --reference source-video --animation dance --all-feedback --json
138
- ```
139
-
140
- Single `--reference` is a declared video **id**; compare uses the explicitly selected local video path. `--all-feedback` reads only the exact current saved session, and retains each item's side/interval. `--json` produces a selection/provenance plan without rendering; optionally add `--out`. PNG writes an additional `.png.json` provenance sidecar with the actual decoded samples and PNG hash. Destinations must be new files outside every input package, in existing directories without symlink parents. The server exposes download blobs, never a destination-path API.
141
-
142
- Strip times require the selected report's exact `frameTimes` and matching animation. Missing or uniform-only clocks refuse PNG with `Report has no frameTimes — regenerate with pose 0.30.0 or later`; JSON retains requested frames with `time:null`. The other compare side may lack a clock: it can still be posed at the common seconds, while its frame label stays unknown. No frame rate is inferred from frame count, duration or video metadata.
143
-
144
- PNG export uses the pinned published `@rig-c/play` canvas player and real Node Canvas2D, with `ffmpeg` (5.1 or later, for `-fps_mode`) and `ffprobe` available on PATH for reference decoding. Missing capabilities fail by name. Each requested sample chooses the nearest actual decoded reference PTS (ties earlier); both rigs seek that actual PTS using the viewer's fixed step. JSON distinguishes requested time, actual PTS, delta, decoded frame index, actual player times and independently known producer frames. Requests outside the common domain refuse instead of clamping. The PNG shows source rows, chronological frame columns, and full source hashes/revisions/tool and renderer versions. Batch groups stack vertically. Exports are bounded to64 intervals,256 total frames,64 megapixels and16384 pixels per dimension; larger selections must be split.
145
-
146
- The footer fits its measured text while retaining full hashes. RGB/dark-tint features unsupported by the published Canvas2D path refuse with `STRIP_UNSUPPORTED_TINT`; they are not silently omitted.
147
-
148
- ## One-command owner handoff
149
-
150
- `open` validates and hashes an existing folder, starts a background loopback
151
- instance or reuses the exact matching one, prints its hash-bound URL, and exits.
152
- By default the URL goes through the managed front at `127.0.0.1:4318`; `--direct`
153
- prints the instance's own port instead (see [Managed front and write capability](#managed-front-and-write-capability)):
154
-
155
- ```sh
156
- bun run open -- /absolute/output --reference /absolute/original.mp4
157
- # Installed executable mapping:
158
- rig-review open /absolute/output --reference /absolute/original.mp4 --animation dance
159
- rig-review feedback /absolute/output --reference /absolute/original.mp4
160
- rig-review open /absolute/a --compare /absolute/b --reference /absolute/original.mp4 --animation dance
161
- rig-review feedback /absolute/a --compare /absolute/b --reference /absolute/original.mp4 --animation dance
162
- # Producer handoff (single review only): the config, parts and inspected rig an Apply rebuilds from
163
- rig-review open /absolute/output --config /absolute/output/rig/config.json [--parts /absolute/output/rig/out] [--source <png> --full <dir> [--head <dir>]] [--keypoints <file>]
164
- rig-review feedback /absolute/output --config /absolute/output/rig/config.json
165
- ```
166
-
167
- `--config`, `--parts`, `--source`, `--full`, `--head` and `--keypoints` declare the producer
168
- handoff (see `docs/REVIEW_PACKAGE.md`, *Producer handoff*). They win over the
169
- review.json `producer` block, which wins over discovery (`config.json`, then
170
- `rig/config.json` inside the opened folder, then `../config.json` beside an opened
171
- `out/`, named as discovered outside the folder). For the `check/` package of a
172
- `rig-parts build` output the next places are the build's own config: `config.json`
173
- in the build root, else beside it (`<out>/../config.json`, where `rig-parts build
174
- --config <dir>/config.json --out <dir>/out` leaves it); the parts directory is the
175
- build root. Declaring `--config` at exactly one of those two files is taken the same
176
- way, and that file's folder is then the root for the other flags. Every other path must sit inside the
177
- package root; a path outside it or a symlink that leaves it is refused by name.
178
- The handoff files are hashed into the reviewed identity, so `feedback` and
179
- `control` take the same flags to retrieve the same session, and changing the
180
- config bytes starts a new pending review. `--keypoints` (#111) is the body-18 keypoint
181
- file a cloth recipe Apply hands to `rig-parts propose`; it is hashed into the identity
182
- only when declared, so a handoff without it keeps its identity. `--parts`,
183
- `--source`/`--full`/`--head` and `--keypoints` need `--config`; `--source` and `--full` go together; compare takes none of them.
184
-
185
- `open` chooses an explicit `--animation` first, then the normalized report's
186
- `animation` after validating actual model membership, then a unique actual clip
187
- (or unique common clip in compare). Thus `rig-review open /absolute/output
188
- --reference /absolute/original.mp4` opens a report-bound `dance` without the flag
189
- and prints `animation: dance (from report.json)` on stderr. Conflicting A/B reports,
190
- a stale report name or multiple clips without a report refuse by name; no first
191
- entry, filename or frame rate supplies a guess. Explicit invalid names never fall
192
- back. The selected clip actually initializes single playback, while an explicit
193
- checkpoint link or later manual choice takes priority. Incompatible automatic
194
- checkpoints do not apply another clip's time or axis. Static/source-only packages
195
- remain inspectable without claiming clip selection; explicit unvalidatable clips
196
- refuse. Single review sessions remain package-wide; animation selection does not
197
- create an animation-specific approval identity. Opening never
198
- approves a review. The command prints the matching feedback retrieval command on
199
- stderr; stdout contains the URL alone. A selected external video takes precedence
200
- at automatic startup; an explicit checkpoint link or manual checkpoint selection
201
- still chooses that checkpoint's reference.
202
-
203
- The selected video is a separate CLI capability, not a manifest path escape or a
204
- file copied into the source package. Its name and streaming byte hash, plus the
205
- base package hash, appear as `referenceBinding` in single review responses,
206
- sessions and feedback exports. Use the same `--reference` when retrieving that
207
- submission; omitting it reads the separate reference-free identity. Changed
208
- package or reference bytes start pending state. Existing historical retrieval
209
- preserves earlier recorded tool versions over exactly the same reference content.
210
-
211
- The single strip panel can export the selected external video using that same
212
- binding. Headless equivalent (the original declared-reference form stays valid):
213
-
214
- ```sh
215
- rig-review strip /absolute/output --reference-video /absolute/original.mp4 --animation dance --frames 176:184 --out /absolute/external/strip.png
216
- ```
217
-
218
- Only tool-owned instances with matching content, canonical input locations, tool
219
- fingerprint and a private capability handshake are reused. The URL hash is
220
- checked before serving the app; a stale link never selects another instance's
221
- package. Input hashes are freshly computed, not trusted from mtime caches. Idle
222
- open-managed workers stop after 30 minutes. Their private registry, requests and
223
- logs live beside feedback under `~/.local/state/rig-c-review/open/`; nothing is
224
- written into supplied folders. `RIG_REVIEW_STATE_DIR` selects another state root
225
- outside all input packages, judged by its canonical location (symlinks in the path
226
- and its existing ancestors resolved). Existing foreground review commands still work.
227
-
228
- No browser is opened, no LAN listener is added, and no misleading phone/QR URL is
229
- printed: every current instance binds only `127.0.0.1`.
230
-
231
- The printed feedback retrieval command uses POSIX shell quoting for literal path
232
- arguments. It is not a PowerShell or cmd.exe command string.
233
-
234
- ### Launcher contract
235
-
236
- A launcher may start `rig-review open` as a child process and rely on its stdout,
237
- stderr and exit code alone:
238
-
239
- 1. On success stdout is exactly one line, the URL. `open` never writes anything
240
- else to stdout.
241
- 2. On success stderr carries the human lines in this fixed order: for the `check/`
242
- package of a `rig-parts build` output, `Package: rig-parts build check package <path>`
243
- and the handoff line (`Handoff <how>: <config path> sha256 <hash> · parts <dir>`, or
244
- `Handoff: <refusal>`); the animation diagnostic (when a clip is selected), the control notice (with `--control`),
245
- then the copyable retrieval line `Review started|reused; feedback: rig-review feedback …`.
246
- 3. A refusal exits with code 1, writes nothing to stdout, and its first stderr line
247
- starts with a name, followed by a colon and the explanation:
248
- - `OPEN_INPUT_MISSING <path>`: the input or `--compare` path does not exist, or is neither a directory nor a regular file.
249
- - `OPEN_INPUT_UNRECOGNIZED <path>`: the folder holds no recognized model, skeleton, motion, report, reference or pose sequence and is not a `rig-parts build` output root, or the file is not a JSON manifest.
250
- - `OPEN_REFERENCE_REQUIRED`: `--compare` was given without an explicit `--reference`.
251
- - `OPEN_ANIMATION_UNKNOWN <name>`: the explicit or report-named clip (JSON-quoted) is not in the compiled model.
252
- - `OPEN_ANIMATION_AMBIGUOUS`: no flag or report selects one clip, or the A/B reports conflict.
253
- - `OPEN_ANIMATION_UNVALIDATABLE`: a clip was requested but no compiled model can validate it.
254
- - `OPEN_OPTION_INVALID <flag>`: an unknown, duplicate, empty or value-less option, a handoff flag without `--config`, `--source` without `--full` (or the reverse), or a handoff flag with `--compare`.
255
- - `OPEN_STATE_INSIDE_PACKAGE <package | build root | handoff root>`: the state directory (`RIG_REVIEW_STATE_DIR` or the default), resolved through symlinks, lies inside an input package, the `rig-parts build` root a check package belongs to, or the producer handoff's root.
256
- - `OPEN_HANDOFF_INVALID <flag or path>`: a declared or discovered producer handoff path is refused — `HANDOFF_PATH_OUTSIDE_ROOT`, `HANDOFF_SYMLINK_ESCAPE`, `HANDOFF_INPUT_MISSING`, `HANDOFF_INPUT_INVALID` or `HANDOFF_PATH_INVALID`, named after the colon.
257
- 4. Exit codes: 0 applied, 1 refused. (`control` keeps its own exit 2 for an
258
- unapplied command.)
259
-
260
- Package validation and worker startup refusals keep their existing messages; the
261
- named refusals above are decided before any state is written.
262
-
263
- ### Managed front and write capability
264
-
265
- `open` is **managed by default**. It ensures one detached loopback front at
266
- `127.0.0.1:4318` (`RIG_REVIEW_FRONT_PORT` overrides) for the state directory and
267
- prints `http://127.0.0.1:4318/instances/<key>/?input=…&handoff=…#cap=…`. Every
268
- inspection therefore shares one stable browser origin, so Settings, panel sizes and
269
- source-bound drafts survive a reopen. The front only routes: it takes the worker
270
- address from that key's private open record, allows a closed per-method route
271
- list, bounds bodies (16 KiB control, 1 MiB session/strip) and upstream time,
272
- limits concurrent upstream streams, re-verifies the worker's nonce on every
273
- request, and refuses an unknown, expired or replaced instance by name
274
- (`FRONT_INSTANCE_UNKNOWN`, `FRONT_INSTANCE_EXPIRED`, `FRONT_INSTANCE_REPLACED`).
275
- Worker error text is not passed through; failures become named `FRONT_*` codes.
276
- Package assets keep their `sandbox` CSP through the front. It never reads packages.
277
- Idle fronts exit after 30 minutes once no live instance remains.
278
-
279
- If another listener holds the front port, `open` fails with
280
- `FRONT_PORT_BUSY: 127.0.0.1:4318 is held by another listener; use --direct` and
281
- does not fall back silently. `open --direct` keeps the per-instance port and
282
- prints that worker's own URL.
283
-
284
- One origin cannot isolate scripts that run on it, so writes are bound to a
285
- per-instance capability rather than to the page's origin or the key in its path.
286
- Each worker mints a 256-bit capability kept only in its private 0600 open record.
287
- `open` (managed or direct) and the foreground `review`/`review compare` commands
288
- print it in the `#cap=` fragment, which browsers never send to a server. That
289
- printed link goes only to the terminal of the command that started the instance.
290
- Output meant for a log does not carry it: the install smoke (`bun run smoke`, whose output CI publishes) prints the same
291
- URL as `#cap=<redacted>` and fails if any capability value reaches its stdout. The page
292
- moves it into memory and that tab's `sessionStorage`, then removes the fragment
293
- from the address bar. Every mutation sends it as `X-Review-Capability`; the front
294
- checks it before forwarding and the worker compares it again in constant time
295
- before its existing same-origin check. No endpoint returns it, so a page opened
296
- for one instance cannot submit to another. A copied view link (without the
297
- fragment) opens read-only and says *Read-only view: reopen with rig-review open to
298
- edit*; run `open` again for a writable link.
299
-
300
- ## Control the current inspection page
301
-
302
- Opt in once when opening a fixed package, then keep that page open:
303
-
304
- ```sh
305
- rig-review open /absolute/output --reference /absolute/original.mp4 --control
306
- rig-review control /absolute/output --reference /absolute/original.mp4
307
- ```
308
-
309
- The second command reads the connected page's actual inspection state and its
310
- `pageId`/`revision`; it does not open a browser or start another worker. A page
311
- learns from `GET /api/access` (`control: true|false`) whether its instance serves
312
- control; an instance opened without `--control` is never asked for
313
- `/api/control/config`. To change
314
- that page, use the observed values and a closed inspection command:
315
-
316
- ```sh
317
- rig-review control /absolute/output --reference /absolute/original.mp4 --page '<pageId>' --expected-revision 0 --request-id theme-1 --command '{"type":"theme","value":"light"}'
39
+ ```text
40
+ http://127.0.0.1:4318/instances/<key>/?input=…&handoff=…#cap=…
318
41
  ```
319
42
 
320
- Use the same reference/compare/animation inputs as the controlled `open`. Multiple
321
- pages require an explicit `--page`. The URL and source binding stay unchanged;
322
- existing ordinary servers do not become controllable. Starting the opt-in worker
323
- is a one-time handoff, not a new link for each parameter change.
324
-
325
- Commands cover appearance, automatic layout, synchronized or independent pane
326
- cameras, playback, single animation, time and actual inspection layers. Camera
327
- pan is measured in viewport CSS pixels (right/down positive). New inspections
328
- start with camera sync on: zoom relative to fit and pan normalized by each pane's
329
- viewport move all visible measurable reference/rig panes together. Turn sync off
330
- with `{"type":"camera-sync","value":false}` to inspect each pane independently;
331
- turning it off preserves current views. Turning it on aligns the visible panes
332
- to the last active pane. Camera/fit targets are `rig|reference` in single review
333
- and `a|b|reference` in compare. For legacy clients, compare `target:"rig"` explicitly
334
- moves both A/B even when sync is off; its CSS readback aliases A. Comparison retains its
335
- fixed animation and actual video-master seek path. A single-video time request
336
- uses the actual decoded timestamp for both reference and rig; the applied
337
- acknowledgement reports that time, which may differ from the requested seconds.
338
- Unsupported features, stale
339
- page/source/revision, unsubmitted feedback drafts and out-of-range values refuse
340
- by name. No command changes a package/reference, authors motion, or submits or
341
- approves feedback. Local controls and remote readback use the same inspection
342
- state. A live view-link/history change supersedes pending remote work, and
343
- commands refuse while that view is being restored.
344
-
345
- Remote inspection readback also includes versioned numeric `view` coordinates
346
- when the page can measure them: rig/world center and zoom, reference/image center
347
- and zoom, each panel's current CSS viewport size and visible rectangle. World
348
- coordinates use Y up; reference image coordinates use Y down. Compare reports
349
- actual A and B rectangles separately; unavailable measurements are null. The
350
- canonical view also reports `cameraSync` and `activeCamera`. These
351
- are the same current cameras used by the local controls, rather than a second
352
- simulated view. Camera commands still use the documented viewport CSS pan.
353
-
354
- The CLI waits up to10 seconds for the page's actual acknowledgment. `queued` or
355
- `dispatched` is not success; non-applied commands exit2 and may carry
356
- `timedOut:true`. Reuse the same `--request-id` and original request to obtain its
357
- receipt without applying it twice. `--wait-ms 0` returns the queued receipt;
358
- `--wait-ms` is bounded to30000. A timeout does not mean an already delivered action
359
- was rolled back. Tokens stay in private tool state, not URLs or command output.
360
-
361
- ## Keep and share an inspection
362
-
363
- The view link stores the current numeric inspection in a bounded, versioned
364
- `#review=` fragment. It includes the exact loaded source/model binding, selected
365
- animation/time and reference selection, appearance/layers, camera sync and the
366
- active pane, and each canonical camera center with visible bounds. Reloading a matching link restores a paused view;
367
- it does not restart an idle loop or submit a review. Coordinates adapt to the
368
- current viewport instead of replaying another window's CSS pan offsets.
369
- Historical links without a sync mode restore with sync off to preserve their
370
- saved views; fresh inspections start with sync on. The page keeps the current
371
- fragment in the address bar as you inspect, so copying the address copies the
372
- view; there is no separate copy button. Legacy modeling/motion
373
- workspace selections are accepted without restoring a top-level workspace switch. A link
374
- for a different source binding is refused.
375
-
376
- Ordinary form inputs, selected panels, view state and unsent feedback/checkpoint
377
- choices are kept in browser local storage for the same source. The draft scope
378
- includes the source hash (`single:<id>:<sourceHash>`; compare uses both sides'
379
- id and hash), so two revisions of one package keep separate drafts. Opening a page
380
- deletes no draft; saving evicts the least-recently-written drafts beyond 32, but
381
- never one written within the last 24 hours. Browser
382
- Settings (below) remain, while a source-bound appearance override does not
383
- carry over. URL view state takes precedence over the locally saved view. Each
384
- Inspector section's own scroll position is part of this source-bound draft.
385
-
386
- Local recovery compares the original submitted server-session fingerprint,
387
- including its update time. It cannot replace a newer server review: conflicting
388
- or corrupt saved data remains available to download, with an explicit warning;
389
- recovering a conflicting draft requires choosing a pending review. Storage
390
- errors never count as a successful save. Nothing is written to the server or
391
- input package merely by restoring a page. Browser storage is convenient local
392
- recovery, not a durable backup; submit the review (and retrieve it with
393
- `rig-review feedback`) when it must be kept.
394
- Control capabilities are excluded from links and browser persistence.
395
-
396
- ## Workspace settings and panels
397
-
398
- The review page has a left column (Parts explorer, with checkpoints and the
399
- reference select folded above it), the stage, the Inspector below it with
400
- **Setup** and **Animate** sections, and a status footer. The compare page
401
- keeps sources on the left and the Parts explorer on the right. The page
402
- header carries the **Setup** / **Animate** mode switch next to the package
403
- title; it is the only way to change the Inspector's section. Setup is the
404
- mesh-review composition (selected part, part presets, modeling-review
405
- proposals) and holds nothing that seeks or plays; the angle inspection, the
406
- transport and frame navigation are in Animate. **Write review** at the top
407
- right opens the review dialog, a modal holding the review status, reviewed
408
- source, selected checkpoint, structured feedback and frame-strip export, with
409
- its own **Submit review**. Escape or Close dismisses it and returns focus to
410
- **Write review**; the draft keeps saving whether the dialog is open or not,
411
- and the footer and the dialog both show its state.
412
- The left column's header (Parts title and search) stays fixed at the top;
413
- the checkpoint strip, the part list and Settings scroll beneath it as one
414
- body. A drawer keeps its title and close button in place while its body
415
- scrolls. The per-inspection **Repeat idle** toggle sits in the Animate
416
- transport row.
417
-
418
- Animate's **Environment** block drives the player's physics while you
419
- inspect: a **Wind** base (−100…100), **Breeze**, a **Gust** pulse
420
- (peak 0…200, duration 0.1…20 s, **Repeat** every max(6, duration + 0.6) s)
421
- and **Gravity** (0…4), applied through `player.environment` in the same frame
422
- step that draws. The gust clock is the stepped player time, so pausing freezes
423
- it. **Jolt** sends a translate-and-return impulse through `player.moved`;
424
- with **Drag to trail** on, dragging the rig pane moves the rig's physics
425
- instead of the camera (Space still pans). **Reset physics** returns the
426
- runtime start values and re-poses from the current time, clearing the physics
427
- history. Until a control is touched the player stays at the runtime start values. The
428
- state is kept in the local draft and recorded in feedback as
429
- `context.environment`; nothing is written to the model or motion. The compare
430
- page does not have this block.
431
-
432
- Below it, the **Physics tuning** panel (`#physics-panel`) edits the physics
433
- constraints through the producer. Each row is a constraint of the inspected
434
- model, matched to the handoff config's `constraints[]` entry of the same name
435
- and kind; a row is editable only when every field the panel edits reads the
436
- same on both sides. Rows the model has and the config does not declare (the
437
- rig stage's `turn` twins), the config's transform constraints and any row that
438
- does not map 1:1 are listed under **Not editable** with the reason. Picking a
439
- row opens its editor: a slider and number field for each of `strength`,
440
- `damping`, `inertia`, `mass`, `wind`, `gravity` and `mix` the config declares,
441
- and toggles for the `x`, `y` and `rotate` components (0 is off). A **Chain**
442
- (from bone parentage: links on parent bones that move the same components;
443
- pick one from the list or with **Pick on stage**) takes a **Ramp**: one value
444
- per link, linear from root to tip. Every edit is a config patch op at
445
- `/constraints/<i>/<field>` that also records the constraint's `name`; the
446
- pending ops are listed with a per-op **Revert** and kept in the local draft.
447
- **Apply** sends them as one patch (`rig`, then `check`, about 20–50 s on a real
448
- package); the scratch build reloads under the same wind, gust, jolt and gravity
449
- state Apply was pressed in, and the readout shows the `ANIMATIONS_DO_NOT_FOLD`
450
- and `TEXTURE_STRETCH` lines before and after. BEFORE for the inspected build
451
- is its own check: once the baseline reproduces, review runs `rig-parts check`
452
- on that baseline output in the background (about 16 s on a real package) and
453
- stores the summary beside the baseline record, so a reload reads it; until it
454
- finishes the line says "check running…", a producer refusal is shown verbatim,
455
- and "not in check summary" remains only when the baseline did not reproduce. A
456
- refusal is the producer's line verbatim (`CONFIG_CONSTRAINT_*`,
457
- `CONFIG_NAME_RESOLVES`, rigc's `A23` for a mass of 0) and the stage is
458
- unchanged. **Reset** returns to the inspected build and clears the pending ops.
459
- Submit refuses a review written on an applied build whose check failed
460
- (`EDIT_SUBMIT_UNCHECKED`).
461
-
462
- With pinned published Play **1.14.0**, **Preview pending physics** previews
463
- `strength`, `damping`, `inertia`, `wind`, `gravity` and `mix` through the live
464
- player's public `setPhysicsParameters` / `readPhysicsParameters` /
465
- `clearPhysicsParameters`. Slider input updates the standing patch while dragging;
466
- the label separates the pending override from the last evaluated effective values.
467
- The next evaluation uses it after authored keys and sliders, before physics. While
468
- paused, the page explicitly re-poses the held time and draws; it never seeks or
469
- resets history to preview. `mass` and `x` / `y` / `rotate` stay Apply-only.
470
- **Clear preview** disables preview and removes its overrides, keeping the pending
471
- ops and their Apply bytes. Authored values resume on the next evaluation; the
472
- integrated history is retained, so clearing does not undo motion caused earlier.
473
- Apply and a model replacement clear the preview; Reset clears both preview and
474
- pending ops. Preview state is never recorded as applied or submitted.
475
-
476
- Play retains the current standing patch across play and seek, with no history of
477
- patch changes. A backward seek after an edit recomputes the past using the patch
478
- standing now, rather than reproducing the old trajectory. A newly opened track
479
- first evaluates its ordinary authored pose, then receives the patch as pending;
480
- at seek time 0 its effective values can therefore still be authored until the next
481
- evaluation. No live track means a named refusal; preview never creates one.
482
-
483
- Below it, the **Idle tracks** panel (`#idle-tracks`) edits the idle through
484
- the producer. It lists the handoff config's `motion.tracks`: a single track
485
- opens amp, period, phase and base (base can be added or taken out); a chain
486
- track opens one amp per link and lag, and draws a handle at each link on the
487
- stage (drag up or down; arrow keys step 0.05°). Period is a select of the
488
- divisors of `motion.duration` (`duration / 1 … 12`) — the only periods the
489
- producer accepts — beside a field for typing one, which review's pre-check
490
- refuses with the producer's own line (`CONFIG_PERIOD_DIVIDES_DURATION`). A
491
- track is added by bone + property or by chain (it starts still: amplitude 0,
492
- the whole idle's period) and removed with **Remove**; a second track on a
493
- property already keyed (a chain link's `rotate`, a blink member) is refused
494
- before Apply by the producer's `CONFIG_BONE_PROPERTY_KEYED_ONCE`. The blink's
495
- `t` is a marker above the transport scrub, dragged along the timeline (arrow
496
- keys step one 12 fps frame), with `squash` and `brow_drop` sliders. The
497
- pending ops (`/motion/tracks/<i>/…`, `/motion/blink/…`) are listed with a
498
- per-op **Revert** and kept in the local draft. While edits are pending the
499
- **preview** drives the edited bones through the player's `adjust` with the
500
- producer's sine on the bone the idle keys (`<bone>_ctl` under ctl keys, read
501
- off the handoff rig). It evaluates from Play's `BoneAdjust` context's
502
- `trackTime`, including seek replay, and the turn dial receives the same context.
503
- It reads no displayed player clock and adds no compensating re-pose after seek.
504
- With unchanged patch, animation, environment, reset and step partition, seek and
505
- continuous evaluation agree bit for bit. It is labelled "preview — Apply decides"; it is never
506
- recorded or submitted, and a property the inspected idle does not key yet is
507
- not previewed (`IDLE_PREVIEW_TARGET_UNKNOWN`). **Apply** clears the preview and
508
- sends one patch (`rig --idle-keys <the inspected rig's>`, then `check`, about
509
- 30 s on a real package); the built idle replaces the preview, and the
510
- readout shows the `CHAIN_LAG`, `TIP_OVER_ROOT` and `BREATH_VISIBLE` lines and
511
- check's `loop:` line before and after. **Reset** clears the preview and the
512
- pending ops and returns to the inspected build.
513
-
514
- Below that, the **Readings** block (`#readings`) runs the compiler's own
515
- readings of the drawn animation: a **Folds** toggle, a **Pass-through**
516
- cloth:leg pair and a **Seam** slot:slot pair (slot names suggested from the
517
- model), an **Overlaps** toggle and an **Angle range** bone against a relative
518
- bone. **Run readings** sends the chosen readings (names only) to
519
- `POST /api/readings`; the server runs one `rigc render --animation <drawn>
520
- --folds | --pass-through <cloth>:<leg> | --seam <a>:<b> | --overlaps |
521
- --angle-range <bone>:<relative>` per reading on the inspected build and on the
522
- latest scratch build of the session (if an Apply built one), into scratch
523
- under the state directory (`readings/<package>/<build>/<key>/`; an identical
524
- reading on the same build and compiler is reused; about 0.5–5 s each on a real
525
- package). The frames each reading's JSON lists become ticks under the
526
- transport scrub — inspected above, scratch below, in two colours — and
527
- clicking a tick seeks to that frame's time (frame / fps). Each reading gets a
528
- worst-frame line, `<build> · <reading> <pair>: worst at frame N — <figure> ·
529
- <rigc's line>`, which also seeks; folds and overlaps list each folding mesh or
530
- moving pair under it. `--angle-range` writes no file, so its ticks are the
531
- lowest and highest frames its printed line names. An unknown slot or bone is
532
- rigc's to refuse, and its line is shown verbatim. While the Environment
533
- controls are applied, their current wind and gravity go along as a one-step
534
- `--environment` schedule (only when the installed rigc's `render --help` lists
535
- the flag). A reading is not a gate; nothing is written into the package. The
536
- pairs of the last run are recorded in feedback as `context.readings`.
537
-
538
- In the Environment block, **Use for readings** (issue #112) turns the controls
539
- into a `rigc-environment/1` schedule from the clip's first frame — one step when
540
- the wind is steady, else one step per 12 fps frame carrying the breeze and the
541
- repeating gust (equal to the panel's wind at every frame time; between frames
542
- rigc holds the frame's value, and the panel states how far that can be from the
543
- live wind) — and sends it with readings and with Apply, whose check then runs
544
- `rig-parts check --environment`. The wind clock restarts with the clip, so what
545
- you watch is the world the readings and bars are taken in. **Export schedule**
546
- downloads the same bytes; **Check under this world** runs `rig-parts check
547
- --environment` on the build on the stage (needs `@rig-c/parts` 1.48.0) and shows
548
- its `ANIMATIONS_DO_NOT_FOLD` and `TEXTURE_STRETCH` lines beside the same build's
549
- default-world check. A refused schedule (`CHECK_ENVIRONMENT`) is shown verbatim.
550
- Feedback records the schedule's bytes and sha256 as `context.environmentSchedule`;
551
- nothing is authored into the rig.
552
-
553
- Below Readings, the **Requirements** block (`#requirements`) declares what the
554
- scene asks of the motion (issue #113): pick a kind (contact, follow, aim,
555
- range, stretch, seam), an animation and the subject — the slot picked on the
556
- stage pre-fills the slot, its mesh attachment and its bone — then set each
557
- bar on a slider whose track marks the last measured value. **Check** sends the
558
- typed requirements (names and numbers only) to `POST /api/requirements`;
559
- review writes them as a `spine-parts-requirements/1` file (`spec`, `fps`,
560
- `requirements`; never `may_fold`, scene targets or the part exception lists)
561
- into scratch and runs the installed producer's `rig-parts check --rig <rig>
562
- --out out --parts <handoff parts> --requirements requirements.json` on the
563
- inspected rig spec or the latest scratch build. Each requirement gets one
564
- line with its worst frame (click to seek); `REQUIREMENTS_*`,
565
- `CHECK_REQUIREMENT_MET` and `CHECK_REQUIREMENT_MEASURABLE` lines are shown
566
- verbatim. A kind the rig spec cannot offer (no mesh slot, no ik or transform
567
- constraint, fewer than two slots) is disabled with the reason. The file rides
568
- submitted feedback by hash and content as `context.requirements`.
569
-
570
- The view bar is one row and keeps the layer toggles in reach in every section: **Images**
571
- (`I`), **Mesh edges** (`M`) and **Bone points** (`B`). The keys do nothing while
572
- a text field, textarea, select or editable text has focus, with Ctrl/Alt/Cmd
573
- held, or on key repeat; Space stays pan and Enter stays frame entry. A disabled
574
- toggle shows why as a tooltip, and the footer shows the same layer status. The
575
- footer also gathers the message, player, layout, renderer, idle, remote control
576
- and draft state; **View region** opens the numeric view. Side by side,
577
- Reference and Rig are in the **View** menu at the right end of the view bar,
578
- labelled with the current choice.
579
-
580
- The side and bottom panels are resizable. The separators are thin (4px)
581
- lines whose colour is never the scrollbar's; the grab area is wider than the
582
- line. Drag a separator, or focus it
583
- and use the arrow keys (Shift for larger steps) and Home/End. Escape, a
584
- cancelled pointer or window blur during a drag returns the previous size. The
585
- stage keeps a usable minimum. When the window is too narrow, the side columns
586
- (left first on the compare page), and when it is too short, the Inspector, become explicit
587
- drawers opened from buttons above the stage instead of shrinking to nothing. Responsive clamping
588
- never overwrites the desired size, so a wider window restores it.
589
-
590
- Settings, at the end of the left column's scrolling body, holds browser defaults: Theme, Sync cameras by
591
- default, Repeat idle by default, and Reset layout. Defaults seed a new
592
- inspection only. Theme is the page's only appearance control: it shows the
593
- current inspection's appearance, and changing it applies it and saves it as the
594
- default. A view link or source draft keeps its own appearance, camera
595
- sync and idle choice, and restoring it neither changes the saved defaults nor
596
- starts playback. Reset layout restores panel sizes and nothing else.
597
-
598
- These defaults and desired panel sizes are one versioned, closed record in
599
- browser `localStorage` (`rig-review-preferences/1`). It holds no source path,
600
- hash, feedback, review decision or capability. The first load migrates the
601
- earlier same-origin appearance preference. If storage is denied, unavailable
602
- or the record is corrupt, the page uses defaults and every control keeps
603
- working for that page; a corrupt record is refused rather than overwritten
604
- until you change a setting. Several open tabs merge their writes per field, so
605
- changing the theme in one tab does not undo a panel size changed in another.
606
- Separate browser profiles keep separate preferences.
607
-
608
- Browser storage is per origin. Managed opens share the front's one origin, so
609
- Settings, panel sizes and drafts carry across reopens. `--direct` and foreground
610
- serving use per-port origins and keep them separate.
611
-
612
-
613
- ## Inspect animation keys
614
-
615
- Animate → **Transport** opens the read-only Timeline, Curves and Bone tree views of the build currently on stage. The timeline preserves every stored bone/slot track and key time; click a key to seek, or scrub the transport to move its playhead. Bone selection filters its direct tracks. Tree, Parts and stage selections stay synchronized; enable **Bone points** to pick a bone on stage. Selecting a bone highlights all directly attached Parts rows without inventing a selected slot.
616
-
617
- Curves show the compiled skeleton’s exact linear, stepped or cubic Bézier segments and stored handles. The separately labeled **Runtime evaluation** cursor/readout uses Play’s ten-piece Float32 interpolation, which can differ from the cubic. Its local value includes setup rotation/translation and scale multiplication, in stored timeline order, before constraints and previews. Rotation is unwrapped local degrees. **Inspect time** accepts exact seconds; frame labels require the existing explicit clock. These views author no keys and change no package, motion, patch or review format. See [animation inspection](docs/ANIMATION_INSPECTION.md) for the numeric validation and reproduction commands.
618
-
619
- ## Inspect a part
620
-
621
- The Parts explorer (left column; right on the compare page) selects an actual model slot on a specific single/A/B side. All rows follow the public held snapshot's exact draw-order z, including hidden/null slots. Search matches the exact source slot names: a row is shown when its name contains the typed text, case-sensitively, with no case folding or normalization. Arrow keys/Home/End move between result buttons, Enter/Space selects, and Escape returns to search. Whole rig restores full drawing. Selecting a drawn row fits its conservative world bounds within the existing camera zoom range; Sync cameras applies the same normalized camera action to every visible pane, while independent mode changes only that selected pane. Hidden/empty geometry reports `PART_FIT_UNAVAILABLE` and keeps the camera.
622
-
623
- Solo preserves the full held pose's resolved clips and filters only drawing. In context, the whole rig remains visible with an amber dashed conservative selection box; this box is geometry/clip bounds, not exact source-alpha coverage. Selection changes no track, skin, set, motion, source asset or review decision. The selected slot's side/mode survives source-bound links and local drafts. The Setup section shows actual posed mesh counts or region corners, page/region, current skin/set context and declared attachment candidates. **Use slot for feedback** explicitly prefills the target; selecting a row alone submits nothing.
624
-
625
- The pinned published `@rig-c/play`1.14.0 supplies `slotIdentity()` and frame generation. The explorer atomically retains the public identity snapshot with the same-generation held frame: exact attachment key, owning skin, active set/skin, full z order and explicit hidden/no-attachment/timeline-null/set-hide reasons. Hidden/null slots keep null keys with their reported reason; declarations and texture/UV names never replace that public result. An identity-known attachment need not have a visible primitive or positive fit bounds. The current UI follows its selected slot as animation changes, updating the displayed identity to the actual held frame.
626
-
627
- Source-bound links/drafts and explicit feedback preserve that side/slot plus exact attachment/owning skin/set/active skin/null reason. A restored explicit target is checked after restoring its animation/time/set and drawing; `PART_IDENTITY_MISMATCH` rolls back rather than silently substituting another key. Older slot-only links remain readable and resolve from the current authoritative snapshot. Runtime generation is current observation/feedback evidence, never a persistent identity. Named vertex weights and original source alpha remain unavailable in this inspection; packed pages are not original part images.
628
-
629
- This explorer is inspection. Producer-backed optimization and candidate Apply/undo/export remain separate work; the explorer does not generate mesh, rig or motion bytes.
630
-
631
- ## Chest regions (designate, rebuild, watch)
632
-
633
- In **Setup**, the view bar's **Chest regions** button (pressed state; hidden in Animate) opens the chest-region editor bar directly under the view bar and enables editing on the rig pane. The bar has an **R** and an **L** group, each with **X**, **Y**, **R** (core radius, 6–120) and **Band** (4–120) in crop pixels, x screen-right and y down; **Mirror R → L** copies R's y, core radius and band to L; **Download JSON**; and **Apply · Rebuild**.
634
-
635
- On the rig pane each region is drawn as two circles: the core (full weight) and a dashed outer circle at core + band, R in one colour and L in another (two theme tokens per side), with a centre handle, a core handle on the right edge, a band handle on the top edge and a label `R (x,y) r:… b:…` under the outer circle. Dragging the centre moves the region, dragging the core edge sets r and dragging the band handle sets the band, all in crop pixels through the current camera scale. Each drag and each committed bar value is clamped live with the public example rebuild script's non-overlap rule: integer values, both cores plus 8 px must fit the centre distance minus a 2 px margin (both shrink by one factor) and the bands share what remains. A side the rule changed flashes its label and the footer says `CHEST_REGIONS_CLAMPED`. A handle drag never moves the camera; a drag anywhere else, or with Space held, still pans, and the wheel still zooms.
43
+ ## What you can do
636
44
 
637
- The bar opens on **Circles** (the R / L groups above); **Landmarks** beside it (#105) states the chest the way the producer's `meshes.<part>.contour.bust` does. Three clicks on the part place the **right apex**, the **left apex** and the **cleavage** (each pick button arms the point its click places, then the next unplaced one; a placed marker drags), with **X** / **Y** fields for each in rig pixels, an **α** slider (core ratio, the producer's default 0.22), a **Clearance** slider (rig px, default 3.8) and optional **Bone R** / **Bone L** (empty is the producer's default, `boob_r` / `boob_l`). The two circles are drawn live with the producer's own derivation (`@rig-c/parts` `deriveBustRegions`): with `d` the distance from an apex to the cleavage, `R = round(d − clearance)`, `r = round(alpha × R)` and `band = R − r`, centred on the apex, named `bust_r` / `bust_l`, spacing 14. Review pre-checks the producer's one geometric refusal on the same numbers — `CONFIG_BUST_CLEARANCE` when `clearance <= 0`, `R_r + R_l >= |apex_r − apex_l|` or a radius is not positive — draws both circles in the overlap colour, shows the producer's detail, and Apply sends nothing. Otherwise Apply · Rebuild sends `{part, bust, configHash}`: the part's entry with `contour.bust` in place of `contour.regions` (the hand-written regions are dropped; every other field as written) goes to the same `rig-parts remesh --mesh` run as Circles, and the producer derives the circles again and judges the bones (`CONFIG_NAME_RESOLVES`) and a region declared twice (`CONFIG_REGION_NAME_UNIQUE`), verbatim. The part is the contour part whose entry already states `contour.bust`, else the one the bones name, else the only contour part. A config entry that states `contour.bust` seeds an untouched Landmarks mode.
638
-
639
- Rig pixels are the whole figure's frame, which is the crop frame the model's stage fixes (below), so a click maps through the same crop → world mapping. A part's own image is that frame less the part's `parts.json` box less the producer's 4 px pad: rig = part image + (box.x − 4, box.y − 4) (`src/chest-landmarks.ts` `partToRig` / `rigToPart`).
640
-
641
- **Apply · Rebuild** runs the installed producer on the designated regions and reloads the result on the stage, so the loop is point → rebuild → watch → repeat → submit. Review builds no mesh itself and never runs a manifest command:
642
-
643
- - **Producer.** `@rig-c/parts`' `rig-parts`, resolved the way `rigc review` resolves `rig-review`: `node_modules/@rig-c/parts/package.json` in the nearest directory walking up from the package, then from the directory `rig-review open` was run in; its `bin` entry is read from that manifest, never from `PATH`. The compiler is resolved walking up from `rig-parts`' own package: `@rig-c/core` first, then legacy `rig-c` only when the scoped package is absent. The selected package name and version are reported as `compiler`. Absent: `MODELING_PRODUCER_NOT_INSTALLED: … — bun add -d @rig-c/parts`; older than the first release with `remesh` (1.40.0): `MODELING_PRODUCER_TOO_OLD <found> 1.40.0`. Review never installs anything.
644
- - **Inputs.** The producer handoff (`docs/REVIEW_PACKAGE.md`, *Producer handoff*): `open --config` (and `--parts`), else the review.json `producer` block, else `config.json`, then `rig/config.json` inside the opened folder, then `../config.json` beside an opened `out/`. The parts directory defaults to `out/` beside the config and holds `parts.json`, `parts/` and the inspected `rig/`. The editor bar shows, before any Apply, how the config was found (declared, discovered, or discovered outside the opened folder), its path and SHA-256, and the baseline. Without a handoff the bar shows `MODELING_INPUT_UNAVAILABLE: config.json — looked at …` and Apply refuses `MODELING_HANDOFF_MISSING`, naming `--config`. The request carries only `{part, regions, configHash}`; the server reads the config, replaces `meshes.<part>.contour.regions` with the designated regions (every other field as written) and runs `rig-parts remesh --rig <out>/rig --config <config> --parts <out> --part <part> --mesh mesh.json --out revision`, then `rigc build --profile rig-play --pack` with the revision's own `--page-edges` / `--pack-shape`, both with the scratch directory as working directory. A region row with equal semi-axes and no clip is sent as the config's circle, any other as the 32-sample polygon. An untouched editor starts from the config's own circle regions and contour numbers.
645
- - **Baseline.** Once per session, when the handoff and the producer both resolve, review runs `rig-parts rig --config <config> --parts <parts> --out <state>/baseline/<key>/baseline` and compares every file it writes (rig.json, motion.json, mesh_report.json, sets.json, images/*) with the inspected rig directory: `baseline: reproduced — rig.json <hash> = inspected <hash>`, or `baseline: not reproduced — <first differing path> · <what Apply can still do here>`. The key is the package hash, the handoff record and the producer versions (no clock), so a reopen reuses the result. Ops that rebuild the whole rig refuse `EDIT_BASELINE_MISMATCH <path>` unless the baseline reproduced; `remesh` keeps its own `REMESH_BASELINE_MISMATCH`. A build the config does not reproduce — a hand repair — is not open to every Apply (#148): retune carries a **mesh** repair over but holds every image, so a repair that redrew an image refuses every physics and idle edit (`RETUNE_REQUEST_HELD: images/<part>.png`); remesh from a config entry refuses the repaired parts and re-fits the others; a mesh edit is held to the inspected mesh, not the config; and remesh, its mesh edit and retune all refuse an inspected `rig.json` (or `mesh_report.json`, `motion.json`) that is not the writer's text (`…_INPUT_WRITTEN` — re-serialise it as rig writes it, JSON with one space of indent and a closing newline, in a copy). The baseline line and `EDIT_BASELINE_MISMATCH` name which of these hold here, and a producer refusal review can explain carries `review: …` after the producer's own line (docs/REVIEW_PACKAGE.md, *Baseline reach*). Once it reproduces, `rig-parts check` runs on the baseline output in the background (into `<state>/baseline/<key>/inspected-check/`, with Apply's default `--page-edges free --pack-shape polygon`) and its summary is stored beside the baseline record as `inspected-check.json`; that is the physics readout's BEFORE for the inspected build, and a reopen reads it instead of re-running.
646
- - **Scratch.** `<state>/rebuild/<package source hash>/<designation hash>/` under `RIG_REVIEW_STATE_DIR` (refused, by its canonical location, inside the package, its build root or the handoff root), named from the package, the config bytes, the part, the entry and both tool versions — no clock. The same designation again reuses its build and says so; a new one replaces the previous scratch build on the stage (the earlier applied records' builds are kept beside the scratch root for undo, at most eight per package). The inspected package's files are only read.
647
- - **Watching.** The producer's lines stream into the editor bar (the latest line and a short log); a refusal shows the producer's first named line verbatim (for example `FAIL REMESH_REQUEST_HELD: …`) and the stage is unchanged. On success the compiled scratch build (served read-only under `/rebuild/<id>/`, its model and pages hashed) replaces the drawn rig in place with the same camera, animation, time and part set; playback resumes with physics when it was running. **Reset** returns the stage to the inspected build. **Undo last Apply** (the *Applied records* list at the top of the Inspector, its depth in the label) returns the stage to the previous applied build, or to the inspected build when none is left; a predecessor whose build was evicted re-runs its recorded step and must come back byte for byte, else `UNDO_PREDECESSOR_CHANGED: <what>` (docs/REVIEW_PACKAGE.md, *Undo across Applies*). A change to the handoff config ends the session's undo stack (reopen): Undo is then refused `FRONT_INSTANCE_EXPIRED`. The Cuts and Cloth blocks mark a pending op applied only while the record on the stage holds it, so after an undo they show the predecessor's ops as applied, and none on the inspected build. The mark shows what the build on the stage carries, not every op applied so far: while a cloth build is on the stage, the Cuts block marks nothing.
648
- - **Record.** The last applied state (`rig-review-applied/1`: part, regions, for a Landmarks Apply also `bust` — the entry as sent, with `regions` its two derived circles — config hash, entry SHA-256, producer and compiler versions, the producer's line and the scratch model's SHA-256) stays in the inspection draft, and feedback added while that build is on the stage carries it as `context.applied`. The review's `reviewedSource` still names the inspected build.
649
- - **Config patch.** The same loop takes any config edit as data: `POST /api/rebuild {patch}` with a `rig-review-config-patch/1` document (JSON Pointer ops over the handoff config, refused `EDIT_PATCH_STALE` when made against other config bytes) rebuilds the whole rig (`rig`) or one part (`remesh`) into scratch, compiles it and runs `rig-parts check` on it; the page's `window.rigReviewEditor.applyPatch(patch)` streams, reloads and resets like Apply, and the draft and `context.applied` carry `rig-review-applied/2` (the patch, its hash, the producer's line, check's summary and the diff of every document the producer writes, per file, with the images by hash). Schema and refusals: `docs/REVIEW_PACKAGE.md`, *Config patch and Apply*.
650
-
651
- The editor's values are the fields of Setup's **Chest regions · detail** block (R is region 1, L is region 2), which also holds what the bar does not: per region a name, a bone chosen from the loaded model's actual bones, the two semi-axes, a spacing and an optional medial clip x, plus the contour settings (tolerance, margin, spacing, max edge, stray, bridges). Setting R in the bar writes both semi-axes. A region with equal semi-axes and no clip exports as the examples' circle, `{"name", "shape": "circle", "cx", "cy", "r", "bone", "spacing", "band"}`; unequal semi-axes or a clip export as the generator's 32-sample polygon on its 1/256-pixel grid and are drawn filled, without handles.
652
-
653
- The overlay needs the loaded model to fix the crop: its declared stage must be the crop frame with the root at its bottom centre (`x = -width/2`, `y = 0`) and the drawn root bone must sit at the world origin, so crop `(x, y)` is world `(x - width/2, height - y)`. Otherwise the editor bar and the detail block show `CHEST_REGION_MAPPING_UNAVAILABLE: <what is missing>`, nothing is drawn, and the values still edit numerically and export from a crop size you enter.
654
-
655
- **Download JSON** (and the detail block's **Download chest-regions.json**) saves two files in the browser: `chest-regions.json`, formatted exactly as the public example files, and `chest-regions.binding.json`, which names the package source hash, the rig and motion SHA-256 values, the export time, the viewer and runtime versions and the mapping used. An export holds 1–8 regions, the set Apply accepts (#118): a detail row without a centre x is not designated, so one row exports alone; two regions keep the public examples' text byte for byte. Export refuses `CHEST_REGIONS_COUNT`, `CHEST_REGION_NAME_DUPLICATE`, `CHEST_REGION_BONE_UNKNOWN <bone>`, `CHEST_REGION_OUT_OF_CROP <name>`, `CHEST_REGION_OFF_GRID` and `CHEST_REGIONS_OVERLAP <a> <b>` (every pair is tested). Two circles overlap when their centre distance minus both cores is less than the sum of their bands; any other pair is tested through polygons. The generator refuses per mesh vertex, so this check can refuse a pair it would accept, never the reverse.
656
-
657
- Field values stay in the inspection's local draft. They never enter the review session's feedback, nothing is written into the package, and an export is review evidence, not an approval (see `docs/DESIGN.md`).
658
-
659
- ## Mesh panel (density, regions, segments → remesh)
660
-
661
- In **Setup**, under Chest regions · detail, the **Mesh** panel (`#mesh-panel`, #106) edits the selected part's `meshes.<part>` entry from the producer handoff's config — the entry `rig-parts remesh --mesh` takes — and rebuilds that one part. Select a mesh part in the parts list; the panel starts from the config's own entry (or the draft's pending entry for that part).
662
-
663
- - **Modes.** The two modes whose fields the panel draws: **contour** (sliders for `tolerance`, `margin`, `spacing`, and optional `maxEdge`, `budget`, `stray`, each with a checkbox that declares or removes it; `bridges` as a checkbox) and **grid** (`grid`, optional top-level `stray`). In both: `r`, optional `exponent`, `rule` (absent / `distance` / `heat`), and the weight cut `influences.maxInfluences` and `influences.floor`. Each slider's range is review's drawing range only; the number field beside it takes any value and the producer judges it. An `auto` entry is shown with `MESH_PANEL_MODE` and no controls; an `influence` entry is not offered either, because `remesh --help` names grid, contour and auto entries and not influence (that is the producer's to document). The mode itself is not switched here.
664
- - **Regions** (contour). **Add circle** adds a circle region (drawn like the chest editor's circles, in the mesh-region colour); **Draw polygon** arms the stage: each click places a point in rig px (whole pixels, so on the producer's 1/256 grid), **Enter** (or the button again) closes it with three or more points and **Escape** drops it. **Drag on stage** lets a polygon vertex or a circle centre be dragged; a drag never pans the camera. Each region row has its name, a bone chosen from the loaded model's bones, `spacing` and `band` (and `cx`, `cy`, `r` for a circle) and **Remove**. A `contour.bust` in the entry stays as written (the chest editor's Landmarks mode edits it).
665
- - **Segments.** The entry's `segments` as written, each removable; **Add** appends a bone picked from the loaded model's bones. In contour mode each string segment (a chain) has a **ribs stations** field (empty: no ribs); removing a segment drops its ribs.
666
- - **Pending entry.** The diff against the config's entry, one line per JSON Pointer (`/meshes/<part>/contour/spacing 28 → 20`). Review pre-checks the pending entry with the config loader's rules and names (`CONFIG_MESH_MODE`, `CONFIG_FIELD_PRESENT`, `CONFIG_FIELD_TYPE`, `CONFIG_REGION_NAME_UNIQUE`, `CONFIG_NAME_RESOLVES`, `CONFIG_RIBS_MODE`, `CONFIG_STRAY_MODE`) and Apply is disabled while one stands. What the outline refuses at the rig stage — a spacing whose keep radius snaps to 0 (`CONTOUR_PARAMETER`), a bridge no quad makes (`CONTOUR_BRIDGE`), a heat source (`RIG_HEAT_*`), a remesh rule (`REMESH_*`) — is sent and comes back as the producer's line.
667
- - **Apply.** The pending entry becomes one config patch, `replace /meshes/<part>` with the whole entry and nothing else, run by `remesh --part <part>` through the patch runner (*Config patch* below): remesh into scratch, compile, check. On a build the revision replaces the drawn rig in place (same camera, animation, time and world), the **Mesh edges** layer turns on so the new triangulation is drawn, and the panel shows the part's vertex / triangle / hull counts before and after from the revision's `remesh.json`, one line comparing `remesh.json`'s after-count with the revision model's triangles for the slot and the triangles the overlay draws (`MESH_COUNTS_AGREE` / `MESH_COUNTS_DIFFER`, every figure named), and `TEXTURE_STRETCH` before (the build that was on the stage; for the inspected build its background check) and after. **Folds reading** runs the #108 folds reading of the drawn animation on the inspected build and on this revision and puts one worst-frame line per build in the status. A refusal leaves the stage and the pending entry as they were and the status line is the producer's line verbatim (for example `FAIL REMESH_CANDIDATE_REFUSED: … CONTOUR_PARAMETER: contour mesh "<part>", spacing — is 0.003; its keep radius … snaps to 0 grid units of 1/256 px …`).
668
- - **Reset** returns the pending entry to the config's and the stage to the inspected build.
669
- - **Record.** The draft keeps `#mesh-pending` (`{schema:"rig-review-mesh-entry/1", part, entry}`, only while it differs from the config). A built Apply's `context.applied` is `rig-review-applied/2` with `mesh` added: the part and its counts before and after.
670
-
671
- ## Weights (setup pose, watch only)
672
-
673
- In **Setup**, after the Mesh panel, the **Weights** block (`#weights`, #107; **Weights** in the block row) shows the selected slot's mesh weights as the compiled model document stores them. **Show weights** draws the slot's mesh at the setup pose over a veil that dims the rest of the rig: each vertex is a dot and each triangle is filled with the mean of its vertices' colours.
674
-
675
- - **Bone weight.** Pick a bone (the slot's influencing bones first, then every other bone of the model): each vertex is coloured by that bone's stored weight on the weight ramp, 0 → 1 (`--rv-weight-0`, `--rv-weight-half`, `--rv-weight-1`); a bone that does not influence a vertex is 0 there. The legend shows the ramp and the bone.
676
- - **Max influence.** Each vertex takes its dominant bone's colour (the largest stored weight; the first stored on a tie), from `--rv-weight-bone-1` … `-6` in the influencing bones' order; the legend names each bone with its colour.
677
- - **Hover.** The vertex under the pointer (within 10 px) is ringed and labelled with its index, the bone and the weight drawn; the block's readout lists all of its stored influences. Values are printed as stored, never rounded or normalised.
678
- - **Source.** The vertices and weights are the slot's attachment at the setup pose (`skins[].attachments.<slot>.<name>.vertices` of `rigc-compiled/3`: `{weighted:true, bindings}` — per vertex `{bone, x, y, weight}` — or `{weighted:false, xy}`). An unweighted mesh is its slot's bone at 1.0, named. A linked mesh reads its source mesh. A never-played player reads only the public setup identity, including any setup-slider attachment switch. Positions, triangles and hull come directly from published Play 1.14.0's `readSetupMesh`, which includes selected setup-slider deform; Review supplies no skinning formula. Its active skin selection (null for a fresh player) stays distinct from the supplying table used for stored metadata. Hidden/null identities keep their public reason and are never guessed from declarations. The stage player is not touched, so the drawing can sit apart from an animated pose on the stage.
679
- - **Refusals.** A package without a compiled model document refuses `WEIGHTS_MODEL_UNAVAILABLE`; no slot `WEIGHTS_SELECT_SLOT`; a slot without a mesh `WEIGHTS_NO_MESH <slot>`; a malformed attachment `WEIGHTS_MODEL_INVALID`.
680
-
681
- Nothing here edits, applies or records a weight: the block adds nothing to the draft or to feedback, and weight editing is out of scope. With **Edit mesh** on, the weights layer is not drawn.
682
-
683
- ## Bones (config anchors, check, rebuild)
684
-
685
- In **Setup**, after Weights, the **Bones** block (`#bones-panel`, #109; **Bones** in the block row) edits where the producer config puts its bones. It reads the handoff config's `bones` as written (`GET /api/rebuild` returns them as `bones`): a single bone `{name, parent, at, tip?}` or a chain `{chain, parent, points, tip}` whose links are `<chain>0 … <chain>n` (rig-parts AUTHORING, `bones`).
686
-
687
- - **Stage mode.** **Edit bones on stage** (`#bones-edit`, `aria-pressed`; it pauses playback and ends when Setup is left) draws every anchor on `#rig-overlay` at its config position: an origin (`at`) as a square, a chain point as a circle, a tip as a diamond, joined along the chain or from the bone to its tip. Positions are rig px, y down, mapped to the rig world through the same crop → world mapping the chest and weights views use (the model's stage). They are the config's setup positions; an animated stage pose can sit apart from them. Without that mapping no handle is drawn and the block says why.
688
- - **Drag.** Drag a handle (within 9 px) to move it; the pointer's delta is snapped to whole rig px and added to the config's value, written with the decimals that value already has (an integer stays an integer). **Bone** (`#bones-select`) picks a bone, whose anchors can also be typed. Each move is one config patch op, `replace` at `/bones/<i>/at`, `/bones/<i>/points/<k>` or `/bones/<i>/tip`, recording the bone's name (a chain's `chain`) beside the index; the server refuses an index that no longer holds that name (`EDIT_PATCH_NAME_MISMATCH`). The pending ops are listed with their delta and a Revert each; the draft keeps them (`#bones-pending`, `{schema:"rig-review-bones-draft/1", edits}`, restored only while each anchor still holds the value it was made from).
689
- - **Check.** `POST /api/bones-check` applies the ops to a scratch copy of the handoff config and runs the installed producer on it: `propose --from-config` prints its LINT lines, the LINT count line and one coverage line per bone (shown as printed; a LINT line blocks nothing — propose exits 1 on any, and Apply stays enabled), and `compare --left <handoff config> --right <patched>` gives each moved bone's origin (and tip) distance and the summary lines. `propose --from-config` reads the source painting: a handoff without one (declare it with `--source <png> --full <dir>`) shows `MODELING_INPUT_UNAVAILABLE: source painting — …` in place of the LINT lines, and compare still runs. Any change to the pending ops clears the last Check's lines.
690
- - **Apply · Reset.** Apply sends the ops as one `rig` patch through the generic runner (`rig --config`, compile, `check`): a whole-rig op, so it runs only when the baseline reproduced the inspected rig (`EDIT_BASELINE_MISMATCH` otherwise). A producer or rigc refusal (`RIG_TIP_DISTINCT` for a tip dragged onto its origin, `CONFIG_NAME_RESOLVES`, rigc's `A51`, `RIG_HEAT_BONE_SOURCE`, …) is shown verbatim and leaves the stage as it was. On a build the re-weighted rig reloads in place, and the Weights block reads the revision. Reset returns to the inspected build and clears the pending ops.
691
-
692
- Adding, removing or re-parenting bones is out of scope.
693
-
694
- ## Rig fields (regions, still row, stops, inherit, turn)
695
-
696
- In **Setup**, after Bones, the **Rig fields** block (`#rig-fields`, #110; **Fields** in the block row) edits the producer config's small fields. `GET /api/rebuild` returns them as written in `fields` (`regions`, `motion.blink`, `stops`, `bones`, `turn`); the block's state line counts what the config states. Every edit is one config patch op at the field's own pointer, listed with a Revert; the draft keeps the pending ops (`#rig-fields-pending`, `{schema:"rig-review-rig-fields-draft/1", edits}`, restored only while each field still holds the value the edit was made from).
697
-
698
- - **`regions.<part>`** — the bone a rigid part rides. Pick the part (selecting a region part on the stage picks it) and its bone among the bones the config declares (chain links expanded, and `root`): `replace /regions/<part>`. A meshed part is not offered.
699
- - **`motion.blink.still.<part>`** — a lash's still row, `{row, bone}`: rows above `row` (rig px, y down) are drawn by `<part>_still` on `bone` and do not blink. The block offers the region parts on a bone the blink's `eyes` names; **Add still row** starts at the eye bone's origin row on its parent. The row is drawn across the part on the stage and dragged in whole rig px (its number and bone can also be typed): `add /motion/blink/still/<part>` (with `add /motion/blink/still {}` first when the config states no `still`), **Remove** sends `remove`. It sits in Setup, beside the part it cuts, rather than in Animate's blink controls.
700
- - **`stops[i]`** — a stop's inward and outward bounds (degrees). Picking a stop draws its two arcs on the stage at the first stopped link's origin, around the rest aim at the touch point (`at` in `against`'s frame, as rig-c's `expandStops` aims it); dragging an arc end sets that bound in whole degrees (never below 0). The op replaces the whole entry, every other key as written, and names its `chain`: `replace /stops/<i>`. Adding a stop is out of scope.
701
- - **Inherit** — a single bone's `inherit`, or a chain link's entry in `link_inherit`, picked among rig-c's five modes (`normal`, `onlyTranslation`, `noRotationOrReflection`, `noScale`, `noScaleOrReflection`; absent is `normal`): `add` (or `remove`) at `/bones/<i>/inherit` or `/bones/<i>/link_inherit`, naming the bone.
702
- - **`turn`** — the pivot and a part's depth, each a slider with a number: `replace /turn/pivot`, `replace /turn/depths/<part>`. The **dial** holds the drawn build's `turn` bone at x rig px through play's `adjust` (shared with the idle preview), labelled *preview — Apply decides*: it shows the build on the stage, not the pending depths, and it is never recorded. Apply and Reset set it back to 0. `turn.deform` is out of scope.
703
-
704
- **Apply** sends the pending ops (checked against the block's scope, `RIG_FIELDS_OP_OUT_OF_SCOPE` otherwise) as one `rig` patch through the generic runner (`rig --config`, compile, `check`) — a whole-rig op, so it runs only when the baseline reproduced the inspected rig. rig-parts has no `retune` op yet; when it has, stops and inherit can move to it. The producer's refusal (`CONFIG_STILL_OFF_THE_BLINK`, `RIG_STILL_ROW_CLEAR`, `CONFIG_BONE_INHERIT`, `CONFIG_TURN_PIVOT_BEHIND_NEAREST` and the other `CONFIG_TURN_*`, `CONFIG_NAME_RESOLVES`, rigc's `RIG_STOP_BOUNDS`, …) is shown as printed and leaves the stage as it was. On a build the revision reloads in place, the status line names the per-file diff, and the readout shows check's `BLINK_NO_HOLE` and `FEET_STAY_LEVEL` lines before (the build that was on the stage, or the inspected build's stored check) and after. **Angle range** runs the angle-range reading (#108) on the inspected and the latest scratch build with one click: the inherit bone against its parent, else a re-boned part's new bone against the bone it rode, else the part's bone against its parent. **Reset** returns to the inspected build, clears the pending ops and sets the dial to 0.
705
-
706
- ## Cuts (cut polygons, patch boxes, limb joints → rig-parts build)
707
-
708
- In **Setup**, after Rig fields, the **Cuts** block (`#cuts-panel`, #115; **Cuts** in the block row) edits what the producer's assemble stage reads: the config's `assemble.cuts` (a plan part's layer cut by a polygon into a new part), `assemble.patches` (a part cut from the painting by a box) and `limbs` (a limb cut at its joints). `GET /api/rebuild` returns them as written in `cuts.config` (`assemble.plan`, `assemble.cuts`, `assemble.patches`, `limbs`) with `cuts.build`, the refusal that stops an Apply here (null when the handoff declares the painting and the runs). Everything is in rig px of the setup pose, the space of `parts.json`, mapped to the stage through the same crop → world mapping the chest, mesh and bones views use.
709
-
710
- - **Stage mode.** **Edit cuts on stage** (`#cuts-edit`) draws each cut polygon and patch box with a handle on every vertex and corner, each limb joint (the lower segment's bone origin) as a diamond, and a declared `range` as an arc from the joint (min to max degrees from the lower bone's rest direction, counter-clockwise on screen, rigc's sense). Drag a handle (within 9 px); the pointer's delta snaps to whole rig px. **Entry** (`#cuts-select`) picks a cut, a patch or a joint, whose numbers can also be typed.
711
- - **Ops.** A vertex is `replace /assemble/cuts/<i>/polygon/<k>` naming the cut by its `into`; a box is `replace /assemble/patches/<i>/box` naming the patch; a range is `add`, `replace` or `remove` at `/limbs/<limb>/segments/<k>/range` naming the segment's `part`; a joint is a bones op (`/bones/<i>/at` or `/bones/<i>/points/<k>`), since the joint is its bone's origin. The server checks the recorded name against the index (`EDIT_PATCH_NAME_MISMATCH`). **Place cut** / **Place patch** add a new part by clicking its polygon's points or its box's two corners: `add /assemble/cuts/-` (`{from, into, polygon, draw, overlap}`) or `add /assemble/patches/-` (`{name, box, alpha, draw}`), and `add /regions/<name>` with the bone typed beside it. Review refuses before sending only what is not the loader's type (`CUTS_VALUE_INVALID` for a negative or non-integer point, `CUTS_POLYGON_SHORT`); everything else is the producer's. The draft keeps the pending ops (`#cuts-pending`).
712
- - **Apply · Reset.** Apply sends the ops as one `rig` patch. The server runs it as `rig-parts build` (*Config patch and Apply*, Routing) from the declared painting and runs into scratch — a whole-rig op behind the reproduced baseline — and the scratch build reloads in place. The producer's refusal is shown as printed (`ASSEMBLE_CUT_INSIDE`, `ASSEMBLE_CUT_OVERLAP`, `ASSEMBLE_CUT_PIXELS`, `ASSEMBLE_UNDER_SIZE`, `ASSEMBLE_UNDER_DRAW`, `CONFIG_LIMB_RANGE`, `CONFIG_LIMB_BONE`, `CONFIG_PART_UNIQUE`, …), and a check bar that is not met (`SEAM_OVER_RANGE`, `LIMB_ONE_BONE`, `CHECK_SEAM_WITHIN_BAR`) keeps the build with check's FAIL line. The readout shows the `cut:` / `limb cut:` / `limb cap:` lines, the recomposite figures before and after with the scratch build's `recomposite_error_rig.png`, the setup-pose `seam` and `seam pair` lines, which parts changed and the build's wall clock, which the applied record keeps. Reset returns to the inspected build and clears the pending ops.
713
-
714
- Review paints no image: a new cut names no `under`, and a patch may only keep, move or drop an `under` or `cap` the config already names (`EDIT_PATCH_PATH_HELD`). Removing a cut or a patch is out of scope.
715
-
716
- ## Mesh edit (typed delta, preview only)
717
-
718
- In **Setup**, the view bar's **Edit mesh** button (pressed state; hidden in Animate) edits the selected slot's actual mesh attachment **in setup space**: the attachment's entry in the compiled model document (`skeleton.model.json`), its full-double setup world coordinates, triangles and hull from published Play 1.14.0's `readSetupMesh(assets, {slot, attachment, skin})`, shared with Weights. A never-played player reads only the public setup identity, including any setup-slider attachment switch; the named geometry read owns setup-slider deform. Its active skin selection (null for a fresh player) is distinct from the supplying table returned as `skin`. Hidden/null identities remain unavailable with the public reason; Review never guesses a mesh from declarations. A package without the model document (a Spine export alone, or the bundled demo) refuses `MESH_EDIT_MODEL_UNAVAILABLE` and the editor does not open. With **Whole rig** selected it refuses `MESH_EDIT_SELECT_SLOT`; a slot whose setup attachment is not a mesh refuses `MESH_EDIT_NO_MESH <slot>`, and so does a linked mesh (edit its source mesh). Entering pauses playback (the stage keeps the pose it held, under the veil; the mesh is drawn at the setup pose with the stage's own camera), turns the chest-region editor off and opens the mesh editor bar under the view bar. The rig pane dims except the slot's own triangles; the mesh is drawn at full opacity with every vertex as a handle, hull edges, interior triangulation edges and added constraint edges each in their own theme token.
719
-
720
- The bar has four sub-modes, one pressed at a time, with single keys that avoid the layer keys `I` / `M` / `B`:
721
-
722
- | Key | Sub-mode | Pointer |
45
+ | Task | Command or panel | Destination |
723
46
  | --- | --- | --- |
724
- | `V` | Modify | drag a vertex, or an edge to move both ends; Shift-click adds to the selection; drag empty space to box-select; arrow keys nudge the selection 1 crop px (Shift 10) |
725
- | `C` | Create | click empty space to add a vertex; click an edge to split it at the click point |
726
- | `X` | Delete | click a vertex to remove it and its edges (a vertex that ends a constraint edge is refused until that edge is removed, `MESH_EDIT_CONSTRAINT_END`); click an added constraint edge to remove it (hull edges are refused, `MESH_EDIT_HULL_EDGE`; triangulation edges belong to the producer's retriangulation, `MESH_EDIT_TRIANGULATION_EDGE`) |
727
- | `E` | Edges | click vertex A then vertex B to add a constraint edge; click a constraint edge to remove it; a hull edge is refused and an existing edge records nothing |
47
+ | **Hand off and compare** | | |
48
+ | <a id="one-command-owner-handoff"></a>Open an inspection | `open` | [Owner handoff — WORKFLOWS](docs/WORKFLOWS.md#one-command-owner-handoff) |
49
+ | <a id="existing-output-handoff"></a>Inspect an existing folder | `check`, `review` | [Existing output handoff — WORKFLOWS](docs/WORKFLOWS.md#existing-output-handoff) |
50
+ | <a id="compare-two-outputs"></a>Compare two builds against a video | `compare` | [Compare two outputs — WORKFLOWS](docs/WORKFLOWS.md#compare-two-outputs) |
51
+ | <a id="feedback-ownership"></a>Retrieve submitted feedback | `feedback` | [Feedback ownership — WORKFLOWS](docs/WORKFLOWS.md#feedback-ownership) |
52
+ | <a id="frame-strip-export"></a>Export feedback intervals as strips | `strip` | [Frame strip export — WORKFLOWS](docs/WORKFLOWS.md#frame-strip-export) |
53
+ | **Inspect** | | |
54
+ | <a id="workspace-settings-and-panels"></a>Navigate the workspace | Setup, Animate, Settings | [Workspace panels — PANELS](docs/PANELS.md#workspace-settings-and-panels) |
55
+ | <a id="inspect-animation-keys"></a>Inspect stored animation keys | Timeline, Curves, Bone tree | [Animation keys — PANELS](docs/PANELS.md#inspect-animation-keys) |
56
+ | <a id="inspect-a-part"></a>Select and fit a model slot | Parts explorer | [Part inspection — PANELS](docs/PANELS.md#inspect-a-part) |
57
+ | <a id="weights-setup-pose-watch-only"></a>Inspect setup-pose weights | Weights | [Weights — PANELS](docs/PANELS.md#weights-setup-pose-watch-only) |
58
+ | Steer model turn dials and compare at one angle | Turn pad | [Turn inspection — PANELS](docs/PANELS.md#turn-pad) |
59
+ | **Edit through the producer** | | |
60
+ | <a id="chest-regions-designate-rebuild-watch"></a>Designate chest regions and rebuild | Chest regions | [Chest regions — PANELS](docs/PANELS.md#chest-regions-designate-rebuild-watch) |
61
+ | <a id="mesh-panel-density-regions-segments--remesh"></a>Rebuild a part from mesh parameters | Mesh | [Mesh parameters — PANELS](docs/PANELS.md#mesh-panel-density-regions-segments--remesh) |
62
+ | <a id="bones-config-anchors-check-rebuild"></a>Check and rebuild config anchors | Bones | [Bones — PANELS](docs/PANELS.md#bones-config-anchors-check-rebuild) |
63
+ | <a id="rig-fields-regions-still-row-stops-inherit-turn"></a>Adjust producer config fields | Rig fields | [Rig fields — PANELS](docs/PANELS.md#rig-fields-regions-still-row-stops-inherit-turn) |
64
+ | <a id="cuts-cut-polygons-patch-boxes-limb-joints--rig-parts-build"></a>Designate cuts, patches and limb joints | Cuts | [Cuts — PANELS](docs/PANELS.md#cuts-cut-polygons-patch-boxes-limb-joints--rig-parts-build) |
65
+ | <a id="mesh-edit-typed-delta-preview-only"></a>Preview a setup-space delta and rebuild | Edit Mesh | [Mesh edit — PANELS](docs/PANELS.md#mesh-edit-typed-delta-preview-only) |
66
+ | <a id="retarget-map-flags--rig-pose-watch"></a>Retarget a declared pose sequence | Retarget | [Retarget — PANELS](docs/PANELS.md#retarget-map-flags--rig-pose-watch) |
67
+ | **Control and share** | | |
68
+ | <a id="control-the-current-inspection-page"></a>Control an opted-in inspection | `control` | [Inspection control — WORKFLOWS](docs/WORKFLOWS.md#control-the-current-inspection-page) |
69
+ | <a id="keep-and-share-an-inspection"></a>Restore a view link or local draft | View link, draft | [Keep and share — WORKFLOWS](docs/WORKFLOWS.md#keep-and-share-an-inspection) |
70
+ | <a id="launcher-contract"></a>Start or reuse the launcher | `open` | [Launcher contract — WORKFLOWS](docs/WORKFLOWS.md#launcher-contract) |
71
+ | <a id="managed-front-and-write-capability"></a>Use the managed front and write capability | `open` | [Managed front — WORKFLOWS](docs/WORKFLOWS.md#managed-front-and-write-capability) |
728
72
 
729
- Hit-testing is in screen pixels through the current camera: a vertex within 6 px (8 on touch) wins over an edge within 4 px. In a sub-mode a press never pans; Space-drag still pans and the wheel still zooms. Escape leaves the sub-mode, a second Escape leaves mesh edit. **Undo** / **Redo** (Cmd/Ctrl-Z, Shift-Cmd/Ctrl-Z) step one action at a time — an edge or multi-selection drag is one action of several `move` ops. **Reset** drops the attachment's whole delta after a confirmation.
730
-
731
- Everything is a typed delta over the loaded mesh, in setup-pose crop pixels of the rig canvas (x right, y down, the chest regions' crop mapping): `{schema:"rig-review-mesh-edit/1", slot, skin, attachment, source:{vertexCount, triangleCount, hullLength, hash}, units, basis:"setup", preview, ops}` with ordered ops `move {v, from, to}`, `add {at, onEdge?}`, `remove {v}`, `edge {a, b}` and `unedge {a, b}`. Source vertex ids are the attachment's indices; an added vertex takes the next id; a removed id is never reused. `hullLength` is the model's `hull` (the first `hull` vertices, in order, are the hull) and `hash` is the SHA-256 of `JSON.stringify` of the attachment's entry in the hash-verified model document. Source positions are not rounded (a `move`'s `from` is the vertex's exact setup position; `to` and `add.at` are rounded to 0.01 px). This is the input `rig-parts remesh --delta` reads as it is (see [docs/REVIEW_PACKAGE.md](docs/REVIEW_PACKAGE.md#mesh-edit-export)); the former last-drawn-pose basis is gone. The overlay previews moved, added and removed vertices and the constraint edges over the original triangulation; nothing is retriangulated in the browser (the status line says *preview only — the producer retriangulates*).
732
-
733
- **Download JSON** saves `mesh-edit.json` and `mesh-edit.binding.json` (package source hash, rig and motion SHA-256, the attachment and its hash, export time, viewer and runtime versions and the mapping). **Apply · Rebuild** (#135) sends the delta to the installed producer as `POST /api/rebuild {part, delta, configHash}`: the server writes it as `mesh-edit.json` (the download's bytes) into the scratch directory and runs `rig-parts remesh --rig <handoff rig> --config <handoff config> --parts <handoff parts> --part <part> --delta mesh-edit.json --out rig`, then the shared tail (per-file diff, compile with remesh.json's pack flags, hash). The producer must read `--delta` (`@rig-c/parts` 1.43.0 or later, read from its own `package.json`), else `MODELING_PRODUCER_TOO_OLD <found> 1.43.0`. The delta's schema, basis, source counts and `source.hash` and every op are the producer's judgment: its `REMESH_DELTA_*` / `REMESH_*` line comes back verbatim and the stage keeps its model. A built revision loads in place and the editor re-reads that revision's setup mesh; `context.applied` becomes `rig-review-applied/3` (the delta by content, `deltaSha256` of its bytes, the producer's line, `op: "remesh"`, the per-file diff and the part's counts). While the revision is on the stage, Reset returns to the inspected build and the draft's delta is restored over it. A delta drawn over the revision's mesh is refused by the producer (`REMESH_DELTA_SOURCE`): its input is always the handoff rig. The delta rides in the inspection's local draft bound to the attachment hash (at most 300 ops per attachment); it never enters the review session and nothing is written into the package.
734
-
735
- For inspection automation, `window.rigReviewEditor.meshEdit()` returns the open editor's source (setup-pose crop px per vertex, unrounded), its ops and each drawn vertex as page client pixels, or `null` when the editor is closed; it reads only.
736
-
737
- ## Retarget (map, flags → rig-pose, watch)
738
-
739
- In **Animate**, the **Retarget** block (#114) puts a pose sequence the package declares onto the inspected rig through the installed `@rig-c/pose`. The sequence is a `rig-pose-sequence/1` pose reference the package declares (a `references` entry of type `pose` in `review.json`, or a `*pose*.json` in an opened folder); review takes no other file. The block's controls:
73
+ ## License
740
74
 
741
- - **Map** — a `rig-pose-map/1` editor. Start from **Body-18 chains** (trunk, both arms, both legs, no bone picked) or load a map file (read in the page and sent by content, never by path). Per chain: include it or leave it out, pick the rig bone that carries each segment (a name the rig lacks stays listed as *not in the rig*, so Apply shows the producer's own refusal), and its `bend`. The translated bone and the two rig points of `translate.scale` are picked the same way. **Map JSON** shows the whole document; aims, ranges, alternates and the rest are edited there and taken with **Use this JSON**. A bone not yet picked is refused before anything is sent (`RETARGET_MAP_INCOMPLETE`).
742
- - **Clean** — clean first (default; a sequence that already carries clean's record starts with it off), `--loop`, `--legs`, `--rest`, the stillness hold and `--hips-inferred`. With clean off the sequence goes to retarget as the package holds it, and an uncleaned one is refused by the producer (`POSE_NOT_CLEANED`).
743
- - **Retarget** — the new animation's name (`--animation`), `--flip`, `--short`, `--min-score`, `--bends refuse|clamp`, `--bend-tolerance`, `--leg-scale min,max`, `--plant-feet`, `--stance`, `--raised-by-head`, `--carry-idle`, and the person when the sequence holds more than one.
75
+ MIT. See [LICENSE](LICENSE).
744
76
 
745
- **Apply · Retarget** sends `POST /api/rebuild {retarget: {sequence, map, flags, configHash}}` — a reference id, the map by content and typed flags; the server maps the flags to argv. Into a scratch directory under the review state directory it writes the sequence (`pose.json`) and the map (`map.json`), runs `rig-pose clean --pose pose.json --out pose.clean.json …` and `rig-pose retarget --rig <handoff rig> --map map.json --sequence pose.clean.json --out motion.json …`, copies the handoff rig's files beside the new `motion.json` as `rig/`, and finishes with the shared tail: the per-file diff against the inspected rig and `rigc build` with Apply's default pack flags, using the `rigc` installed beside `@rig-c/pose`. The package's `motion.json` is never written; review writes no keys. Every `FAIL` line of a refusal is shown verbatim — `POSE_NOT_CLEANED`, `POSE_BEND_ONE_WAY`, `RAISED_HEAD`, `MOTION_NAME_FREE`, `MAP_RESOLVES` / `MAP_BONE_EXISTS` — and the stage keeps its model. A built job loads in place, the new animation is chosen, the sequence is shown as the reference beside the rig, and the run's FEET, STANCE, FLIP, SHORT, END-ON, LEG-SCALE and REACH lines are listed; each line seeks to the first frame it names, at the motion's own key time for that frame (the run's report `frameTimes`). While that build is on the stage, its report `frameTimes` are the new animation's clock: the transport counter reads `F<n> / <last>` (`frameTimes · … · this build's report`) instead of `rate unknown`, and the rig pane's header names the animation, frame and time the drawn pose is at (`Generated rig · walk · F14 · 0.583 s`) — the same clock a seek sets. Reset or another rebuild drops that clock. `context.applied` becomes `rig-review-applied/4`, with the `rig-review-retarget/1` record (sequence by reference, path and hash; map by content and hash; flags and their argv), the producer's line, what the scratch motion holds against the inspected rig's, the per-file diff and the listed lines. **Reset** returns to the inspected build. `rig-pose compare --sheet` is not run: it needs a detector pass over the render, which review does not run.
77
+ ## Links
746
78
 
747
- To reproduce an Apply outside review, run the same producer with the record's flags in an empty directory holding the sequence as `pose.json` and the record's map as `map.json`: `rig-pose clean --pose pose.json --out pose.clean.json <op.clean…>` then `rig-pose retarget --rig <handoff rig> --map map.json --sequence pose.clean.json --out motion.json <op.args…>`. Its `motion.json` is byte-equal to the scratch build's `rig/motion.json`.
79
+ <a id="development"></a><a id="releasing"></a>
80
+ Development and releasing: [Contributing workflows](https://github.com/rig-c/review/blob/main/CONTRIBUTING.md#development).
748
81
 
749
- ## License
750
-
751
- MIT. See [LICENSE](LICENSE).
82
+ - [Package contract](docs/REVIEW_PACKAGE.md)
83
+ - [Product design](docs/DESIGN.md)
84
+ - [Animation inspection](docs/ANIMATION_INSPECTION.md)
85
+ - [Installed-producer fixture](docs/LIVE_FIXTURE.md)