@rig-c/core 2.61.0 → 2.62.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
@@ -617,7 +617,7 @@ commands take it and what its default is.
617
617
  | `bench <rung> --candidate <dir>` | one rung of the benchmark ladder |
618
618
  | `guide <RULE>` / `guide §<n>[.<m>[.<k>]] [--all]` | the guide's row for the rule a failure line names — every `FAIL` line, `vote --record` refusal, `ingest` finding and coded compile error ends with ` (see rigc guide <RULE>)`; only legacy uncoded library errors point at `§5.1` — or a section's heading, own prose and subsection index, read from the installed `docs/AUTHORING.md` when it runs. `--all` includes the subsection bodies; a leaf prints its full text either way. Sizes describe full ledger spans. `--all` with a rule is refused, exit 2; an unknown name is refused with the nearest real ones, exit 1 |
619
619
  | `skills install [--dir …] [--copy]` | links every agent skill the package ships into `.agents/skills` (or `--dir`) as relative symlinks, or copies them with `--copy` — where Codex, Gemini CLI and Antigravity look. A second run has nothing to do; an entry already there that is not this command's is refused by name and nothing is written |
620
- | `review <dir> […]` | the human review of a build, handed to the `rig-review` bin of `@rig-c/review` installed beside the package, with every argument untouched and its exit code returned: `rigc review open <dir>` prints a loopback URL to give a person, `check <dir>` validates the folder it would serve, `feedback <dir>` prints what they left, and `rigc review --help` is review's own usage. rigc does not depend on it: without it the command prints `rigc review: @rig-c/review is not installed — npm i -D @rig-c/review` and exits 2 |
620
+ | `review <dir> […]` | the human review of a build, handed to the `rig-review` bin of `@rig-c/review` installed beside the package, with every argument untouched and its exit code returned: `rigc review open <dir>` prints a loopback URL to give a person, `check <dir>` validates the folder it would serve, `feedback <dir>` prints what they left, and `rigc review --help` is review's own usage. rigc does not depend on it: without it the command prints `rigc review: @rig-c/review is not installed — npm i -D @rig-c/review` and exits 2. An empty argument (`""`) is handed over only where the running Bun keeps one when it starts a process — Bun 1.2.0 drops it, measured by a probe rather than by version — and is otherwise refused by name, `rigc review: argument N is empty, and Bun <version>, the Bun running this, drops an empty argument when it starts a process — …`, exit 2 |
621
621
 
622
622
  `diff`, `bonedist`, `check` and `bench` measure against something you were given; the
623
623
  first three work on any reference you have, and `bench` is a repository workflow that needs a clone
package/docs/AUTHORING.md CHANGED
@@ -7382,7 +7382,7 @@ retain the minimal inputs and report that code rather than inventing author data
7382
7382
  | <a id="CE_STOP_LINK_UNDRIVEN"></a>`CE_STOP_LINK_UNDRIVEN` | “&lt;stop.who&gt;: links[&lt;k&gt;] "&lt;link&gt;" is written by no constraint of this rig spec — a stop holds a link something swings (a physics, ik, transform or path constraint in "constraints"), and its copy and clamp run right after that. A physics constraint in the motion spec's table is emitted after every rig constraint, past the clamp; declare it here” | Declare the swinging physics, ik, transform or path constraint in the rig spec's constraints before its stop clamp. |
7383
7383
  | <a id="CE_STOP_LINK_PARENT_CHAIN"></a>`CE_STOP_LINK_PARENT_CHAIN` | “&lt;stop.who&gt;: links[&lt;k&gt;] "&lt;links[k]&gt;" does not hang below "&lt;names.drawn[k - 1]&gt;", the drawn link of links[&lt;k - 1&gt;] — a chain's next link hangs from the drawn link above it, so the clamp that holds that one carries this one; parent it there” | Parent each lower link under the preceding generated drawn link. |
7384
7384
  | <a id="CE_STOP_AGAINST_MOVES_WITH_STOP"></a>`CE_STOP_AGAINST_MOVES_WITH_STOP` | “&lt;stop.who&gt;: "against" "&lt;stop.against&gt;" hangs below "&lt;under&gt;", which the stop moves — the point would swing with the chain it holds off; name the leg” | Name an against bone outside the subtree moved by the stop. |
7385
- | <a id="CE_STOP_LOCAL_BOUND_RANGE"></a>`CE_STOP_LOCAL_BOUND_RANGE` | “&lt;stop.who&gt;: "&lt;field&gt;&lt;end&gt;" &lt;range[end]&gt; puts &lt;what&gt;'s bound at &lt;value&gt;°, past ±&lt;BOUND&gt;° — the clamp reads a local rotation in (-180, 180], so a bound past it is never reached and the range it states is not the one that holds”<br>On the first link it goes on: “. "&lt;stop.links[0]&gt;" hangs at &lt;at.hang&gt;° from its root at [&lt;at.root&gt;], and "&lt;stop.names.point&gt;" ("at" [&lt;stop.at&gt;] on "&lt;stop.against&gt;") sits at [&lt;at.point&gt;] — Spine world, y up, at the setup pose — so the aim from that root to the point lies &lt;off&gt;° off the hang (rest &lt;rest&gt;°): \|rest\| &lt;off&gt;° + "&lt;end&gt;" &lt;e&gt;° is &lt;sum&gt;°”, then one of “, above &lt;BOUND&gt;°. Move "&lt;stop.names.point&gt;" to within &lt;BOUND − e&gt;° (&lt;BOUND&gt;° − "&lt;end&gt;") of the direction "&lt;stop.links[0]&gt;" hangs in from its root, &lt;at.hang&gt;°, or state "&lt;end&gt;" at most &lt;BOUND − off&gt;°”, “, above &lt;BOUND&gt;°, and no point lowers it: \|rest\| is above 0° for every point a stop accepts. State "&lt;end&gt;" below &lt;BOUND&gt;°”, “, below -&lt;BOUND&gt;°. State "&lt;end&gt;" at least &lt;−BOUND − off&gt;°”<br>Here the values named in the message include: “inward”, “outward”. | On the first link the bound is \|rest\| + the end, where rest is the angle from the link's hang to the aim at `<chain>_touch`, both read from the link's root: move `<chain>_touch` (its `at` on `against`) to within 180° − `outward` of the direction the link hangs in from its root — on a link hanging straight down with `outward` at most 90°, any point below the root — or lower `outward` to at most 180° − \|rest\|. A leg that rises from behind the cloth, as on a prone figure, puts a point at its far end above the root; put it at the knee. On a lower link, bring `lower`'s ends within ±180° of the link's own rest bend. |
7385
+ | <a id="CE_STOP_LOCAL_BOUND_RANGE"></a>`CE_STOP_LOCAL_BOUND_RANGE` | “&lt;stop.who&gt;: "&lt;field&gt;&lt;end&gt;" &lt;range[end]&gt; puts &lt;what&gt;'s bound at &lt;value&gt;°, past ±&lt;BOUND&gt;° — the clamp reads a local rotation in (-180, 180], so a bound past it is never reached and the range it states is not the one that holds”<br>On the first link it goes on: “. "&lt;stop.links[0]&gt;" hangs at &lt;at.hang&gt;° from its root at [&lt;at.root&gt;], and "&lt;stop.names.point&gt;" ("at" [&lt;stop.at&gt;] on "&lt;stop.against&gt;") sits at [&lt;at.point&gt;] — Spine world, y up, at the setup pose — so the aim from that root to the point lies &lt;off&gt;° off the hang (rest &lt;rest&gt;°): \|rest\| &lt;off&gt;° + "&lt;end&gt;" &lt;e&gt;° is &lt;sum&gt;°”, then one of “, above &lt;BOUND&gt;°. Move "&lt;stop.names.point&gt;" to within &lt;BOUND − e&gt;° (&lt;BOUND&gt;° − "&lt;end&gt;") of the direction "&lt;stop.links[0]&gt;" hangs in from its root, &lt;at.hang&gt;°, or state "&lt;end&gt;" at most &lt;BOUND − off&gt;°”, “, above &lt;BOUND&gt;°, and no point lowers it: \|rest\| is above 0° for every point a stop accepts. State "&lt;end&gt;" below &lt;BOUND&gt;°”, “, below -&lt;BOUND&gt;°. State "&lt;end&gt;" at least &lt;−BOUND − off&gt;°”<br>Here the values named in the message include: “inward”, “outward”. | On the first link the bound is \|rest\| + the end, where rest is the angle from the link's hang to the aim at `<chain>_touch`, both read from the link's root: move `<chain>_touch` (its `at` on `against`) to within 180° − `outward` of the direction the link hangs in from its root — on a link hanging straight down with `outward` at most 90°, any point below the root — or lower `outward` to at most 180° − \|rest\|. A leg that rises from behind the cloth, as on a prone figure, puts a point at its far end above the root; put it at the knee. On a lower link, bring `lower`'s ends within ±180° of the link's own rest bend. The values you stated (`inward`, `outward`, `at`) are echoed as written; every figure the expansion derived prints to three decimals, a figure past ±180° rounded away from zero so it never reads as on the bound, and a remedy (`at most`, `at least`, `within`) rounded toward zero, so stating the printed value is accepted. |
7386
7386
  | <a id="CE_STOP_DEPENDENCY_ORDER"></a>`CE_STOP_DEPENDENCY_ORDER` | “&lt;who&gt;: "&lt;tip&gt;" hangs below "&lt;name&gt;", which a later stop generates — declare that stop first, so its angles are known when this one is read”<br>Also: “&lt;value&gt;: stop "&lt;value&gt;"” | Declare the stop generating the parent before the stop depending on it. |
7387
7387
  | <a id="CE_STOP_ANCESTOR_MANIFEST_POSITION"></a>`CE_STOP_ANCESTOR_MANIFEST_POSITION` | “&lt;who&gt;: "&lt;name&gt;" takes its place from the manifest ("from"), and a stop's angles are read off the setup pose this rig spec states — state its x, y and rotation”<br>Also: “&lt;value&gt;: stop "&lt;value&gt;"” | State x, y and rotation for the named setup-pose ancestor instead of from. |
7388
7388
  | <a id="CE_STOP_FRAME_PARENT_TRANSFORM"></a>`CE_STOP_FRAME_PARENT_TRANSFORM` | “&lt;who&gt;: "&lt;parent&gt;", where the frame hangs, is scaled unevenly, sheared or reflected at the setup pose — the frame's angles add to its parent's only through a rotation and an even scale, and inward is a turn's sense”<br>Also: “&lt;value&gt;: stop "&lt;value&gt;"” | Use a frame parent with rotation and even positive scale, without shear or reflection. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/core",
3
- "version": "2.61.0",
3
+ "version": "2.62.0",
4
4
  "description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -91,6 +91,7 @@
91
91
  "explain": "bun cli.ts explain",
92
92
  "selftest": "bun selftest.ts",
93
93
  "smoke": "bun scripts/install_smoke.ts",
94
+ "verify:nova": "bun scripts/verify_nova.ts",
94
95
  "typecheck": "bunx tsc --noEmit",
95
96
  "typecheck:runs": "bunx tsc --noEmit -p tsconfig.runs.json",
96
97
  "lint": "bunx eslint .",
@@ -2239,6 +2239,39 @@ function resolveDelegate(name: string): string | null {
2239
2239
  return null;
2240
2240
  }
2241
2241
 
2242
+ /**
2243
+ * Whether the Bun running this hands an empty argument to a process it starts
2244
+ * (issue #1465). Bun 1.2.0 — the declared minimum — does not: `["a", "", "b"]`
2245
+ * reaches the child as `["a", "b"]`, through `spawnSync` and `Bun.spawnSync`
2246
+ * alike and whatever the child is; 1.4.2 hands it over. That is measured here
2247
+ * rather than read off a version number, by starting this Bun twice with
2248
+ * `-e` and comparing how many arguments arrived: `[pad, "x"]` and
2249
+ * `[pad, "", "x"]` differ by one exactly where the empty one survives. The pad
2250
+ * is there because 1.2.0's `-e` also swallows the first argument after the
2251
+ * script; it is the same on both runs, so the difference is the empty one's.
2252
+ */
2253
+ export function spawnKeepsEmptyArguments(): boolean {
2254
+ const count = (args: string[]): string =>
2255
+ spawnSync(process.execPath, ['-e', 'process.stdout.write(String(process.argv.length))', ...args], { encoding: 'utf8' }).stdout;
2256
+ return Number(count(['pad', '', 'x'])) === Number(count(['pad', 'x'])) + 1;
2257
+ }
2258
+
2259
+ /**
2260
+ * The refusal for a delegated command line this Bun cannot hand over whole, or
2261
+ * `null` (issue #1465): an empty argument, under a Bun that drops one, would
2262
+ * reach the package with the others and nothing would say so. `keeps` is asked
2263
+ * only when there is an empty argument, so a command line without one never
2264
+ * starts the probe.
2265
+ */
2266
+ export function emptyArgumentRefusal(pkg: string, raw: readonly string[], keeps: () => boolean, bun: string): string | null {
2267
+ const at = raw.indexOf('');
2268
+ if (at < 0 || keeps()) return null;
2269
+ return (
2270
+ `argument ${at + 1} is empty, and Bun ${bun}, the Bun running this, drops an empty argument when it starts a process — ` +
2271
+ `${pkg} would receive the others without it, and nothing would say so. Give it a value, or run rigc under a Bun that keeps it`
2272
+ );
2273
+ }
2274
+
2242
2275
  /**
2243
2276
  * A delegating command's body (`CommandDoc.delegate`, issue #1457): resolve the
2244
2277
  * package's bin and run it under the Bun running this — the bin is a Bun script
@@ -2261,6 +2294,8 @@ function cmdDelegate(command: string, raw: readonly string[]): void {
2261
2294
  2,
2262
2295
  );
2263
2296
  }
2297
+ const dropped = emptyArgumentRefusal(delegate.package, raw, spawnKeepsEmptyArguments, process.versions.bun ?? 'unknown');
2298
+ if (dropped !== null) throw new DelegateError(dropped, 2);
2264
2299
  const ran = spawnSync(process.execPath, [bin, ...raw], { stdio: 'inherit' });
2265
2300
  // ⚠️ The exit and the signal are read before `error`: Bun 1.2.0 — the declared minimum — sets `error` on every
2266
2301
  // child that exits non-zero, so reading it first refused review's own exit 7 as "could not run". `error` is a
package/src/stop.ts CHANGED
@@ -126,6 +126,37 @@ interface FirstLink {
126
126
  point: readonly [number, number];
127
127
  }
128
128
 
129
+ /**
130
+ * Every number a stop's refusal prints, in one place (issue #1463). How it is
131
+ * printed depends on what the number is to the author:
132
+ *
133
+ * - `stated` — a value the author typed (`inward`, `outward`, `at`), echoed
134
+ * exactly as JavaScript reads it back, so it can be found in the rig spec.
135
+ * It is never rounded: two stated ends 2.60251 and 2.60249 would both print
136
+ * 2.603, and the refusal would read "2.603 is greater than 2.603";
137
+ * - `reading` — a figure the expansion derived (a hang, a world position, a
138
+ * rest angle), at the three decimals the compiler's other degree refusals
139
+ * print, rounded to nearest;
140
+ * - `past` — a figure refused for lying past ±180°, rounded **away from
141
+ * zero**, so it never prints on the bound: 180.0000001 prints 180.001,
142
+ * not 180.000, which would read as inside the range;
143
+ * - `remedy` — a figure the refusal offers the author to state ("at most",
144
+ * "at least", "within"), rounded **toward zero**, so stating the printed
145
+ * value is accepted rather than refused again by the digits dropped.
146
+ *
147
+ * The rule is that rounding never moves a figure across the line the
148
+ * refusal is about: a `past` figure stays past it, a `remedy` stays inside.
149
+ */
150
+ function stopFigure(v: number, how: 'stated' | 'reading' | 'past' | 'remedy'): string {
151
+ if (how === 'stated') return String(v);
152
+ const near = Number(v.toFixed(3));
153
+ let r = near;
154
+ if (how === 'past' && Math.abs(near) < Math.abs(v)) r = near + Math.sign(v) * 0.001;
155
+ if (how === 'remedy' && Math.abs(near) > Math.abs(v)) r = near - Math.sign(v) * 0.001;
156
+ const text = r.toFixed(3);
157
+ return text === '-0.000' ? '0.000' : text;
158
+ }
159
+
129
160
  /** An angle in (-540, 540) brought into (-180, 180] by at most one whole turn. */
130
161
  function halfTurn(deg: number): number {
131
162
  if (deg > 180) return deg - 360;
@@ -216,7 +247,7 @@ export function expandStops(spec: RigSpec, where: string, plant: StopPlant = {})
216
247
  }
217
248
  if (out.inward > out.outward) {
218
249
  throw new CompileError(
219
- `${who}: "${field}inward" ${out.inward} is greater than "${field}outward" ${out.outward} — the range is [inward, outward] in degrees counted positive away from "${against}", and this one holds no angle`, 'CE_STOP_RANGE_REVERSED',
250
+ `${who}: "${field}inward" ${stopFigure(out.inward, 'stated')} is greater than "${field}outward" ${stopFigure(out.outward, 'stated')} — the range is [inward, outward] in degrees counted positive away from "${against}", and this one holds no angle`, 'CE_STOP_RANGE_REVERSED',
220
251
  );
221
252
  }
222
253
  return out;
@@ -350,15 +381,16 @@ export function expandStops(spec: RigSpec, where: string, plant: StopPlant = {})
350
381
  const e = stop[end];
351
382
  const off = Math.abs(rest);
352
383
  const sum = off + e;
384
+ const position = (p: readonly number[], how: 'stated' | 'reading'): string => p.map((v) => stopFigure(v, how)).join(', ');
353
385
  const geometry =
354
- `. "${stop.links[0]}" hangs at ${at.hang}° from its root at [${at.root.join(', ')}], and "${stop.names.point}" ("at" [${stop.at.join(', ')}] on "${stop.against}") ` +
355
- `sits at [${at.point.join(', ')}] — Spine world, y up, at the setup pose — so the aim from that root to the point lies ${off}° off the hang (rest ${rest}°): ` +
356
- `|rest| ${off}° + "${end}" ${e}° is ${sum}°`;
357
- if (sum < -BOUND) return `${geometry}, below -${BOUND}°. State "${end}" at least ${-BOUND - off}°`;
386
+ `. "${stop.links[0]}" hangs at ${stopFigure(at.hang, 'reading')}° from its root at [${position(at.root, 'reading')}], and "${stop.names.point}" ("at" [${position(stop.at, 'stated')}] on "${stop.against}") ` +
387
+ `sits at [${position(at.point, 'reading')}] — Spine world, y up, at the setup pose — so the aim from that root to the point lies ${stopFigure(off, 'reading')}° off the hang (rest ${stopFigure(rest, 'reading')}°): ` +
388
+ `|rest| ${stopFigure(off, 'reading')}° + "${end}" ${stopFigure(e, 'stated')}° is ${stopFigure(sum, 'past')}°`;
389
+ if (sum < -BOUND) return `${geometry}, below -${BOUND}°. State "${end}" at least ${stopFigure(-BOUND - off, 'remedy')}°`;
358
390
  if (e >= BOUND) return `${geometry}, above ${BOUND}°, and no point lowers it: |rest| is above 0° for every point a stop accepts. State "${end}" below ${BOUND}°`;
359
391
  return (
360
- `${geometry}, above ${BOUND}°. Move "${stop.names.point}" to within ${BOUND - e}° (${BOUND}° − "${end}") of the direction "${stop.links[0]}" hangs in from its root, ${at.hang}°, ` +
361
- `or state "${end}" at most ${BOUND - off}°`
392
+ `${geometry}, above ${BOUND}°. Move "${stop.names.point}" to within ${stopFigure(BOUND - e, 'remedy')}° (${BOUND}° − "${end}") of the direction "${stop.links[0]}" hangs in from its root, ${stopFigure(at.hang, 'reading')}°, ` +
393
+ `or state "${end}" at most ${stopFigure(BOUND - off, 'remedy')}°`
362
394
  );
363
395
  };
364
396
  const bounds = (stop: ReadStop, rest: number, sign: number, range: { inward: number; outward: number }, field: string, what: string, first?: FirstLink): [number, number] => {
@@ -368,7 +400,7 @@ export function expandStops(spec: RigSpec, where: string, plant: StopPlant = {})
368
400
  if (Math.abs(value) > BOUND) {
369
401
  const why = first === undefined ? '' : aimed(stop, rest, end, first);
370
402
  throw new CompileError(
371
- `${stop.who}: "${field}${end}" ${range[end]} puts ${what}'s bound at ${value}°, past ±${BOUND}° — the clamp reads a local rotation in (-180, 180], so a bound past it is never reached and the range it states is not the one that holds${why}`, 'CE_STOP_LOCAL_BOUND_RANGE',
403
+ `${stop.who}: "${field}${end}" ${stopFigure(range[end], 'stated')} puts ${what}'s bound at ${stopFigure(value, 'past')}°, past ±${BOUND}° — the clamp reads a local rotation in (-180, 180], so a bound past it is never reached and the range it states is not the one that holds${why}`, 'CE_STOP_LOCAL_BOUND_RANGE',
372
404
  );
373
405
  }
374
406
  }
@@ -422,7 +454,7 @@ export function expandStops(spec: RigSpec, where: string, plant: StopPlant = {})
422
454
  const dx = target[0] - linkFrame.worldX;
423
455
  const dy = target[1] - linkFrame.worldY;
424
456
  if (dx === 0 && dy === 0) {
425
- throw new CompileError(`${who}: "at" [${stop.at.join(', ')}] on "${stop.against}" sits on the root of "${links[0]}" at the setup pose — the frame aims from the root at the point, and there is no direction to a point it stands on`, 'CE_STOP_POINT_AT_ROOT');
457
+ throw new CompileError(`${who}: "at" [${stop.at.map((v) => stopFigure(v, 'stated')).join(', ')}] on "${stop.against}" sits on the root of "${links[0]}" at the setup pose — the frame aims from the root at the point, and there is no direction to a point it stands on`, 'CE_STOP_POINT_AT_ROOT');
426
458
  }
427
459
  const aim = directionDegrees(dx, dy);
428
460
  // Each angle is brought into (-180, 180] by one whole turn at most, so a
@@ -431,7 +463,7 @@ export function expandStops(spec: RigSpec, where: string, plant: StopPlant = {})
431
463
  const rest = halfTurn(halfTurn(linkFrame.worldRotation) - aim);
432
464
  if (rest === 0) {
433
465
  throw new CompileError(
434
- `${who}: "at" [${stop.at.join(', ')}] on "${stop.against}" lies on "${links[0]}"'s own line at the setup pose — inward is the turn from the link toward the point, and a point on the link's line has no side`, 'CE_STOP_POINT_ON_LINK_LINE',
466
+ `${who}: "at" [${stop.at.map((v) => stopFigure(v, 'stated')).join(', ')}] on "${stop.against}" lies on "${links[0]}"'s own line at the setup pose — inward is the turn from the link toward the point, and a point on the link's line has no side`, 'CE_STOP_POINT_ON_LINK_LINE',
435
467
  );
436
468
  }
437
469
  // Inward turns the link toward the aim line: counter-clockwise when the link