@rig-c/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 +46 -708
- package/docs/DESIGN.md +11 -0
- package/docs/LIVE_FIXTURE.md +17 -4
- package/docs/PANELS.md +400 -0
- package/docs/REVIEW_PACKAGE.md +29 -5
- package/docs/WORKFLOWS.md +337 -0
- package/package.json +4 -1
- package/src/compare-session.ts +3 -1
- package/src/feedback-validation.ts +2 -0
- package/src/manifest.ts +2 -1
- package/src/producer.ts +11 -5
- package/src/session.ts +5 -3
- package/src/turn-pad.ts +60 -0
- package/web/compare.css +10 -0
- package/web/compare.ts +17 -5
- package/web/inspection-persistence.ts +4 -2
- package/web/main.ts +20 -7
- package/web/styles.css +10 -0
- package/web/turn-pad.ts +26 -0
package/docs/REVIEW_PACKAGE.md
CHANGED
|
@@ -5,8 +5,9 @@ Adapter**. Review supplies human inspection and feedback bound to the exact
|
|
|
5
5
|
reviewed sources; it neither replaces Core's build validation nor authors motion.
|
|
6
6
|
The app uses the published `@rig-c/play` loader/player to preview artifacts during
|
|
7
7
|
that inspection. This conceptual delivery order is not an import/dependency
|
|
8
|
-
order. Adapter means downstream consumer integration
|
|
9
|
-
|
|
8
|
+
order. Adapter means downstream consumer integration through the published
|
|
9
|
+
`@rig-c/pixi`, `@rig-c/phaser` and `@rig-c/three` packages; Review does not depend
|
|
10
|
+
on them. Existing producer formats and playback dependencies
|
|
10
11
|
remain authoritative.
|
|
11
12
|
|
|
12
13
|
The CLI also accepts an existing output directory. It reads a root `review.json` when present. Otherwise it constructs this contract in memory: optional root `skeleton.model.json`, otherwise the fixed rigc build path `build/skeleton.model.json`; read-only `skeleton.*`, `motion.json` and `report.json` links; root videos (`mp4`, `webm`, `mov`); image filenames containing strip/contact/sheet/filmstrip/띠; and canonical `*pose*.json` references. The root compiled model takes precedence over the build path. These are the only two automatically recognized model paths; discovery never searches recursively. One fixed layout redirects instead (#149): a folder with neither model path that is a `rig-parts build` output root — regular `parts.json` and `rig/rig.json`, a real (not symlinked) `check/` directory, and an existing `check/build/skeleton.model.json` entry — opens as its `check/` package. The package root is then `check/`, exactly as if `check/` had been opened: the same manifest, source hash, `reviewedSource`, session and open key. A folder missing any of those files is not a build root and is refused or adapted as before; a build root that also holds a model of its own opens as itself. The opened directory remains the package root, so root motion/references accompany the build model and its model-relative pages. An existing selected model with an invalid type, dangling symlink, escaping symlink or invalid model envelope is refused rather than skipped. Each reference is shown separately; no source relationship, FPS or video synchronization is inferred. It never inserts or rewrites a manifest or motion file in the source folder. Raw Spine JSON/skel remains read-only evidence with playback unavailable. The folder revision is `folder-adapter/1`; actual changed file bytes are distinguished by the source hash.
|
|
@@ -105,6 +106,29 @@ Both session responses include server-derived `feedbackWarnings:[{id,error}]`. R
|
|
|
105
106
|
The state filename is a digest of identity, revision, source hash and reviewed artifact/tool identity, so package names cannot select arbitrary filesystem paths. Writes are atomic and source files remain untouched. Review/session load and save stream-hash the declared inputs; changed bytes produce HTTP 409 and require restart. Asset reads do not rehash an entire video: single byte ranges return 206 and invalid ranges 416. Asset documents receive a sandbox CSP with no scripts or same-origin permission, so delivered authoring HTML cannot approve its own package. No network listener beyond loopback or cross-origin mutation is offered.
|
|
106
107
|
|
|
107
108
|
|
|
109
|
+
### Turn inspection context
|
|
110
|
+
|
|
111
|
+
`context.turn`, when present, is a closed, bounded record:
|
|
112
|
+
`{schema:"rig-review-turn/1", values:{yaw?,pitch?,body?}, held:boolean, smoothing:number}`.
|
|
113
|
+
Values are the applied dial values, not pending pointer targets. Smoothing is the
|
|
114
|
+
normalized-pointer spring frequency (0 disables it, otherwise at most 100 s⁻¹).
|
|
115
|
+
Only the model's supported actual sliders supply the permitted axes and ranges;
|
|
116
|
+
all offered axes must be present, each finite and within its range. Unknown keys,
|
|
117
|
+
missing axes, unsupported models and out-of-range values refuse with `TURN_*`.
|
|
118
|
+
A comparison validates against both source-bound models' common axes and range
|
|
119
|
+
intersection, irrespective of the feedback's selected side. Model metadata comes
|
|
120
|
+
from the same bytes hashed into `reviewedSource`; there is no filesystem parameter
|
|
121
|
+
or producer-config discovery in this field. Session source rehashing, identity,
|
|
122
|
+
Origin and payload limits still apply. Historical records retain warnings under
|
|
123
|
+
the ordinary legacy-read policy; a fresh save cannot bypass this validation.
|
|
124
|
+
If a scratch rebuild changes supported axes or expands a range, submission still
|
|
125
|
+
uses the original inspected catalog: newly supported values are refused by name
|
|
126
|
+
(`TURN_UNAVAILABLE` or `TURN_STATE_MODEL_MISMATCH`), rather than silently certified
|
|
127
|
+
as inspected inputs. Authoritative scratch-dial validation is outside this field.
|
|
128
|
+
The optional inspection bookmark `selection.turn` has this same shape and is
|
|
129
|
+
checked against the active pad before transactional restoration. It restores
|
|
130
|
+
applied values, not the pointer's pending travel. No package or motion bytes change.
|
|
131
|
+
|
|
108
132
|
## Feedback export
|
|
109
133
|
|
|
110
134
|
The frontend downloads `rig-review-feedback/1` JSON with `id`, `revision`, `sourceHash`, review status, `feedback[]`, and issue-ready `issues[]` entries containing title/body/labels. It also offers Markdown. Each issue contains the frame interval, target, category, summary and package binding. Exports stay local; no issue is posted automatically. These are review annotations, not motion or correction-key artifacts.
|
|
@@ -176,7 +200,7 @@ Every edit card states its edit as one config patch; review applies it to a scra
|
|
|
176
200
|
- `rig-review-applied/2` (patch): `{schema, buildId, patch, patchSha256, configHash, producer:{package,version}, compiler:{package,version}, line, check, diff, modelHash}` — the exact patch (whose `config_sha256` equals `configHash`), its hash, the producer's last line, check's summary and the rig.json diff. Since #136 the record adds `op`, the producer op that ran: `"rig"` or `"retune"` for a `rig` patch, `"remesh"` for a remesh patch (anything else is refused); the job carries the same `op` and its lines name the command. Records without `op` stay valid. Since #111 `op` may be `"cloth"` for a `rig` patch whose ops all lie under `/cloth`, and exactly then the record adds `cloth: {route:"build"|"rig", keypoints:{path, sha256}, written:{count, paths[≤200]}}` — how the config propose wrote was built, the keypoint file it read and where the written config differs from the patched one — and, when the route is `build`, `assembled` as below; the export's `Applied:` line names the written diff and the route. Since #115 `op` may be `"build"` for a `rig` patch, and exactly then the record adds `assembled: {wallMs, recomposite:{before, after}, partsChanged[], seamPairs[≤64]}` — the build's wall clock for that Apply, the recomposite figures, the parts whose PNG changed against the handoff's and the `seam pair` lines; the issue export's `Applied:` line names the wall clock (`· build 118.0 s · parts changed: bottomwear, ribbon_r · recomposite error px 8194 → 8194`). A remesh patch from the Mesh panel (#106) adds `mesh: {part, before:{vertices, triangles, hull}, after:{vertices, triangles, hull}}` — the revision's `remesh.json` counts; `part` must be the part the patch's `remesh --part` names. Since #125 the record adds `files: {documents:[{file, count, paths}], images:{changed, unchanged, names}}` (or `null`) — the scratch rig against the inspected rig, every document the producer writes: `documents` in the order `rig.json`, `motion.json`, `sets.json`, `mesh_report.json` (each `file` at most once; a document neither side has is left out), `count` the number of RFC 6901 pointers at which it differs and `paths` the first ≤ 200 of them (`""` — the whole document — when only one side has it or a side is not JSON, unless the bytes are equal); `images` compares `images/*` by SHA-256 only: `changed` (including an image only one side has, its `images/<name>` in `names`, ≤ 200) and `unchanged`. `diff` stays rig.json's entry as before. Records without `files` (written before #125) stay valid. The list of documents is `PRODUCER_DOCUMENTS` in `src/producer-files.ts`, the same list the baseline compares. The Apply status line and the export state it per file: `motion.json differs at 2 path(s) · rig.json, sets.json, mesh_report.json unchanged · 31 images unchanged`.
|
|
177
201
|
- `rig-review-applied/3` (#135, Edit Mesh's delta): `{schema, buildId, part, delta, deltaSha256, configHash, producer, compiler, line, op, files, mesh?, modelHash}` — the `rig-review-mesh-edit/1` document as sent (basis `"setup"`, `slot` the part, at most 300 ops), the SHA-256 of its bytes, the producer's last line, `op` always `"remesh"`, the per-file diff as above (or `null`) and the part's `remesh.json` counts (`mesh.part` must be `part`). No `check`: `rig-parts remesh` holds check on the revision itself (`REMESH_CHECK_HELD`). See *Mesh edit export*.
|
|
178
202
|
|
|
179
|
-
**Readings (#108).** `POST /api/readings` (single review; same-origin, capability, 64 KiB) takes exactly `{animation, readings:[1–8], environment}`: each reading is `{kind:"folds"}`, `{kind:"overlaps"}`, `{kind:"pass-through", cloth, leg}`, `{kind:"seam", a, b}` or `{kind:"angle-range", bone, relative}`, every name a slot or bone name without `:` or `,` (whether the rig has it is rigc's to say), no reading twice; `environment` is `null`, `{wind:[x,y], gravity:[x,y]}` (x right, y down) or, with *Use for readings* on (#112), the exported `rigc-environment/1` schedule as data. Malformed: HTTP 400 `READINGS_REQUEST_INVALID`. Outcomes (HTTP 200 `{status:"refused", error}`): `READINGS_COMPILER_NOT_INSTALLED` (rig-c beside `@rig-c/parts`, else beside the package or review), `READINGS_COMPILER_TOO_OLD <found> 2.63.0`, `READINGS_NO_MODEL`, `READINGS_STATE_INSIDE_PACKAGE`, `READINGS_BUSY`. The job (`GET /api/readings/<id>`, the id hashed from the package, the request, the inspected model's bytes, the scratch build id, the compiler and whether the schedule was passed): `{schema:"rig-review-readings/1", id, status:"running"|"done"|"failed", error, request, environment:{requested, flag, passed}, compiler, runs:[{build:"inspected"|"scratch", id, modelHash, results[]}]}`. Each result is `{reading, label, status:"read"|"refused"|"failed", fps, frames[] (every frame the reading's JSON lists: folds' meshes' frames, pass-through's frames, seam's opens, overlaps' moving pairs' runs; angle-range's lowest and highest frame from its printed line), source:"json"|"line", worst:{frame, figure, line}|null, items[] (per mesh or pair), lines[] (rigc's printed rows for the reading), refusal (rigc's `rigc: …` line verbatim), exitCode, wallMs}`. The inspected build is `--candidate <the directory of the manifest's model>`; the scratch build is the session's latest built Apply. Each reading runs as `rigc render --candidate <build> --animation <animation> [--environment environment.json] <flag> --out render` in `<state>/readings/<package hash>/<inspected-<model sha256>|scratch-<build id>>/<key>/`, keeps `result.json`, the reading's JSON and `frames.json`, and removes the rendered frames. The schedule `{spec:"rigc-environment/1", steps:[{t:0, wind, gravity}]}` is passed only when `rigc render --help` lists `--environment`. A schedule sent in the request is written as sent (re-serialized, one-space JSON and a newline).
|
|
203
|
+
**Readings (#108).** `POST /api/readings` (single review; same-origin, capability, 64 KiB) takes exactly `{animation, readings:[1–8], environment}`: each reading is `{kind:"folds"}`, `{kind:"overlaps"}`, `{kind:"pass-through", cloth, leg}`, `{kind:"seam", a, b}` or `{kind:"angle-range", bone, relative}`, every name a slot or bone name without `:` or `,` (whether the rig has it is rigc's to say), no reading twice; `environment` is `null`, `{wind:[x,y], gravity:[x,y]}` (x right, y down) or, with *Use for readings* on (#112), the exported `rigc-environment/1` schedule as data. Malformed: HTTP 400 `READINGS_REQUEST_INVALID`. Outcomes (HTTP 200 `{status:"refused", error}`): `READINGS_COMPILER_NOT_INSTALLED` (`@rig-c/core` first, legacy `rig-c` only when absent, beside `@rig-c/parts`, else beside the package or review), `READINGS_COMPILER_TOO_OLD <found> 2.63.0`, `READINGS_NO_MODEL`, `READINGS_STATE_INSIDE_PACKAGE`, `READINGS_BUSY`. The job (`GET /api/readings/<id>`, the id hashed from the package, the request, the inspected model's bytes, the scratch build id, the compiler and whether the schedule was passed): `{schema:"rig-review-readings/1", id, status:"running"|"done"|"failed", error, request, environment:{requested, flag, passed}, compiler, runs:[{build:"inspected"|"scratch", id, modelHash, results[]}]}`. Each result is `{reading, label, status:"read"|"refused"|"failed", fps, frames[] (every frame the reading's JSON lists: folds' meshes' frames, pass-through's frames, seam's opens, overlaps' moving pairs' runs; angle-range's lowest and highest frame from its printed line), source:"json"|"line", worst:{frame, figure, line}|null, items[] (per mesh or pair), lines[] (rigc's printed rows for the reading), refusal (rigc's `rigc: …` line verbatim), exitCode, wallMs}`. The inspected build is `--candidate <the directory of the manifest's model>`; the scratch build is the session's latest built Apply. Each reading runs as `rigc render --candidate <build> --animation <animation> [--environment environment.json] <flag> --out render` in `<state>/readings/<package hash>/<inspected-<model sha256>|scratch-<build id>>/<key>/`, keeps `result.json`, the reading's JSON and `frames.json`, and removes the rendered frames. The schedule `{spec:"rigc-environment/1", steps:[{t:0, wind, gravity}]}` is passed only when `rigc render --help` lists `--environment`. A schedule sent in the request is written as sent (re-serialized, one-space JSON and a newline).
|
|
180
204
|
|
|
181
205
|
**Environment schedule (#112).** The Animate Environment block exports its controls as a `rigc-environment/1` schedule (`src/environment-schedule.ts`): `{spec:"rigc-environment/1", steps:[{t, wind?:[x,y], gravity?:[x,y]}]}`, x right, y down, t = 0 at the clip's first frame, written as one-space JSON and a newline (the bytes downloaded, sent and hashed). With neither breeze nor a repeating gust it is the step form, one step `{t:0, wind:[base,0], gravity:[0,g]}` (#108's standing schedule) — exact. With breeze or repeat on it is the sampled form: one step per frame at 12 fps (rigc render's default and check's IDLE_FPS) from 0 to the clip's last frame (`round(duration·12)`; rigc refuses a step after it), each the panel's wind at that time from a fresh clock (a repeating gust starts at 0, every `max(6, duration + 0.6)` s, turned when the base is negative), a step equal to the one before left out, gravity on the first step. rigc sets a step before the physics step that ends on its frame, as the page sets the wind before the player's step, so the schedule equals the panel's wind at every frame time; between frames rigc holds the frame's value, within `max|dw/dt| / 12` of the panel's (`(breeze ? 8.48 : 0) + (repeat ? peak·π/duration : 0)`, over 12). A one-off Gust (Repeat off) is not in the schedule. Refused by name: `ENVIRONMENT_NOT_APPLIED` (the controls are not engaged; check and render without `--environment` already read the runtime's world), `ENVIRONMENT_SCHEDULE_NO_CLIP`, `ENVIRONMENT_SCHEDULE_TOO_LONG` (more than 600 steps). *Use for readings* sends the drawn clip's schedule with readings and the checked clip's (`idle` when the rig has one) with Apply, and restarts the wind clock with the clip (and at each idle loop) so the person watches the world rigc reads. *Export schedule* downloads `rigc-environment.json`.
|
|
182
206
|
|
|
@@ -190,7 +214,7 @@ Every edit card states its edit as one config patch; review applies it to a scra
|
|
|
190
214
|
|
|
191
215
|
**Self-targeted requirements (#142).** The panel refuses before Add, as `REQUIREMENTS_SELF_TARGET`, a `contact` whose target is its own bone and point and an `aim` whose target bone is the aimed bone: both measure 0 in every frame and the producer reports them as PASS (its own refusal is rig-c/parts#540). A bone's tip against its own origin is not refused; it measures the bone's length. A fresh contact or aim form proposes a target bone other than the subject (the next bone in rig order).
|
|
192
216
|
|
|
193
|
-
**Retarget (#114).** `GET /api/rebuild` adds `retarget: {available, reason, producer, compiler, rig, sequences:[{id, label, path, cleaned, frames, people[]}], bones[], animations[]}`: the installed `@rig-c/pose` (found as `@rig-c/parts` is: the nearest `node_modules` from the package, then from review's working directory; `RETARGET_PRODUCER_NOT_INSTALLED` when absent, never installed) and the
|
|
217
|
+
**Retarget (#114).** `GET /api/rebuild` adds `retarget: {available, reason, producer, compiler, rig, sequences:[{id, label, path, cleaned, frames, people[]}], bones[], animations[]}`: the installed `@rig-c/pose` (found as `@rig-c/parts` is: the nearest `node_modules` from the package, then from review's working directory; `RETARGET_PRODUCER_NOT_INSTALLED` when absent, never installed) and the `@rig-c/core` beside it (legacy `rig-c` only when the scoped package is absent; `RETARGET_COMPILER_NOT_INSTALLED`). The `compiler` record identifies the selected package and version; the remaining inputs are the package's declared `rig-pose-sequence/1` pose references, and the handoff rig's bone and animation names (`RETARGET_SEQUENCE_UNAVAILABLE` when the package declares no sequence). `POST /api/rebuild` with exactly `{retarget: {sequence, map, flags, configHash}}` (same-origin, capability, 64 KiB): `sequence` a declared pose reference id; `map` a `rig-pose-map/1` document of at most 32 KiB whose chains each hold a name and 1–32 bones and joints (everything else is the producer's to judge); `flags` exactly `{animation, person, clean, minScore, flip, short, bends, bendTolerance, legScale, plantFeet, stance, raisedByHead, carryIdle}`, `clean` null or exactly `{loop, legs, rest, hold, hipsInferred}` (windows 1, or odd and 5–61). Malformed: HTTP 400 `RETARGET_REQUEST_INVALID`. Refused before anything runs: `REBUILD_CONFIG_STALE`, `RETARGET_SEQUENCE_UNKNOWN`, `RETARGET_SEQUENCE_UNSUPPORTED` (a static pose), the handoff and state-directory refusals, `REBUILD_BUSY`. The server maps the flags to argv (`op.args` for retarget, `op.clean` for clean, only what the flags state) and runs, in `<state>/rebuild/<package hash>/<retarget id>/`, `rig-pose clean --pose pose.json --out pose.clean.json <op.clean>` (unless `clean` is null) and `rig-pose retarget --rig <handoff rig> --map map.json --sequence <pose.clean.json | pose.json> --out motion.json <op.args>`, copies the handoff rig's regular files (motion.json excepted) and `images/` to `rig/` with `rig/motion.json` the retarget's, then the shared tail: the per-file diff and `rigc build --rig rig/rig.json --motion rig/motion.json --out build --profile rig-play --pack --page-edges free --pack-shape polygon`. The retarget id hashes the package, the handoff config, the record's bytes and both tool versions (no clock); an identical retarget reuses its build. The job (`GET /api/rebuild/<id>`): `{schema:"rig-review-rebuild/1", kind:"retarget", op:"retarget", id, status, reused, request:{record, recordSha256, configHash}, producer, compiler, lines[], producerLine, refusals[] (every FAIL line of the step that refused, verbatim), exitCode, wallMs:{clean, retarget, compile}, files, motion:{animation, animations[], kept, changed[], animationSha256, motionSha256}, readings:[{code, line, frame, time}], build}`. `readings` are the retarget's FEET, STANCE, FLIP, SHORT, END-ON, LEG-SCALE and REACH lines (at most 200), each with the first frame it names and that frame's key time from the run's report `frameTimes` (else the line's `t=`). The record, `rig-review-retarget/1`: `{schema, config_sha256, rig:{path, sha256 (rig.json)}, sequence:{reference, path, sha256}, map, map_sha256 (the bytes of map.json as written: one-space JSON and a newline), flags, producer:{name, version}, op:{cmd:"retarget", args, clean}}`; a reader checks that `op` is its flags' own argv. The applied context of a built retarget is `rig-review-applied/4`: `{schema, buildId, retarget, recordSha256, configHash, producer, compiler, line, motion, files, readings, modelHash}`.
|
|
194
218
|
|
|
195
219
|
**`context.environmentSchedule`.** Feedback written while the Environment controls are applied carries the drawn clip's exported schedule by hash and content: `{schema:"rig-review-environment-schedule/1", sha256, size, text, usedForReadings}` — `text` is exactly the exported bytes (a `rigc-environment/1` schedule), `sha256` and `size` are recomputed over it on save, `usedForReadings` says whether readings and Apply read it. Absent and `null` stay valid. The issue export adds an `Environment schedule:` line.
|
|
196
220
|
|
|
@@ -563,7 +587,7 @@ Browser players retain play's default DOM mode and default mesh backend. Rigid
|
|
|
563
587
|
parts remain DOM images; a WebGL mesh label does not claim full-frame GL drawing.
|
|
564
588
|
WebGL is requested by the published default, with Canvas2D as the supported mesh
|
|
565
589
|
fallback. Accurate public actual-flush telemetry requires play 1.11.1 or later;
|
|
566
|
-
this checkout consumes pinned 1.
|
|
590
|
+
this checkout consumes pinned 1.14.0. The consumer does not probe GL or inspect
|
|
567
591
|
private renderer state. The separate Node strip exporter remains Canvas2D.
|
|
568
592
|
|
|
569
593
|
The backend badge is sampled only after successful draw/frame calls. Fresh mesh
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# Inspection workflows
|
|
2
|
+
|
|
3
|
+
See [workspace panels](PANELS.md) for the controls, or [Start](../README.md#start) for installation.
|
|
4
|
+
|
|
5
|
+
## Existing output handoff
|
|
6
|
+
|
|
7
|
+
Open the current output folder directly, without changing the producing agent or inserting a manifest:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bun run check -- /absolute/path/to/output
|
|
11
|
+
bun run review -- /absolute/path/to/output
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
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.
|
|
15
|
+
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
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.
|
|
19
|
+
|
|
20
|
+
For a curated handoff, [REVIEW_PACKAGE.md](REVIEW_PACKAGE.md) documents optional `review.json` with named checkpoints, references and angle axes:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
bun run review -- /absolute/path/to/package/review.json --checkpoint silhouette
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
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.
|
|
27
|
+
|
|
28
|
+
## Compare two outputs
|
|
29
|
+
|
|
30
|
+
Open two compiled packages against one explicitly selected local reference video:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
bun run review -- compare /absolute/output-a /absolute/output-b --reference /absolute/reference.mp4 --animation dance
|
|
34
|
+
bun run check -- compare /absolute/output-a /absolute/output-b --reference /absolute/reference.mp4 --animation dance
|
|
35
|
+
bun run feedback -- compare /absolute/output-a /absolute/output-b --reference /absolute/reference.mp4 --animation dance
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
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.
|
|
39
|
+
|
|
40
|
+
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](REVIEW_PACKAGE.md#two-package-comparison).
|
|
41
|
+
|
|
42
|
+
## Feedback ownership
|
|
43
|
+
|
|
44
|
+
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.
|
|
45
|
+
|
|
46
|
+
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.
|
|
47
|
+
|
|
48
|
+
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.
|
|
49
|
+
|
|
50
|
+
After the person submits, the producing agent retrieves the stored review without needing the hashed state filename:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
bun run feedback -- /absolute/path/to/output
|
|
54
|
+
# Or the same explicit review.json originally opened:
|
|
55
|
+
bun run feedback -- /absolute/path/to/package/review.json
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
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](REVIEW_PACKAGE.md#historical-feedback-retrieval) for selection and validation rules.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
For grouped issue-ready text, use the same input/reference binding with `--format issue`:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
rig-review feedback /absolute/output --reference /absolute/original.mp4 --format issue
|
|
65
|
+
rig-review feedback /absolute/a --compare /absolute/b --reference /absolute/original.mp4 --animation dance --format issue
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
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.
|
|
69
|
+
|
|
70
|
+
## Frame strip export
|
|
71
|
+
|
|
72
|
+
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.
|
|
73
|
+
|
|
74
|
+
The same export is available without a browser:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
bun src/cli.ts strip /absolute/package/review.json --reference source-video --animation dance --frames 176:184 --out /absolute/external/strip.png
|
|
78
|
+
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
|
|
79
|
+
bun src/cli.ts strip /absolute/package/review.json --reference source-video --animation dance --all-feedback --json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
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.
|
|
83
|
+
|
|
84
|
+
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.
|
|
85
|
+
|
|
86
|
+
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.
|
|
87
|
+
|
|
88
|
+
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.
|
|
89
|
+
|
|
90
|
+
## One-command owner handoff
|
|
91
|
+
|
|
92
|
+
`open` validates and hashes an existing folder, starts a background loopback
|
|
93
|
+
instance or reuses the exact matching one, prints its hash-bound URL, and exits.
|
|
94
|
+
By default the URL goes through the managed front at `127.0.0.1:4318`; `--direct`
|
|
95
|
+
prints the instance's own port instead (see [Managed front and write capability](#managed-front-and-write-capability)):
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
bun run open -- /absolute/output --reference /absolute/original.mp4
|
|
99
|
+
# Installed executable mapping:
|
|
100
|
+
rig-review open /absolute/output --reference /absolute/original.mp4 --animation dance
|
|
101
|
+
rig-review feedback /absolute/output --reference /absolute/original.mp4
|
|
102
|
+
rig-review open /absolute/a --compare /absolute/b --reference /absolute/original.mp4 --animation dance
|
|
103
|
+
rig-review feedback /absolute/a --compare /absolute/b --reference /absolute/original.mp4 --animation dance
|
|
104
|
+
# Producer handoff (single review only): the config, parts and inspected rig an Apply rebuilds from
|
|
105
|
+
rig-review open /absolute/output --config /absolute/output/rig/config.json [--parts /absolute/output/rig/out] [--source <png> --full <dir> [--head <dir>]] [--keypoints <file>]
|
|
106
|
+
rig-review feedback /absolute/output --config /absolute/output/rig/config.json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`--config`, `--parts`, `--source`, `--full`, `--head` and `--keypoints` declare the producer
|
|
110
|
+
handoff (see `docs/REVIEW_PACKAGE.md`, *Producer handoff*). They win over the
|
|
111
|
+
review.json `producer` block, which wins over discovery (`config.json`, then
|
|
112
|
+
`rig/config.json` inside the opened folder, then `../config.json` beside an opened
|
|
113
|
+
`out/`, named as discovered outside the folder). For the `check/` package of a
|
|
114
|
+
`rig-parts build` output the next places are the build's own config: `config.json`
|
|
115
|
+
in the build root, else beside it (`<out>/../config.json`, where `rig-parts build
|
|
116
|
+
--config <dir>/config.json --out <dir>/out` leaves it); the parts directory is the
|
|
117
|
+
build root. Declaring `--config` at exactly one of those two files is taken the same
|
|
118
|
+
way, and that file's folder is then the root for the other flags. Every other path must sit inside the
|
|
119
|
+
package root; a path outside it or a symlink that leaves it is refused by name.
|
|
120
|
+
The handoff files are hashed into the reviewed identity, so `feedback` and
|
|
121
|
+
`control` take the same flags to retrieve the same session, and changing the
|
|
122
|
+
config bytes starts a new pending review. `--keypoints` (#111) is the body-18 keypoint
|
|
123
|
+
file a cloth recipe Apply hands to `rig-parts propose`; it is hashed into the identity
|
|
124
|
+
only when declared, so a handoff without it keeps its identity. `--parts`,
|
|
125
|
+
`--source`/`--full`/`--head` and `--keypoints` need `--config`; `--source` and `--full` go together; compare takes none of them.
|
|
126
|
+
|
|
127
|
+
`open` chooses an explicit `--animation` first, then the normalized report's
|
|
128
|
+
`animation` after validating actual model membership, then a unique actual clip
|
|
129
|
+
(or unique common clip in compare). Thus `rig-review open /absolute/output
|
|
130
|
+
--reference /absolute/original.mp4` opens a report-bound `dance` without the flag
|
|
131
|
+
and prints `animation: dance (from report.json)` on stderr. Conflicting A/B reports,
|
|
132
|
+
a stale report name or multiple clips without a report refuse by name; no first
|
|
133
|
+
entry, filename or frame rate supplies a guess. Explicit invalid names never fall
|
|
134
|
+
back. The selected clip actually initializes single playback, while an explicit
|
|
135
|
+
checkpoint link or later manual choice takes priority. Incompatible automatic
|
|
136
|
+
checkpoints do not apply another clip's time or axis. Static/source-only packages
|
|
137
|
+
remain inspectable without claiming clip selection; explicit unvalidatable clips
|
|
138
|
+
refuse. Single review sessions remain package-wide; animation selection does not
|
|
139
|
+
create an animation-specific approval identity. Opening never
|
|
140
|
+
approves a review. The command prints the matching feedback retrieval command on
|
|
141
|
+
stderr; stdout contains the URL alone. A selected external video takes precedence
|
|
142
|
+
at automatic startup; an explicit checkpoint link or manual checkpoint selection
|
|
143
|
+
still chooses that checkpoint's reference.
|
|
144
|
+
|
|
145
|
+
The selected video is a separate CLI capability, not a manifest path escape or a
|
|
146
|
+
file copied into the source package. Its name and streaming byte hash, plus the
|
|
147
|
+
base package hash, appear as `referenceBinding` in single review responses,
|
|
148
|
+
sessions and feedback exports. Use the same `--reference` when retrieving that
|
|
149
|
+
submission; omitting it reads the separate reference-free identity. Changed
|
|
150
|
+
package or reference bytes start pending state. Existing historical retrieval
|
|
151
|
+
preserves earlier recorded tool versions over exactly the same reference content.
|
|
152
|
+
|
|
153
|
+
The single strip panel can export the selected external video using that same
|
|
154
|
+
binding. Headless equivalent (the original declared-reference form stays valid):
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
rig-review strip /absolute/output --reference-video /absolute/original.mp4 --animation dance --frames 176:184 --out /absolute/external/strip.png
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Only tool-owned instances with matching content, canonical input locations, tool
|
|
161
|
+
fingerprint and a private capability handshake are reused. The URL hash is
|
|
162
|
+
checked before serving the app; a stale link never selects another instance's
|
|
163
|
+
package. Input hashes are freshly computed, not trusted from mtime caches. Idle
|
|
164
|
+
open-managed workers stop after 30 minutes. Their private registry, requests and
|
|
165
|
+
logs live beside feedback under `~/.local/state/rig-c-review/open/`; nothing is
|
|
166
|
+
written into supplied folders. `RIG_REVIEW_STATE_DIR` selects another state root
|
|
167
|
+
outside all input packages, judged by its canonical location (symlinks in the path
|
|
168
|
+
and its existing ancestors resolved). Existing foreground review commands still work.
|
|
169
|
+
|
|
170
|
+
No browser is opened, no LAN listener is added, and no misleading phone/QR URL is
|
|
171
|
+
printed: every current instance binds only `127.0.0.1`.
|
|
172
|
+
|
|
173
|
+
The printed feedback retrieval command uses POSIX shell quoting for literal path
|
|
174
|
+
arguments. It is not a PowerShell or cmd.exe command string.
|
|
175
|
+
|
|
176
|
+
### Launcher contract
|
|
177
|
+
|
|
178
|
+
A launcher may start `rig-review open` as a child process and rely on its stdout,
|
|
179
|
+
stderr and exit code alone:
|
|
180
|
+
|
|
181
|
+
1. On success stdout is exactly one line, the URL. `open` never writes anything
|
|
182
|
+
else to stdout.
|
|
183
|
+
2. On success stderr carries the human lines in this fixed order: for the `check/`
|
|
184
|
+
package of a `rig-parts build` output, `Package: rig-parts build check package <path>`
|
|
185
|
+
and the handoff line (`Handoff <how>: <config path> sha256 <hash> · parts <dir>`, or
|
|
186
|
+
`Handoff: <refusal>`); the animation diagnostic (when a clip is selected), the control notice (with `--control`),
|
|
187
|
+
then the copyable retrieval line `Review started|reused; feedback: rig-review feedback …`.
|
|
188
|
+
3. A refusal exits with code 1, writes nothing to stdout, and its first stderr line
|
|
189
|
+
starts with a name, followed by a colon and the explanation:
|
|
190
|
+
- `OPEN_INPUT_MISSING <path>`: the input or `--compare` path does not exist, or is neither a directory nor a regular file.
|
|
191
|
+
- `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.
|
|
192
|
+
- `OPEN_REFERENCE_REQUIRED`: `--compare` was given without an explicit `--reference`.
|
|
193
|
+
- `OPEN_ANIMATION_UNKNOWN <name>`: the explicit or report-named clip (JSON-quoted) is not in the compiled model.
|
|
194
|
+
- `OPEN_ANIMATION_AMBIGUOUS`: no flag or report selects one clip, or the A/B reports conflict.
|
|
195
|
+
- `OPEN_ANIMATION_UNVALIDATABLE`: a clip was requested but no compiled model can validate it.
|
|
196
|
+
- `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`.
|
|
197
|
+
- `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.
|
|
198
|
+
- `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.
|
|
199
|
+
4. Exit codes: 0 applied, 1 refused. (`control` keeps its own exit 2 for an
|
|
200
|
+
unapplied command.)
|
|
201
|
+
|
|
202
|
+
Package validation and worker startup refusals keep their existing messages; the
|
|
203
|
+
named refusals above are decided before any state is written.
|
|
204
|
+
|
|
205
|
+
### Managed front and write capability
|
|
206
|
+
|
|
207
|
+
`open` is **managed by default**. It ensures one detached loopback front at
|
|
208
|
+
`127.0.0.1:4318` (`RIG_REVIEW_FRONT_PORT` overrides) for the state directory and
|
|
209
|
+
prints `http://127.0.0.1:4318/instances/<key>/?input=…&handoff=…#cap=…`. Every
|
|
210
|
+
inspection therefore shares one stable browser origin, so Settings, panel sizes and
|
|
211
|
+
source-bound drafts survive a reopen. The front only routes: it takes the worker
|
|
212
|
+
address from that key's private open record, allows a closed per-method route
|
|
213
|
+
list, bounds bodies (16 KiB control, 1 MiB session/strip) and upstream time,
|
|
214
|
+
limits concurrent upstream streams, re-verifies the worker's nonce on every
|
|
215
|
+
request, and refuses an unknown, expired or replaced instance by name
|
|
216
|
+
(`FRONT_INSTANCE_UNKNOWN`, `FRONT_INSTANCE_EXPIRED`, `FRONT_INSTANCE_REPLACED`).
|
|
217
|
+
Worker error text is not passed through; failures become named `FRONT_*` codes.
|
|
218
|
+
Package assets keep their `sandbox` CSP through the front. It never reads packages.
|
|
219
|
+
Idle fronts exit after 30 minutes once no live instance remains.
|
|
220
|
+
|
|
221
|
+
If another listener holds the front port, `open` fails with
|
|
222
|
+
`FRONT_PORT_BUSY: 127.0.0.1:4318 is held by another listener; use --direct` and
|
|
223
|
+
does not fall back silently. `open --direct` keeps the per-instance port and
|
|
224
|
+
prints that worker's own URL.
|
|
225
|
+
|
|
226
|
+
One origin cannot isolate scripts that run on it, so writes are bound to a
|
|
227
|
+
per-instance capability rather than to the page's origin or the key in its path.
|
|
228
|
+
Each worker mints a 256-bit capability kept only in its private 0600 open record.
|
|
229
|
+
`open` (managed or direct) and the foreground `review`/`review compare` commands
|
|
230
|
+
print it in the `#cap=` fragment, which browsers never send to a server. That
|
|
231
|
+
printed link goes only to the terminal of the command that started the instance.
|
|
232
|
+
Output meant for a log does not carry it: the install smoke (`bun run smoke`, whose output CI publishes) prints the same
|
|
233
|
+
URL as `#cap=<redacted>` and fails if any capability value reaches its stdout. The page
|
|
234
|
+
moves it into memory and that tab's `sessionStorage`, then removes the fragment
|
|
235
|
+
from the address bar. Every mutation sends it as `X-Review-Capability`; the front
|
|
236
|
+
checks it before forwarding and the worker compares it again in constant time
|
|
237
|
+
before its existing same-origin check. No endpoint returns it, so a page opened
|
|
238
|
+
for one instance cannot submit to another. A copied view link (without the
|
|
239
|
+
fragment) opens read-only and says *Read-only view: reopen with rig-review open to
|
|
240
|
+
edit*; run `open` again for a writable link.
|
|
241
|
+
|
|
242
|
+
## Control the current inspection page
|
|
243
|
+
|
|
244
|
+
Opt in once when opening a fixed package, then keep that page open:
|
|
245
|
+
|
|
246
|
+
```sh
|
|
247
|
+
rig-review open /absolute/output --reference /absolute/original.mp4 --control
|
|
248
|
+
rig-review control /absolute/output --reference /absolute/original.mp4
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The second command reads the connected page's actual inspection state and its
|
|
252
|
+
`pageId`/`revision`; it does not open a browser or start another worker. A page
|
|
253
|
+
learns from `GET /api/access` (`control: true|false`) whether its instance serves
|
|
254
|
+
control; an instance opened without `--control` is never asked for
|
|
255
|
+
`/api/control/config`. To change
|
|
256
|
+
that page, use the observed values and a closed inspection command:
|
|
257
|
+
|
|
258
|
+
```sh
|
|
259
|
+
rig-review control /absolute/output --reference /absolute/original.mp4 --page '<pageId>' --expected-revision 0 --request-id theme-1 --command '{"type":"theme","value":"light"}'
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Use the same reference/compare/animation inputs as the controlled `open`. Multiple
|
|
263
|
+
pages require an explicit `--page`. The URL and source binding stay unchanged;
|
|
264
|
+
existing ordinary servers do not become controllable. Starting the opt-in worker
|
|
265
|
+
is a one-time handoff, not a new link for each parameter change.
|
|
266
|
+
|
|
267
|
+
Commands cover appearance, automatic layout, synchronized or independent pane
|
|
268
|
+
cameras, playback, single animation, time and actual inspection layers. Camera
|
|
269
|
+
pan is measured in viewport CSS pixels (right/down positive). New inspections
|
|
270
|
+
start with camera sync on: zoom relative to fit and pan normalized by each pane's
|
|
271
|
+
viewport move all visible measurable reference/rig panes together. Turn sync off
|
|
272
|
+
with `{"type":"camera-sync","value":false}` to inspect each pane independently;
|
|
273
|
+
turning it off preserves current views. Turning it on aligns the visible panes
|
|
274
|
+
to the last active pane. Camera/fit targets are `rig|reference` in single review
|
|
275
|
+
and `a|b|reference` in compare. For legacy clients, compare `target:"rig"` explicitly
|
|
276
|
+
moves both A/B even when sync is off; its CSS readback aliases A. Comparison retains its
|
|
277
|
+
fixed animation and actual video-master seek path. A single-video time request
|
|
278
|
+
uses the actual decoded timestamp for both reference and rig; the applied
|
|
279
|
+
acknowledgement reports that time, which may differ from the requested seconds.
|
|
280
|
+
Unsupported features, stale
|
|
281
|
+
page/source/revision, unsubmitted feedback drafts and out-of-range values refuse
|
|
282
|
+
by name. No command changes a package/reference, authors motion, or submits or
|
|
283
|
+
approves feedback. Local controls and remote readback use the same inspection
|
|
284
|
+
state. A live view-link/history change supersedes pending remote work, and
|
|
285
|
+
commands refuse while that view is being restored.
|
|
286
|
+
|
|
287
|
+
Remote inspection readback also includes versioned numeric `view` coordinates
|
|
288
|
+
when the page can measure them: rig/world center and zoom, reference/image center
|
|
289
|
+
and zoom, each panel's current CSS viewport size and visible rectangle. World
|
|
290
|
+
coordinates use Y up; reference image coordinates use Y down. Compare reports
|
|
291
|
+
actual A and B rectangles separately; unavailable measurements are null. The
|
|
292
|
+
canonical view also reports `cameraSync` and `activeCamera`. These
|
|
293
|
+
are the same current cameras used by the local controls, rather than a second
|
|
294
|
+
simulated view. Camera commands still use the documented viewport CSS pan.
|
|
295
|
+
|
|
296
|
+
The CLI waits up to10 seconds for the page's actual acknowledgment. `queued` or
|
|
297
|
+
`dispatched` is not success; non-applied commands exit2 and may carry
|
|
298
|
+
`timedOut:true`. Reuse the same `--request-id` and original request to obtain its
|
|
299
|
+
receipt without applying it twice. `--wait-ms 0` returns the queued receipt;
|
|
300
|
+
`--wait-ms` is bounded to30000. A timeout does not mean an already delivered action
|
|
301
|
+
was rolled back. Tokens stay in private tool state, not URLs or command output.
|
|
302
|
+
|
|
303
|
+
## Keep and share an inspection
|
|
304
|
+
|
|
305
|
+
The view link stores the current numeric inspection in a bounded, versioned
|
|
306
|
+
`#review=` fragment. It includes the exact loaded source/model binding, selected
|
|
307
|
+
animation/time and reference selection, appearance/layers, camera sync and the
|
|
308
|
+
active pane, and each canonical camera center with visible bounds. Reloading a matching link restores a paused view;
|
|
309
|
+
it does not restart an idle loop or submit a review. Coordinates adapt to the
|
|
310
|
+
current viewport instead of replaying another window's CSS pan offsets.
|
|
311
|
+
Historical links without a sync mode restore with sync off to preserve their
|
|
312
|
+
saved views; fresh inspections start with sync on. The page keeps the current
|
|
313
|
+
fragment in the address bar as you inspect, so copying the address copies the
|
|
314
|
+
view; there is no separate copy button. Legacy modeling/motion
|
|
315
|
+
workspace selections are accepted without restoring a top-level workspace switch. A link
|
|
316
|
+
for a different source binding is refused.
|
|
317
|
+
|
|
318
|
+
Ordinary form inputs, selected panels, view state and unsent feedback/checkpoint
|
|
319
|
+
choices are kept in browser local storage for the same source. The draft scope
|
|
320
|
+
includes the source hash (`single:<id>:<sourceHash>`; compare uses both sides'
|
|
321
|
+
id and hash), so two revisions of one package keep separate drafts. Opening a page
|
|
322
|
+
deletes no draft; saving evicts the least-recently-written drafts beyond 32, but
|
|
323
|
+
never one written within the last 24 hours. Browser
|
|
324
|
+
Settings (below) remain, while a source-bound appearance override does not
|
|
325
|
+
carry over. URL view state takes precedence over the locally saved view. Each
|
|
326
|
+
Inspector section's own scroll position is part of this source-bound draft.
|
|
327
|
+
|
|
328
|
+
Local recovery compares the original submitted server-session fingerprint,
|
|
329
|
+
including its update time. It cannot replace a newer server review: conflicting
|
|
330
|
+
or corrupt saved data remains available to download, with an explicit warning;
|
|
331
|
+
recovering a conflicting draft requires choosing a pending review. Storage
|
|
332
|
+
errors never count as a successful save. Nothing is written to the server or
|
|
333
|
+
input package merely by restoring a page. Browser storage is convenient local
|
|
334
|
+
recovery, not a durable backup; submit the review (and retrieve it with
|
|
335
|
+
`rig-review feedback`) when it must be kept.
|
|
336
|
+
Control capabilities are excluded from links and browser persistence.
|
|
337
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/review",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local human rig inspection and structured feedback export",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"review": "bun src/cli.ts",
|
|
9
9
|
"check": "bun src/cli.ts check",
|
|
10
10
|
"demo": "bun src/cli.ts examples/demo/review.json",
|
|
11
|
+
"check:readme": "bun scripts/check-readme.ts",
|
|
11
12
|
"typecheck": "tsc --noEmit",
|
|
12
13
|
"test": "bun test",
|
|
13
14
|
"build": "bun src/build.ts",
|
|
@@ -35,6 +36,8 @@
|
|
|
35
36
|
"web",
|
|
36
37
|
"examples/demo",
|
|
37
38
|
"docs/REVIEW_PACKAGE.md",
|
|
39
|
+
"docs/WORKFLOWS.md",
|
|
40
|
+
"docs/PANELS.md",
|
|
38
41
|
"docs/DESIGN.md",
|
|
39
42
|
"docs/ANIMATION_INSPECTION.md",
|
|
40
43
|
"docs/LIVE_FIXTURE.md",
|
package/src/compare-session.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {sharedTurnDials} from './turn-pad.ts';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { join } from 'node:path';
|
|
@@ -17,7 +18,8 @@ export function parseCompareSession(input:unknown,p:ComparePackage,strict=true):
|
|
|
17
18
|
if(value.corrections!==undefined)throw new Error('compare session.corrections: motion authoring proposals are not accepted');
|
|
18
19
|
if(!Array.isArray(value.feedback)||value.feedback.length>1000)throw new Error('compare session.feedback: expected array <= 1000');
|
|
19
20
|
const feedbackWarnings:FeedbackWarning[]=[];
|
|
20
|
-
const
|
|
21
|
+
const turnDials=sharedTurnDials(p.a.feedbackCatalog.turnDials??[],p.b.feedbackCatalog.turnDials??[]);
|
|
22
|
+
const feedback:CompareFeedback[]=value.feedback.map((item:unknown)=>{const side=choice(object(item,'feedback').side,['a','b'] as const,'feedback.side'),parsed=parseFeedback(item,strict);if(strict)return {...validatePackageFeedback(parsed,p[side],p.response.animation,turnDials),side};const read=readPackageFeedback(parsed,p[side],p.response.animation,turnDials);if(read.warning)feedbackWarnings.push(read.warning);return {...read.feedback,side};});
|
|
21
23
|
if(new Set(feedback.map(f=>f.id)).size!==feedback.length)throw new Error('compare session.feedback: duplicate ids');
|
|
22
24
|
return {...fresh,status:choice(value.status,['pending','approved','changes-requested'] as const,'compare session.status'),feedback,feedbackWarnings,updatedAt:new Date().toISOString()};
|
|
23
25
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type {TurnDial} from './turn-pad.ts';
|
|
1
2
|
import { lastFrame, timeAtFrame, type FrameClock } from './frame-clock.ts';
|
|
2
3
|
|
|
3
4
|
/** Null names mean unavailable metadata; an empty list means known empty. */
|
|
@@ -5,6 +6,7 @@ export interface FeedbackCatalog {
|
|
|
5
6
|
rig:string;bones:string[]|null;slots:string[]|null;
|
|
6
7
|
animations:{name:string;duration:number}[]|null;
|
|
7
8
|
clock:FrameClock|null;clockAnimation:string|null;
|
|
9
|
+
turnDials?:TurnDial[];
|
|
8
10
|
}
|
|
9
11
|
export type FeedbackTargetKind='bone'|'slot'|'rig';
|
|
10
12
|
export interface FeedbackSelection {frameStart:number;frameEnd:number;target:{kind:FeedbackTargetKind;name:string};animation:string|null}
|
package/src/manifest.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {discoverTurnDials} from './turn-pad.ts';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import { toolVersions } from './versions.ts';
|
|
3
4
|
import type { FeedbackCatalog } from './feedback-validation.ts';
|
|
@@ -114,7 +115,7 @@ export async function loadReview(input: string, options: { handoff?: HandoffFlag
|
|
|
114
115
|
// #192: the root is `check/` whether the folder or its review.json was opened, so the build root is kept either way.
|
|
115
116
|
const buildRoot=basename(root)==='check'&&await isBuildRoot(dirname(root))?dirname(root):undefined;
|
|
116
117
|
const reviewedSource:ReviewedSource={packageRevision:manifest.revision,...await toolVersions(),rig:rigPath?{path:rigPath,sha256:fileHashes.get(rigPath)!}:null,motion:motionPath?{path:motionPath,sha256:fileHashes.get(motionPath)!}:null,...(handoff?{handoff:handoff.record}:{})};
|
|
117
|
-
const feedbackCatalog:FeedbackCatalog={rig:manifest.id,bones:modelNames(modelMetadata,'bones'),slots:modelNames(modelMetadata,'slots'),animations:modelAnimations(modelMetadata),clock:report?.clock??null,clockAnimation:report?.animation??null};
|
|
118
|
+
const feedbackCatalog:FeedbackCatalog={rig:manifest.id,bones:modelNames(modelMetadata,'bones'),slots:modelNames(modelMetadata,'slots'),animations:modelAnimations(modelMetadata),clock:report?.clock??null,clockAnimation:report?.animation??null,...(discoverTurnDials(modelMetadata).length?{turnDials:discoverTurnDials(modelMetadata)}:{})};
|
|
118
119
|
return {root,manifestPath,manifest,sourceHash:hash.digest('hex'),reviewedSource,files,report,feedbackCatalog,handoff,handoffReason,...(handoff?.flags?{handoffInputs:handoff.flags}:{}),...(buildRoot?{buildRoot}:{})};
|
|
119
120
|
}
|
|
120
121
|
|
package/src/producer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The installed producer (`@rig-c/parts`' `rig-parts`) and the compiler it gates with (
|
|
2
|
+
* The installed producer (`@rig-c/parts`' `rig-parts`) and the compiler it gates with (`@rig-c/core`'s `rigc`, or legacy `rig-c`), resolved the
|
|
3
3
|
* way `rigc review` resolves `rig-review`: the package's own `package.json` under the nearest `node_modules` walking
|
|
4
4
|
* up from each start directory, its `bin` entry read from that manifest. No PATH lookup, no install, no prompt.
|
|
5
5
|
* Review builds nothing itself: it runs these binaries as child processes with arguments it composes from a typed
|
|
@@ -10,7 +10,8 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
10
10
|
|
|
11
11
|
export const PRODUCER_PACKAGE = '@rig-c/parts';
|
|
12
12
|
export const PRODUCER_BIN = 'rig-parts';
|
|
13
|
-
export const COMPILER_PACKAGE = 'rig-c';
|
|
13
|
+
export const COMPILER_PACKAGE = '@rig-c/core';
|
|
14
|
+
const LEGACY_COMPILER_PACKAGE = 'rig-c';
|
|
14
15
|
export const COMPILER_BIN = 'rigc';
|
|
15
16
|
/** The first `@rig-c/parts` release with `rig-parts remesh` (its CHANGELOG: 1.40.0, "remesh: re-fit one part of a gated rig"). */
|
|
16
17
|
export const REMESH_SINCE = '1.40.0';
|
|
@@ -39,6 +40,11 @@ export function findPackage(name: string, starts: readonly string[], exists: (pa
|
|
|
39
40
|
return null;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
/** Prefer the canonical package across the search roots; use the legacy name only when it is absent. */
|
|
44
|
+
function findCompiler(starts: readonly string[], exists: (path: string) => boolean): string | null {
|
|
45
|
+
return findPackage(COMPILER_PACKAGE, starts, exists) ?? findPackage(LEGACY_COMPILER_PACKAGE, starts, exists);
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
/** `X.Y.Z` → [X, Y, Z]; anything else is null (a pre-release tag is not compared, it is refused as unread). */
|
|
43
49
|
export function parseVersion(text: unknown): [number, number, number] | null {
|
|
44
50
|
const match = typeof text === 'string' ? /^(\d+)\.(\d+)\.(\d+)$/.exec(text.trim()) : null;
|
|
@@ -72,7 +78,7 @@ export function resolveProducer(starts: readonly string[], exists: (path: string
|
|
|
72
78
|
const producer = readTool(manifest, PRODUCER_BIN);
|
|
73
79
|
if ('error' in producer) return { ok: false, error: `${MODELING_PRODUCER_NOT_INSTALLED} (${producer.error})` };
|
|
74
80
|
if (!versionAtLeast(producer.version, REMESH_SINCE)) return { ok: false, error: modelingProducerTooOld(producer.version || 'unknown', REMESH_SINCE) };
|
|
75
|
-
const compilerManifest =
|
|
81
|
+
const compilerManifest = findCompiler([producer.root], exists);
|
|
76
82
|
if (!compilerManifest) return { ok: false, error: MODELING_COMPILER_NOT_INSTALLED };
|
|
77
83
|
const compiler = readTool(compilerManifest, COMPILER_BIN);
|
|
78
84
|
if ('error' in compiler) return { ok: false, error: `${MODELING_COMPILER_NOT_INSTALLED} (${compiler.error})` };
|
|
@@ -129,7 +135,7 @@ export const readingsCompilerTooOld = (found: string, required: string) => `READ
|
|
|
129
135
|
export function resolveCompiler(starts: readonly string[], since: string, exists: (path: string) => boolean = existsSync): { ok: true; compiler: InstalledTool } | { ok: false; error: string } {
|
|
130
136
|
const pair = resolveProducer(starts, exists);
|
|
131
137
|
let compiler: InstalledTool | { error: string } | null = pair.ok ? pair.compiler : null;
|
|
132
|
-
if (!compiler) { const manifest =
|
|
138
|
+
if (!compiler) { const manifest = findCompiler(starts, exists); compiler = manifest ? readTool(manifest, COMPILER_BIN) : null; }
|
|
133
139
|
if (!compiler) return { ok: false, error: READINGS_COMPILER_NOT_INSTALLED };
|
|
134
140
|
if ('error' in compiler) return { ok: false, error: `${READINGS_COMPILER_NOT_INSTALLED} (${compiler.error})` };
|
|
135
141
|
if (!versionAtLeast(compiler.version, since)) return { ok: false, error: readingsCompilerTooOld(compiler.version || 'unknown', since) };
|
|
@@ -150,7 +156,7 @@ export function resolvePose(starts: readonly string[], exists: (path: string) =>
|
|
|
150
156
|
if (!manifest) return { ok: false, error: RETARGET_PRODUCER_NOT_INSTALLED };
|
|
151
157
|
const producer = readTool(manifest, POSE_BIN);
|
|
152
158
|
if ('error' in producer) return { ok: false, error: `${RETARGET_PRODUCER_NOT_INSTALLED} (${producer.error})` };
|
|
153
|
-
const compilerManifest =
|
|
159
|
+
const compilerManifest = findCompiler([producer.root], exists);
|
|
154
160
|
if (!compilerManifest) return { ok: false, error: RETARGET_COMPILER_NOT_INSTALLED };
|
|
155
161
|
const compiler = readTool(compilerManifest, COMPILER_BIN);
|
|
156
162
|
if ('error' in compiler) return { ok: false, error: `${RETARGET_COMPILER_NOT_INSTALLED} (${compiler.error})` };
|
package/src/session.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {TurnState,type TurnDial} from './turn-pad.ts';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { join } from 'node:path';
|
|
@@ -24,7 +25,7 @@ export function parseFeedback(item:unknown,strict=true):Feedback {
|
|
|
24
25
|
export interface FeedbackWarning {id:string;error:string}
|
|
25
26
|
|
|
26
27
|
/** Derive the annotated interval, retaining inspection context as separate data. */
|
|
27
|
-
export function validatePackageFeedback(feedback:Feedback,p:ReviewPackage,animation?:string):Feedback {
|
|
28
|
+
export function validatePackageFeedback(feedback:Feedback,p:ReviewPackage,animation?:string,turnDials:readonly TurnDial[]=p.feedbackCatalog.turnDials??[]):Feedback {
|
|
28
29
|
const context=feedback.context===undefined?{}:object(feedback.context,'feedback.context');
|
|
29
30
|
const interval={frameStart:feedback.frameStart,frameEnd:feedback.frameEnd,inclusive:true};
|
|
30
31
|
if(context.interval!==undefined&&!isDeepStrictEqual(context.interval,interval))throw new Error('feedback.context.interval: must match chosen inclusive frameStart/frameEnd');
|
|
@@ -40,12 +41,13 @@ export function validatePackageFeedback(feedback:Feedback,p:ReviewPackage,animat
|
|
|
40
41
|
if(context.requirements!==undefined)parseRequirementsContext(context.requirements,text=>createHash('sha256').update(text).digest('hex'));
|
|
41
42
|
// The exported environment schedule (#112): absent or null stays valid; a present record carries its bytes and their sha256, recomputed here.
|
|
42
43
|
if(context.environmentSchedule!==undefined)parseScheduleContext(context.environmentSchedule,text=>createHash('sha256').update(text).digest('hex'));
|
|
44
|
+
if(context.turn!==undefined){if(!turnDials.length)throw new Error('TURN_UNAVAILABLE: this inspected model has no supported turn dials');new TurnState(turnDials).validate(context.turn);}
|
|
43
45
|
const selected=animation??(typeof context.animation==='string'?context.animation:null);
|
|
44
46
|
validateFeedbackSelection(p.feedbackCatalog,{frameStart:feedback.frameStart,frameEnd:feedback.frameEnd,target:feedback.target,animation:selected});
|
|
45
47
|
return {...feedback,context:{...context,...(animation===undefined?{}:{animation}),interval} as Json};
|
|
46
48
|
}
|
|
47
|
-
export function readPackageFeedback(feedback:Feedback,p:ReviewPackage,animation?:string):{feedback:Feedback;warning?:FeedbackWarning} {
|
|
48
|
-
try{return {feedback:validatePackageFeedback(feedback,p,animation)};}catch(e){
|
|
49
|
+
export function readPackageFeedback(feedback:Feedback,p:ReviewPackage,animation?:string,turnDials?:readonly TurnDial[]):{feedback:Feedback;warning?:FeedbackWarning} {
|
|
50
|
+
try{return {feedback:validatePackageFeedback(feedback,p,animation,turnDials)};}catch(e){
|
|
49
51
|
// Old submitted evidence remains readable. Only add a missing derived
|
|
50
52
|
// interval; contradictory supplied evidence is preserved with its warning.
|
|
51
53
|
const context=feedback.context;const normalized=context===undefined?{interval:{frameStart:feedback.frameStart,frameEnd:feedback.frameEnd,inclusive:true}}:context&&typeof context==='object'&&!Array.isArray(context)&&!('interval' in context)?{...context,interval:{frameStart:feedback.frameStart,frameEnd:feedback.frameEnd,inclusive:true}}:context;
|