rig-review 0.13.0 → 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
@@ -14,8 +14,9 @@ Review is the human inspection and source-bound feedback gate in this delivery
14
14
  flow. It inspects the upstream build without replacing Core's compiler gate or
15
15
  authoring motion. Review itself uses the published Play runtime for previews;
16
16
  the chain describes delivery and feedback responsibilities, not runtime import
17
- order. Adapter denotes downstream consumer integration, without promising an
18
- unreleased adapter API or changing existing compilation/playback dependencies.
17
+ order. Adapter denotes downstream consumer integration, without depending on the
18
+ published adapters (`@rig-c/pixi`, `@rig-c/phaser`, `@rig-c/three`) or changing
19
+ existing compilation/playback dependencies.
19
20
 
20
21
  ## Start
21
22
 
@@ -26,722 +27,59 @@ npm i -D @rig-c/review
26
27
  npx rig-review open /absolute/path/to/output
27
28
  ```
28
29
 
29
- `rig-review` runs under **Bun 1.3.0 or later**, which must be on `PATH`; CI runs the installed package on 1.3.0 and on the latest Bun. `open` prints a loopback URL and exits; see [One-command owner handoff](#one-command-owner-handoff). `npx rig-review --help` lists every command.
30
-
31
- The package is 0.1.x: the CLI is the supported surface, and no importable API is promised yet (`exports` exposes only `package.json`).
32
-
33
- ## Existing output handoff
34
-
35
- Open the current output folder directly, without changing the producing agent or inserting a manifest:
36
-
37
- ```sh
38
- bun run check -- /absolute/path/to/output
39
- bun run review -- /absolute/path/to/output
40
- ```
41
-
42
- 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.
43
-
44
- 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.
45
-
46
- 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.
47
-
48
- For a curated handoff, [REVIEW_PACKAGE.md](docs/REVIEW_PACKAGE.md) documents optional `review.json` with named checkpoints, references and angle axes:
49
-
50
- ```sh
51
- bun run review -- /absolute/path/to/package/review.json --checkpoint silhouette
52
- ```
53
-
54
- 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.
55
-
56
- ## Compare two outputs
57
-
58
- Open two compiled packages against one explicitly selected local reference video:
59
-
60
- ```sh
61
- bun run review -- compare /absolute/output-a /absolute/output-b --reference /absolute/reference.mp4 --animation dance
62
- bun run check -- compare /absolute/output-a /absolute/output-b --reference /absolute/reference.mp4 --animation dance
63
- bun run feedback -- compare /absolute/output-a /absolute/output-b --reference /absolute/reference.mp4 --animation dance
64
- ```
65
-
66
- 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.
67
-
68
- 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).
69
-
70
- ## Feedback ownership
71
-
72
- 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.
73
-
74
- 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.
75
-
76
- 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.
77
-
78
- After the person submits, the producing agent retrieves the stored review without needing the hashed state filename:
79
-
80
- ```sh
81
- bun run feedback -- /absolute/path/to/output
82
- # Or the same explicit review.json originally opened:
83
- bun run feedback -- /absolute/path/to/package/review.json
84
- ```
85
-
86
- 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.
87
-
88
-
89
- For grouped issue-ready text, use the same input/reference binding with `--format issue`:
90
-
91
- ```sh
92
- rig-review feedback /absolute/output --reference /absolute/original.mp4 --format issue
93
- rig-review feedback /absolute/a --compare /absolute/b --reference /absolute/original.mp4 --animation dance --format issue
94
- ```
95
-
96
- 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.
97
-
98
- ## Development
99
-
100
- From a clone:
101
-
102
- ```sh
103
- bun install
104
- bun run demo
105
- ```
106
-
107
- 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`.
108
-
109
- ```sh
110
- bun run typecheck
111
- bun test
112
- bun run build
113
- bun run smoke
114
- ```
115
-
116
- `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.
117
-
118
- 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.
119
-
120
- ### Releasing
121
-
122
- 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.
123
-
124
- ## Frame strip export
125
-
126
- 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.
127
-
128
- The same export is available without a browser:
129
-
130
- ```sh
131
- bun src/cli.ts strip /absolute/package/review.json --reference source-video --animation dance --frames 176:184 --out /absolute/external/strip.png
132
- 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
133
- bun src/cli.ts strip /absolute/package/review.json --reference source-video --animation dance --all-feedback --json
134
- ```
135
-
136
- 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.
137
-
138
- 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.
139
-
140
- 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.
141
-
142
- 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.
143
-
144
- ## One-command owner handoff
145
-
146
- `open` validates and hashes an existing folder, starts a background loopback
147
- instance or reuses the exact matching one, prints its hash-bound URL, and exits.
148
- By default the URL goes through the managed front at `127.0.0.1:4318`; `--direct`
149
- prints the instance's own port instead (see [Managed front and write capability](#managed-front-and-write-capability)):
150
-
151
- ```sh
152
- bun run open -- /absolute/output --reference /absolute/original.mp4
153
- # Installed executable mapping:
154
- rig-review open /absolute/output --reference /absolute/original.mp4 --animation dance
155
- rig-review feedback /absolute/output --reference /absolute/original.mp4
156
- rig-review open /absolute/a --compare /absolute/b --reference /absolute/original.mp4 --animation dance
157
- rig-review feedback /absolute/a --compare /absolute/b --reference /absolute/original.mp4 --animation dance
158
- # Producer handoff (single review only): the config, parts and inspected rig an Apply rebuilds from
159
- rig-review open /absolute/output --config /absolute/output/rig/config.json [--parts /absolute/output/rig/out] [--source <png> --full <dir> [--head <dir>]] [--keypoints <file>]
160
- rig-review feedback /absolute/output --config /absolute/output/rig/config.json
161
- ```
162
-
163
- `--config`, `--parts`, `--source`, `--full`, `--head` and `--keypoints` declare the producer
164
- handoff (see `docs/REVIEW_PACKAGE.md`, *Producer handoff*). They win over the
165
- review.json `producer` block, which wins over discovery (`config.json`, then
166
- `rig/config.json` inside the opened folder, then `../config.json` beside an opened
167
- `out/`, named as discovered outside the folder). For the `check/` package of a
168
- `rig-parts build` output the next places are the build's own config: `config.json`
169
- in the build root, else beside it (`<out>/../config.json`, where `rig-parts build
170
- --config <dir>/config.json --out <dir>/out` leaves it); the parts directory is the
171
- build root. Declaring `--config` at exactly one of those two files is taken the same
172
- way, and that file's folder is then the root for the other flags. Every other path must sit inside the
173
- package root; a path outside it or a symlink that leaves it is refused by name.
174
- The handoff files are hashed into the reviewed identity, so `feedback` and
175
- `control` take the same flags to retrieve the same session, and changing the
176
- config bytes starts a new pending review. `--keypoints` (#111) is the body-18 keypoint
177
- file a cloth recipe Apply hands to `rig-parts propose`; it is hashed into the identity
178
- only when declared, so a handoff without it keeps its identity. `--parts`,
179
- `--source`/`--full`/`--head` and `--keypoints` need `--config`; `--source` and `--full` go together; compare takes none of them.
180
-
181
- `open` chooses an explicit `--animation` first, then the normalized report's
182
- `animation` after validating actual model membership, then a unique actual clip
183
- (or unique common clip in compare). Thus `rig-review open /absolute/output
184
- --reference /absolute/original.mp4` opens a report-bound `dance` without the flag
185
- and prints `animation: dance (from report.json)` on stderr. Conflicting A/B reports,
186
- a stale report name or multiple clips without a report refuse by name; no first
187
- entry, filename or frame rate supplies a guess. Explicit invalid names never fall
188
- back. The selected clip actually initializes single playback, while an explicit
189
- checkpoint link or later manual choice takes priority. Incompatible automatic
190
- checkpoints do not apply another clip's time or axis. Static/source-only packages
191
- remain inspectable without claiming clip selection; explicit unvalidatable clips
192
- refuse. Single review sessions remain package-wide; animation selection does not
193
- create an animation-specific approval identity. Opening never
194
- approves a review. The command prints the matching feedback retrieval command on
195
- stderr; stdout contains the URL alone. A selected external video takes precedence
196
- at automatic startup; an explicit checkpoint link or manual checkpoint selection
197
- still chooses that checkpoint's reference.
198
-
199
- The selected video is a separate CLI capability, not a manifest path escape or a
200
- file copied into the source package. Its name and streaming byte hash, plus the
201
- base package hash, appear as `referenceBinding` in single review responses,
202
- sessions and feedback exports. Use the same `--reference` when retrieving that
203
- submission; omitting it reads the separate reference-free identity. Changed
204
- package or reference bytes start pending state. Existing historical retrieval
205
- preserves earlier recorded tool versions over exactly the same reference content.
206
-
207
- The single strip panel can export the selected external video using that same
208
- binding. Headless equivalent (the original declared-reference form stays valid):
209
-
210
- ```sh
211
- rig-review strip /absolute/output --reference-video /absolute/original.mp4 --animation dance --frames 176:184 --out /absolute/external/strip.png
212
- ```
213
-
214
- Only tool-owned instances with matching content, canonical input locations, tool
215
- fingerprint and a private capability handshake are reused. The URL hash is
216
- checked before serving the app; a stale link never selects another instance's
217
- package. Input hashes are freshly computed, not trusted from mtime caches. Idle
218
- open-managed workers stop after 30 minutes. Their private registry, requests and
219
- logs live beside feedback under `~/.local/state/rig-c-review/open/`; nothing is
220
- written into supplied folders. `RIG_REVIEW_STATE_DIR` selects another state root
221
- outside all input packages, judged by its canonical location (symlinks in the path
222
- and its existing ancestors resolved). Existing foreground review commands still work.
223
-
224
- No browser is opened, no LAN listener is added, and no misleading phone/QR URL is
225
- printed: every current instance binds only `127.0.0.1`.
226
-
227
- The printed feedback retrieval command uses POSIX shell quoting for literal path
228
- arguments. It is not a PowerShell or cmd.exe command string.
229
-
230
- ### Launcher contract
231
-
232
- A launcher may start `rig-review open` as a child process and rely on its stdout,
233
- stderr and exit code alone:
234
-
235
- 1. On success stdout is exactly one line, the URL. `open` never writes anything
236
- else to stdout.
237
- 2. On success stderr carries the human lines in this fixed order: for the `check/`
238
- package of a `rig-parts build` output, `Package: rig-parts build check package <path>`
239
- and the handoff line (`Handoff <how>: <config path> sha256 <hash> · parts <dir>`, or
240
- `Handoff: <refusal>`); the animation diagnostic (when a clip is selected), the control notice (with `--control`),
241
- then the copyable retrieval line `Review started|reused; feedback: rig-review feedback …`.
242
- 3. A refusal exits with code 1, writes nothing to stdout, and its first stderr line
243
- starts with a name, followed by a colon and the explanation:
244
- - `OPEN_INPUT_MISSING <path>`: the input or `--compare` path does not exist, or is neither a directory nor a regular file.
245
- - `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.
246
- - `OPEN_REFERENCE_REQUIRED`: `--compare` was given without an explicit `--reference`.
247
- - `OPEN_ANIMATION_UNKNOWN <name>`: the explicit or report-named clip (JSON-quoted) is not in the compiled model.
248
- - `OPEN_ANIMATION_AMBIGUOUS`: no flag or report selects one clip, or the A/B reports conflict.
249
- - `OPEN_ANIMATION_UNVALIDATABLE`: a clip was requested but no compiled model can validate it.
250
- - `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`.
251
- - `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.
252
- - `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.
253
- 4. Exit codes: 0 applied, 1 refused. (`control` keeps its own exit 2 for an
254
- unapplied command.)
255
-
256
- Package validation and worker startup refusals keep their existing messages; the
257
- named refusals above are decided before any state is written.
258
-
259
- ### Managed front and write capability
260
-
261
- `open` is **managed by default**. It ensures one detached loopback front at
262
- `127.0.0.1:4318` (`RIG_REVIEW_FRONT_PORT` overrides) for the state directory and
263
- prints `http://127.0.0.1:4318/instances/<key>/?input=…&handoff=…#cap=…`. Every
264
- inspection therefore shares one stable browser origin, so Settings, panel sizes and
265
- source-bound drafts survive a reopen. The front only routes: it takes the worker
266
- address from that key's private open record, allows a closed per-method route
267
- list, bounds bodies (16 KiB control, 1 MiB session/strip) and upstream time,
268
- limits concurrent upstream streams, re-verifies the worker's nonce on every
269
- request, and refuses an unknown, expired or replaced instance by name
270
- (`FRONT_INSTANCE_UNKNOWN`, `FRONT_INSTANCE_EXPIRED`, `FRONT_INSTANCE_REPLACED`).
271
- Worker error text is not passed through; failures become named `FRONT_*` codes.
272
- Package assets keep their `sandbox` CSP through the front. It never reads packages.
273
- Idle fronts exit after 30 minutes once no live instance remains.
274
-
275
- If another listener holds the front port, `open` fails with
276
- `FRONT_PORT_BUSY: 127.0.0.1:4318 is held by another listener; use --direct` and
277
- does not fall back silently. `open --direct` keeps the per-instance port and
278
- prints that worker's own URL.
30
+ `rig-review` is also the convenience package name: `npm i -D rig-review` installs
31
+ the same CLI. `@rig-c/review` remains the canonical package.
279
32
 
280
- One origin cannot isolate scripts that run on it, so writes are bound to a
281
- per-instance capability rather than to the page's origin or the key in its path.
282
- Each worker mints a 256-bit capability kept only in its private 0600 open record.
283
- `open` (managed or direct) and the foreground `review`/`review compare` commands
284
- print it in the `#cap=` fragment, which browsers never send to a server. That
285
- printed link goes only to the terminal of the command that started the instance.
286
- Output meant for a log does not carry it: the install smoke (`bun run smoke`, whose output CI publishes) prints the same
287
- URL as `#cap=<redacted>` and fails if any capability value reaches its stdout. The page
288
- moves it into memory and that tab's `sessionStorage`, then removes the fragment
289
- from the address bar. Every mutation sends it as `X-Review-Capability`; the front
290
- checks it before forwarding and the worker compares it again in constant time
291
- before its existing same-origin check. No endpoint returns it, so a page opened
292
- for one instance cannot submit to another. A copied view link (without the
293
- fragment) opens read-only and says *Read-only view: reopen with rig-review open to
294
- edit*; run `open` again for a writable link.
295
-
296
- ## Control the current inspection page
297
-
298
- Opt in once when opening a fixed package, then keep that page open:
33
+ `rig-review` runs under **Bun 1.3.0 or later**, which must be on `PATH`; CI runs the installed package on 1.3.0 and on the latest Bun. `open` prints a loopback URL and exits; see [One-command owner handoff](#one-command-owner-handoff). `npx rig-review --help` lists every command.
299
34
 
300
- ```sh
301
- rig-review open /absolute/output --reference /absolute/original.mp4 --control
302
- rig-review control /absolute/output --reference /absolute/original.mp4
303
- ```
35
+ The package is 0.x: the CLI is the supported surface, and no importable API is promised yet (`exports` exposes only `package.json`).
304
36
 
305
- The second command reads the connected page's actual inspection state and its
306
- `pageId`/`revision`; it does not open a browser or start another worker. A page
307
- learns from `GET /api/access` (`control: true|false`) whether its instance serves
308
- control; an instance opened without `--control` is never asked for
309
- `/api/control/config`. To change
310
- that page, use the observed values and a closed inspection command:
37
+ Example stdout from `open` (instance values vary):
311
38
 
312
- ```sh
313
- 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=…
314
41
  ```
315
42
 
316
- Use the same reference/compare/animation inputs as the controlled `open`. Multiple
317
- pages require an explicit `--page`. The URL and source binding stay unchanged;
318
- existing ordinary servers do not become controllable. Starting the opt-in worker
319
- is a one-time handoff, not a new link for each parameter change.
320
-
321
- Commands cover appearance, automatic layout, synchronized or independent pane
322
- cameras, playback, single animation, time and actual inspection layers. Camera
323
- pan is measured in viewport CSS pixels (right/down positive). New inspections
324
- start with camera sync on: zoom relative to fit and pan normalized by each pane's
325
- viewport move all visible measurable reference/rig panes together. Turn sync off
326
- with `{"type":"camera-sync","value":false}` to inspect each pane independently;
327
- turning it off preserves current views. Turning it on aligns the visible panes
328
- to the last active pane. Camera/fit targets are `rig|reference` in single review
329
- and `a|b|reference` in compare. For legacy clients, compare `target:"rig"` explicitly
330
- moves both A/B even when sync is off; its CSS readback aliases A. Comparison retains its
331
- fixed animation and actual video-master seek path. A single-video time request
332
- uses the actual decoded timestamp for both reference and rig; the applied
333
- acknowledgement reports that time, which may differ from the requested seconds.
334
- Unsupported features, stale
335
- page/source/revision, unsubmitted feedback drafts and out-of-range values refuse
336
- by name. No command changes a package/reference, authors motion, or submits or
337
- approves feedback. Local controls and remote readback use the same inspection
338
- state. A live view-link/history change supersedes pending remote work, and
339
- commands refuse while that view is being restored.
340
-
341
- Remote inspection readback also includes versioned numeric `view` coordinates
342
- when the page can measure them: rig/world center and zoom, reference/image center
343
- and zoom, each panel's current CSS viewport size and visible rectangle. World
344
- coordinates use Y up; reference image coordinates use Y down. Compare reports
345
- actual A and B rectangles separately; unavailable measurements are null. The
346
- canonical view also reports `cameraSync` and `activeCamera`. These
347
- are the same current cameras used by the local controls, rather than a second
348
- simulated view. Camera commands still use the documented viewport CSS pan.
349
-
350
- The CLI waits up to10 seconds for the page's actual acknowledgment. `queued` or
351
- `dispatched` is not success; non-applied commands exit2 and may carry
352
- `timedOut:true`. Reuse the same `--request-id` and original request to obtain its
353
- receipt without applying it twice. `--wait-ms 0` returns the queued receipt;
354
- `--wait-ms` is bounded to30000. A timeout does not mean an already delivered action
355
- was rolled back. Tokens stay in private tool state, not URLs or command output.
356
-
357
- ## Keep and share an inspection
358
-
359
- The view link stores the current numeric inspection in a bounded, versioned
360
- `#review=` fragment. It includes the exact loaded source/model binding, selected
361
- animation/time and reference selection, appearance/layers, camera sync and the
362
- active pane, and each canonical camera center with visible bounds. Reloading a matching link restores a paused view;
363
- it does not restart an idle loop or submit a review. Coordinates adapt to the
364
- current viewport instead of replaying another window's CSS pan offsets.
365
- Historical links without a sync mode restore with sync off to preserve their
366
- saved views; fresh inspections start with sync on. The page keeps the current
367
- fragment in the address bar as you inspect, so copying the address copies the
368
- view; there is no separate copy button. Legacy modeling/motion
369
- workspace selections are accepted without restoring a top-level workspace switch. A link
370
- for a different source binding is refused.
371
-
372
- Ordinary form inputs, selected panels, view state and unsent feedback/checkpoint
373
- choices are kept in browser local storage for the same source. The draft scope
374
- includes the source hash (`single:<id>:<sourceHash>`; compare uses both sides'
375
- id and hash), so two revisions of one package keep separate drafts. Opening a page
376
- deletes no draft; saving evicts the least-recently-written drafts beyond 32, but
377
- never one written within the last 24 hours. Browser
378
- Settings (below) remain, while a source-bound appearance override does not
379
- carry over. URL view state takes precedence over the locally saved view. Each
380
- Inspector section's own scroll position is part of this source-bound draft.
381
-
382
- Local recovery compares the original submitted server-session fingerprint,
383
- including its update time. It cannot replace a newer server review: conflicting
384
- or corrupt saved data remains available to download, with an explicit warning;
385
- recovering a conflicting draft requires choosing a pending review. Storage
386
- errors never count as a successful save. Nothing is written to the server or
387
- input package merely by restoring a page. Browser storage is convenient local
388
- recovery, not a durable backup; submit the review (and retrieve it with
389
- `rig-review feedback`) when it must be kept.
390
- Control capabilities are excluded from links and browser persistence.
391
-
392
- ## Workspace settings and panels
393
-
394
- The review page has a left column (Parts explorer, with checkpoints and the
395
- reference select folded above it), the stage, the Inspector below it with
396
- **Setup** and **Animate** sections, and a status footer. The compare page
397
- keeps sources on the left and the Parts explorer on the right. The page
398
- header carries the **Setup** / **Animate** mode switch next to the package
399
- title; it is the only way to change the Inspector's section. Setup is the
400
- mesh-review composition (selected part, part presets, modeling-review
401
- proposals) and holds nothing that seeks or plays; the angle inspection, the
402
- transport and frame navigation are in Animate. **Write review** at the top
403
- right opens the review dialog, a modal holding the review status, reviewed
404
- source, selected checkpoint, structured feedback and frame-strip export, with
405
- its own **Submit review**. Escape or Close dismisses it and returns focus to
406
- **Write review**; the draft keeps saving whether the dialog is open or not,
407
- and the footer and the dialog both show its state.
408
- The left column's header (Parts title and search) stays fixed at the top;
409
- the checkpoint strip, the part list and Settings scroll beneath it as one
410
- body. A drawer keeps its title and close button in place while its body
411
- scrolls. The per-inspection **Repeat idle** toggle sits in the Animate
412
- transport row.
413
-
414
- Animate's **Environment** block drives the player's physics while you
415
- inspect: a **Wind** base (−100…100), **Breeze**, a **Gust** pulse
416
- (peak 0…200, duration 0.1…20 s, **Repeat** every max(6, duration + 0.6) s)
417
- and **Gravity** (0…4), applied through `player.environment` in the same frame
418
- step that draws. The gust clock is the stepped player time, so pausing freezes
419
- it. **Jolt** sends a translate-and-return impulse through `player.moved`;
420
- with **Drag to trail** on, dragging the rig pane moves the rig's physics
421
- instead of the camera (Space still pans). **Reset physics** returns the
422
- runtime start values and re-poses from the current time, clearing the physics
423
- history. Until a control is touched the player stays at the runtime start values. The
424
- state is kept in the local draft and recorded in feedback as
425
- `context.environment`; nothing is written to the model or motion. The compare
426
- page does not have this block.
427
-
428
- Below it, the **Physics tuning** panel (`#physics-panel`) edits the physics
429
- constraints through the producer. Each row is a constraint of the inspected
430
- model, matched to the handoff config's `constraints[]` entry of the same name
431
- and kind; a row is editable only when every field the panel edits reads the
432
- same on both sides. Rows the model has and the config does not declare (the
433
- rig stage's `turn` twins), the config's transform constraints and any row that
434
- does not map 1:1 are listed under **Not editable** with the reason. Picking a
435
- row opens its editor: a slider and number field for each of `strength`,
436
- `damping`, `inertia`, `mass`, `wind`, `gravity` and `mix` the config declares,
437
- and toggles for the `x`, `y` and `rotate` components (0 is off). A **Chain**
438
- (from bone parentage: links on parent bones that move the same components;
439
- pick one from the list or with **Pick on stage**) takes a **Ramp**: one value
440
- per link, linear from root to tip. Every edit is a config patch op at
441
- `/constraints/<i>/<field>` that also records the constraint's `name`; the
442
- pending ops are listed with a per-op **Revert** and kept in the local draft.
443
- **Apply** sends them as one patch (`rig`, then `check`, about 20–50 s on a real
444
- package); the scratch build reloads under the same wind, gust, jolt and gravity
445
- state Apply was pressed in, and the readout shows the `ANIMATIONS_DO_NOT_FOLD`
446
- and `TEXTURE_STRETCH` lines before and after. BEFORE for the inspected build
447
- is its own check: once the baseline reproduces, review runs `rig-parts check`
448
- on that baseline output in the background (about 16 s on a real package) and
449
- stores the summary beside the baseline record, so a reload reads it; until it
450
- finishes the line says "check running…", a producer refusal is shown verbatim,
451
- and "not in check summary" remains only when the baseline did not reproduce. A
452
- refusal is the producer's line verbatim (`CONFIG_CONSTRAINT_*`,
453
- `CONFIG_NAME_RESOLVES`, rigc's `A23` for a mass of 0) and the stage is
454
- unchanged. **Reset** returns to the inspected build and clears the pending ops.
455
- Submit refuses a review written on an applied build whose check failed
456
- (`EDIT_SUBMIT_UNCHECKED`).
457
-
458
- With pinned published Play **1.14.0**, **Preview pending physics** previews
459
- `strength`, `damping`, `inertia`, `wind`, `gravity` and `mix` through the live
460
- player's public `setPhysicsParameters` / `readPhysicsParameters` /
461
- `clearPhysicsParameters`. Slider input updates the standing patch while dragging;
462
- the label separates the pending override from the last evaluated effective values.
463
- The next evaluation uses it after authored keys and sliders, before physics. While
464
- paused, the page explicitly re-poses the held time and draws; it never seeks or
465
- resets history to preview. `mass` and `x` / `y` / `rotate` stay Apply-only.
466
- **Clear preview** disables preview and removes its overrides, keeping the pending
467
- ops and their Apply bytes. Authored values resume on the next evaluation; the
468
- integrated history is retained, so clearing does not undo motion caused earlier.
469
- Apply and a model replacement clear the preview; Reset clears both preview and
470
- pending ops. Preview state is never recorded as applied or submitted.
471
-
472
- Play retains the current standing patch across play and seek, with no history of
473
- patch changes. A backward seek after an edit recomputes the past using the patch
474
- standing now, rather than reproducing the old trajectory. A newly opened track
475
- first evaluates its ordinary authored pose, then receives the patch as pending;
476
- at seek time 0 its effective values can therefore still be authored until the next
477
- evaluation. No live track means a named refusal; preview never creates one.
478
-
479
- Below it, the **Idle tracks** panel (`#idle-tracks`) edits the idle through
480
- the producer. It lists the handoff config's `motion.tracks`: a single track
481
- opens amp, period, phase and base (base can be added or taken out); a chain
482
- track opens one amp per link and lag, and draws a handle at each link on the
483
- stage (drag up or down; arrow keys step 0.05°). Period is a select of the
484
- divisors of `motion.duration` (`duration / 1 … 12`) — the only periods the
485
- producer accepts — beside a field for typing one, which review's pre-check
486
- refuses with the producer's own line (`CONFIG_PERIOD_DIVIDES_DURATION`). A
487
- track is added by bone + property or by chain (it starts still: amplitude 0,
488
- the whole idle's period) and removed with **Remove**; a second track on a
489
- property already keyed (a chain link's `rotate`, a blink member) is refused
490
- before Apply by the producer's `CONFIG_BONE_PROPERTY_KEYED_ONCE`. The blink's
491
- `t` is a marker above the transport scrub, dragged along the timeline (arrow
492
- keys step one 12 fps frame), with `squash` and `brow_drop` sliders. The
493
- pending ops (`/motion/tracks/<i>/…`, `/motion/blink/…`) are listed with a
494
- per-op **Revert** and kept in the local draft. While edits are pending the
495
- **preview** drives the edited bones through the player's `adjust` with the
496
- producer's sine on the bone the idle keys (`<bone>_ctl` under ctl keys, read
497
- off the handoff rig). It evaluates from Play's `BoneAdjust` context's
498
- `trackTime`, including seek replay, and the turn dial receives the same context.
499
- It reads no displayed player clock and adds no compensating re-pose after seek.
500
- With unchanged patch, animation, environment, reset and step partition, seek and
501
- continuous evaluation agree bit for bit. It is labelled "preview — Apply decides"; it is never
502
- recorded or submitted, and a property the inspected idle does not key yet is
503
- not previewed (`IDLE_PREVIEW_TARGET_UNKNOWN`). **Apply** clears the preview and
504
- sends one patch (`rig --idle-keys <the inspected rig's>`, then `check`, about
505
- 30 s on a real package); the built idle replaces the preview, and the
506
- readout shows the `CHAIN_LAG`, `TIP_OVER_ROOT` and `BREATH_VISIBLE` lines and
507
- check's `loop:` line before and after. **Reset** clears the preview and the
508
- pending ops and returns to the inspected build.
509
-
510
- Below that, the **Readings** block (`#readings`) runs the compiler's own
511
- readings of the drawn animation: a **Folds** toggle, a **Pass-through**
512
- cloth:leg pair and a **Seam** slot:slot pair (slot names suggested from the
513
- model), an **Overlaps** toggle and an **Angle range** bone against a relative
514
- bone. **Run readings** sends the chosen readings (names only) to
515
- `POST /api/readings`; the server runs one `rigc render --animation <drawn>
516
- --folds | --pass-through <cloth>:<leg> | --seam <a>:<b> | --overlaps |
517
- --angle-range <bone>:<relative>` per reading on the inspected build and on the
518
- latest scratch build of the session (if an Apply built one), into scratch
519
- under the state directory (`readings/<package>/<build>/<key>/`; an identical
520
- reading on the same build and compiler is reused; about 0.5–5 s each on a real
521
- package). The frames each reading's JSON lists become ticks under the
522
- transport scrub — inspected above, scratch below, in two colours — and
523
- clicking a tick seeks to that frame's time (frame / fps). Each reading gets a
524
- worst-frame line, `<build> · <reading> <pair>: worst at frame N — <figure> ·
525
- <rigc's line>`, which also seeks; folds and overlaps list each folding mesh or
526
- moving pair under it. `--angle-range` writes no file, so its ticks are the
527
- lowest and highest frames its printed line names. An unknown slot or bone is
528
- rigc's to refuse, and its line is shown verbatim. While the Environment
529
- controls are applied, their current wind and gravity go along as a one-step
530
- `--environment` schedule (only when the installed rigc's `render --help` lists
531
- the flag). A reading is not a gate; nothing is written into the package. The
532
- pairs of the last run are recorded in feedback as `context.readings`.
533
-
534
- In the Environment block, **Use for readings** (issue #112) turns the controls
535
- into a `rigc-environment/1` schedule from the clip's first frame — one step when
536
- the wind is steady, else one step per 12 fps frame carrying the breeze and the
537
- repeating gust (equal to the panel's wind at every frame time; between frames
538
- rigc holds the frame's value, and the panel states how far that can be from the
539
- live wind) — and sends it with readings and with Apply, whose check then runs
540
- `rig-parts check --environment`. The wind clock restarts with the clip, so what
541
- you watch is the world the readings and bars are taken in. **Export schedule**
542
- downloads the same bytes; **Check under this world** runs `rig-parts check
543
- --environment` on the build on the stage (needs `@rig-c/parts` 1.48.0) and shows
544
- its `ANIMATIONS_DO_NOT_FOLD` and `TEXTURE_STRETCH` lines beside the same build's
545
- default-world check. A refused schedule (`CHECK_ENVIRONMENT`) is shown verbatim.
546
- Feedback records the schedule's bytes and sha256 as `context.environmentSchedule`;
547
- nothing is authored into the rig.
548
-
549
- Below Readings, the **Requirements** block (`#requirements`) declares what the
550
- scene asks of the motion (issue #113): pick a kind (contact, follow, aim,
551
- range, stretch, seam), an animation and the subject — the slot picked on the
552
- stage pre-fills the slot, its mesh attachment and its bone — then set each
553
- bar on a slider whose track marks the last measured value. **Check** sends the
554
- typed requirements (names and numbers only) to `POST /api/requirements`;
555
- review writes them as a `spine-parts-requirements/1` file (`spec`, `fps`,
556
- `requirements`; never `may_fold`, scene targets or the part exception lists)
557
- into scratch and runs the installed producer's `rig-parts check --rig <rig>
558
- --out out --parts <handoff parts> --requirements requirements.json` on the
559
- inspected rig spec or the latest scratch build. Each requirement gets one
560
- line with its worst frame (click to seek); `REQUIREMENTS_*`,
561
- `CHECK_REQUIREMENT_MET` and `CHECK_REQUIREMENT_MEASURABLE` lines are shown
562
- verbatim. A kind the rig spec cannot offer (no mesh slot, no ik or transform
563
- constraint, fewer than two slots) is disabled with the reason. The file rides
564
- submitted feedback by hash and content as `context.requirements`.
565
-
566
- The view bar is one row and keeps the layer toggles in reach in every section: **Images**
567
- (`I`), **Mesh edges** (`M`) and **Bone points** (`B`). The keys do nothing while
568
- a text field, textarea, select or editable text has focus, with Ctrl/Alt/Cmd
569
- held, or on key repeat; Space stays pan and Enter stays frame entry. A disabled
570
- toggle shows why as a tooltip, and the footer shows the same layer status. The
571
- footer also gathers the message, player, layout, renderer, idle, remote control
572
- and draft state; **View region** opens the numeric view. Side by side,
573
- Reference and Rig are in the **View** menu at the right end of the view bar,
574
- labelled with the current choice.
575
-
576
- The side and bottom panels are resizable. The separators are thin (4px)
577
- lines whose colour is never the scrollbar's; the grab area is wider than the
578
- line. Drag a separator, or focus it
579
- and use the arrow keys (Shift for larger steps) and Home/End. Escape, a
580
- cancelled pointer or window blur during a drag returns the previous size. The
581
- stage keeps a usable minimum. When the window is too narrow, the side columns
582
- (left first on the compare page), and when it is too short, the Inspector, become explicit
583
- drawers opened from buttons above the stage instead of shrinking to nothing. Responsive clamping
584
- never overwrites the desired size, so a wider window restores it.
585
-
586
- Settings, at the end of the left column's scrolling body, holds browser defaults: Theme, Sync cameras by
587
- default, Repeat idle by default, and Reset layout. Defaults seed a new
588
- inspection only. Theme is the page's only appearance control: it shows the
589
- current inspection's appearance, and changing it applies it and saves it as the
590
- default. A view link or source draft keeps its own appearance, camera
591
- sync and idle choice, and restoring it neither changes the saved defaults nor
592
- starts playback. Reset layout restores panel sizes and nothing else.
593
-
594
- These defaults and desired panel sizes are one versioned, closed record in
595
- browser `localStorage` (`rig-review-preferences/1`). It holds no source path,
596
- hash, feedback, review decision or capability. The first load migrates the
597
- earlier same-origin appearance preference. If storage is denied, unavailable
598
- or the record is corrupt, the page uses defaults and every control keeps
599
- working for that page; a corrupt record is refused rather than overwritten
600
- until you change a setting. Several open tabs merge their writes per field, so
601
- changing the theme in one tab does not undo a panel size changed in another.
602
- Separate browser profiles keep separate preferences.
603
-
604
- Browser storage is per origin. Managed opens share the front's one origin, so
605
- Settings, panel sizes and drafts carry across reopens. `--direct` and foreground
606
- serving use per-port origins and keep them separate.
607
-
608
-
609
- ## Inspect animation keys
610
-
611
- 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.
612
-
613
- 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.
614
-
615
- ## Inspect a part
616
-
617
- 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.
618
-
619
- 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.
620
-
621
- 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.
622
-
623
- 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.
624
-
625
- 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.
626
-
627
- ## Chest regions (designate, rebuild, watch)
628
-
629
- 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**.
630
-
631
- 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
632
44
 
633
- 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.
634
-
635
- 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`).
636
-
637
- **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:
638
-
639
- - **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 the `rig-c` that `rig-parts` itself finds walking up from its own package. 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.
640
- - **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.
641
- - **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.
642
- - **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.
643
- - **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.
644
- - **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.
645
- - **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*.
646
-
647
- 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.
648
-
649
- 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.
650
-
651
- **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.
652
-
653
- 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`).
654
-
655
- ## Mesh panel (density, regions, segments → remesh)
656
-
657
- 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).
658
-
659
- - **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.
660
- - **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).
661
- - **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.
662
- - **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.
663
- - **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 …`).
664
- - **Reset** returns the pending entry to the config's and the stage to the inspected build.
665
- - **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.
666
-
667
- ## Weights (setup pose, watch only)
668
-
669
- 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.
670
-
671
- - **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.
672
- - **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.
673
- - **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.
674
- - **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.
675
- - **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`.
676
-
677
- 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.
678
-
679
- ## Bones (config anchors, check, rebuild)
680
-
681
- 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`).
682
-
683
- - **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.
684
- - **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).
685
- - **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.
686
- - **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.
687
-
688
- Adding, removing or re-parenting bones is out of scope.
689
-
690
- ## Rig fields (regions, still row, stops, inherit, turn)
691
-
692
- 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).
693
-
694
- - **`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.
695
- - **`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.
696
- - **`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.
697
- - **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.
698
- - **`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.
699
-
700
- **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.
701
-
702
- ## Cuts (cut polygons, patch boxes, limb joints → rig-parts build)
703
-
704
- 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.
705
-
706
- - **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.
707
- - **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`).
708
- - **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.
709
-
710
- 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.
711
-
712
- ## Mesh edit (typed delta, preview only)
713
-
714
- 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.
715
-
716
- The bar has four sub-modes, one pressed at a time, with single keys that avoid the layer keys `I` / `M` / `B`:
717
-
718
- | Key | Sub-mode | Pointer |
45
+ | Task | Command or panel | Destination |
719
46
  | --- | --- | --- |
720
- | `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) |
721
- | `C` | Create | click empty space to add a vertex; click an edge to split it at the click point |
722
- | `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`) |
723
- | `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) |
724
72
 
725
- 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.
726
-
727
- 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*).
728
-
729
- **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.
730
-
731
- 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.
732
-
733
- ## Retarget (map, flags → rig-pose, watch)
734
-
735
- 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
736
74
 
737
- - **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`).
738
- - **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`).
739
- - **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).
740
76
 
741
- **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
742
78
 
743
- 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).
744
81
 
745
- ## License
746
-
747
- 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)