Files
solvecraft/tests/kernel-cases/README.md
T
jyviko 34c5bc74a5 Wave 270/509: kernel-cases: a continuity check, and ladder 07/015 graded by it (and 3 more)
4 commits, squashed; the originals stay reachable from tag archive/merge-main-develop.

- d8282b336 jyviko: doc, engine: corner: g2 sets each corner back for its G2 patch
- 8fbb575a5 jyviko: kernel-cases: a continuity check, and ladder 07/015 graded by it
- 808913777 jyviko: kernel: G2 corners at concave corners, mixed ones refused
- ba38fbd2a jyviko: kernel-cases: ladder 07/019, a concave corner's G2 patch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-10-26 13:37:34 -04:00

134 lines
8.8 KiB
Markdown

# Kernel regression rig
Every geometry case the old kernel (truck) got wrong, recorded in an issue, a pull request or a
crash report, is a use case here with its own right answer. The kernel, SMLib, is held to all of
them. `cargo xtask kernel-cases` runs them and writes [`docs/kernel-cases.md`](../../docs/kernel-cases.md).
```sh
cargo xtask kernel-cases # every case
cargo xtask kernel-cases --cli target/release/solidcraft-cli --out /tmp/run # with a CLI already built
cargo run -p solidcraft-cli --features smlib -- kernel-cases tests/kernel-cases/booleans/<id> # one case
```
The CLI links SMLib by default (`scripts/fetch-smlib.sh` fetches the prebuilt library first);
built with `--no-default-features`, every operation answers that SMLib is not built. The run's output folder (`--out`, default `target/kernel-cases`)
gets `results.json`, every case's full result, and `dumps/<area>-<case>/`: the inputs of each
SMLib call a case failed in, for replay (the CLI's `--dumps=DIR`, which the rig always passes).
## Rules
- **The answer never comes from a kernel.** Not from truck's output, nor any commercial
kernel's. It comes from analysis or a hand calculation, or else from the invariants below.
- **No user designs.** A crash report or a repro design becomes a small synthetic recipe that
shows the same geometry. Check for private content before committing anything.
- **No workaround on SolidCraft's side.** A case that fails stays failing until the kernel is
fixed. SMLib defects go to tangency-labs/solidcore as issues (`priority: high`,
`bug`), and the fix goes into the fork.
## Layout
One directory per area, one per case: `tests/kernel-cases/<area>/<id>/`.
| Area | Owner |
|---|---|
| `booleans` (combine, cuts, holes, patterns, split, mirror, draft) | booleans/shell/import/crash rig |
| `shell` (shell, offset, thicken) | booleans/shell/import/crash rig |
| `import` (STEP and IGES geometry) | booleans/shell/import/crash rig |
| `crashes` (crashes and hangs inside kernel operations) | booleans/shell/import/crash rig |
| `fillets`, `sweeps`, `threads`, `sheet-metal`, `tessellation` | history rig |
`<id>` is `<source number>-<short-name>`, such as `1894-prism-cut-touching-walls`.
Each case has:
- `recipe.json`: an engine command script (`{"commands": [...]}`, as `solidcraft-cli run`
takes) or an oracle recipe (`tests/oracle/*/recipe.json`). `$CASE` in a string is the case
directory, for an input file such as `$CASE/input.step` (written by hand from the STEP or IGES
specification, never a user's file), and `$OUT` a scratch directory outside the repository for
files the case writes (an export).
- `expected.json`: the right answer, graded by `solidcraft-cli kernel-cases`.
A script may start with `"base": "<case directory>"` (relative to its own directory): that case's
commands, and its own base's, run first. The ladder's cases build on each other this way.
Each area also has `skipped.json` (or several, `skipped-<part>.json`, where an area is triaged in
parts): every issue and PR looked at that did not become a case, with one line each saying why.
```json
[{"source": "tangency-labs/solidcraft#123", "why": "UI only: the selection highlight, no geometry"}]
```
`sources.json` says what kind each source number is (an open or closed issue, a merged PR, one of
the frozen truck-geometry PRs, a `kernel-paused` PR), for the report's coverage by source.
## expected.json
```json
{
"sources": ["tangency-labs/solidcraft#1894", "tangency-labs/solidcraft#1629"],
"what": "a prism cut whose walls only touch the target's walls",
"source": "analytic",
"method": "box 40x30x20 minus the prism 10x30x5 flush with the side: V = 24000 - 1500",
"body_count": 1,
"volume_mm3": 22500.0,
"area_mm2": 5200.0,
"faces": 8
}
```
| Field | Meaning |
|---|---|
| `sources` | The issues, PRs or crash reports the case comes from (`owner/repo#N`). |
| `what` | One line: the geometry that went wrong. |
| `source` | `analytic` (numbers worked out by hand; `method` gives the formulas) or `invariants` (no number is known: only the outcome, validity and counts are graded). |
| `method` | How the numbers were derived, or which invariants hold and why. |
| `outcome` | `solid` (default: the script succeeds), `refused` (the right answer is a clean error, never a panic or a hang), or `solid_or_refused` (either a clean error or a result graded as usual, where the geometry has no single manifold answer, such as two bodies touching along a curve only). |
| `valid` | Default `true`: every body is one closed shell (or `shells` in all) that passes the record's structure and geometry checks and SMLib's `check(All)`. `false` only where the result is legitimately not a solid (a sheet). Where SMLib can't take the record yet (a surface kind its conversion lacks), its check gives no verdict and is not graded. |
| `shells` | The total number of shells, where it isn't one per body: a body with a sealed void (2), several lumps kept in one body. |
| `body_count`, `faces`, `edges`, `vertices` | Exact counts, where the geometry fixes them. Faces and edges count whole analytic surfaces and curves once (a full cylinder side is one face). |
| `volume_mm3`, `area_mm2`, `center_of_mass`, `bbox` | As in `tests/oracle`: totals over every body, relative tolerance 1e-3 for volume and area, 1e-3 of the box diagonal for positions. |
| `faces`, `edges`, `vertices`, `shells`, `body_count` as `[min, max]` | An inclusive range, where the geometry only bounds a count (a modelled thread's faces). |
| `continuity` | `{near, within_mm, clear_mm, curvature_jump_per_mm}`: over every tangent seam lying wholly within `within_mm` of `near` (a corner's patch and its neighbours), the curvature jump across it (the faces' normal curvatures square to the seam, from their exact surfaces, at 65 stations a seam) is at most `curvature_jump_per_mm` at every station at least `clear_mm` along the seam from a vertex where two of those seams turn (where a neighbour's own G1 seam ends, which no surface is G2 to both sides of). A rolling ball's corner jumps 1 / r everywhere. |
| `watertight` | `true`: each body's display mesh (chord tolerance `tolerance.mesh_mm`, default 0.01 mm) has no open and no over-shared edges, with vertices closer than `tolerance.weld_mm` (default 1e-4 mm) taken as one. |
| `timeout_s` | A longer timeout for this case than the run's (at most 1800 s), for cases that are slow by nature. A case that only needs it on a loaded machine shouldn't set it. |
| `tolerance` | Optional overrides: `rel`, `abs_mm`, `mesh_mm`, `weld_mm`. |
| `missing` | A capability SolidCraft has no command or parameter for yet (`"solid.thread has no start count"`): the case is reported **not supported yet** without running, so the gap stays visible. |
## The complexity ladder
`ladder/` orders kernel operations by complexity instead of by where they went wrong: tiers from
primitives through degenerate booleans, analytic and spline profiles, holes and taps, threads,
fillets, chamfers, shells, patterns, to whole parts. Later tiers build on earlier tiers' shapes (a
running part grows one tier at a time through `base`), so the last case carries everything.
- `ladder/<NN-tier>/<NNN-name>/`: cases in the format above, each a test with its own answer.
- `ladder/ladder.json`: the tiers in order and, per tier, its cases in order (paths under
`tests/kernel-cases/`, so a tier may also list a rig case from another area).
- `ladder/golden.json`: per case the status, the commands' time and what was made (counts,
volume, area). Runs compare with it and flag slowdowns (over twice the time and
0.25 s more) and changed counts or statuses. It is re-recorded only with a reason.
- `tools/kernel-ladder/cases.py` writes the recipes and answers (standard-library Python; each
`method` gives the derivation).
```sh
cargo xtask kernel-cases --ladder --cli target/release/solidcraft-cli
cargo xtask kernel-cases --ladder --cli target/release/solidcraft-cli --record "why"
```
Ladder runs measure one case at a time (unless `--jobs`) and keep their output in
`target/kernel-ladder` (`--out`); `--record` rewrites the golden and sorts each tier by time,
fastest first; commit both files. The report, `docs/kernel-ladder.md`, gives per tier the counts
per status and the **frontier**: the first case in ladder order that fails.
## Results
Per case: **pass**; **fail** with the numbers; **not supported yet** (the kernel says
so, or the case names a `missing` capability); **crash** (a panic caught by the guards, or the process died); **hang** (no result before
the timeout).
## Exporting to solidcore
solidcore#349 proposes a graded case format for the Rust port. The cases here are SolidCraft's
copy and the source; each one maps onto it field by field (`sources`, the derivation, the
graded quantities and invariants), so an exporter can write them out for solidcore unchanged.