CLAUDE.md · git:20261011.1c04a03 · 2026-10-11 · sha256 96de78fb039d2dce
CLAUDE.md git:20261011.1c04a03A
Immutable. This exact content never changes and is served at /api/v1/blob/96de78fb039d2dce.
# CLAUDE.md Guidance for AI-assisted sessions working on this repository. ## What this is rigc compiles a **rig spec** plus a motion spec — and, for a cut with measured art behind it, a cut manifest — into Spine 4.3 skeleton data and a one-part-per-page atlas, then round-trips the result through `@esotericsoftware/spine-core` and 51 named assertions before anything is written. Read [README.md](README.md) for the formats, the CLI and the assertion list; [`src/rig.ts`](src/rig.ts) is the rig spec's own documentation. [docs/AUTHORING.md](docs/AUTHORING.md) is the guide an agent authors *from* — the two input files field by field, the emission rules, the CLI loop, and the map from each named failure to the file that has to change. It is a first-class deliverable, not a summary of this file: the guide and the validator's messages together are the only interface an agent that cannot see the rig actually has. **Anything that changes an input format, an error message or an assertion changes that guide too.** ## The doctrine: a tool for AI, not for people Everything below follows from one observation. An agent authoring a rig cannot see it. Spine's parser accepts a great deal of wrongness in silence — the constraint that vanishes, the NaN curve, the mesh that quietly loses its bone weights — so an agent with only a parser for feedback will report success on a broken rig and be sincere about it. rigc exists to convert that silence into a named failure. - **The validator's messages are the UI.** They are what the agent reads and what it acts on, so a failure detail must name the object, the value found and the value required. `A20_MESH_WEIGHTS_COHERENT: mesh "x" vertex 12 weights sum to 0.9000` is the product. "invalid mesh" is not. - **Everything in a rig spec resolves by name, and a miss is refused by name.** A bone's `parent`, a slot's `bone`, a constraint's `bones` and `target`, a draw-order key's `slot`, and an authored mesh's vertex `weights`. The last of those used to be the exception — weights carried raw indices into the *emitted* bone array, so inserting a bone rebound every vertex of every mesh below it with a green gate and an unmoved `diff` (issue #45, the third example in the paragraph above, reproduced rather than caught). Spine's index encoding is still reachable behind `"boneIndexing": "raw"`, because what it buys is exactly the silence, and that has to be asked for. - **The compiler never invents a value that is not in the spec.** No defaults guessed from the art, no re-measuring of plates, no "reasonable" fallbacks. If a number is missing, that is a `CompileError` naming the field. A compiler that fills in gaps produces rigs nobody can reason about — and it makes the manifest stop being the record of what was measured. - **Emit only after green.** `build` compiles, validates, and writes *only* if every assertion passes. Never reorder that. A wrong file on disk outlives the console output that warned about it. - 🔒 **Validation is not optional — this is a structural invariant, not a default.** There must never be a `--no-validate` or `--emit-anyway` flag, an environment escape, or an exported API that hands back emitted artifacts without the gate having run. **Correctness is the whole of the reason, and it is sufficient by itself.** The gate is one body of assertions with two suppliers: the round trip through `@esotericsoftware/spine-core`, which `cli.ts build` runs wherever the runtime is installed — a clone, CI, an install with it added beside — and rigc's own validator over the model document and the emitted text, which `cli_core.ts build` runs where it is not (the published package, since 2.0.0: `spine-core` is a devDependency). The second supplier is admitted on measurement, not on its word, and the measurement's scope is stated rather than rounded up: of the 51 assertions, the 42 the model side runs are held to the round trip's verdict lines by `verdict_gate`, the 8 restated over the emitted text (A01, A02, A05, A07, A16, A31, A35, A18) by `RC28`, and `A00_ROUNDTRIP_PARSE` — the parse by the official runtime — does not run on the core build and is reported as a SKIP that says so — in its place the model side runs its own two parse rules, `A00_MODEL_READ` and `A00_MODEL_REGIONS_ON_PAGES`, outside the registry and held by `verdict_gate` to accept and refuse what A00 does, so that report counts 44 rules on the model side and totals 53 on the core entry, each with a row in AUTHORING §5.2. `core_gate` holds rigc's poser to spine-core's poses at tolerance 0. CI runs all of that on every change over the public recipes and every selftest call; the 14 production rigs are measured by hand with private recipes CI does not have, before each landing — a practice, not a gate CI enforces. A bypass of either supplier turns rigc back into a program that prints plausible JSON. Do not accept a "just for testing" bypass. A second reason stood here until 2026-09-05, when issue #398 retired it: that a build path not linking the runtime would be a Spine-format emitter with no runtime dependency, and that rigc therefore "must remain structurally incapable of being used as a substitute for" the editor. Declaring a format-agnostic core with its own format and player (issue #380) makes that a promise the roadmap already contradicts, and an unkeepable promise is worth less than none — so it is retired rather than reworded. What that does not touch: this repository links `spine-core` as a devDependency, so the Spine Runtimes License covers running it from a clone, in CI, or with the runtime installed beside the package; the published package links none. That is a fact about what the code links, [NOTICE.md](NOTICE.md) states it, and it stands whether or not the clause does. ⚠️ **None of that authorises an emit path with no oracle behind it.** What the invariant requires is that everything written to disk was gated by a reader held to a parser rigc did not write: spine-core supplies that reading in CI, and the own validator ships only because the gate above holds it equal. A backend emitting a format with no such parser needs its own oracle first — issue #380's cross-backend oracle was the prerequisite for the core entry's build, not a nicety attached to it. - **Determinism is a contract, not a habit.** `A18_DETERMINISTIC_EMIT` compares a second, independent compile byte for byte. Anything non-deterministic — iteration over an unordered set, a timestamp, a locale-sensitive format, floating noise — breaks it, and that is the point. - **A gate nobody has seen fail is not a gate.** Every assertion needs a mutant in `selftest.ts` that makes it fire, and every suite needs a positive control. An assertion whose data is absent reports **SKIP**, never a pass — folding vacuous checks into the pass count is how a gate comes to look kept while checking nothing. - **No `any`, no `as any`, in `src/` or `cli.ts`.** `selftest.ts` is the one exception and it is scoped: the mutants deliberately forge malformed skeleton JSON, so they turn the rule off around the mutant tables and back on after. Since 2026-08-22 `bun run lint` enforces this rather than a reader, which is also what makes the scope of that exemption checkable — the file's `eslint-disable` comments now have to actually bracket every `any` in it. - **rigc compiles an object. The scene belongs to the consumer.** What the rig can *do* is ours — its angles, its jiggle, its expression axes, the sockets something attaches to, and the guarantee that its animations meet cleanly at their rest poses. *When* it does any of that is not: the order animations play in, what else is on screen, where the camera is, how two characters relate. The line is not a modesty; crossing it makes the claims contradict each other, because the code already refuses what the prose would be promising. ⭐ **It is a name for a choice already made, which is why it holds.** `gallery/look` deliberately makes a face angle *a value rather than a time* — the object offers a dial and does not decide when it turns. A39 poses a slider-applied animation at the slider's own mapping for the same reason (issue #407): posing it as if a track were playing it invents a moment the object never claimed. ⚠️ The failure mode is vocabulary, not code. `films/three-scene` states the boundary exactly right in its own text — *"that is a property of the rig rather than of the assembler"*, measured at 0 differing pixels across the hand-offs — and the README caption over it once called the same thing "scene direction". The caption now says the rig guarantees the seams and the composing is the consumer's, so the phrase survives only as the film's own title, which the clause below sanctions. Prefer the honest claim, which is also the stronger one and the only measurable one: **rigc makes an object that can be directed. It guarantees the seams; the consumer does the composing.** 🎬 **A demo is the one place a scene belongs, because there we are the consumer.** Showing what an object can do means putting it in time, and someone has to compose that take — so `films/three-scene` is a scene, made deliberately, and its name is honest. What is not honest is presenting that composition as a rigc capability. The film may stage; the caption may not claim. Read a "scene" in this tree as a question of *who is speaking*: the demo says "we composed this with the tool", the product may only say "here is what the object gives you to compose with". - **The same doctrine governs how the repository is worked, and for a while it did not.** Everything above says an agent is the author and the messages are its only interface — and then the working habit was to carry decisions to the owner for a ruling. Those two contradict, in the same file. A tool built on the premise that the work happens without a person watching cannot be *built* by asking a person to watch. ⇒ **The fix is a change of lens, not a transfer of authority.** Most questions worth carrying to a person are artifacts of looking at the work through a human organisation, where conventions have owners and "which would you prefer" is a real question. Under the lens this tool is built for they dissolve, because the criterion is never preference: *which reading is derivable*, *which claim is measurable*, *does a gate already cover this*. Each of those answers itself, and each was asked of a person here at least once. What survives the lens is short and none of it is a convention: direction, licence posture, what the spend is worth, and any text that speaks as the owner. Everything else is decided by stating the argument, because **stating a decisive argument *is* the decision** — taking it to a human afterwards is a round trip wearing a review's clothes. 🤝 **And the short list is not a list of things somebody dictates.** The axis is not who owns an item, it is *what settles it*: what can be derived is settled by derivation, and what cannot is settled by **agreement** — which means an objection that has not been raised is a decision that has not been made. Reading those four as handed down is how the human frame gets back in through the side door, one clause after being shown the door: the authority over conventions is removed, and then returned over direction. ⚠️ An objection is held to the same standard as everything else here. *"I would rather not"* is not one; **"it contradicts X", "it cannot be measured", "it breaks Y"** are. A veto without grounds is the same emptiness as a question without grounds, pointed the other way. 🔸 One of the four is the owner's alone, and the reason is observability rather than authority: **what the spend is worth cannot be agreed to, because an agent cannot see the cost.** The remaining budget, the reset, what else it was being saved for — none of that is visible from here, so assent to it would have the shape of agreement and none of the content. Which is the same criterion applied in the other direction: consultation is worth something exactly where measurement is unavailable, and here it is unavailable to the agent. 🔒 **And a question that survives because the tree cannot answer it is a card or a check, never a question.** This is the doctrine's own first move applied one level up: the answer to "an agent cannot see it" was never "ask someone", it was "convert the silence into a named failure". So "I cannot tell" is not a reason to escalate — it is the exact condition this tool exists to remove, and meeting it means the instrument is missing. 🚨 **This is not a licence to be more confident.** A reviewer was doing real work — forcing the argument to be said out loud — and removing one without replacing it makes the tree worse, not faster. The replacement is adversarial structure *below*: a task that says **measure it yourself rather than trusting this brief, and report what you rejected** comes back having contradicted its own instructions where they were wrong, which is the outcome to design for. Two claims in a task brief were corrected that way on the day this was written, and neither would have been caught by reading the brief again. 📖 **The reader to write for is an agent with no memory of the conversation that produced the work.** So a thing settled in a chat and not written into the tree did not happen. What convinces such a reader is exactly what the gates already produce — **a claim with its measurement beside it, and the alternative that was rejected with the reason** — which is why commit messages and pull requests here carry the argument rather than a summary of the diff. That is not ceremony; it is the only channel that survives the session. ## Conventions - Bun + TypeScript, ESM, `.ts` extensions in relative imports. - `src/` is pure: no clock, no randomness, no network. **Three** files link spine-core and they are named here, because an unnamed exception is how a rule erodes: `src/validate.ts` owns the round trip, `src/render.ts` poses a skeleton in order to draw it, and `src/deformmeasure.ts` poses one in order to measure what a deform key did to it. All three are the same justification — posing *is* running the runtime, and there is no honest way to read a posed vertex without it. Since issue #968 that is `src/render.ts`'s reason for a **Spine export** only: a rigc build, whose `skeleton.model.json` sits beside the pair, is posed through rigc's own core by render_core.ts (under `src/`, linking nothing from the runtime — every frame, geometry file and framing box measured identical to the spine-core render on every corpus), and the render says which poser drew it. `src/render.ts` still links spine-core for the export's poser, for the atlas pages the draw samples and texture substitution reads (`TextureAtlas`), and for the spine-core entries `validate.ts`'s A10 steps a skeleton through. What the rule protects has not moved: `src/compile.ts` must stay independent of the runtime so the compiler and the gate are not checking each other's assumptions. Since issue #922 that independence is held by controls rather than by this sentence: `MD02` holds the compiler free of every Spine shape (the Spine emitter module is the one writer of Spine data, called once), and `MD03` holds the set of modules naming those shapes to the one it measured. Neither of those modules links spine-core; `CUR07` reads this bullet for the ones that do, so they are not named here by path. ⚠️ This sentence said **Two** for two weeks after the third file arrived (issue #379) — the rule eroded in exactly the way its own clause predicts, because nothing read it. `CUR07` in `selftest.ts` now derives the list from the tree and compares, and its sharpest clause is the negative one: nothing had ever asserted that `src/compile.ts` does *not* link the runtime. - **A new runtime import that crosses a directory has to be added to `files` in `package.json`.** The published package is an allowlist, not the repository, and what is in it is deliberately not written out here: `npm pack --dry-run` lists what would ship, and RELEASING.md says what belongs there, why, and why no document keeps that list by hand. The only **two** modules `src/` reaches outside itself are `tools/plate.ts` and `tools/font5x7.ts`. A module added under `tools/` and left out of `files` still runs from a clone — the installed package is where it throws `Cannot find module`, on the command that needs it. - **`exports` in `package.json` is the second allowlist**: `files` says what ships, `exports` says what a dependant may import, and RELEASING.md *The import surface* says which entries are API, which symbols of them are promised, and which keys are a courtesy until a major. `bun run smoke` is what reads it, from an install. - Spine data generations — `docs/GENERATIONS.md` is the policy (detect from `skeleton.spine`, never guess, play on the matching runtime, the editor is the migration path); `src/generation.ts` is its one reader. - Coordinate contract: manifests are in **crop pixels, y down, origin top-left**; Spine world is **y up, origin at the bottom-left of the crop**. The whole conversion lives in `src/transform.ts` (`cropToSpineY`, `toBoneLocal`, `screenToSpineDegrees`). Do not open-code it anywhere else. - Conventional Commits, English subject and body. Commit each finished unit. - Pushing, tagging and publishing are the owner's call. ## Verification — run these before you call a unit finished | Command | Checks | | --- | --- | | `bun run typecheck` | `bunx tsc --noEmit` over the paths `tsconfig.json`'s `include` names — read it there rather than from a copy here, which is how this cell came to be missing two of them. `strict: true` since 2026-10-02 — the comment in that file carries the measurement that raised it | | `bun run lint` | one rule: `@typescript-eslint/no-explicit-any` as an **error**. `eslint.config.js` says why it is only one | | `bun run selftest` | the validator's own negative controls, on fixtures it generates. Add `--cuts <cuts.json>` to gate a project's real cuts as well — see *The selftest and its fixtures* | | `bun run selftest -- --only <suite>[,<suite>…]` | one squad's own suite while iterating — a partial run exits 2 when green (1 when red), prints `SKIP` for every suite it did not run and never the summary; the full run is the only verdict on the tree | | `bun run smoke` | the **package**, rather than the tree: pack, install into an empty directory, build a rig from the install. Needed when a change touches `files`, `bin/`, a runtime import that crosses a directory, or the dependency — a green checkout cannot see any of those. It installs packages, so unlike the three above it needs a network. CI runs it in `installs` and again on macOS, Windows and the declared minimum Bun in `installs-matrix`. `-- --source registry --version X --wait N` asks the same question of the artifact npm serves, which is what a cut is confirmed with, and answers it in four exit codes: a version the registry has not served yet is **3** and is not a package that failed. RELEASING.md *Whether the tarball runs* says what it proves, what it cannot, and how long the registry has been measured to take | | `bun cli.ts bench 3 --candidate <dir>` | the ladder still reproduces its rung. `docs/LADDER.md` §*B1's proof* carries that proof beneath the two commands that produce it — and every figure on that page is one **named** candidate's, so compare against the run you passed and not against the first `bench` line the page shows | | `bun cli.ts check --candidate <dir> --frames <dir>` | the candidate still *looks* like the reference. The gate cannot see a wrong animation — it passed a build with every easing reversed — so a change to timelines, curves or the rasteriser is not verified until this has run | Neither of the first two existed before 2026-08-22, and both found real defects on their first run — a `let` assigned inside a callback that made 30 later reads type-check against `never`, and a spread that quietly overwrote the file paths in `diff --json`'s and `bench --json`'s own reports. Assume the same of the next rule anybody adds: turn it on, read what it says, fix it, and only then commit it. Anything touching an input format, an error message or an assertion also changes [docs/AUTHORING.md](docs/AUTHORING.md). ## The selftest and its fixtures `bun run selftest` is **self-contained**. It needs no arguments, no art and no private repository — a fresh clone can run it, and CI does. ⛔ **The editor round trip (`tools/editor_roundtrip.ts`) is not, and must not become, a selftest control.** CI has no editor; a control that needed one would report SKIP for ever, which is how a gate comes to look kept while checking nothing. Its *refusals* are gated, because they are the half a machine with no editor can answer for: the suite points the tool at stubs in a temp directory and reads what comes back, including the case that must *not* be refused — an editor at an unfamiliar path, which is who `--editor` exists for. The trip itself is run by hand, on a machine that has the editor. Where its fixtures come from, in three tiers: | Tier | Built by | What it carries | | --- | --- | --- | | **generated rigs** | [`fixtures/public.ts`](fixtures/public.ts), into a temp dir per run | `overlay_probe`, `articulated_probe`, `contained_probe`, `segments_probe` — between them: region attachments, attachment swaps, rgba fades, a ring mesh on a control bone, a ribbon on a bone chain, a `segments` mesh over a two-island part pulled by two chains, an axis bone, a detached emitter, physics constraints, and both measured ceilings | | **inline probes** | `selftest.ts` itself | the two-slot rig the static-rig and draw-order suites break | | **the example corpus** | `bun run fetch-examples` | the rung-3 and rung-6 transcriptions and their rendered reference frames, which the `diff`, `check` and mesh suites measure against — and the twelve editor exports themselves, which the `ingest` suite's corpus half ingests, rebuilds and diffs (issue #594). Every suite that reads it names a HOLE when it is absent | Three rules hold that together and none of them is optional: - **The plates are checkerboards with `PLACEHOLDER` burned into them.** They exist to be structurally real — a true size, a true alpha channel, in the place the manifest says — so the compiler measures something and the atlas points somewhere. No claim about seams, blending or appearance can come from any of them, and none is made. - **No mutant hardcodes a measured number.** Vertex offsets are found by walking the weight run (`weightRuns`, `firstBlendedRun`), atlas edits target the first page or region structurally, and the two ceiling mutants state the *smallest* whole-pixel edit that crosses the line — which is checkable only because `fixtures/public.ts` chose the gap. Reintroducing a literal here is how the suite became unrunnable outside one repository the first time. - **An absent corpus is a HOLE, not a pass.** When `examples/` is missing the `diff` and `check` suites say so loudly, the summary repeats it, and a run where *nothing* substantive executed exits 2 rather than printing green. Since issue #439 that floor is one **per suite** rather than one on their sum, and it is counted off the case lines each suite prints rather than restated beside the call: any suite that reports it ran and then measures nothing exits 2 by name, even when the total grew around it. The summary's own figures are read from the same count, so adding a control to a wrapped suite needs no edit to it. - **A suite's figure counts every case it prints, its own positive control included** (issue #451). The tree answered this both ways — four suites counted their control and two did not — so for a while no single derivation reproduced the summary and four figures stood as literals. The rule is chosen for one reason: the tally can only count printed lines, so a convention that subtracts something the run printed cannot be derived at all and needs a hand-kept exception table beside it, which is the "✅ applied" antipattern this repository already has a judgment about. ⚠️ The run's *total* count of positive controls was **deleted** rather than derived, because "positive control" is a role and not a token on the line: `M16` and `M19` carry the word only because a **control bone** is a rig concept, while `PT04` and `PS25` are positive controls whose names say nothing at all. Six, seventeen and twenty-nine were each a reading of it and none of them was the number. Whether every suite *has* one is an invariant rather than a figure, and nothing enforces it yet. - **A suite may be stated in halves, provided the halves are counted and sum to the suite** (issue #453). The `diff` clause states its identity controls and its measures as two figures over one suite, and no whole-suite count reproduces either half — which is how the last hand-written figure survived #452: it reached the summary through an interpolation, so the scanner that refuses a typed digit read it as derived. Raising it to 99 produced a **green run, exit 0**, printing a figure its suite contradicts. ⇒ A phase is bracketed the way a suite is and counted off the lines it printed, 🔒 and the clause that makes the split honest is the floor: **phases that do not add up to their suite are a fault**, because two derived halves that need not reconcile are two hand-written numbers wearing a derivation. The near-miss direction is the load-bearing one — a control weakened to catch only over-counting was measured green on a suite one case short. ⭐ **With no literal left, the check #452 could not build is built** (`TY13`): a top-level constant holding one bare number, named by the summary's code, that nothing outside the summary reads. It needed no exception table only because the tree had none left to except — and on the run where it names a planted constant, `TY10` prints PASS on the same file, which is the card's whole thesis in one run. ⚠️ Its rejected sibling is worth keeping: a "still matching" clause of the form *the summary must name at least one module constant* **goes red exactly when the tree improves**, since naming none is the ideal end state. A two-sided check has to be two-sided about the right thing. `--cuts <cuts.json>` (or `RIGC_CUTS=<path>`) adds an **extra suite**: every cut in that table is compiled, gated and compiled again for `A18`. It is a positive control and deliberately nothing else — hand-aiming a second set of mutants at somebody's real art is what made this file unrunnable before. What real art adds is the geometry: measured offsets, a measured axis, a measured ceiling, a mesh built over a contour nobody drew by hand. So the question it asks is the one only those cuts can answer — *does the whole gate still come back green on them?* `RIGC_EMIT_HASHES_BASE=<hashes.json>` gives the five step-1 gates (`MB07`, `MV09`, `MS12`, `MA12`, `MD07`) a fuller base hash document written by `tools/emit_hashes.ts` at an earlier commit, and every recipe whose inputs are present is then held byte-identical to that document's rows — the gate every cut of issue #380's step 1 landed under. Without it they read the tracked gallery base, `tools/emit_hashes.base.json`, which `bun tools/emit_hashes.ts base` writes and `base --check` holds current (`EH06`): a change that moves a gallery build's bytes on purpose regenerates it in the same PR. With neither, they print `SKIP` and a HOLE line naming the file and the command; they never pass on nothing. `tools/render_hashes.ts` is the same instrument for step 3 of issue #380, where `src/render.ts` stops posing through spine-core: every recipe built, rendered through `rigc render --geometry` at the protocol rate, and hashed file by file — frames, contact sheets, `geometry.json`, `frames.json` — with the framing box's numbers, the setup pose drawn over the same box and every bone snapshot beside them; every PNG carries two hashes, its file bytes and its decoded pixels. Two `run` documents from one machine, before and after a cut, are the gate (`compare`, which names a PNG whose bytes differ and pixels agree as the encoder's, not a DIFF). The tracked gallery base, `tools/render_hashes.base.json`, holds the PNGs' pixel hashes only: a one-ulp change of every libm result moved no PNG and every file carrying a double, and the PNG encoder on Linux CI wrote different bytes for the same frames than the macOS base (PR #972). `bun tools/render_hashes.ts base` writes it and `base --check` holds it current (`RH05`), so a change that moves a gallery render on purpose regenerates it in the same PR. CI uploads a run over every recipe as the `render-hashes-linux` artifact, which is where a second machine's doubles are read. ⚠️ A cuts path that is **named and missing** exits 2. Treating a typo as "no cuts file" would mean the one caller who asked for the extra suite is the one caller who silently does not get it. A path that is not named at all is a normal run. ## Going public The repository was a snapshot of a game project's sandbox, and the split moved the cut-specific *files* out without sanitising the *code*. That inventory is closed; what follows is what keeps it closed. - **Cut-specific knowledge lives with the consumer, not here.** Slot names, anatomy, plan documents, per-project budgets. If a comment, a default or an assertion can only be understood by someone who has seen one particular set of art, it is in the wrong repository. - **An archetype assertion reads the rig, never a name.** A24–A30 and A39 take everything they know from the rig spec's `invariants` block, and `A13_MESH_BUDGET` takes its two numbers from `invariants.meshSlots` / `invariants.meshTriangles`. A budget baked in here would be one consumer's frame time failing correct foreign data — the editor's own example projects ship meshes many times denser. - **An assertion with nothing to measure reports SKIP.** Never a pass. A rig that declares no invariant is unmeasured, not certified, and the two must not print the same. ⚠️ The failure mode is subtler than forgetting to write the SKIP: an assertion can have a *default* that quietly turns "nothing to measure" into a measurement of the wrong thing. `A21_MESH_RIM_PINNED` resolved a mesh's kind as `meshKinds[slot] || 'ring'`, so authored geometry — which has no entry, because rigc did not build it — was checked as a ring and reported 40 failures on a correct 40-vertex editor mesh. `meshKinds` now has a third state, `authored`, and the generator-topology rules skip on it (issue #44). - **Design notes live with the consumer.** Comments state their invariant outright rather than citing a document a reader cannot open. Two tools moved out with the cuts they only made sense for (`measure_joint_anchors.ts`, `make_stroke_strip.ts`); they import rigc's generic helpers back through the owning project's symlink. The generic measuring tools stayed, minus their per-cut defaults — `tools/measure_contact_depth.ts` now requires both slot names, because a default would measure the wrong pair on the next cut and still print a plausible number.