spine-rigc 2.47.1 → 2.48.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/docs/FACE.md CHANGED
@@ -246,7 +246,7 @@ face. A cylinder is one number pretending to be a surface, and past a certain
246
246
  density the pretence is what fails.
247
247
 
248
248
  ⭐ **Two things arrive together, and both are generated rather than written.**
249
- [AUTHORING §3.4](AUTHORING.md#grid--a-lattice-over-the-part-window)'s `grid`
249
+ [AUTHORING §3.4](AUTHORING.md#344-grid--a-lattice-over-the-part-window)'s `grid`
250
250
  generator builds the lattice from the column positions alone, and reproduces this
251
251
  example's hand-numbered 25 vertex pairs, 32 triangles and hull walk exactly. That
252
252
  is the prerequisite: a turn needs interior vertices to move, and
@@ -256,7 +256,7 @@ is the prerequisite: a turn needs interior vertices to move, and
256
256
  part's own pixel grid gives every mesh vertex its own `z`, sampled where the
257
257
  vertex actually is, and `yaw`/`pitch` project off it with the same closed form as
258
258
  §1 — only the source of `z` changes. What it buys is stated where it is authored:
259
- [AUTHORING §3.4](AUTHORING.md#depth--give-every-vertex-its-own-z-instead-of-one-cylinder-radius),
259
+ [AUTHORING §3.4](AUTHORING.md#345-depth--give-every-vertex-its-own-z-instead-of-one-cylinder-radius),
260
260
  with the sampling order, the refusals and the one that matters most (a sheet cut
261
261
  to the art covers **none** of a contour mesh's vertices, because they all sit on
262
262
  the silhouette and outside it).
package/docs/INGEST.md CHANGED
@@ -578,8 +578,9 @@ they came from. Leave it there — §2.4 is why.
578
578
 
579
579
  #### Every finding code, and what to do about it
580
580
 
581
- A finding line reads `<gutter> <CODE>: <where> — <detail>`, and the code is the part
582
- that is the same on every run. This table is the whole set: which gutter it prints
581
+ A finding line reads `<gutter> <CODE>: <where> — <detail> (see rigc guide <CODE>)`, and the
582
+ code is the part that is the same on every run; `rigc guide <CODE>` prints its row of the
583
+ table below, and says it read this file. This table is the whole set: which gutter it prints
583
584
  under, whether it changes the exit code, what it means, and what to do about it.
584
585
  **Nothing here refuses the file** — a construct the spec format cannot hold is recorded
585
586
  rather than rejected — so an exit of 1 means *a blocker was recorded*, and both specs
@@ -591,37 +592,37 @@ stage (§2.0's stage box, below).)
591
592
 
592
593
  | code | gutter | exit | what it means | what to do |
593
594
  | --- | --- | --- | --- | --- |
594
- | `ANIMATION_GROUP` | `BLOCK` | 1 | the animation carries a group the motion spec has no home for. The detail names the ten it does carry. `drawOrderFolder` is the group to know about: the runtime reads it and builds a timeline from it, and no export in this corpus carries one | transcribe that group by hand (§2), or accept that the rebuild does not carry it |
595
- | `ATTACHMENT_<TYPE>` | `BLOCK` | 1 | an attachment of a type rigc does not emit; the code is composed from the type, so on the one type left it reads `ATTACHMENT_POINT`. rigc emits region, mesh, linkedmesh, boundingbox, clipping and path, and `point` is the one deferred type | the rebuild will not have that attachment at all. `docs/SPEC_COVERAGE.md` part 1-6 says what a deferred type would carry |
596
- | `ATTACHMENT_LINK_GEOMETRY` | `LOSS` | 0 | a **linked mesh** that also states `uvs`, `triangles`, `vertices`, `hull` or `edges`. The parser returns from the `source` branch before `readVertices` (`SkeletonJson.ts:582-586`), so those keys are read by nothing at all and the attachment draws the geometry its `source` names; the rig spec has no home for them either, because `build` refuses geometry on a link by name. The detail lists the keys and the source | nothing. The rebuild is the mesh the runtime was already drawing — and if those keys were the geometry you meant, take `source` off and author it as a mesh of its own. `A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN` is the same fact at the gate |
597
- | `ATTACHMENT_SEQUENCE` | `BLOCK` | 1 | a `sequence` block — a numbered image series — that the rig spec cannot say **as written**. A well-formed block on a region, mesh or linked mesh is carried field for field, with no `image` on the loose route (the frames `<path><number>` are the art), and a `sequence` timeline with it; what is left here is a block the parser reads into a series other than the one written — no `count` (0 regions), a `setup` past the end (clamped), a fraction — or one on a `boundingbox`, `clipping` or `path`, where the parser never reads it. The detail quotes the block and says which | the rebuild draws the single region the attachment names. Fix the block in the source — a `count` is the usual one — and ingest again |
598
- | `ATTACHMENT_TIMELINE` | `BLOCK` | 1 | an attachment timeline that is neither `deform` nor `sequence`. Both are carried, and `readAnimation` tests an attachment timeline for exactly those two names and ignores anything else (`SkeletonJson.js:1147-1201`) — so what reaches this line is a name outside the format, which no player plays either | fix the timeline's name in the source, or accept that the rebuild does not carry it |
599
- | `BONE_FIELD` | `BLOCK` | 1 | a bone field with no rig-spec field, so it is dropped. A 4.0/4.1 export spelling `transform` where 4.3 spells `inherit` lands here; so does a misspelling | check the name against AUTHORING §3 first — a typo and an unsupported field read exactly the same |
600
- | `BONE_TIMELINE` | `BLOCK` | 1 | a bone timeline the motion spec has no track for. The detail names the eleven it has, read off the table. Every bone timeline the runtime plays has a track, `inherit` included — the eleventh case of the runtime's own bone switch, a stepped mode per key — so this is reachable only for a name the **parser** throws on too (`Invalid timeline type for a bone`), the position `PHYSICS_TIMELINE` is in | check the spelling; there is no bone timeline left for the rebuild to be missing |
601
- | `CONSTRAINT_FIELD` | `BLOCK` | 1 | as `BONE_FIELD`, on a constraint, with its type named beside it | as `BONE_FIELD` |
602
- | `CONSTRAINT_KEY_RESTATED` | `LOSS` | 0 | an `ik` or `transform` track whose keys do not all state the same fields. The motion spec takes one field set per track, so a field **any** key states is written on **every** key of the spec at the value the parser would have read there — and the line is printed only for a value the **file** will still carry. The emitter leaves a value out wherever it is the one the parser reads without it ([AUTHORING §10.6c](AUTHORING.md)), so a restated default is the source's own text again and says nothing; what is left is a value the table has no row for, or a transform key's `mixY` the source left out beside a `mixX` that is not 1, which the emitter keeps because that is where the editor writes it (§10.6c's *only at 1*). Measured on the twelve exports: 0 tracks print it | nothing. Same values, and where this prints, a larger file — the rebuild plays what the source plays |
603
- | `CONSUMER_DRIVEN_MIX` | `JUDGE` | 0 | an `ik` or `transform` constraint resting at 0 on every mix it reads — an ik's `mix`; a transform's mixes for the `to` properties it declares — that no animation keys above 0, reading every key the way `A47`/`A48` do: an omitted mix is the parser's 1 and a Bezier handle above 0 lifts a 0 → 0 pair. Nothing in the file ever switches it on, and the file cannot say whether that is a leftover or a mix a game sets from code, so the rig spec **declares** it in `invariants.consumerDrivenMix` and the rebuild's gate SKIPs it by name. It is a judgement for `DURATION`'s reason: a statement the skeleton does not carry, made and printed — and not a `LOSS`, because the rebuilt skeleton is the source's bytes. A transform that declares no `to` at all is not a candidate: `A48` refuses it with its own sentence, which no declaration answers | nothing, if a game drives that mix. If it is a leftover, delete the entry and rest a mix it reads above 0 — or remove the constraint — and the gate measures it again |
604
- | `CONSTRAINT_TYPE` | `BLOCK` | 1 | a constraint whose `type` is none rigc knows, so the whole constraint is dropped rather than approximated | the rebuild has no such constraint; check the spelling before assuming the type is unsupported |
605
- | `DURATION` | `JUDGE` | 0 | skeleton JSON has no duration field at all. The largest key time is used, which is what a runtime plays to — and wrong for an animation that holds its last pose past its last key | if you know the real number, edit `duration` in the motion spec. It costs nothing: the declared duration is checked against the compiled keys |
606
- | `GENERATION_UNKNOWN` | `BLOCK` | 1 | `skeleton.spine` names no generation rigc knows, or the header states none at all. A version is read as its LEADING `major.minor` token — a down-export writes `4.0-from-4.1.24`, which is 4.0 data from a 4.1 editor — and it is never rounded to the nearest generation: rounding `3.8.99` up hands 3.8 data to a 4.2 runtime, and it poses as NaN | check the string against the file you were handed. A real generation rigc does not list is worth reporting, with the string beside it |
607
- | `GENERATION_UNSUPPORTED` | `BLOCK` | 1 | the file is Spine data from another generation and this reader reads 4.3. The detail names the generation, the string it was read from, and what a 4.3 reader loses on **this** file: constraints parked in the top-level `ik` / `transform` / `path` / `physics` / `slider` arrays 4.3 folded into `constraints` and this reader never opens, bones carrying `transform` where 4.3 spells `inherit`, and physics constraints omitting `inertia` / `damping`, whose default is not the same number in 4.2 as in 4.3 | re-export the file as 4.3 from an editor of its own generation, or transcribe it by hand (§2). Reading it with **that generation's** defaults is not in this tool |
608
- | `HEADER_BOOKKEEPING` | `LOSS` | 0 | a header field the editor writes and the rig spec has no home for — `hash`, the editor's project hash, which is a value about a file rigc did not write. Dropped, and nothing reads it back. `audio` is not on this line: the rig spec states it ([AUTHORING §3.1](AUTHORING.md)) and `ingest` carries it, `null` included | nothing. It is one of the two declared exceptions §2.3's pass line is stated apart from |
609
- | `HEADER_ORIGIN` | `LOSS` | 0 | the source declares an extent and omits `x`/`y`. Inside a declared extent an omitted origin **is** 0, so the spec states it — and the rebuild then spells two fields the source did not | nothing. Same box, different bytes — which is why byte identity is not the claim for an export that takes this branch |
610
- | `HEADER_REDERIVED` | `LOSS` | 0 | `skeleton.spine`: the rebuild writes the version of the runtime rigc links. The line says whether that is the same string the source states | nothing — but read the line: a 4.2 export rebuilds as 4.3 in that one field, and a source from another generation raises `GENERATION_UNSUPPORTED` beside it, which is the blocker about the DATA rather than about the string |
611
- | `IK_KEY_FIELD` | `BLOCK` | 1 | a key field on an `ik` timeline that is not part of its shape | check the spelling; an unknown field is dropped from the rebuilt track |
612
- | `NO_STAGE` | `BLOCK` `JUDGE` | 1 | the skeleton declares no stage, and one of two things follows. A **judgement** — exit 0 — when `--stage x,y,w,h` supplied a box, because nothing measured the box you gave it. A **blocker** when the header states **half** a stage — an origin with no extent, or one extent without the other — which the rig spec cannot hold; the detail names the fields it states. A header with **none** of the four is not a finding at all: it is carried as `"width": null, "height": null` and rebuilds byte for byte | for the judgement, nothing if the box came from the project the file came from. For the blocker, supply the box with `--stage`, or take the stray field(s) out of the source and the absence is carried. It cannot be derived: posing the rig gives the animated extent, which is a different number |
613
- | `PATH_TIMELINE` | `BLOCK` | 1 | a path-constraint timeline the motion spec has no track for — it carries position, spacing and mix | transcribe it, or accept that the rebuild plays nothing there |
614
- | `PHYSICS_DRIVES_NOTHING` | `LOSS` | 0 | a physics constraint none of whose `x`, `y`, `rotate`, `scaleX`, `shearX` is above 0 — absent, or stated at 0 or below. `PhysicsConstraint.update` applies a component only above 0 (`PhysicsConstraint.js:112`), so it moves no bone, and `build` refuses exactly that shape by name at `A23_PHYSICS_CONSTRAINT_EFFECTIVE`. The rig spec **omits** it, together with every timeline keyed to it (a track naming it would be an unknown constraint to the rebuild, refused at compile) and its place on any skin's `physics` list; the detail names each, and the values it did state. Measured on a generated rig through spine-core, posing the source with and without such a constraint differs by **0** on every bone world value — and by at most 9e-8 when it sits on the root, which is the runtime's `modifyWorld` recomputing a local transform it had no reason to, not a component. ⚠️ **One thing does move:** a duration is the last key an animation has left, so an omitted timeline that held the last key shortens the rebuilt animation, and the detail says which animation and both lengths | nothing, if it was meant to do nothing. If it was meant to jiggle, the file never said so: give it the component it should drive and it is carried like any other. Where the detail names a shortened animation and the length matters to whatever loops it, key something at the length it had |
615
- | `PHYSICS_GLOBAL_REACHES_NOTHING` | `LOSS` | 0 | a physics timeline keyed under the **empty** name — the one that names no constraint, which the runtime applies to every physics constraint declaring that property global (`"strengthGlobal": true` for `strength`; `reset` resets every physics constraint and asks no flag) — in a file where no physics constraint the rebuild carries declares it. The motion spec spells that timeline `"physics": "*"` and `build` refuses one that reaches nobody by name, so the rig spec **omits** it: in the source it walked every constraint and wrote into none. A constraint `PHYSICS_DRIVES_NOTHING` omitted counts as not carried — it was the only thing such a timeline could reach, and it moved no bone. ⚠️ As with that row, a duration is the last key an animation has left, so an omitted timeline that held the last key shortens the rebuilt animation and the detail says both lengths. An unnamed timeline that **does** reach a constraint is not a finding at all: it is carried as `"*"` and rebuilt under the empty name byte for byte | nothing, if it was meant to do nothing. If it was meant to drive the constraints, the file never said which: set `"<property>Global": true` on them in the rig spec and key it as `"physics": "*"` |
616
- | `PHYSICS_TIMELINE` | `BLOCK` | 1 | the same for a physics constraint, whose eight the motion spec carries in full — so this is reachable only for a name the **parser** falls through too | as `PATH_TIMELINE` |
617
- | `SLIDER_TIMELINE` | `BLOCK` | 1 | the same for a slider, which carries time and mix | as `PATH_TIMELINE` |
618
- | `SLOT_FIELD` | `BLOCK` | 1 | as `BONE_FIELD`, on a slot | as `BONE_FIELD` |
619
- | `SLOT_TIMELINE` | `BLOCK` | 1 | a slot timeline the motion spec has no track for. The spec has a track for all six the format has, so what still reaches this line is a name **outside** the format — `sequence` written on a slot rather than an attachment is the likeliest — and the detail says so: the runtime's own reader throws `Invalid timeline type for a slot` on it, so no player loads that file either. The detail names the tracks the spec does have and, when the format has any it lacks, those too, **both read off the tables**. `rgb` and `alpha` are carried under their own names and on their own key times, never folded into one `rgba` — that would state each channel at the other's key times, a value nobody keyed | fix the timeline's name, or accept that the rebuild plays nothing there |
620
- | `SPEC_REFUSED` | `BLOCK` | 1 | the specs were written and **rigc's own parser refuses one of them** — the detail carries that refusal word for word, after the file and the spec it is about. It is the one finding that is not about a single construct: it is whatever `parseRigSpec` or `parseMotionSpec` names, from a shape the format holds and the spec cannot say (a constraint that is `skinRequired` under no skin) to a defect in this decompiler | read the quoted sentence against the skeleton: it names the object. Both specs are on disk for exactly that, and `build` will refuse them until the shape has a spelling — [AUTHORING §5.1](AUTHORING.md) is the list of what a parser says |
621
- | `STAGE_BOX_REWRITTEN` | `LOSS` | 0 | under `--stage-box <slot>`: the bounding box read as the stage states its corners in another order than `build` writes them (bottom-left first, counter-clockwise), or a `color` — the editor writes one on every box it exports. The rebuilt spec asks for the box (`skeleton.stageBox`), and `build` writes it from the stage, so the rebuild carries the same rectangle spelled `build`'s way. The detail names what changes | nothing. The stage is the same four numbers; only the box's spelling moves |
622
- | `TIMELINE_FIELD` | `BLOCK` | 1 | a key field on a bone, path, physics or slider timeline that is not part of that timeline's shape. On an `inherit` key that includes a `curve`: the parser reads `time` and `inherit` there and nothing else, and `build` refuses a curve on that track by name | check the spelling; the field is dropped from the rebuilt key |
623
- | `TIMELINE_KEY_RESTATED` | `LOSS` | 0 | an editor omits a channel that equals the parser's default, and the motion spec's `v` is positional, so the omission is written out at that default **in the spec** — and the line is printed only where the **file** will carry it too. The emitter leaves a channel out wherever it is the one the parser reads without it ([AUTHORING §10.6c](AUTHORING.md)), so on every key kind with a row the rebuild is the source's own text and nothing is said — measured on the twelve exports, 0 tracks print it. What still prints it is a timeline whose keys have no row (`shearx`, `alpha`, path `spacing`, …), and an `inherit` key: one that omits the mode is written as `normal` — the parser's default — and one spelled with a capital first letter (`NoScale`) as the editor's `noScale`, the same mode either way | nothing. The same values the runtime reads, spelled out — a larger file and the same animation |
624
- | `TRANSFORM_KEY_FIELD` | `BLOCK` | 1 | as `IK_KEY_FIELD`, on a `transform` timeline | as `IK_KEY_FIELD` |
595
+ | <a id="ANIMATION_GROUP"></a>`ANIMATION_GROUP` | `BLOCK` | 1 | the animation carries a group the motion spec has no home for. The detail names the ten it does carry. `drawOrderFolder` is the group to know about: the runtime reads it and builds a timeline from it, and no export in this corpus carries one | transcribe that group by hand (§2), or accept that the rebuild does not carry it |
596
+ | <a id="ATTACHMENT_<TYPE>"></a>`ATTACHMENT_<TYPE>` | `BLOCK` | 1 | an attachment of a type rigc does not emit; the code is composed from the type, so on the one type left it reads `ATTACHMENT_POINT`. rigc emits region, mesh, linkedmesh, boundingbox, clipping and path, and `point` is the one deferred type | the rebuild will not have that attachment at all. `docs/SPEC_COVERAGE.md` part 1-6 says what a deferred type would carry |
597
+ | <a id="ATTACHMENT_LINK_GEOMETRY"></a>`ATTACHMENT_LINK_GEOMETRY` | `LOSS` | 0 | a **linked mesh** that also states `uvs`, `triangles`, `vertices`, `hull` or `edges`. The parser returns from the `source` branch before `readVertices` (`SkeletonJson.ts:582-586`), so those keys are read by nothing at all and the attachment draws the geometry its `source` names; the rig spec has no home for them either, because `build` refuses geometry on a link by name. The detail lists the keys and the source | nothing. The rebuild is the mesh the runtime was already drawing — and if those keys were the geometry you meant, take `source` off and author it as a mesh of its own. `A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN` is the same fact at the gate |
598
+ | <a id="ATTACHMENT_SEQUENCE"></a>`ATTACHMENT_SEQUENCE` | `BLOCK` | 1 | a `sequence` block — a numbered image series — that the rig spec cannot say **as written**. A well-formed block on a region, mesh or linked mesh is carried field for field, with no `image` on the loose route (the frames `<path><number>` are the art), and a `sequence` timeline with it; what is left here is a block the parser reads into a series other than the one written — no `count` (0 regions), a `setup` past the end (clamped), a fraction — or one on a `boundingbox`, `clipping` or `path`, where the parser never reads it. The detail quotes the block and says which | the rebuild draws the single region the attachment names. Fix the block in the source — a `count` is the usual one — and ingest again |
599
+ | <a id="ATTACHMENT_TIMELINE"></a>`ATTACHMENT_TIMELINE` | `BLOCK` | 1 | an attachment timeline that is neither `deform` nor `sequence`. Both are carried, and `readAnimation` tests an attachment timeline for exactly those two names and ignores anything else (`SkeletonJson.js:1147-1201`) — so what reaches this line is a name outside the format, which no player plays either | fix the timeline's name in the source, or accept that the rebuild does not carry it |
600
+ | <a id="BONE_FIELD"></a>`BONE_FIELD` | `BLOCK` | 1 | a bone field with no rig-spec field, so it is dropped. A 4.0/4.1 export spelling `transform` where 4.3 spells `inherit` lands here; so does a misspelling | check the name against AUTHORING §3 first — a typo and an unsupported field read exactly the same |
601
+ | <a id="BONE_TIMELINE"></a>`BONE_TIMELINE` | `BLOCK` | 1 | a bone timeline the motion spec has no track for. The detail names the eleven it has, read off the table. Every bone timeline the runtime plays has a track, `inherit` included — the eleventh case of the runtime's own bone switch, a stepped mode per key — so this is reachable only for a name the **parser** throws on too (`Invalid timeline type for a bone`), the position `PHYSICS_TIMELINE` is in | check the spelling; there is no bone timeline left for the rebuild to be missing |
602
+ | <a id="CONSTRAINT_FIELD"></a>`CONSTRAINT_FIELD` | `BLOCK` | 1 | as `BONE_FIELD`, on a constraint, with its type named beside it | as `BONE_FIELD` |
603
+ | <a id="CONSTRAINT_KEY_RESTATED"></a>`CONSTRAINT_KEY_RESTATED` | `LOSS` | 0 | an `ik` or `transform` track whose keys do not all state the same fields. The motion spec takes one field set per track, so a field **any** key states is written on **every** key of the spec at the value the parser would have read there — and the line is printed only for a value the **file** will still carry. The emitter leaves a value out wherever it is the one the parser reads without it ([AUTHORING §10.6c](AUTHORING.md)), so a restated default is the source's own text again and says nothing; what is left is a value the table has no row for, or a transform key's `mixY` the source left out beside a `mixX` that is not 1, which the emitter keeps because that is where the editor writes it (§10.6c's *only at 1*). Measured on the twelve exports: 0 tracks print it | nothing. Same values, and where this prints, a larger file — the rebuild plays what the source plays |
604
+ | <a id="CONSUMER_DRIVEN_MIX"></a>`CONSUMER_DRIVEN_MIX` | `JUDGE` | 0 | an `ik` or `transform` constraint resting at 0 on every mix it reads — an ik's `mix`; a transform's mixes for the `to` properties it declares — that no animation keys above 0, reading every key the way `A47`/`A48` do: an omitted mix is the parser's 1 and a Bezier handle above 0 lifts a 0 → 0 pair. Nothing in the file ever switches it on, and the file cannot say whether that is a leftover or a mix a game sets from code, so the rig spec **declares** it in `invariants.consumerDrivenMix` and the rebuild's gate SKIPs it by name. It is a judgement for `DURATION`'s reason: a statement the skeleton does not carry, made and printed — and not a `LOSS`, because the rebuilt skeleton is the source's bytes. A transform that declares no `to` at all is not a candidate: `A48` refuses it with its own sentence, which no declaration answers | nothing, if a game drives that mix. If it is a leftover, delete the entry and rest a mix it reads above 0 — or remove the constraint — and the gate measures it again |
605
+ | <a id="CONSTRAINT_TYPE"></a>`CONSTRAINT_TYPE` | `BLOCK` | 1 | a constraint whose `type` is none rigc knows, so the whole constraint is dropped rather than approximated | the rebuild has no such constraint; check the spelling before assuming the type is unsupported |
606
+ | <a id="DURATION"></a>`DURATION` | `JUDGE` | 0 | skeleton JSON has no duration field at all. The largest key time is used, which is what a runtime plays to — and wrong for an animation that holds its last pose past its last key | if you know the real number, edit `duration` in the motion spec. It costs nothing: the declared duration is checked against the compiled keys |
607
+ | <a id="GENERATION_UNKNOWN"></a>`GENERATION_UNKNOWN` | `BLOCK` | 1 | `skeleton.spine` names no generation rigc knows, or the header states none at all. A version is read as its LEADING `major.minor` token — a down-export writes `4.0-from-4.1.24`, which is 4.0 data from a 4.1 editor — and it is never rounded to the nearest generation: rounding `3.8.99` up hands 3.8 data to a 4.2 runtime, and it poses as NaN | check the string against the file you were handed. A real generation rigc does not list is worth reporting, with the string beside it |
608
+ | <a id="GENERATION_UNSUPPORTED"></a>`GENERATION_UNSUPPORTED` | `BLOCK` | 1 | the file is Spine data from another generation and this reader reads 4.3. The detail names the generation, the string it was read from, and what a 4.3 reader loses on **this** file: constraints parked in the top-level `ik` / `transform` / `path` / `physics` / `slider` arrays 4.3 folded into `constraints` and this reader never opens, bones carrying `transform` where 4.3 spells `inherit`, and physics constraints omitting `inertia` / `damping`, whose default is not the same number in 4.2 as in 4.3 | re-export the file as 4.3 from an editor of its own generation, or transcribe it by hand (§2). Reading it with **that generation's** defaults is not in this tool |
609
+ | <a id="HEADER_BOOKKEEPING"></a>`HEADER_BOOKKEEPING` | `LOSS` | 0 | a header field the editor writes and the rig spec has no home for — `hash`, the editor's project hash, which is a value about a file rigc did not write. Dropped, and nothing reads it back. `audio` is not on this line: the rig spec states it ([AUTHORING §3.1](AUTHORING.md)) and `ingest` carries it, `null` included | nothing. It is one of the two declared exceptions §2.3's pass line is stated apart from |
610
+ | <a id="HEADER_ORIGIN"></a>`HEADER_ORIGIN` | `LOSS` | 0 | the source declares an extent and omits `x`/`y`. Inside a declared extent an omitted origin **is** 0, so the spec states it — and the rebuild then spells two fields the source did not | nothing. Same box, different bytes — which is why byte identity is not the claim for an export that takes this branch |
611
+ | <a id="HEADER_REDERIVED"></a>`HEADER_REDERIVED` | `LOSS` | 0 | `skeleton.spine`: the rebuild writes the version of the runtime rigc links. The line says whether that is the same string the source states | nothing — but read the line: a 4.2 export rebuilds as 4.3 in that one field, and a source from another generation raises `GENERATION_UNSUPPORTED` beside it, which is the blocker about the DATA rather than about the string |
612
+ | <a id="IK_KEY_FIELD"></a>`IK_KEY_FIELD` | `BLOCK` | 1 | a key field on an `ik` timeline that is not part of its shape | check the spelling; an unknown field is dropped from the rebuilt track |
613
+ | <a id="NO_STAGE"></a>`NO_STAGE` | `BLOCK` `JUDGE` | 1 | the skeleton declares no stage, and one of two things follows. A **judgement** — exit 0 — when `--stage x,y,w,h` supplied a box, because nothing measured the box you gave it. A **blocker** when the header states **half** a stage — an origin with no extent, or one extent without the other — which the rig spec cannot hold; the detail names the fields it states. A header with **none** of the four is not a finding at all: it is carried as `"width": null, "height": null` and rebuilds byte for byte | for the judgement, nothing if the box came from the project the file came from. For the blocker, supply the box with `--stage`, or take the stray field(s) out of the source and the absence is carried. It cannot be derived: posing the rig gives the animated extent, which is a different number |
614
+ | <a id="PATH_TIMELINE"></a>`PATH_TIMELINE` | `BLOCK` | 1 | a path-constraint timeline the motion spec has no track for — it carries position, spacing and mix | transcribe it, or accept that the rebuild plays nothing there |
615
+ | <a id="PHYSICS_DRIVES_NOTHING"></a>`PHYSICS_DRIVES_NOTHING` | `LOSS` | 0 | a physics constraint none of whose `x`, `y`, `rotate`, `scaleX`, `shearX` is above 0 — absent, or stated at 0 or below. `PhysicsConstraint.update` applies a component only above 0 (`PhysicsConstraint.js:112`), so it moves no bone, and `build` refuses exactly that shape by name at `A23_PHYSICS_CONSTRAINT_EFFECTIVE`. The rig spec **omits** it, together with every timeline keyed to it (a track naming it would be an unknown constraint to the rebuild, refused at compile) and its place on any skin's `physics` list; the detail names each, and the values it did state. Measured on a generated rig through spine-core, posing the source with and without such a constraint differs by **0** on every bone world value — and by at most 9e-8 when it sits on the root, which is the runtime's `modifyWorld` recomputing a local transform it had no reason to, not a component. ⚠️ **One thing does move:** a duration is the last key an animation has left, so an omitted timeline that held the last key shortens the rebuilt animation, and the detail says which animation and both lengths | nothing, if it was meant to do nothing. If it was meant to jiggle, the file never said so: give it the component it should drive and it is carried like any other. Where the detail names a shortened animation and the length matters to whatever loops it, key something at the length it had |
616
+ | <a id="PHYSICS_GLOBAL_REACHES_NOTHING"></a>`PHYSICS_GLOBAL_REACHES_NOTHING` | `LOSS` | 0 | a physics timeline keyed under the **empty** name — the one that names no constraint, which the runtime applies to every physics constraint declaring that property global (`"strengthGlobal": true` for `strength`; `reset` resets every physics constraint and asks no flag) — in a file where no physics constraint the rebuild carries declares it. The motion spec spells that timeline `"physics": "*"` and `build` refuses one that reaches nobody by name, so the rig spec **omits** it: in the source it walked every constraint and wrote into none. A constraint `PHYSICS_DRIVES_NOTHING` omitted counts as not carried — it was the only thing such a timeline could reach, and it moved no bone. ⚠️ As with that row, a duration is the last key an animation has left, so an omitted timeline that held the last key shortens the rebuilt animation and the detail says both lengths. An unnamed timeline that **does** reach a constraint is not a finding at all: it is carried as `"*"` and rebuilt under the empty name byte for byte | nothing, if it was meant to do nothing. If it was meant to drive the constraints, the file never said which: set `"<property>Global": true` on them in the rig spec and key it as `"physics": "*"` |
617
+ | <a id="PHYSICS_TIMELINE"></a>`PHYSICS_TIMELINE` | `BLOCK` | 1 | the same for a physics constraint, whose eight the motion spec carries in full — so this is reachable only for a name the **parser** falls through too | as `PATH_TIMELINE` |
618
+ | <a id="SLIDER_TIMELINE"></a>`SLIDER_TIMELINE` | `BLOCK` | 1 | the same for a slider, which carries time and mix | as `PATH_TIMELINE` |
619
+ | <a id="SLOT_FIELD"></a>`SLOT_FIELD` | `BLOCK` | 1 | as `BONE_FIELD`, on a slot | as `BONE_FIELD` |
620
+ | <a id="SLOT_TIMELINE"></a>`SLOT_TIMELINE` | `BLOCK` | 1 | a slot timeline the motion spec has no track for. The spec has a track for all six the format has, so what still reaches this line is a name **outside** the format — `sequence` written on a slot rather than an attachment is the likeliest — and the detail says so: the runtime's own reader throws `Invalid timeline type for a slot` on it, so no player loads that file either. The detail names the tracks the spec does have and, when the format has any it lacks, those too, **both read off the tables**. `rgb` and `alpha` are carried under their own names and on their own key times, never folded into one `rgba` — that would state each channel at the other's key times, a value nobody keyed | fix the timeline's name, or accept that the rebuild plays nothing there |
621
+ | <a id="SPEC_REFUSED"></a>`SPEC_REFUSED` | `BLOCK` | 1 | the specs were written and **rigc's own parser refuses one of them** — the detail carries that refusal word for word, after the file and the spec it is about. It is the one finding that is not about a single construct: it is whatever `parseRigSpec` or `parseMotionSpec` names, from a shape the format holds and the spec cannot say (a constraint that is `skinRequired` under no skin) to a defect in this decompiler | read the quoted sentence against the skeleton: it names the object. Both specs are on disk for exactly that, and `build` will refuse them until the shape has a spelling — [AUTHORING §5.1](AUTHORING.md) is the list of what a parser says |
622
+ | <a id="STAGE_BOX_REWRITTEN"></a>`STAGE_BOX_REWRITTEN` | `LOSS` | 0 | under `--stage-box <slot>`: the bounding box read as the stage states its corners in another order than `build` writes them (bottom-left first, counter-clockwise), or a `color` — the editor writes one on every box it exports. The rebuilt spec asks for the box (`skeleton.stageBox`), and `build` writes it from the stage, so the rebuild carries the same rectangle spelled `build`'s way. The detail names what changes | nothing. The stage is the same four numbers; only the box's spelling moves |
623
+ | <a id="TIMELINE_FIELD"></a>`TIMELINE_FIELD` | `BLOCK` | 1 | a key field on a bone, path, physics or slider timeline that is not part of that timeline's shape. On an `inherit` key that includes a `curve`: the parser reads `time` and `inherit` there and nothing else, and `build` refuses a curve on that track by name | check the spelling; the field is dropped from the rebuilt key |
624
+ | <a id="TIMELINE_KEY_RESTATED"></a>`TIMELINE_KEY_RESTATED` | `LOSS` | 0 | an editor omits a channel that equals the parser's default, and the motion spec's `v` is positional, so the omission is written out at that default **in the spec** — and the line is printed only where the **file** will carry it too. The emitter leaves a channel out wherever it is the one the parser reads without it ([AUTHORING §10.6c](AUTHORING.md)), so on every key kind with a row the rebuild is the source's own text and nothing is said — measured on the twelve exports, 0 tracks print it. What still prints it is a timeline whose keys have no row (`shearx`, `alpha`, path `spacing`, …), and an `inherit` key: one that omits the mode is written as `normal` — the parser's default — and one spelled with a capital first letter (`NoScale`) as the editor's `noScale`, the same mode either way | nothing. The same values the runtime reads, spelled out — a larger file and the same animation |
625
+ | <a id="TRANSFORM_KEY_FIELD"></a>`TRANSFORM_KEY_FIELD` | `BLOCK` | 1 | as `IK_KEY_FIELD`, on a `transform` timeline | as `IK_KEY_FIELD` |
625
626
 
626
627
  ⚠️ **An attachment's `name` has no row, because nothing about it is lost.** `ingest`
627
628
  carries a stated `name` verbatim — equal to its key or not, exactly as the source spells
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "2.47.1",
3
+ "version": "2.48.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": {
@@ -14,6 +14,12 @@ the parse itself — and writes **only if every one is green**. You cannot see t
14
14
  rig you are authoring. The validator's messages and the shipped guides are the
15
15
  whole interface, and this skill only says which of them to open.
16
16
 
17
+ 📖 **A failure line ends with the command that prints its row of the guide** —
18
+ ` (see rigc guide <RULE>)`. Run that command first, before reading anything else:
19
+ it prints that rule's row of the shipped guide and nothing else, and `rigc guide §<n>`
20
+ (and `§<n>.<m>`, `§<n>.<m>.<k>`)
21
+ prints one section of `docs/AUTHORING.md` — a compile error points at `§5.1`.
22
+
17
23
  ## Non-negotiables
18
24
 
19
25
  - **Validation is never bypassed.** `build` writes nothing on a red gate; there is
@@ -12,6 +12,12 @@ breathes, blinks, moves its eyes and turns a few degrees off axis. Every rule be
12
12
  is owned by [FACE.md](https://github.com/rig-c/core/blob/main/docs/FACE.md); this file says when to open it and what
13
13
  it will not do for you.
14
14
 
15
+ 📖 **A failure line ends with the command that prints its row of the guide** —
16
+ ` (see rigc guide <RULE>)`. Run that command first, before reading anything else:
17
+ it prints that rule's row of the shipped guide and nothing else, and `rigc guide §<n>`
18
+ (and `§<n>.<m>`, `§<n>.<m>.<k>`)
19
+ prints one section of `docs/AUTHORING.md` — a compile error points at `§5.1`.
20
+
15
21
  ## Non-negotiables
16
22
 
17
23
  - **Validation is never bypassed.** `build` writes nothing on a red gate, and there
@@ -13,6 +13,12 @@ complaint about it, re-express it, or extend it. Every rule below is owned by
13
13
  [INGEST.md](https://github.com/rig-c/core/blob/main/docs/INGEST.md); this file says when to open it and what it will
14
14
  not do for you.
15
15
 
16
+ 📖 **A failure line ends with the command that prints its row of the guide** —
17
+ ` (see rigc guide <RULE>)`. Run that command first, before reading anything else:
18
+ it prints that rule's row of the shipped guide and nothing else, and `rigc guide §<n>`
19
+ (and `§<n>.<m>`, `§<n>.<m>.<k>`)
20
+ prints one section of `docs/AUTHORING.md` — a compile error points at `§5.1`.
21
+
16
22
  ## Non-negotiables
17
23
 
18
24
  - **Validation is never bypassed.** `validate` reads a foreign file as it is, and
@@ -13,6 +13,12 @@ Spine animation somebody would choose coming back. Every rule below is owned by
13
13
  [MOTION.md](https://github.com/rig-c/core/blob/main/docs/MOTION.md); this file says when to open it and what it will
14
14
  not do for you.
15
15
 
16
+ 📖 **A failure line ends with the command that prints its row of the guide** —
17
+ ` (see rigc guide <RULE>)`. Run that command first, before reading anything else:
18
+ it prints that rule's row of the shipped guide and nothing else, and `rigc guide §<n>`
19
+ (and `§<n>.<m>`, `§<n>.<m>.<k>`)
20
+ prints one section of `docs/AUTHORING.md` — a compile error points at `§5.1`.
21
+
16
22
  ## Non-negotiables
17
23
 
18
24
  - **Validation is never bypassed.** `build` writes nothing on a red gate, and there
@@ -12,6 +12,12 @@ PNGs in, a bone hierarchy out. Every rule below is owned by
12
12
  [RIGGING.md](https://github.com/rig-c/core/blob/main/docs/RIGGING.md); this file says when to open it and what it
13
13
  will not do for you.
14
14
 
15
+ 📖 **A failure line ends with the command that prints its row of the guide** —
16
+ ` (see rigc guide <RULE>)`. Run that command first, before reading anything else:
17
+ it prints that rule's row of the shipped guide and nothing else, and `rigc guide §<n>`
18
+ (and `§<n>.<m>`, `§<n>.<m>.<k>`)
19
+ prints one section of `docs/AUTHORING.md` — a compile error points at `§5.1`.
20
+
15
21
  ## Non-negotiables
16
22
 
17
23
  - **Validation is never bypassed.** `build` writes nothing on a red gate, and there
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import type { AssertionProfile } from './kinds.ts';
10
10
  import type { VerdictLists } from './harness.ts';
11
+ import { failLine } from '../guide.ts';
11
12
 
12
13
  export const VALIDATE_PROFILES: readonly AssertionProfile[] = ['spine', 'spine-html'];
13
14
 
@@ -41,7 +42,8 @@ export function reportLines(report: VerdictLists & { profile: AssertionProfile }
41
42
  for (const name of report.passed) lines.push(` PASS ${name}`);
42
43
  for (const s of report.skipped) lines.push(` SKIP ${s.assertion}: ${s.reason}`);
43
44
  for (const p of report.profileSkipped) lines.push(` PROF ${p.assertion}: ${p.kind} rule, not in profile "${report.profile}"`);
44
- for (const f of report.failures) lines.push(` FAIL ${f.assertion}: ${f.detail}`);
45
+ // Each FAIL line ends with the command that prints the rule's row of the guide (issue #1397): `failLine`.
46
+ for (const f of report.failures) lines.push(failLine(f.assertion, f.detail));
45
47
  // How many of them MEASURED anything, which is the figure the rows above do
46
48
  // not hand a reader (issue #568). Counting `PASS` lines answers a different
47
49
  // question — before the sweep that closed #568 a run could print seven of
@@ -26,7 +26,7 @@ import { surveyOfBuild } from '../deformbuild.ts';
26
26
  import type { DeformSurvey } from '../deformsurvey.ts';
27
27
  import { deformReportBlock } from '../deformreport.ts';
28
28
  import { type DiffAnimationPair, diffLines, diffSkeletons } from '../diff.ts';
29
- import { ingest, INGEST_GUTTERS, type IngestFinding, IngestSpecRefused, type IngestStage } from '../ingest.ts';
29
+ import { findingLine, ingest, type IngestFinding, IngestSpecRefused, type IngestStage } from '../ingest.ts';
30
30
  import { PARSER_DEFAULTS, parserReading } from '../keyorder.ts';
31
31
  import { MODEL_DOCUMENT_FILE, modelDocument } from '../model.ts';
32
32
  import { parseMotionSpec } from '../motion.ts';
@@ -37,7 +37,8 @@ import { BACKGROUND, contactSheet, type Frame, FRAMES_SIDECAR, FRAMES_SPEC, type
37
37
  import { type CompileResult } from '../types.ts';
38
38
  import { attachmentRegionJoins } from '../region_joins.ts';
39
39
  import { cmdRepack } from './repack.ts';
40
- import { ATLAS_ABSENT, COMMANDS, type CommandRun, modelTextBeside, resolveBuild, type BuildGate, runBuild, PACKAGE_ROOT, DEFAULT_CHAINFIT_OUT, DEFAULT_POSE_OUT, DEFAULT_SKILLS_DIR, ExplainError, meshBudget, meshDepthNote, meshFit, meshInfluenceNote, PAGE_GRID_UNLOCATED, readAnimationFlag, readJsonFile, readPoserFlag, readSkeletonText, readVersion, resolveCut, resolveDrawable, runCheck, SkillsInstallError, STAGELESS_FRAMING, UsageError, writeJson, documentStageBeside } from './shared.ts';
40
+ import { GUIDE_FILE, GUIDE_RULE_TABLES_ELSEWHERE, guideAnswer, refusalLine } from '../guide.ts';
41
+ import { ATLAS_ABSENT, COMMANDS, type CommandRun, modelTextBeside, resolveBuild, type BuildGate, runBuild, PACKAGE_ROOT, DEFAULT_CHAINFIT_OUT, DEFAULT_POSE_OUT, DEFAULT_SKILLS_DIR, ExplainError, GuideError, meshBudget, meshDepthNote, meshFit, meshInfluenceNote, PAGE_GRID_UNLOCATED, readAnimationFlag, readJsonFile, readPoserFlag, readSkeletonText, readVersion, resolveCut, resolveDrawable, runCheck, SkillsInstallError, STAGELESS_FRAMING, UsageError, writeJson, documentStageBeside } from './shared.ts';
41
42
  import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
42
43
  import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
43
44
 
@@ -1492,12 +1493,9 @@ export function cmdIngest(flags: Record<string, string>, positional: string[]):
1492
1493
  // read past a hundred DURATION lines to find it.
1493
1494
  // The gutter words are `INGEST_GUTTERS`, which is also the column
1494
1495
  // `docs/INGEST.md` §2.0's finding-code table is keyed on (issue #675); the
1495
- // pad to one width is this printer's, so the codes line up.
1496
- const width = Math.max(...Object.values(INGEST_GUTTERS).map((gutter) => gutter.length));
1496
+ // line, its pad and its pointer to the code's row are `findingLine`'s (#1397).
1497
1497
  for (const kind of ['blocker', 'judgement', 'lossy'] as const) {
1498
- for (const finding of result.findings.filter((f) => f.kind === kind)) {
1499
- console.log(` ${INGEST_GUTTERS[kind].padEnd(width)} ${finding.code}: ${finding.where} — ${finding.detail}`);
1500
- }
1498
+ for (const finding of result.findings.filter((f) => f.kind === kind)) console.log(findingLine(finding));
1501
1499
  }
1502
1500
  console.log(`rigc: wrote ${join(outDir, 'rig.json')}`);
1503
1501
  console.log(`rigc: wrote ${join(outDir, 'motion.json')}`);
@@ -1699,6 +1697,36 @@ export function cmdSkills(flags: Record<string, string>, positional: string[]):
1699
1697
  );
1700
1698
  }
1701
1699
 
1700
+ // ---------------------------------------------------------------------------
1701
+ // guide — the row a failure line names, or one section (issue #1397)
1702
+ // ---------------------------------------------------------------------------
1703
+
1704
+ /**
1705
+ * `rigc guide <RULE>` / `rigc guide §<n>[.<m>]`: a slice of the `docs/AUTHORING.md`
1706
+ * this package ships, read now — never a copy (`src/guide.ts` says why). A
1707
+ * finding code's table is read from the shipped document it lives in, and the
1708
+ * answer names that file.
1709
+ */
1710
+ export function cmdGuide(flags: Record<string, string>, positional: string[]): void {
1711
+ const flagged = Object.keys(flags);
1712
+ if (flagged.length > 0) throw new UsageError(`guide takes no flags, and ${flagged.map((f) => `--${f}`).join(', ')} was given — name one rule or one §section`);
1713
+ if (positional.length !== 1) {
1714
+ throw new UsageError(
1715
+ positional.length === 0
1716
+ ? 'guide takes one rule name — the one a failure line names before its colon — or one §<n> / §<n>.<m>'
1717
+ : `guide takes one rule name or one §section, and ${positional.length} were given: ${positional.map((p) => JSON.stringify(p)).join(', ')}`,
1718
+ );
1719
+ }
1720
+ const elsewhere: Record<string, string> = {};
1721
+ for (const table of GUIDE_RULE_TABLES_ELSEWHERE) {
1722
+ const path = join(PACKAGE_ROOT, table.file);
1723
+ if (existsSync(path)) elsewhere[table.file] = readFileSync(path, 'utf8');
1724
+ }
1725
+ const answer = guideAnswer(positional[0], { guide: readFileSync(join(PACKAGE_ROOT, GUIDE_FILE), 'utf8'), elsewhere });
1726
+ if (!answer.ok) throw new GuideError(refusalLine(answer.rule, answer.detail));
1727
+ for (const line of answer.lines) console.log(line);
1728
+ }
1729
+
1702
1730
  // ---------------------------------------------------------------------------
1703
1731
  // build — the second entry's gate (issue #1060, step 4e of #380)
1704
1732
  // ---------------------------------------------------------------------------
@@ -1786,5 +1814,6 @@ export const CORE_COMMAND_RUNS: Readonly<Record<string, CommandRun>> = {
1786
1814
  inspect: ({ flags }) => cmdInspect(flags),
1787
1815
  pose: ({ flags }) => cmdPose(flags),
1788
1816
  chainfit: ({ flags }) => cmdChainFit(flags),
1817
+ guide: ({ flags, positional }) => cmdGuide(flags, positional),
1789
1818
  skills: ({ flags, positional }) => cmdSkills(flags, positional),
1790
1819
  };
package/src/cli/repack.ts CHANGED
@@ -50,7 +50,7 @@
50
50
  import { parseAtlasText } from '../atlas.ts';
51
51
  import { relativeImagesPath } from '../compile.ts';
52
52
  import { CompileError } from '../errors.ts';
53
- import { ingest, INGEST_GUTTERS, IngestError, type IngestFinding, type IngestOptions, IngestSpecRefused, type IngestStage } from '../ingest.ts';
53
+ import { findingLine, ingest, IngestError, type IngestFinding, type IngestOptions, IngestSpecRefused, type IngestStage } from '../ingest.ts';
54
54
  import { MODEL_DOCUMENT_FILE, pagesOfAtlas, spineFileSha256 } from '../model.ts';
55
55
  import { atlasRefusals, documentDisagreements, firstLineApart, type InputPage, keptPageRefusals, liftAtlas, polygonOwnedTexels, regionDifferences, REPACK_WORK_PREFIX, RepackError, skeletonDifferences } from '../repack.ts';
56
56
  import {
@@ -215,10 +215,9 @@ function stageLine(
215
215
 
216
216
  /** The findings `ingest` reported, in its own format, blockers first. */
217
217
  function findingLines(findings: readonly IngestFinding[]): string[] {
218
- const width = Math.max(...Object.values(INGEST_GUTTERS).map((gutter) => gutter.length));
219
218
  const lines: string[] = [];
220
219
  for (const kind of ['blocker', 'judgement', 'lossy'] as const) {
221
- for (const f of findings.filter((x) => x.kind === kind)) lines.push(` ${INGEST_GUTTERS[kind].padEnd(width)} ${f.code}: ${f.where} — ${f.detail}`);
220
+ for (const f of findings.filter((x) => x.kind === kind)) lines.push(findingLine(f));
222
221
  }
223
222
  return lines;
224
223
  }
package/src/cli/shared.ts CHANGED
@@ -40,6 +40,7 @@ import { FOLDS_FILE } from '../folds.ts';
40
40
  import { parseJsonWithPosition } from '../json-position.ts';
41
41
  import { RUNG_IDS } from '../ladder.ts';
42
42
  import { MODEL_DOCUMENT_FILE, MODEL_DOCUMENT_SPEC, modelDocument, spineFileSha256 } from '../model.ts';
43
+ import { compileErrorLine } from '../guide.ts';
43
44
  import { PACKAGE_ROOT, readPackageMeta, readVersion } from '../package_meta.ts';
44
45
  import { DEFAULT_MAX_RESIDUAL, DEFAULT_SCALE_MAX, DEFAULT_SCALE_MIN } from '../pose.ts';
45
46
  import {
@@ -942,6 +943,9 @@ export const DEFAULT_SKILLS_DIR = '.agents/skills';
942
943
  /** An install refused before its first write — nothing to install, or an entry in the way. Exit 1. */
943
944
  export class SkillsInstallError extends Error {}
944
945
 
946
+ /** `guide` asked for a rule or a section the guide does not have (issue #1397): a refusal by `GUIDE_RULE_KNOWN` or `GUIDE_SECTION_KNOWN`, whose message is the whole line. Exit 1. */
947
+ export class GuideError extends Error {}
948
+
945
949
  // ---------------------------------------------------------------------------
946
950
  // usage / per-command help
947
951
  // ---------------------------------------------------------------------------
@@ -1743,6 +1747,23 @@ export const COMMANDS: CommandDoc[] = [
1743
1747
  },
1744
1748
  },
1745
1749
  },
1750
+ {
1751
+ name: 'guide',
1752
+ runtime: false,
1753
+ spineFormat: false,
1754
+ usage: [
1755
+ 'rigc guide <RULE> (the guide\'s row for the rule a failure line names — the line ends with this command)',
1756
+ 'rigc guide §<n> | §<n>.<m> | §<n>.<m>.<k> (one section of docs/AUTHORING.md: "## <n>.", the m-th ### under it, the k-th #### under that)',
1757
+ ],
1758
+ flags: [],
1759
+ notes: [
1760
+ 'guide reads the docs/AUTHORING.md this command ships with, at the moment it runs, and',
1761
+ 'prints a slice of it — the row(s) whose first cell names the rule, each under the',
1762
+ 'file, section and heading it sits in, or one section as it stands. An ingest finding',
1763
+ 'code\'s row lives in docs/INGEST.md\'s finding-code table, and its answer names that',
1764
+ 'file. A name the guide does not have is refused with the nearest real names, exit 1.',
1765
+ ],
1766
+ },
1746
1767
  {
1747
1768
  name: 'skills',
1748
1769
  runtime: false,
@@ -2758,7 +2779,8 @@ function refuse(err: unknown, command: string | undefined, USAGE: string, entry:
2758
2779
  process.exit(2);
2759
2780
  }
2760
2781
  if (err instanceof CompileError) {
2761
- console.error(`rigc compile error: ${err.message}`);
2782
+ // Ends with ` (see rigc guide §5.1)` (issue #1397): a compile error names no rule, so it points at the section.
2783
+ console.error(compileErrorLine(err.message));
2762
2784
  // The drops the compile recorded before it stopped, in the same line the
2763
2785
  // green build prints (issue #671). They were reported from the compile
2764
2786
  // RESULT alone, so the run that failed BECAUSE a file was missing was the
@@ -2890,5 +2912,12 @@ function refuse(err: unknown, command: string | undefined, USAGE: string, entry:
2890
2912
  console.error(`rigc skills install: ${err.message}`);
2891
2913
  process.exit(1);
2892
2914
  }
2915
+ // A rule or section the guide does not have (issue #1397): exit 1 like a lookup that found nothing, and the
2916
+ // message names what was asked and the nearest real names — the usage under it would bury them.
2917
+ if (err instanceof GuideError) {
2918
+ // The message is the whole line — `FAIL GUIDE_RULE_KNOWN: …` or `FAIL GUIDE_SECTION_KNOWN: …`, with its pointer.
2919
+ console.error(err.message);
2920
+ process.exit(1);
2921
+ }
2893
2922
  throw err;
2894
2923
  }
@@ -19,6 +19,7 @@ import { buildPreview, buildPreviewPanes, PLAYER_LINE, type PreviewGate, type Pr
19
19
  import { atlasPageNames } from '../render.ts';
20
20
  import { BoneDistError } from '../bonedist.ts';
21
21
  import { assertionCountForProfile, CLI_DEFAULT_PROFILE, reportLines, validate, type ValidateProfile } from '../validate.ts';
22
+ import { failLine } from '../guide.ts';
22
23
  import { type CliRefusal, type CommandRun, DEFAULT_BALLOT, PACKAGE_ROOT, DEFAULT_LEDGER, parseJsonNamed, readAnimationFlag, readJsonFile, readPackageMeta, readSkeletonText, readVersion, resolveBuild, resolveCut, resolveViewable, runCheck, spinePairOf, type BuildGate, runBuild, readProfile, STAGELESS_FRAMING, UsageError, writeJson, modelTextBeside } from './shared.ts';
23
24
  import { cmdRepack } from './repack.ts';
24
25
  import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
@@ -457,7 +458,7 @@ function cmdVoteRecord(flags: Record<string, string>): void {
457
458
 
458
459
  const { refusals, line } = verifyResult(manifest, result, { attempts, again });
459
460
  if (line === null) {
460
- for (const refusal of refusals) console.error(` FAIL ${refusal.rule}: ${refusal.detail}`);
461
+ for (const refusal of refusals) console.error(failLine(refusal.rule, refusal.detail));
461
462
  console.error(`rigc: ${refusals.length} refusal(s) — nothing appended to ${ledgerPath}`);
462
463
  process.exit(1);
463
464
  }