diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 674b063b..2ef0d1de 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,6 +5,13 @@ on: branches: [main] paths: - "docs/**" + # The reference pages are generated from each crate's manifest, README + # and CHANGELOG.md, and rustdoc from its doc comments, so a crate-only + # change still alters the published site. + - "crates/**" + - "tools/xtask/**" + - "Cargo.toml" + - "Cargo.lock" - ".github/workflows/docs.yml" workflow_dispatch: @@ -22,15 +29,38 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de + with: + toolchain: 1.88.0 + - name: Cache Rust build + # Dependencies only (rust-cache drops workspace artifacts), saved from + # main, which is the only ref this workflow runs on. + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + key: docs - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 20 cache: npm cache-dependency-path: docs/package-lock.json + - name: Generated docs must match the crates they are derived from + run: cargo xtask docs --check - run: npm ci working-directory: docs - run: npm run docs:build working-directory: docs + # rustdoc is nested under the VitePress output so it ships in the one + # Pages artifact; the crate reference pages link to it at + # /kernel/api/rustdoc//. The gate builds the same docs with + # -D warnings, so a broken intra-doc link fails before it lands. + - name: Build the Rust API reference + env: + RUSTDOCFLAGS: "-D warnings" + run: cargo doc --workspace --all-features --no-deps + - name: Nest rustdoc under the site at /api/rustdoc/ + run: | + mkdir -p docs/.vitepress/dist/api/rustdoc + cp -r target/doc/. docs/.vitepress/dist/api/rustdoc/ - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: diff --git a/.github/workflows/roadmap.yml b/.github/workflows/roadmap.yml index 651e269b..a95f258c 100644 --- a/.github/workflows/roadmap.yml +++ b/.github/workflows/roadmap.yml @@ -2,23 +2,17 @@ name: Roadmap freshness # The roadmap explains ordering and reasoning; the project board carries status. # This gate stops the roadmap drifting back into a status page that goes stale. -# It also covers crate `PLAN.md` design notes, which drifted the same way -# (kernel#25), so their paths must trigger it too. on: push: branches: [main] paths: - "docs/ROADMAP.md" - - "crates/PLAN.md" - - "crates/**/PLAN.md" - "scripts/check-roadmap-freshness.py" - ".github/workflows/roadmap.yml" pull_request: paths: - "docs/ROADMAP.md" - - "crates/PLAN.md" - - "crates/**/PLAN.md" - "scripts/check-roadmap-freshness.py" - ".github/workflows/roadmap.yml" # Milestones change outside of any commit, so re-check on a schedule too. diff --git a/AGENTS.md b/AGENTS.md index b4d4405f..6f95878e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,53 +1,104 @@ -# Axiolid contribution rules +# axiolid/kernel -Axiolid is a standalone pure-Rust, IFC-agnostic geometry kernel. No source-format or vendor types may enter `crates/`. The `axiolid-model` DAG is the input seam; applications select operation providers. - -Read the closest nested `AGENTS.md` for directory-specific rules. Preserve the pure-Rust, format-agnostic boundary: no IFC, file-format, or vendor model types in `crates/`. Run the focused crate checks while iterating and `scripts/gate.sh` before landing workspace-wide changes. +A standalone, pure-Rust, format-agnostic geometry kernel: neutral geometry +values, explicit operation contracts, and replaceable execution providers. +The `axiolid-model` DAG is the input seam, and applications select providers. +This file is the only `AGENTS.md`. Before editing a crate, read its +`README.md` and the `//!` docs of the module you touch (ADR 0078). ## Layout -- `crates/`: nested ownership tree containing publishable kernel packages; `crates/facade/axiolid` is the opt-in Rust facade and `crates/facade/axiolid-capi` is the sole unsafe C ABI boundary. -- `tools/xtask/`: local-only architecture checker and generated-document owner. -- `tools/benchmark/`: local-only measurement area — Criterion microbenchmarks, iai-callgrind instruction-count regression benchmarks, end-to-end scenarios, scaling assertions, and small regression datasets. -- `native/`: source-build and installed-package CMake integration; immutable source pins only. -- `docs/adr/`: durable architecture decisions; ADR 0035 owns the current package topology. -- `docs/architecture/`: current and generated crate/dependency maps. -- `docs/research/`: prior-art evidence. -- `architecture/capability-ledger.toml`: every geometry capability OCCT and CGAL have, graded against Axiolid, with reference paths and tracking issues. **Start here when choosing capability work:** `cargo xtask gaps` prints what is ready, by priority; `cargo xtask gaps show ` gives the evidence and the OCCT/CGAL packages to read. You do not need either library checked out. -- `scripts/`: feature, release, conformance, and mutation-verified architecture gates. +- `crates/`: publishable packages, nested by ownership: `foundation/` + (`axiolid-core`, the dependency root), `representations/`, `contracts/`, + `algorithms/`, `providers/`, `execution/`, `facade/` (`axiolid`, and + `axiolid-capi`, the only unsafe C ABI boundary). +- `tools/`: `xtask` (architecture, closure, context, docs, FFI and + ledger checks), `benchmark`, `oracle`. Local-only, never published. +- `tests/`: black-box consumer fixtures and downstream/native probes. +- `native/`: CMake source-build and installed-package integration. +- `docs/`: the VitePress site. ADRs live in `docs/adr/`. + `docs/architecture/` holds the machine-checked declarations: + `closure-profiles.toml` lists minimal downstream closures, + `capability-ledger.toml` grades every OCCT/CGAL capability against + Axiolid, and `semver-exceptions.toml` lists accepted breaking changes. + Its generated pages (the architecture maps, `docs/reference/`) come + from `cargo xtask docs` and are never edited by hand: change the + crate's manifest, `README.md` or `CHANGELOG.md` and regenerate. -## Commands +## Dependency rule -```bash -cargo fmt --all -- --check -cargo build --workspace -cargo test --workspace --all-features -cargo clippy --workspace --all-targets --all-features -- -D warnings -cargo xtask architecture check -bash scripts/probe_layering_gate.sh -bash scripts/field_gate.sh -bash scripts/geometry-feature-matrix.sh -scripts/check-capi.sh -scripts/check-native-packaging.sh -scripts/gate.sh -``` +Production direction is +`foundation <- representations <- contracts <- algorithms/providers <- execution <- facade`. +This is a role DAG, not a licence to depend on every earlier layer: each +crate's exact internal edges are allowlisted in its +`[package.metadata.axiolid]`, and `cargo xtask architecture check` enforces +them together with naming (ADR 0064), placement, unsafe policy and +format neutrality. Contracts never depend on providers or dispatch. +Algorithms do not select execution policy. Upward edges are dev-only and +limited to conformance tests. A closure change is an API change: update +`expected_internal` deliberately and record why in an ADR, never to +silence the gate. ADR 0035 owns the package topology. + +## Behaviour rules + +- No IFC, file-format, vendor, renderer or GPU-API types in `crates/`. + No C++ dependency path. +- `unsafe` is forbidden everywhere except `axiolid-capi`, which denies + unsafe operations inside unsafe functions. +- Refuse with a typed error or diagnostic. Never return substitute + geometry, a guessed default or a silently degraded result. Broad-phase + candidates are never labelled exact. +- A provider advertises only what it implements. It lands after its typed + contract, refusal behaviour and conformance suite, and it needs a portable + scalar correctness oracle before claiming an operation trait. Concrete + providers stay optional. Never tessellate exact intent silently. +- A capability claim needs an implementation, typed refusal and + conformance evidence; package metadata is not one. A performance claim + needs a committed benchmark confirmed in wall clock, not only in + instruction counts. +- Output order is deterministic. CPU dispatch is chosen at runtime, never + by `target-cpu=native`. +- Public values implement `Debug` and `Clone`; add other standard traits + only when they are semantically valid. Split a module before unrelated + data, validation and algorithms grow together; add no placeholder files. -`scripts/gate.sh` needs `cargo-semver-checks` for the breaking-change gate. -Install it once; pin the version because newer releases require a newer -rustc than this workspace uses: +## Open work + +Open work lives in GitHub issues (ADR 0078). A marker in code names its +issue as `TODO(#N)`. Do not add plans, checklists, progress logs or +nested `AGENTS.md` files: `cargo xtask context check` rejects them. +To choose capability work, start from the ledger: `cargo xtask gaps` lists +what is ready by priority, and `cargo xtask gaps show ` gives the +evidence and the OCCT/CGAL packages to read. A landing that changes a +row's level edits that row in the same commit. `scoped` rows and +`needs_decision` issues are maintainer decisions: ask, don't start coding. + +## Gate + +Iterate with focused crate checks, then run the full gate before landing +workspace-wide changes. Judge it by exit code: ```bash -cargo install cargo-semver-checks --version 0.44.0 --locked +scripts/gate.sh ``` -The gate fails loudly if it is missing rather than skipping the check. - -Benchmarks are not part of `scripts/gate.sh`; run them explicitly: +It needs `cargo-semver-checks`, pinned because newer releases need a +newer rustc: `cargo install cargo-semver-checks --version 0.44.0 --locked`. +The main steps can run alone: ```bash -cargo bench -p axiolid-benchmark --bench micro # wall-clock, informative -cargo bench -p axiolid-benchmark --bench scenario # end-to-end via the facade -bash scripts/bench-regression.sh # instruction counts, CI gate +cargo xtask architecture check # after metadata changes: cargo xtask architecture docs +cargo xtask architecture closure check +cargo xtask context check +cargo xtask docs --check # after README, CHANGELOG or metadata changes: cargo xtask docs +cargo xtask gaps check +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test --workspace --all-features +scripts/geometry-feature-matrix.sh +scripts/check-capi.sh ``` -The workspace has no C++ dependency path. `axiolid-capi` is the sole audited unsafe Rust boundary and must deny unsafe operations in unsafe functions; every other facade, contract, representation, and foundation crate forbids unsafe code. Concrete execution providers must remain optional and require a portable scalar correctness oracle before claiming an operation trait. +Mutation-verify a new gate before trusting it: break the rule, watch it +fail, restore (`scripts/probe_*_gate.sh`). Benchmarks are not in the gate; +`tools/benchmark/README.md` says how to run them. Records (changelog, +ADRs, research) follow `docs/guide/contributing.md`. diff --git a/Cargo.lock b/Cargo.lock index 80a5cc95..b96ec955 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,7 +88,7 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "axiolid" -version = "0.3.0" +version = "0.4.0" dependencies = [ "ahash", "axiolid-backend-cpu", @@ -684,7 +684,7 @@ dependencies = [ [[package]] name = "axiolid-ray-mesh" -version = "0.3.0" +version = "0.4.0" dependencies = [ "axiolid-core", "axiolid-guarantees", diff --git a/Cargo.toml b/Cargo.toml index 59afae9a..142527b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,7 +75,7 @@ readme = "README.md" [workspace.dependencies] # Format-agnostic Axiolid geometry kernel. Leaf crates remain independently consumable. -axiolid = { path = "crates/facade/axiolid", version = "0.3.0", default-features = false } +axiolid = { path = "crates/facade/axiolid", version = "0.4.0", default-features = false } axiolid-core = { path = "crates/foundation/core", version = "0.3.0" } axiolid-mesh = { path = "crates/representations/discrete/mesh", version = "0.3.0" } axiolid-pointcloud = { path = "crates/representations/discrete/pointcloud", version = "0.3.0" } @@ -117,7 +117,7 @@ num-integer = { version = "0.1.46", default-features = false, features = ["std"] axiolid-predicates = { path = "crates/algorithms/predicates", version = "0.3.1", default-features = false } axiolid-exact = { path = "crates/algorithms/exact", version = "0.1.1" } axiolid-linear-intersection = { path = "crates/algorithms/query/intersection/linear", version = "0.3.0", default-features = false } -axiolid-ray-mesh = { path = "crates/algorithms/query/intersection/ray-mesh", version = "0.3.0", default-features = false } +axiolid-ray-mesh = { path = "crates/algorithms/query/intersection/ray-mesh", version = "0.4.0", default-features = false } axiolid-reference = { path = "crates/algorithms/reference", version = "0.3.0" } axiolid-mesh-compile = { path = "crates/execution/compile", version = "0.3.4" } # axiolid-oracle is publish = false (test-only oracle crate, tools/oracle) and diff --git a/GAP2-FINDING.md b/GAP2-FINDING.md deleted file mode 100644 index e38dec2a..00000000 --- a/GAP2-FINDING.md +++ /dev/null @@ -1,126 +0,0 @@ -# Gap 2 finding: the production boolean is NOT the weak link - -## What I claimed in the audit - -> "Ours computes intersections in f64 ... our exactness stops at the test -> boundary ... wiring exact predicates into boolmesh's production path is the -> priority, because it decides whether the speed advantage we publish is a -> fair comparison." - -That recommendation was wrong, and the measurements below are why. Recording -it here rather than quietly dropping it. - -## What the probes actually measured - -### Probe 1 — near-coincident faces at origin scale - -Swept a cutter face across a host face, 1e0 down to 1e-15, comparing against -the exact analytic volume: - -``` -eps=1e-1 err=1.110e-16 -eps=1e-8 err=1.110e-16 -eps=1e-15 err=1.110e-16 -``` - -One ULP at every separation. No lost slab, no flipped predicate. The -`interpolate`/`intersect` pair carries an upstream comment claiming they are -"carefully designed to minimize rounding error and to remove it at edge -cases"; at this scale the claim holds up. - -Of the ten sign decisions in the boolean kernels, six are direct coordinate -comparisons (already exact -- comparing two f64s is not a rounding problem), -and only four involve an interpolated value. - -### Probe 2 — large coordinates - -``` -base=0 half-cut exact -base=1e3 half-cut rel err 1.192e-8 -base=1e6 half-cut rel err 1.626e-2 -base=1e7 half-cut rel err 9.068e-1 <-- 91% wrong -``` - -This looks like the smoking gun for gap 2. It is not. - -### Probe 3 — attribution - -Measuring the INPUT mesh before any boolean runs: - -``` -base=0 input_mesh_volume rel_err=4.337e-16 -base=1e3 input_mesh_volume rel_err=5.862e-10 -base=1e6 input_mesh_volume rel_err=5.668e-3 -base=1e7 input_mesh_volume rel_err=2.528e-1 <-- 25% wrong ALREADY -``` - -A quarter of the error at base 1e7 exists before the boolean is called. The -divergence tracks ULP-at-magnitude almost exactly, which is the signature of -catastrophic cancellation in the divergence-theorem volume sum: it adds terms -of order 1e21 to produce an answer of order 1e-3. - -## Conclusion - -The large-coordinate failure is real and worth fixing, but it is NOT -"the boolean needs exact predicates". It is coordinate magnitude: geometry -far from the origin loses relative precision everywhere -- in the volume -measure, in the mesh, and only then in the boolean. - -The standard fix is a local origin / RTC (relative-to-centre) offset, which -ifc-lite implements (`router/rtc_offset.rs`) and axiolid does not. That is a -different and cheaper change than a filtered-exact cascade through the -boolean kernels, and it fixes the measure and the mesh too. - -## Revised recommendation - -1. **Do not** rewrite the boolean kernels for exactness on this evidence. - Measured at origin scale, they are already at one ULP. -2. **Do** add an RTC/local-origin facility, and a gate that fails when a - fixture's own input mesh cannot be measured to tolerance -- that would - have caught this class of bug without anyone reading the boolean at all. -3. Keep the probes as a regression test with HONEST thresholds: exact at - origin scale, documented degradation at 1e6+, so the limit is stated - rather than discovered by a user with a site survey in national grid - coordinates. - ---- - -## Resolution (follow-up commit) - -The RTC/local-origin fix landed in `axiolid-measure`, not in the boolean. - -`volume_properties` and `surface_properties` now sum about a local origin -(the mesh's first vertex) instead of the world origin. Volume and centroid are -translation-invariant, so this changes nothing mathematically and everything -numerically. - -### Measured, 0.1 m box at origin 1e7 - -| quantity | before | after | -|---|---|---| -| volume relative error | 2.5e-1 (25%) | 7.5e-9 | -| centroid drift | 8.16e6 m | < 1e-6 m | - -The residual 7.5e-9 is the input's own representable-grid floor: at 1e7 the -spacing between adjacent f64 values is ~1.9e-9 m, so a 0.1 m box's corners -cannot be placed more precisely than that. The test asserts against that -computed floor rather than a hardcoded constant, so it stays honest at other -magnitudes instead of encoding one fixture's luck. - -### What was NOT broken - -`second_moments` is documented as being *about the origin* — its value -legitimately depends on the origin, so re-basing it would change the contract -rather than its conditioning. Left alone deliberately. - -`surface_properties` was not actually broken either. Mutation testing showed -its centroid test stays green with the world origin restored, because surface -weights are areas (~1e-2) rather than volumes and the cancellation is far -milder. The re-basing there is precautionary and is documented as such in the -test, so nobody later reads it as evidence of a fixed bug. - -### Gate quality - -Mutation-verified: reverting `base` to `Point3::ZERO` turns both volume tests -red and restoring it turns them green, so the gate provably detects the defect -it was written for rather than passing vacuously. diff --git a/PLAN-exact-curves-2.md b/PLAN-exact-curves-2.md deleted file mode 100644 index 2e99df8b..00000000 --- a/PLAN-exact-curves-2.md +++ /dev/null @@ -1,184 +0,0 @@ -# Plan: arc speedup, #119, publish axiolid-exact, #120 (round 2) - -User (2026-09-24): speed up arc booleans, then #119, then #120; publish -axiolid-exact before or after, whichever makes sense. - -## S. Arc overlay broad phase — done, 4e9e2f9 -Padded f64 edge boxes (chord box + sagitta), box-filtered crossing and -shared-edge tests, sorted start index for linking. 256x256 wavy rings -779 ms -> 7 ms. 12/12 mutants. - -## #119 tranche 1: exact analytic intersections (this session) -Full #119 (NURBS x NURBS marching, tangent/overlap in general) is OCCT -IntPatch scale (~32k lines). Tranche 1 is the analytic core every curved -boolean needs, built on axiolid-exact so decisions are exact: - -1. Curve/surface (B9): Line3, Circle3, Ellipse3 x Plane, Cylinder, Cone, - Sphere, Torus. Substitute the curve's polynomial / half-angle rational - parametrization into the surface's implicit equation, written for the - GIVEN f64 frame (non-unit axes kept as |z|^2 factors, not normalised), - so coefficients are exact dyadics; isolate real roots with IntPoly / - RealRoot. Report per hit: parameter (exact root + f64), point (f64), - multiplicity (tangent iff p' also vanishes). Identically zero poly => - curve lies in the surface (Contained). Half-angle misses theta = pi: - test that point exactly. Cone slope = tan(semi_angle) rounded once - (documented: the cone is the one with that f64 slope). -2. Curve/curve 2D (B8): Line2/Circle2/Ellipse2 pairs via axiolid-exact - conic (M3) — gives conic.rs its first user. -3. Surface/surface (B10): exact_surface_intersection's degeneracy tests - (parallel, tangent, coincident: raw `== 0.0` on rounded f64) become - exact sign decisions. Unrepresentable quartics stay refused by name. -Ledger: B8/B9/B10 stay narrow (NURBS generality open) with the analytic -subset named; #119 stays open for NURBS. - -## Publish axiolid-exact 0.1.0 — after #119 tranche 1 -#119 is the last planned change likely to add exact API (poly -arithmetic helpers); publishing after it avoids a 0.1 -> 0.2 break days -later. Overlay/construct next versions need it on crates.io anyway. - -## #120 tranche 2 -Candidates (pick by value/effort after #119): -- arc prism cut by a plane (column under a sloped roof): cylinder x plane - = ellipse, vertical edges x plane = points (#119 pieces). -- disconnected coaxial results returned as several solids. -- stepped coaxial spans (see boolean_stepped.rs for the polygon path). - -## Progress - -- Broad phase: done, 4e9e2f9 (gate + CI green). 2-116x on the scaling bench. -- #119 tranche 1 (exact analytic curve/surface and curve/curve): done - locally; 12 tests, 6/6 mutants caught (scripts/probe_exact_curve_mutants.py). - B8/B9 ledger rows extended, still narrow (B-spline operands). -- #119 tranche 2: done, f99e346 (gate green). Sphere/plane tangency, - cylinder/plane parallel/perpendicular/tangent and cone/plane - perpendicularity decided exactly; 37 tests, 5/5 mutants caught - (scripts/probe_exact_surface_mutants.py). Three of the old float - decisions were proven wrong on exact inputs, each now a regression test. -- axiolid-exact 0.1.0: published to crates.io from f99e346, checksum - 8584a50f..., tag axiolid-exact-v0.1.0; a fresh registry consumer builds. -- #119 stays open: B-spline operands, general quadric/quadric curves. -- Next: #120 tranche 2. - - -## #120 A: arc prism cut by a sloped plane (option 1, user-approved 2026-09-24) - -Problem: a sloped plane cuts a cylinder wall in an ellipse. Its pcurve on the -cylinder is v(u) = mean + a cos u + b sin u, which no Curve2 variant held -exactly (B-spline only approximates, u is an angle). - -Steps (each gated before the next): -1. axiolid-curve: `Curve2::Sinusoid(Sinusoid2 { mean, cosine, sine })`, - point(t) = (t, mean + cosine cos t + sine sin t). Additive - (#[non_exhaustive]). ADR 0071. -2. axiolid-evaluate: domain2 (full turn), evaluate2, derivative2, - second_derivative2, closed-form inversion (t = p.x). Graph validation - accepts finite coefficients. -3. construct: arc-prism builder takes lower/upper levels (Flat | Sloped - plane). Straight walls: exact Line3/Line2. Curved walls: Ellipse3 in its - principal frame (t = cylinder angle - const), Sinusoid2 pcurve on the - cylinder, Ellipse2 pcurve on the sloped cap. Flat/flat output unchanged. -4. Public: clip an ArcPrism by a HalfSpace. Plane must clear both caps over - the whole section (arc interior extremes included); plane above/below the - whole prism -> unchanged / Degenerate; crossing a cap or vertical plane -> - refused by name. -5. Tests: B-rep volume from sampled cap faces (walls vertical, so - V = sum of int z n_z dA over the caps) against the closed form; geometric - audit (pcurves lifted onto 3D curves); refusals; mutation probe. -6. Ledger C9 + B1, changelogs, gate, push, CI, #120 comment. - -Status: done, 1f4f1b5 (CI green). 14 clip tests, 11/11 mutants. - - -## #120 remaining (user: "do the remaining 120 work", 2026-09-24) - -Issue scope: close C9/D3 to implemented, or to narrow with the refused subset -named. Remaining refusals: stepped spans, planes crossing a cap, non-coaxial -curved solids, cones/spheres/tori/NURBS. - -Key observation: every remaining VERTICAL-column case (stepped coaxial -booleans, a plane crossing a cap, a stepped result under a roof) is one shape: -a planar arrangement of cells, each cell carrying a stack of z-intervals -bounded by planes (flat or sloped). Cell data depends only on which operands -contain the cell, so it is a function of a membership bitmask. One builder -for that shape replaces two diverging special cases. - -Steps (each gated before the next): -T1. axiolid-overlay: exact N-operand arc arrangement (`arc_arrangement`): - split all operand edges at all crossings (exact), label each piece's left - and right membership masks, dedupe shared pieces, snap f64 vertices; - `region(pred)` links the pieces bounding {mask : pred(mask)} into - outer/hole rings of piece uses. Refactor `exact_arc::boolean` to share - the ring assembly. One vertex table => every face built from it agrees on - every vertex bit-for-bit (the reason not to call arc_overlay per band). -T2. construct: column-solid builder. Input: arrangement, planes, and - mask -> intervals [lo plane, hi plane]. Walls per piece per maximal - symmetric-difference span; caps per (plane, facing) via region(); - vertical edges split at every height met at a vertex (heights snapped - within tolerance, zero-length sides dropped -> triangle walls); connected - components -> one ExactBRep each; void shells attached to their outer. - Refuse by name: bands touching only along an edge (non-manifold). -T3. Stepped coaxial booleans (arc and polygon prisms) through T2: the - `_solids` variants and single-solid variants stop refusing stepped spans. - Provenance names: walls from (operand, ring, edge), caps from the operand - whose bottom/top lies at that level. -T4. clip_arc_prism_exact: a plane crossing a cap through T2 (cells split by - the plane's intersection lines with the top/bottom levels). -T5. Non-coaxial curved / cones / spheres / tori / NURBS: general - surface-surface B-rep boolean (OCCT BOPAlgo scale). Not attempted here; - ledger rows stay narrow with this subset named, per the issue's scope - rule. Propose a follow-up issue. -T6. Ledger C9/D3, changelogs, ADR 0072, gate, push, CI, #120 comment. - -Status: T1-T4 done (484c96d arrangement, ab708eb measure fix, column -commit on top). T5 not attempted: non-coaxial curved / cones / spheres / -tori need a general surface-surface B-rep boolean; C9 stays narrow with that -subset named; follow-up issue proposed to the user, not filed. Cavities are -refused until tessellation/measure read void shells. - -### #120 tranche 3 design notes (column builder) - -- Result solid = union of "column cells": plan region R_k x height interval - [lo_k(p), hi_k(p)] where lo/hi are flat or sloped planes (z = a + gx x + gy y). -- Input: arrangement of all section rings (ArcArrangement), and per arrangement - face a list of disjoint z-intervals (bottom plane, top plane). Adjacent - faces with identical interval lists merge (arrangement.regions predicate). -- Faces emitted: - * caps: for each distinct (plane, side) group, regions of the arrangement - where that plane is an interval end -> planar face with arc/line pcurves - (sloped plane: Ellipse2 in plane frame as in sloped_cap_loop). - * walls: for each arrangement edge, the two adjacent cells' interval lists - differ -> the vertical strip set difference (symmetric in z) along the - edge is a wall. Each wall strip on one side between two planes becomes - a face on the carrier (plane for line, cylinder for arc) bounded by - bottom/top curves (line/circle/ellipse) and vertical lines. -- Vertical edges at arrangement vertices must be split at every height - where any incident wall or cap boundary meets that vertex -> build per - vertex a sorted list of distinct z values and emit vertical edges between - consecutive ones, shared by all walls at that vertex. -- Mesh compiler tessellates only solids()[0].outer: build ONE shell per - connected component; results with an enclosed void are refused by name - (a stepped column with an internal cavity cannot occur for coaxial - prism booleans of two operands anyway -- verify). -- Planes compared exactly? Levels come from input heights / half-space - planes directly (no derived values), so equality is by value equality of - the defining coefficients -> merge caps only when coefficients are equal. - -### T2 implementation decisions (column.rs) -- Two phases: (A) abstract faces over edge keys Rim(piece, class rep plane) / - Vert(arr vertex, height cluster k); (B) union-find faces by edge keys -> - shells; each key must be used exactly twice (>2 = touching along an edge, - refused by name); emit one ExactBRep per outer shell. -- Height classes per piece: planes equal (tol) at start/mid/end of the piece. - Vertex heights: cluster rim endpoint heights per arrangement vertex (tol). -- Walls: per piece, symmetric difference of the two side stacks in class - gaps; maximal same-side runs = one face. Built on the DIRECTED piece with - solid on the left, always face Forward (the tested prism convention); - rim uses Reversed when the directed piece runs against the piece. -- Caps: ArcArrangement::regions(pred: some block ends on plane P from that - side); up = Forward, down = Reversed (build_arc_rings convention). -- Outer vs void shell: signed volume from caps only (walls vertical => - n_z = 0): sum over caps of +-(h A + gx Mx + gy My), Green closed forms for - segments and arcs. Voids attach to the single outer shell; several outer - shells plus a void is refused (unreachable for 2-operand prism booleans: - a void needs a difference whose tool is enclosed, so one component). -- Mesh compiler must tessellate void shells too (currently outer only). diff --git a/PLAN-exact-curves.md b/PLAN-exact-curves.md deleted file mode 100644 index 85597a23..00000000 --- a/PLAN-exact-curves.md +++ /dev/null @@ -1,78 +0,0 @@ -# Exact curves: nested radicals, conics, #155, #120 - -Standing goal (user, 2026-09): take on the "not done" list of `axiolid-exact` -(values with more than one nested square root, conics), then #155 (exact arc -booleans) and #120 (curved B-rep booleans) so the crate has real users. - -## Milestones - -| # | What | State | -|---|------|-------| -| M1 | `Tower`: nested square roots, any depth (capped at 6) | done, a6636bb | -| M2 | `IntPoly` + Sturm + `RealRoot`: exact real roots of integer polys | done, 9659680 | -| M3 | `Conic`: exact line/conic and conic/conic points | done, 17961b2 | -| M4 | #155: exact arc overlay replaces `cavalier_contours` | done, ADR 0070 | -| M5 | #120: coaxial curved booleans with holes and raised bases | done (C9 stays narrow; #120 open on #119) | -| M6 | ledger rows, ADR, changelogs, close issues, gate, push, CI | pending | - -M1-M3 pushed (a85fd09), gate + CI green. - -## M4 design: exact arc overlay (#155) - -Public API unchanged: `arc_overlay(subject, clip, op, tolerance)`. -`tolerance` only feeds `validate_arc_ring`; no decision uses it. - -### Arc edge, exactly -Edge P0 -> P1 (f64), bulge b, chord D = P1 - P0, perp(x, y) = (-y, x). -- Circle (conic form, all coefficients exact dyadic): with k = 4b, - kC = k*M + (1 - b^2) * perp(D) (M = chord midpoint), - k^2 |X|^2 - 2 X.(k*kC) + |kC|^2 - |D|^2 (1 + b^2)^2 = 0. -- On-arc test for X on the circle: X is on the arc iff - orient(P0, P1, X) == -sign(b), or X is P0 or P1. (The chord line splits - the circle into two arcs; b > 0 runs CCW through the right side.) -- Rational parametrization by s in [0, b] (s = b at P0, s = 0 at P1): - w = b (1 + s^2)^2, g = (b - s)(1 + b s), - X(s) = P0 + (g / w) * ((1 - s^2) D - 2 s perp(D)). - Dyadic s gives an exact dyadic homogeneous point on the arc. This is the - half-angle substitution s = tan(phi/2); the bulge is the parameter bound. -- Order along the arc: A before B iff sign(b) * orient(P0, A, B) > 0. - -### Exact points -x = (ax + bx sqrt(d)) / w, y = (ay + by sqrt(d)) / w, all dyadic, w != 0. -Covers f64 vertices, segment/segment (rational), line/circle and -circle/circle (one radical per point). Predicates on up to three points -embed them in one `Tower` (depth <= 3) and run filter-then-exact. - -### Algorithm -1. Validate; orient both operands CCW. -2. Split points per edge: all subject x clip edge pairs (closed edges, so - touching vertices count). Circle/circle via the radical line. - Co-circular and collinear overlaps: split at endpoints lying on the other. -3. Sort split points along each edge exactly, dedup by exact equality. -4. Sample each sub-edge at an exact dyadic interior point (bisection on the - edge parameter, exact comparisons; seeded by f64). -5. Classify the sample against the other operand: - - on its boundary: shared edge; compare tangent directions exactly. - - else ray parity with a dyadic ray direction, re-chosen whenever the ray - hits a vertex or is tangent to a circle (finitely many bad directions). -6. Select (A = subject, B = clip): - - union: A out of B, B out of A, shared same-direction once. - - intersection: A in B, B in A, shared same-direction once. - - difference: A out of B, B in A reversed, shared opposite once. - - xor: all non-shared, the "in" ones reversed; shared dropped. -7. Link at exact vertices; with several choices prefer the same operand's - next edge (touching configurations), else refuse rather than guess. -8. Nest by containment depth (even = outer, odd = hole), exact tests. -9. Output: vertices rounded to f64 once, at the end; sub-arc bulges from the - original circle and the rounded endpoints. - -### Honesty boundary -Topology (crossings, order, in/out, linking, nesting) is exact for the given -f64 input. Output coordinates of constructed points are rounded to f64 once. -Self-intersecting input rings stay outside the contract, as before. - -## M5 scope (#120) -General exact curved B-rep booleans are out of reach here; #120 lands as -`narrow` with the refused subset named. In scope: coaxial prismatic solids -with arc sections (cylinders, rounded walls, round openings), including -results with holes (currently refused), on the M4 overlay. diff --git a/PLAN-geom-layer.md b/PLAN-geom-layer.md deleted file mode 100644 index cf926fc4..00000000 --- a/PLAN-geom-layer.md +++ /dev/null @@ -1,58 +0,0 @@ -# Geometry capability layer — working plan - -Branch `feat/geom-capability-layer`, worktree `/mnt/backup/wt/kernel-geom`. - -## Goal - -Close the three audited gaps against ifc-lite, and add the convex-collision -+ spatial-index layer that lets downstream apps express model-checking rules. - -## Hard constraint: a concurrent session shares this repo - -Another session is expanding `primitives2d`/`primitives3d` and landing on -`main` (HEAD `ae894d8` is theirs). Rules for this branch: - -- **Never edit** `foundation/core/src/primitives2.rs`, `primitives3.rs`, - `mat4.rs`, or anything else in `foundation/core/src/` unless unavoidable. -- New capability goes in **new crates / new files**. -- **Never `git add -A`.** Stage explicit paths only. -- Root `Cargo.toml` is the one shared file both sessions must touch - (workspace members + version pins). Keep edits to appended lines so a - merge is trivial. - -## Workstreams - -| # | Gap | Where | Status | -|---|-----|-------|--------| -| 1 | Constrained Delaunay + quality refinement | new crate `algorithms/planar/triangulate` | pending | -| 2 | Exact arithmetic in the production boolean | `providers/mesh/boolmesh/src/csg/` | pending | -| 3 | Persistent editable half-edge topology | new crate `representations/topology/dcel` | pending | -| 4 | Convex collision: SAT, GJK, EPA, MPR | new crate `algorithms/query/collide` | pending | -| 5 | Octree + k-d tree | new files in `algorithms/query/spatial` | pending | - -## Validation strategy - -- Every new crate carries its own tests; no capability lands untested. -- Gap 2 is the risky one: it changes a shipped numerical path. Required - evidence before commit: - - the existing differential corpus still passes, - - a case that is WRONG before and RIGHT after (otherwise the change is - unmotivated), - - a benchmark delta, because exactness costs time and the cost must be - stated rather than discovered later by a user. -- `scripts/gate.sh` must pass before any push. -- Mutation-test new gates: a check that cannot fail is not a check. - -## Risks / rollback - -- Gap 2 could regress boolean performance badly. If the cost is - unacceptable, the fallback is a filtered cascade (fast path first, exact - only on a straddling filter) rather than unconditional exact arithmetic. -- Drift on `main` from the concurrent session: rebase before push, re-run - the gate on the rebased commit, never force-push shared history. - -## Next concrete action - -Gap 1: scaffold `algorithms/planar/triangulate` with a CDT that preserves -constraint edges, then add Ruppert/Chew refinement behind an explicit -quality target. diff --git a/README.md b/README.md index 73b32bd8..3941ceb3 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,11 @@ Read the [documentation site](https://axiolid.github.io/kernel/) for architectur ## Quick start -Add the facade for core values, meshes, and the portable CPU shell: +The facade compiles nothing until you name a capability. `standard` gives +core values, meshes and the portable CPU shell: -```toml -[dependencies] -axiolid = { git = "https://github.com/axiolid/kernel.git" } +```bash +cargo add axiolid --features standard ``` The always-available core vocabulary is deliberately small: @@ -51,18 +51,11 @@ assert!(origin.is_finite()); assert!(tolerance.linear() >= 0.0); ``` -For narrow dependency graphs, depend directly on leaf crates such as `axiolid-core`, `axiolid-mesh`, or `axiolid-reference`. Feature bundles are named for capability—not an input format: - -```toml -axiolid = { git = "https://github.com/axiolid/kernel.git", features = ["discrete"] } -``` - -General NURBS algorithms are independently opt-in and also included in the -broader `parametric` bundle: - -```toml -axiolid = { git = "https://github.com/axiolid/kernel.git", default-features = false, features = ["nurbs"] } -``` +Feature bundles are named for capability, not an input format +(`--features discrete`, `--features nurbs`, …). For narrow dependency +graphs, depend on leaf crates directly, such as `cargo add axiolid-core +axiolid-mesh`. The [crate reference](https://axiolid.github.io/kernel/reference/) +lists every crate with its features, API documentation and latest changes. See [Getting started](https://axiolid.github.io/kernel/guide/getting-started) before selecting a bundle. Native consumers use the generated [`axiolid.h`](./crates/facade/axiolid-capi/include/axiolid.h) boundary through the stable `Axiolid::axiolid` [CMake source/package workflow](./docs/architecture/native-distribution.md); the [v0.4 ABI, ownership, refusal, and concurrency contract](./docs/architecture/c-abi-v0.4.md) is explicit. diff --git a/RTC-FINDING.md b/RTC-FINDING.md deleted file mode 100644 index b6f709c4..00000000 --- a/RTC-FINDING.md +++ /dev/null @@ -1,140 +0,0 @@ -# RTC step 1: does survey-scale quantisation actually break anything? - -Probe result for the question left open by `fix(measure)`: the input mesh's -own vertices are quantised to ~2 nm at base 1e7, and no downstream fix can -recover that. Is it a real problem? - -**Answer: not for the reason I predicted. Two of my three claims were wrong.** - -## Claim 1: "coincident faces get snapped to different values, creating slivers" - -**Refuted.** 0 of 5 magnitudes lost exact coincidence. - -``` -COINCIDENCE base=0e0 drift=0.000e0 ulp=2.220e-16 sign=Zero -COINCIDENCE base=1e3 drift=0.000e0 ulp=1.137e-13 sign=Zero -COINCIDENCE base=1e5 drift=1.455e-11 ulp=1.455e-11 sign=Zero -COINCIDENCE base=1e6 drift=0.000e0 ulp=1.164e-10 sign=Zero -COINCIDENCE base=1e7 drift=0.000e0 ulp=1.863e-9 sign=Zero -``` - -Even where two *different* arithmetic paths to the same intended coordinate -drifted by a full ULP (base 1e5), `orient3d` still certified `Zero`. The -reason is structural: rounding is deterministic and the predicate is exact -for any finite binary64 input, so coincidence authored consistently survives -regardless of magnitude. - -The end-to-end check agrees. Two boxes sharing a face exactly, union at every -magnitude: - -``` -FLUSH base=0e0 .. 1e7 volume=2.00000000000000000 rel_err=0.000e0 -FLUSH summary: 0 of 5 magnitudes wrong -``` - -Exact at 1e7. The sliver failure mode does not occur. - -## Claim 2: "rebasing makes the filter hit its cheap path more often, so it's faster" - -**Refuted.** Escalation rate is flat. - -``` -ESCALATION base=0e0 .. 1e7 1/2000 = 0.1% fell back to exact -``` - -Identical at every magnitude. The filter's error bound scales with the -operands, so its decision threshold scales too -- magnitude does not push it -toward the slow path. **The performance argument for RTC was wrong, and the -claim should not be repeated.** - -## What IS real: thin features at large coordinates - -``` -PLATE base=1e6 t=1e-4 rel_err=1.667e-5 <-- WRONG -PLATE base=1e7 t=1e-2 rel_err=1.758e-3 <-- WRONG -PLATE base=1e7 t=1e-4 rel_err=1.681e-5 <-- WRONG -``` - -But the attribution probe shows this is **not the boolean**: - -``` -ATTR base=1e7 t=1e-2 input_vol_rel_err=6.913e0 corner_drift=0.000e0 -ATTR base=1e7 t=1e-4 input_vol_rel_err=1.614e5 corner_drift=0.000e0 -ATTR base=1e7 t=1e-6 input_vol_rel_err=2.965e8 corner_drift=0.000e0 -``` - -`corner_drift = 0` everywhere: the authored geometry is **exact**. The plate's -vertices are where they should be. What fails is *measuring* it -- the -divergence-theorem sum over a 1e-6 m feature at 1e7 m cancels catastrophically, -giving errors up to 2.9e8 relative. - -Note the direction: the measurement error (2.9e8) is many orders worse than -the boolean's output error (1.8e-3). The boolean is more accurate than the -tool used to check it. - -`fix(measure)` already re-bases to the mesh's first vertex, which fixes this -for a mesh whose own extent is small. It does not help when the *feature* is -tiny relative to the mesh -- a 1e-6 m plate is below the 1.9e-9 m grid only -by a factor of 500, so its own corners span just a few hundred ULPs. - -## Conclusion - -The originally-stated motivation for an RTC/local-origin facility -- exact -predicates breaking on coincident geometry -- **does not reproduce**. Neither -does the performance argument. - -The real limit is narrower and different: *mass properties of features whose -size approaches the representable grid at their location*. That is a -measurement conditioning problem, already partly addressed, and it does not -require a coordinate-frame facility, an API change, or an offset ownership -model. - -**Recommendation: do not build the RTC facility.** Document the measured -limit, keep these probes as regression tests, and revisit only if a real -workload produces a failure that these probes do not cover. - - -## Reproducing - -``` -cargo test -p axiolid-predicates --test survey_scale -- --nocapture -cargo test -p axiolid-mesh-boolean-boolmesh --test survey_scale -- --nocapture -``` - -## Correction: the per-triangle follow-up was measured against the wrong baseline - -An earlier revision of this document recommended re-basing the volume sum -per-triangle instead of per-mesh, citing an eleven-orders-of-magnitude -improvement. **That recommendation was wrong and is withdrawn.** - -The comparison used `boolmesh/tests/support.rs::volume` as the "per_mesh" -baseline. That helper sums about the WORLD origin. Production -`volume_properties` has summed about a local origin since the RTC fix -(`fix(measure): sum mass properties about a local origin`), so the numbers -compared a per-triangle variant against code that no longer exists in the -measurement path. - -Measured against production directly, on the same thin-plate fixtures: - -``` -PLATE base=1e7 t=1e-2 production=0.000e0 per_triangle=9.313e-10 -PLATE base=1e7 t=1e-4 production=1.355e-16 per_triangle=5.069e-10 -PLATE base=1e7 t=1e-6 production=0.000e0 per_triangle=1.563e-10 -WORST production=1.355e-16 per_triangle=9.313e-10 -``` - -Production is exact or within one ULP at every magnitude and thickness -tested. The per-triangle variant is **worse at every non-zero magnitude**, -by up to six orders of magnitude. - -Why: per-mesh re-basing subtracts a nearby origin once, so every coordinate -entering the cross product is already edge-sized. The per-triangle variant -still forms `a . (ab x ac)` with `a` at full world magnitude -- it shrinks -two operands and leaves the third large. Per-mesh re-basing shrinks all -three. - -**There is no remaining measurement defect to fix.** The thin-plate error -reported in the step-1 probe belongs to the test helper, not to any shipped -code path. The conclusion of this document stands unchanged and is now -stronger: do not build the RTC facility, and do not change -`volume_properties` either. diff --git a/architecture/AGENTS.md b/architecture/AGENTS.md deleted file mode 100644 index c185c3f6..00000000 --- a/architecture/AGENTS.md +++ /dev/null @@ -1,45 +0,0 @@ -# architecture/ - -Machine-checked architecture declarations. - -`closure-profiles.toml` declares minimal downstream dependency closures. Each -profile names an isolated consumer fixture under `tests/consumers/`, the exact -internal packages that must be present, and the packages that must be absent. - -Verify with: - -```bash -cargo xtask architecture closure check -cargo xtask architecture closure explain -``` - -A closure change is an API change. Update `expected_internal` deliberately and -record the reason in an ADR — never to silence a failing gate. - -## Capability ledger (what to build next) - -`capability-ledger.toml` grades every geometry capability found in OCCT and -CGAL against Axiolid (implemented, narrow, scoped or absent), with evidence -paths, reference packages at pinned commits, and the tracking issue. -`reference-packages.toml` lists every package path in those pinned trees. - -```bash -cargo xtask gaps # ready work by priority, then blocked work -cargo xtask gaps show 119 # or a row id (B10) or issue key -cargo xtask gaps list --area brep --open -``` - -A landing that changes a row's level edits the row in the same commit. The -gate (`cargo xtask gaps check`) rejects evidence that no longer resolves, -reference paths outside the pinned trees, and dangling issue keys. It does -not judge whether a grade is true; review does. - -`scoped` rows are deliberately not built and carry a `scope_rationale`. -Reopening one is a maintainer decision: argue against the rationale in an -issue first. An issue with `needs_decision` is listed apart by `gaps`; ask -the maintainer, do not pick an option and start coding. - -Re-pinning OCCT or CGAL means regenerating `reference-packages.toml` from the -new tree and re-checking every row's reference paths. The OCCT/CGAL sources -are not needed for anything else: the ledger carries what the comparison -learned from them. diff --git a/crates/AGENTS.md b/crates/AGENTS.md deleted file mode 100644 index 6659af57..00000000 --- a/crates/AGENTS.md +++ /dev/null @@ -1,40 +0,0 @@ -# Crates - -This directory is physically organized by architectural ownership. Folders communicate ownership; Cargo packages remain the actual dependency/trust/compilation boundaries. - -## Direct children - -- `foundation/` — unique dependency root (`axiolid-core`). -- `representations/` — portable analytic, region, topology, B-rep, mesh, sampled-field, and authored-graph values. -- `contracts/` — guarantees, common vocabulary, mesh admissibility, and operation-specific portable schemas. -- `algorithms/` — format-neutral reference, parametric, construction, planar, query, sampled, and repair implementations. -- `providers/` — concrete optional operation providers. -- `execution/` — provider dispatch, graph execution, and CPU/GPU contexts/adapters. -- `facade/` — feature-gated public `axiolid` package. - -Read the nested `AGENTS.md` before editing a child. - -## Dependency rules - -`cargo xtask architecture check` validates package metadata, explicit members, exact internal dependency allowlists, production/build role direction, nested placement, source-format neutrality, placeholders, unsafe policy, and generated-doc freshness. - -Production direction is: - -```text -foundation <- representations <- contracts <- algorithms/providers <- execution <- facade -``` - -This is a role DAG, not a license to depend on every earlier layer. Exact package edges remain allowlisted. Contracts may depend on representation values required by typed schemas, but never providers or dispatch. Algorithms do not select execution policy. Explicit dev-only upward edges are limited to integration/conformance tests. - -## Required gates - -From repository root: - -```bash -cargo xtask architecture check -scripts/probe_layering_gate.sh -scripts/geometry-feature-matrix.sh -cargo test --workspace --all-features -``` - -Run `cargo xtask architecture docs` after metadata/package changes, then rerun the checker. diff --git a/crates/PLAN.md b/crates/PLAN.md deleted file mode 100644 index b42b0b80..00000000 --- a/crates/PLAN.md +++ /dev/null @@ -1,38 +0,0 @@ -# Geometry package plan - -Status: active after ADR 0035 -Last updated: 2026-09-01 - -Read `AGENTS.md` for standing ownership rules. Canonical implemented structure is in [ADR 0035](../docs/adr/0035-nested-ownership-and-capability-contracts.md) and the generated [crate map](../docs/architecture/crate-map.md). - -## Standing invariants - -- Nested ownership folders and 32 explicit Cargo packages. -- Foundation/representation packages stay independently consumable. -- Stable, typed operation contracts for tessellation, mesh Boolean, mesh section, and graph-to-mesh. -- Exact B-rep and mesh result domains remain separate; `MeshCompiler` is explicitly mesh-valued. -- `axiolid-dispatch` owns provider ordering/fallback; contracts never select providers. -- `axiolid-reference` is the portable oracle; adopted implementations are providers. -- `axiolid-field` values and `axiolid-field-ops` algorithms are separate. -- `openbim.geometry` claim mappings remain external; Rust contracts contain no Pkl runtime or schema types. - -## Shape of the work - -1. Add a portable provider only after its typed contract, evidence, refusal, and conformance behavior are executable. -2. Expand exact operations without silently tessellating exact intent. -3. Add measured SIMD/parallel/GPU providers with differential reference tests. -4. Add bounded execution caches and workload diagnostics without leaking plans into contracts. -5. Extend downstream format adapters while keeping all source-format interpretation outside Axiolid. - -## Required gates - -```bash -cargo +1.88.0 xtask architecture check -scripts/probe_layering_gate.sh -scripts/probe_boolean_contract.py -scripts/geometry-feature-matrix.sh -scripts/gate.sh -cd docs && npm ci --ignore-scripts && npm run docs:build -``` - -Performance claims require repeatable benchmarks. Capability claims require implementation, diagnostics, and conformance evidence; scaffolded schemas are not capabilities. diff --git a/crates/algorithms/AGENTS.md b/crates/algorithms/AGENTS.md deleted file mode 100644 index 1bf4ccf1..00000000 --- a/crates/algorithms/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Algorithms - -Format-neutral implementations. Children group reference, parametric, construction, planar, query, sampled, and repair algorithms. diff --git a/crates/algorithms/construction/AGENTS.md b/crates/algorithms/construction/AGENTS.md deleted file mode 100644 index d399b3e0..00000000 --- a/crates/algorithms/construction/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Construction algorithms - -`construct/` owns explicit geometry construction operations; result domains stay explicit. diff --git a/crates/algorithms/construction/brep-boolean/AGENTS.md b/crates/algorithms/construction/brep-boolean/AGENTS.md deleted file mode 100644 index 082538ea..00000000 --- a/crates/algorithms/construction/brep-boolean/AGENTS.md +++ /dev/null @@ -1,35 +0,0 @@ -# axiolid-brep-boolean - -General exact booleans between exact B-reps with analytic faces: the -general-fuse pipeline of ADR 0075, built in stages. - -## Pipeline and module ownership - -- `section.rs`: section edges -- every face pair's exact intersection curve - (`axiolid_nurbs::exact_surface_intersection`), cut where it crosses a - boundary edge (transversally: against the ADJACENT face's surface, or - across a seam against the plane through the ruling), membership from - `axiolid_measure::FaceDomain`. -- `split.rs`: one face cut along its section edges into regions, in the - face's parameters, with exact pcurves (planes and cylinders in stage 1). -- Later slices add classification, selection and sewing, each in its own - module. - -## Rules - -- Never approximate: a pair or configuration a stage cannot build exactly is - refused by name (`BooleanError`), never meshed or fitted. -- Decisions come from exact predicates or certified membership; a point too - close to a boundary to decide is refused, not guessed. -- Every result test checks the exact volume identity with - `axiolid_measure::exact_properties` once solids are assembled. - -## Verification - -- `tests/section_edges.rs`: section edges lie on both boundaries, close into - loops, and match closed-form curves. -- `tests/split_faces.rs`: regions cover each face exactly and match - closed-form areas. -- Never cut a section with an exact curve/curve test against a boundary - edge: two curves on one surface meet only up to the rounding of their - doubles. Cut against a transverse surface. diff --git a/crates/algorithms/construction/brep-boolean/Cargo.toml b/crates/algorithms/construction/brep-boolean/Cargo.toml index ff8a60e9..561dbb78 100644 --- a/crates/algorithms/construction/brep-boolean/Cargo.toml +++ b/crates/algorithms/construction/brep-boolean/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-brep.workspace = true diff --git a/crates/algorithms/construction/brep-boolean/README.md b/crates/algorithms/construction/brep-boolean/README.md new file mode 100644 index 00000000..afa48d08 --- /dev/null +++ b/crates/algorithms/construction/brep-boolean/README.md @@ -0,0 +1,27 @@ +# axiolid-brep-boolean + +Exact union, intersection and difference of two exact B-rep solids whose +faces lie on planes, cylinders, elliptical cylinders, cones, spheres, tori +and B-spline surfaces: the general-fuse pipeline of ADR 0075 (section edges, +face splitting in each face's own parameters, certified classification, +sewing). Operands that touch rather than cross are handled. Nothing is +meshed or fitted: a configuration the pipeline cannot build exactly is +refused with a typed error. It does not tessellate its result and does not +work on meshes; mesh booleans are operation providers selected through the +execution layer. + +```bash +cargo add axiolid-brep-boolean +``` + +- API documentation: [docs.rs/axiolid-brep-boolean](https://docs.rs/axiolid-brep-boolean) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-brep-boolean) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) + +## Design notes + +`axiolid-construct` keeps narrower exact booleans that predate this crate: +planar polyhedra (`polyhedron::boolean_polyhedra_exact`) and coaxial +prisms and plane cuts over one arc arrangement (`boolean_exact`). They are +independent exact paths, and this crate's tests check vertical-column +results against them. diff --git a/crates/algorithms/construction/brep-boolean/src/lib.rs b/crates/algorithms/construction/brep-boolean/src/lib.rs index 0c08aa6e..9bdcdc15 100644 --- a/crates/algorithms/construction/brep-boolean/src/lib.rs +++ b/crates/algorithms/construction/brep-boolean/src/lib.rs @@ -15,6 +15,11 @@ //! surface, sections running along existing edges, tangent contact, and //! solids meeting along an edge or at a point are all handled, not //! refused. +//! +//! Nothing is approximated. A configuration a step cannot build exactly is +//! refused by name ([`BooleanError`]), never meshed or fitted, and every +//! decision comes from an exact predicate or certified membership: a point +//! too close to a boundary to decide is refused, not guessed. mod assemble; mod classify; diff --git a/crates/algorithms/construction/construct/AGENTS.md b/crates/algorithms/construction/construct/AGENTS.md deleted file mode 100644 index d091b5ba..00000000 --- a/crates/algorithms/construction/construct/AGENTS.md +++ /dev/null @@ -1,67 +0,0 @@ -# `axiolid-construct` - -Scalar geometry-construction algorithms over neutral representations. This is **L2**: -it accepts exact profiles, curves, primitives, certified NURBS traces, and explicit -policy; it creates the broad `TriMesh` reference path plus focused exact extrusion and -analytic B-rep arrangements; it owns no DAG, cache, execution context, or operation-provider dispatch. - -## Entry points - -- `profile`: lower profile values to sampled rings and triangulate caps. -- `extrude`: mesh extrusion plus exact extrusion of every `Profile` variant -- - rectangle (sharp/hollow), circle, ellipse, contour (arcs and holes), section, - centre-line, derived and composite (disjoint members as separate solids). Exact output owns every 3D support, - pcurve, and native span. -- `revolve`: exact full-turn revolution of any profile that lowers to a contour, - circles and sections with holes included, sweeping cylinders, cones, planar - annuli and tori, with each hole a void shell; `sweep`, `loft` place/stitch - station rings into discrete solids. -- `center_line`: turn constant-width centre-line profiles into rings. -- `half_space`: construct a finite clipping proxy for an unbounded half-space. -- `trimmed_intersection`: promote one certified affine trace into two closed trimmed - faces on its boundary-owned patch and an explicit embedded pcurve on the containing - unsplit face; see ADR 0029. - -`BACKEND_ID` is `scalar-generate`. Use it for every diagnostic raised here; do -not report `scalar-compile` after this split. - -## Invariants - -- Every tolerance-sensitive entry point receives an explicit `Tolerance` and/or - chord budget. Never introduce a global default epsilon. -- Profile mismatch, unbounded construction, invalid frames, insufficient rings, - and unmet subdivision budgets refuse with `GeomError`; never invent a mesh. -- Shared loft/stitching logic owns winding and cap pairing. Do not duplicate it - in individual sweep families. -- This crate must not depend on `axiolid-model`, an execution/backend crate, or - any L3 crate. `cargo xtask architecture check` enforces the declared internal - dependency allowlist and production role-DAG edge; `scripts/probe_layering_gate.sh` - mutation-verifies that enforcement. -- Discrete sweeps remain the broad reference path for `sweep` and `loft`. Exact - solid output covers every profile variant for extrusion, and full-turn - revolution for any profile that lowers to a contour; the certified affine - trimmed-intersection arrangement is a separate exact surface slice. - What still refuses is geometry the kernel cannot represent exactly rather - than unwritten work -- partial-turn revolution, a profile straddling the - revolution axis, oblique circle/ellipse extrusion, non-conformal derived - transforms, ellipse revolution, and Boolean families beyond - vertical columns -- and must refuse rather than tessellate; see ADR 0020, - ADR 0023, ADR 0024, ADR 0029, and ADR 0053-0059. -- Coaxial booleans and plane cuts that are not one prism (stepped spans, - a cut crossing a cap) go through `column.rs` over one exact - `ArcArrangement` (ADR 0072); `boolean_column.rs` adapts the entry points. - Every face of a column solid takes its vertices from that one - arrangement -- do not build bands separately and glue them, the rounded - crossings will not agree. A cavity is a void shell of the solid around - it; the mesh compiler tessellates void shells facing into the cavity. -- Composite profiles are unioned by `profile_lower::composite_regions` over - one `ArcArrangement`; each connected piece is built on its own and - `assemble::merge_solids` carries disjoint pieces as separate solids of one - `ExactBRep`. A revolved section's holes join as voids through - `ExactBRepBuilder::append`. - -## Tests - -Unit-like generation tests live in `tests/` here. Tests that verify a generated -mesh is accepted by an L3 Boolean provider stay in `axiolid-mesh-compile/tests/`: -letting this L2 crate dev-depend on that provider violates the tier boundary. diff --git a/crates/algorithms/construction/construct/Cargo.toml b/crates/algorithms/construction/construct/Cargo.toml index e0246031..f9e52f1d 100644 --- a/crates/algorithms/construction/construct/Cargo.toml +++ b/crates/algorithms/construction/construct/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-brep.workspace = true diff --git a/crates/algorithms/construction/construct/PLAN.md b/crates/algorithms/construction/construct/PLAN.md deleted file mode 100644 index b51eed59..00000000 --- a/crates/algorithms/construction/construct/PLAN.md +++ /dev/null @@ -1,91 +0,0 @@ -# axiolid-construct — known limits and open work - -Findings that outlived the session that produced them. Each entry records -what was measured, what was tried, and what a fix would actually involve, -so a later pass does not re-derive it or repeat a reverted approach. - -## Repeated grid-aligned subtraction refuses (open) - -`boolean_polyhedra_exact` refuses partway through a long chain of -axis-aligned differences. The depth-2 Menger sponge void set (147 -consecutive subtractions from a unit cube) refuses at subtraction 82. -Reproduce with the benchmark harness at `axiolid/benchmarks`: -`AXIOLID_MENGER_DEPTH=3 cargo run --release -- 1`. - -### What was measured - -The refusal is `every probe direction met a vertex or edge exactly`. The -cause is NOT an unlucky ray: 4 directions and 12 directions both fail at -exactly step 82. The blocking face was identified as a collapsed quad -- - -``` -(1.0, 1.0, 0.3333333333333333 ) (0.6666666666666666, 1.0, 0.333...3) -(1.0, 1.0, 0.33333333333333326) (0.6666666666666666, 1.0, 0.333...3) -``` - --- spanning a third of the model, vertices paired, the pairs ONE ULP -apart. It encloses no area, so its normal is meaningless and every ray -meets it edge-on. No probe direction can classify a face with no plane. - -Such rings arise because `plane_crossing` builds intersection coordinates -in f64 (ADR 0045). A vertex that should be shared between operands lands -a few ULPs apart after several operations, and splitting through it emits -a face that corresponds to nothing in the modelled solid. - -### What was tried and reverted - -Commit `40b5069` dropped split fragments enclosing no area at f64 -precision, and was reverted. It DID clear the blockage -- all 147 -subtractions completed -- but the answers were wrong: depth-2 volume was -short by 4.57e-4 against a cell volume of 1.37e-3, roughly a third of a -cell. Deleting the collapsed faces opens holes in the shell, and an open -shell integrates to a wrong volume. - -That is worse than the refusal it replaced. A refusal is actionable; a -plausible wrong volume is silent. **Do not reintroduce a drop-based fix.** - -### Why exact construction is not the answer either - -ADR 0045 declines exact constructions, and the benchmark data supports -that for this case. Against CGAL's exact-construction kernel on the -thin-overlap sweep, Axiolid tracks within ~1.5x down to 1e-12, and at -1e-15 both are catastrophically wrong because the ambiguity is in the -input representation rather than the arithmetic. - -### The actual shape of a fix - -The collapsed rings are a SYMPTOM. Two candidate directions, neither -attempted: - -1. Make coincident split points exactly coincident, so the drift never - arises. This is snapping, which ADR 0045 rejects by name -- it would - need a superseding ADR. -2. Refuse on a collapsed ring instead of deleting it, keeping the - diagnosis without the wrong answer. Strictly better than the current - state, and does not touch ADR 0045. - -`boolean_polyhedra_exact` has no production consumers today, so neither -is urgent. The blast radius of `plane_crossing` is one call site. - -## Thin overlap below 1e-12 produces an open shell (open) - -Found by the contact matrix, not by the sponge. Two unit boxes -overlapping by `eps` along +X: - -``` -eps 1e-3 : 14 faces, 28 tris, 28 usable, 0 degenerate, 0 boundary edges -eps 1e-6 : same -eps 1e-9 : same -eps 1e-12: 14 faces, 28 tris, 20 usable, 8 degenerate, 8 boundary edges -eps 1e-15: same -``` - -From 1e-12 the union and intersection come back with a hole, so they -cannot be measured. This is NOT a precision floor: a 1e-12 slab on unit -boxes is ~4503 ULPs wide, comfortably resolvable in f64. It is a defect -in how a very thin overlap is split. - -`tests/contact_matrix.rs` asserts this limit explicitly rather than -loosening its expectations. When it is fixed, delete the -`Outcome::Unmeasurable(_) if eps <= 1e-12` branches and the sweep will -hold the fix in place. diff --git a/crates/algorithms/construction/construct/README.md b/crates/algorithms/construction/construct/README.md new file mode 100644 index 00000000..9e026a7e --- /dev/null +++ b/crates/algorithms/construction/construct/README.md @@ -0,0 +1,28 @@ +# axiolid-construct + +Solid generation from exact inputs: extrusion, revolution, sweeps, lofts, +centre-line profiles, half-space clipping proxies, offsets, fillets and +chamfers on supported families, and focused exact booleans (planar +polyhedra, coaxial column solids). Every `Profile` variant extrudes to an +exact B-rep, and full-turn revolution covers any profile that lowers to a +contour; sweeps and lofts produce meshes by default. Geometry the kernel cannot +represent exactly is refused, never tessellated in its place. The crate +takes geometry and returns geometry: it owns no operation graph, cache, +execution context or provider dispatch (ADR 0023); `axiolid-mesh-compile` +does those and calls in here. + +```bash +cargo add axiolid-construct +``` + +- API documentation: [docs.rs/axiolid-construct](https://docs.rs/axiolid-construct) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-construct) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) + +## Design notes + +Tests that check a generated mesh is accepted by a mesh Boolean provider +live in `crates/execution/compile/tests/`, not here. A dev-dependency on a +provider or execution crate would pull this algorithms crate's tests above +its tier; the architecture check only enforces the tier edge for normal +dependencies, so the allowlist in `Cargo.toml` is what keeps it out. diff --git a/crates/algorithms/construction/construct/src/column.rs b/crates/algorithms/construction/construct/src/column.rs index 692e1f93..cdf24fcb 100644 --- a/crates/algorithms/construction/construct/src/column.rs +++ b/crates/algorithms/construction/construct/src/column.rs @@ -5,7 +5,9 @@ //! each cell the solid occupies a stack of height intervals, each bounded //! below and above by a plane (flat, or sloped as in ADR 0071). The cells //! come from one exact [`ArcArrangement`], so every face shares its vertices -//! with every other face by index. +//! with every other face by index. Building bands separately and gluing them +//! does not work: their crossings are rounded independently and will not +//! agree. //! //! # Faces //! diff --git a/crates/algorithms/construction/construct/src/polyhedron.rs b/crates/algorithms/construction/construct/src/polyhedron.rs index aa3b4168..43b6e4b6 100644 --- a/crates/algorithms/construction/construct/src/polyhedron.rs +++ b/crates/algorithms/construction/construct/src/polyhedron.rs @@ -11,6 +11,16 @@ //! input planes, so a vertex is at worst one intersection away from input //! data. Classification then asks a certified predicate which side of the //! other solid a fragment lies on. +//! +//! # Collapsed fragments are not dropped +//! +//! Split points are constructed in f64 (ADR 0045, `plane_crossing`), so +//! after many operations a vertex two operands should share can land a few +//! ULPs apart, and a split through it emits a fragment that encloses no +//! area. Deleting such fragments was tried and reverted (`40b5069`): the +//! chain then completes, but the holes it leaves make the shell integrate to +//! a plausibly wrong volume. A refusal is actionable; a wrong volume is +//! silent. Do not reintroduce a drop-based fix (#199). use crate::boolean_exact::unsupported; use axiolid_contracts::{GeomError, GeomResult}; diff --git a/crates/algorithms/construction/construct/tests/contact_matrix.rs b/crates/algorithms/construction/construct/tests/contact_matrix.rs index 059bb729..f0ed3947 100644 --- a/crates/algorithms/construction/construct/tests/contact_matrix.rs +++ b/crates/algorithms/construction/construct/tests/contact_matrix.rs @@ -328,7 +328,7 @@ fn a_shrinking_negative_overlap() { let a = box_solid([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]); let b = box_solid([1.0 - eps, 0.0, 0.0], [2.0 - eps, 1.0, 1.0]); - // KNOWN GAP, measured not assumed: from eps = 1e-12 the union comes + // TODO(#200): KNOWN GAP, measured not assumed: from eps = 1e-12 the union comes // back with 8 degenerate triangles and 8 boundary edges -- a hole in // the shell, so it cannot be measured. That is NOT a precision floor: // a 1e-12 slab on unit boxes is ~4503 ULPs wide, comfortably diff --git a/crates/algorithms/construction/construct/tests/extrusion.rs b/crates/algorithms/construction/construct/tests/extrusion.rs index 56715773..2e6c7b31 100644 --- a/crates/algorithms/construction/construct/tests/extrusion.rs +++ b/crates/algorithms/construction/construct/tests/extrusion.rs @@ -1,8 +1,9 @@ //! Gates for profile flattening and extrusion. //! -//! Signed volume is the single check that catches wrong winding, missing caps, -//! and inverted sides simultaneously: it is positive exactly when the solid is -//! closed and outward-oriented, and its magnitude is `area * depth`. +//! Signed volume catches missing caps and inverted sides at once: its +//! magnitude is `area * depth` and its sign is the orientation. It cannot see +//! a flipped cap in the z = 0 plane, which contributes nothing to the +//! integral, so directed-edge parity (`assert_edge_manifold`) gates winding. use axiolid_construct::extrude::{extrude, extrude_profile, outward_orientation}; use axiolid_construct::profile::{profile_rings, triangulate, Rings}; diff --git a/crates/algorithms/discrete/decimate/Cargo.toml b/crates/algorithms/discrete/decimate/Cargo.toml index 4297699f..9480888e 100644 --- a/crates/algorithms/discrete/decimate/Cargo.toml +++ b/crates/algorithms/discrete/decimate/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/algorithms/discrete/decimate/PLAN.md b/crates/algorithms/discrete/decimate/PLAN.md deleted file mode 100644 index b87b58b9..00000000 --- a/crates/algorithms/discrete/decimate/PLAN.md +++ /dev/null @@ -1,46 +0,0 @@ -# axiolid-decimate — plan - -Status: edge-collapse decimation implemented with a bounded, reported -deviation. This is planning context, not standing agent instruction. - -## Implemented - -- `decimate` with two targets: `TriangleBudget` and `MaxDeviation`. A budget - still honours the caller's tolerance as a deviation ceiling, so "reduce to - N triangles" cannot licence arbitrary damage. -- Deviation is cumulative per vertex, so repeated collapses cannot drift past - the bound one small step at a time. -- `DecimateReport` states collapses performed, refusals by cause, and the - largest distance any vertex actually moved. -- Deterministic candidate order (length, then vertex index), verified by a - repeated-run equality test. -- Collapse safety: the link condition (endpoints must share exactly the two - triangles on the edge) and a normal-inversion check. - -## Verified by mutation - -- Removing the link condition fails `an_unsafe_collapse_is_refused_not_performed`. - -## Known verification gap - -The **normal-inversion branch is not covered by a mutation probe.** On every -fixture tried, the link condition rejects an unsafe collapse before the -inversion check runs, so removing the inversion check alone leaves all tests -green. The branch is written and reachable in principle, but its absence is -currently undetectable by this suite. - -Closing this needs a fixture where a collapse satisfies the link condition -and *still* flips a normal — a non-convex configuration where the merged -vertex crosses a neighbouring triangle's plane while keeping exactly two -shared neighbours. That is a genuine fixture-construction problem, not a -line of code, which is why it is recorded rather than quietly left. - -## Not implemented - -- Isotropic remeshing and subdivision refinement (out of scope for #75). -- Sharp-feature detection. Boundary vertices are preserved implicitly by the - link condition rather than by an explicit crease angle test; a caller - cannot yet opt into losing them. -- Quadric error metrics. The current cost is edge length and the placement is - the midpoint, which is simple and predictable but not optimal for a given - triangle budget. diff --git a/crates/algorithms/discrete/decimate/README.md b/crates/algorithms/discrete/decimate/README.md new file mode 100644 index 00000000..1ce08b23 --- /dev/null +++ b/crates/algorithms/discrete/decimate/README.md @@ -0,0 +1,19 @@ +# axiolid-decimate + +Edge-collapse decimation of triangle meshes with a bounded, reported +deviation. The caller asks for a triangle budget or a maximum deviation; +either way the result never moves a vertex further than the caller's +bound, and `DecimateReport` states the collapses performed, the refusals +by cause and the largest distance any vertex actually moved. Collapses that +would invert a triangle or create a non-manifold edge are refused. Output +is deterministic. It does not remesh isotropically, detect sharp features +or use quadric error metrics: the cost is edge length and the new vertex +is the edge midpoint. For adding triangles instead, see `axiolid-refine`. + +```bash +cargo add axiolid-decimate +``` + +- API documentation: [docs.rs/axiolid-decimate](https://docs.rs/axiolid-decimate) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-decimate) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/discrete/decimate/src/collapse.rs b/crates/algorithms/discrete/decimate/src/collapse.rs index 61390702..a459e054 100644 --- a/crates/algorithms/discrete/decimate/src/collapse.rs +++ b/crates/algorithms/discrete/decimate/src/collapse.rs @@ -193,7 +193,7 @@ fn run( /// Attempt one collapse, returning the new triangle list or nothing. /// -/// Rejects the three ways a collapse damages a mesh: +/// Rejects the two ways a collapse damages a mesh: /// /// - **Inversion.** A triangle whose normal flips has turned inside out. A /// decimator that permits this produces exactly the defect @@ -201,8 +201,10 @@ fn run( /// - **Non-manifold edges.** Collapsing an edge whose endpoints share /// neighbours other than the two triangles on it welds unrelated sheets /// together. -/// - **Boundary loss.** A vertex on a boundary keeps its position rather -/// than being averaged inward, so the silhouette survives. +/// +/// Boundary vertices are not pinned: a collapse may move a vertex on an open +/// boundary to an edge midpoint like any other vertex, and the deviation +/// bound is what limits how far the silhouette moves. fn try_collapse( triangles: &[[u32; 3]], positions: &[Point3], diff --git a/crates/algorithms/discrete/decimate/tests/decimation.rs b/crates/algorithms/discrete/decimate/tests/decimation.rs index 5cc00ac9..dc258704 100644 --- a/crates/algorithms/discrete/decimate/tests/decimation.rs +++ b/crates/algorithms/discrete/decimate/tests/decimation.rs @@ -203,7 +203,8 @@ fn a_ragged_index_buffer_is_refused() { /// Verified by mutation: removing the link condition makes this test fail. /// The normal-inversion guard alongside it is NOT verified by this fixture /// -- the link condition rejects first, so the inversion branch never runs. -/// See the crate PLAN.md for that gap. +/// TODO(#201): add a fixture that passes the link condition and still flips a +/// normal. #[test] fn an_unsafe_collapse_is_refused_not_performed() { let positions = vec![ diff --git a/crates/algorithms/discrete/decompose/Cargo.toml b/crates/algorithms/discrete/decompose/Cargo.toml index 88245661..220dd8ce 100644 --- a/crates/algorithms/discrete/decompose/Cargo.toml +++ b/crates/algorithms/discrete/decompose/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] ahash.workspace = true diff --git a/crates/algorithms/discrete/decompose/README.md b/crates/algorithms/discrete/decompose/README.md new file mode 100644 index 00000000..8d96c644 --- /dev/null +++ b/crates/algorithms/discrete/decompose/README.md @@ -0,0 +1,17 @@ +# axiolid-decompose + +Convex decomposition of a closed triangle-mesh solid. `Strategy::Exact` +splits at reflex features until every part is convex and the union +reproduces the input; `Strategy::Approximate` stops once each part is +within a stated concavity bound, giving far fewer parts. The returned +`Decomposition` always says which it is, and the approximate path reports +the concavity it actually reached. It works on meshes only, not on exact +B-reps. + +```bash +cargo add axiolid-decompose +``` + +- API documentation: [docs.rs/axiolid-decompose](https://docs.rs/axiolid-decompose) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-decompose) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/discrete/minkowski/Cargo.toml b/crates/algorithms/discrete/minkowski/Cargo.toml index f24ee540..3525cab5 100644 --- a/crates/algorithms/discrete/minkowski/Cargo.toml +++ b/crates/algorithms/discrete/minkowski/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/algorithms/discrete/minkowski/README.md b/crates/algorithms/discrete/minkowski/README.md new file mode 100644 index 00000000..ff890d2c --- /dev/null +++ b/crates/algorithms/discrete/minkowski/README.md @@ -0,0 +1,17 @@ +# axiolid-minkowski + +Minkowski sum and difference of closed planar-faced (triangle-mesh) solids. +The sum of two convex solids is computed exactly as the hull of pairwise +vertex sums; non-convex operands are decomposed into convex parts and the +pairwise sums unioned through a caller-supplied mesh Boolean provider, +under a budget. The difference is computed as an erosion, not as a hull of +pairwise differences, and refuses a non-convex subject rather than return a +result that is too large. Curved operands are refused. + +```bash +cargo add axiolid-minkowski +``` + +- API documentation: [docs.rs/axiolid-minkowski](https://docs.rs/axiolid-minkowski) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-minkowski) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/discrete/refine/Cargo.toml b/crates/algorithms/discrete/refine/Cargo.toml index 4dabb841..e3937a56 100644 --- a/crates/algorithms/discrete/refine/Cargo.toml +++ b/crates/algorithms/discrete/refine/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] ahash.workspace = true diff --git a/crates/algorithms/discrete/refine/README.md b/crates/algorithms/discrete/refine/README.md new file mode 100644 index 00000000..3937b392 --- /dev/null +++ b/crates/algorithms/discrete/refine/README.md @@ -0,0 +1,17 @@ +# axiolid-refine + +Mesh refinement and Laplacian smoothing with bounded, reported deviation. +Refinement splits triangles; when the source surface of a tessellated +B-rep is supplied, each new vertex is placed on that surface instead of at +the edge midpoint, so refinement converges on the real geometry rather than +subdividing the facets. Smoothing keeps boundary vertices bit-identical by +default. It does not reduce triangle counts (see `axiolid-decimate`) and +does not implement limit-surface subdivision schemes. + +```bash +cargo add axiolid-refine +``` + +- API documentation: [docs.rs/axiolid-refine](https://docs.rs/axiolid-refine) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-refine) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/exact/AGENTS.md b/crates/algorithms/exact/AGENTS.md deleted file mode 100644 index e6b76145..00000000 --- a/crates/algorithms/exact/AGENTS.md +++ /dev/null @@ -1,40 +0,0 @@ -# axiolid-exact instructions - -Purpose: exact *constructions* over `f64` input (ADR 0068). Signs of values -`f64` cannot hold -- where segments cross, where a line meets a circle -- -decided in two passes over one expression: outward-rounded intervals, then -dyadic big integers (`num-bigint`) only if the interval cannot decide. - -Allowed internal dependencies: `axiolid-core`, `axiolid-guarantees`. The one -external is `num-bigint`, kept behind `Dyadic` so it can be swapped. Keep -`axiolid-predicates` free of it: consumers who need only certified signs of -input points must not pay for big integers. - -## Module ownership - -`arith.rs` the shared `Arith` trait; `interval.rs` fast tier; `dyadic.rs` -exact tier; `certify.rs` the two-tier driver and `ExactError`; `root.rs` -`(a + b*sqrt(c)) / d` signs and comparisons; `construct.rs` public -constructions (crossings, line/circle hits, ordering along a line). - -## Invariants - -- One expression, two tiers. Write sign questions as `SignExpr` against - `Arith`, never as a separate f64 fast path, so the filter and the exact - fallback cannot evaluate different polynomials. -- No division, no square roots evaluated. Clear denominators; decide - root signs by squaring with case analysis (`root.rs`). -- An interval may say "undecided"; it must never be wrong. A decided - interval sign equals the exact sign (property-tested over all f64 - regimes, including subnormals and overflow). -- `approx_*` methods are for output only. Decisions use sign questions. -- Structural answers beat evaluation where they are provable: the two hits - of one circle are ordered by branch, not by computing `x - x`, which no - interval can certify as zero. - -## Gates - -```bash -cargo test -p axiolid-exact -cargo bench -p axiolid-exact --bench exact # escalation rate beside ns/call -``` diff --git a/crates/algorithms/exact/Cargo.toml b/crates/algorithms/exact/Cargo.toml index 58b04a9a..7e595020 100644 --- a/crates/algorithms/exact/Cargo.toml +++ b/crates/algorithms/exact/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/algorithms/exact/README.md b/crates/algorithms/exact/README.md new file mode 100644 index 00000000..834d3a8b --- /dev/null +++ b/crates/algorithms/exact/README.md @@ -0,0 +1,25 @@ +# axiolid-exact + +Filtered exact arithmetic for *constructions* over `f64` input (ADR 0068): +signs and orderings of values `f64` cannot hold, such as where two segments +cross, where a line meets a circle, or roots of the form +`(a + b*sqrt(c)) / d`. Each sign question is one expression evaluated first +in outward-rounded interval arithmetic and, only if that cannot decide, in +exact dyadic big-integer arithmetic (`num-bigint`). It has no division and +evaluates no square roots; approximate values are available for output +only. + +```bash +cargo add axiolid-exact +``` + +- API documentation: [docs.rs/axiolid-exact](https://docs.rs/axiolid-exact) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-exact) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) + +## Design notes + +Choosing between this crate and `axiolid-predicates`: if the question is the +sign of a polynomial in the *input* coordinates (orientation, in-circle), +use `axiolid-predicates`, which needs no big integers. If the question is +about a *constructed* point (a crossing, a line/circle hit), use this crate. diff --git a/crates/algorithms/exact/src/construct.rs b/crates/algorithms/exact/src/construct.rs index 6422c2c1..b3d58ee0 100644 --- a/crates/algorithms/exact/src/construct.rs +++ b/crates/algorithms/exact/src/construct.rs @@ -3,7 +3,13 @@ //! Each construction's result is kept symbolically -- as its inputs plus a //! recipe -- and every question about it is a [`SignExpr`], answered by the //! interval filter or, when that cannot decide, exactly. Nothing is rounded -//! until a caller asks for an approximate value for output. +//! until a caller asks for an approximate value for output: the `approx_*` +//! methods are for output only, and a decision is always asked as a sign +//! question. +//! +//! Where an answer is provable from structure, it is not evaluated: the two +//! hits of one circle are ordered by [`Branch`], because subtracting two +//! equal intervals can never certify zero. use axiolid_core::Point2; use axiolid_guarantees::{Certified, Sign}; diff --git a/crates/algorithms/parametric/AGENTS.md b/crates/algorithms/parametric/AGENTS.md deleted file mode 100644 index c09c6784..00000000 --- a/crates/algorithms/parametric/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Parametric algorithms - -`nurbs/` owns general parametric analysis and exact shape-preserving transformations. diff --git a/crates/algorithms/parametric/evaluate/AGENTS.md b/crates/algorithms/parametric/evaluate/AGENTS.md deleted file mode 100644 index a7513e8b..00000000 --- a/crates/algorithms/parametric/evaluate/AGENTS.md +++ /dev/null @@ -1,60 +0,0 @@ -# axiolid-evaluate instructions - -Purpose: the scalar evaluation oracle for parametric geometry (ADR 0012, ADR 0036). - -Allowed internal dependencies: `axiolid-core`, `axiolid-contracts`, -`axiolid-curve`, `axiolid-surface`. Do not add mesh, spatial, measure, or -provider dependencies — the point of this package is that a parametric consumer -(NURBS, CAD) acquires evaluation without the `axiolid-reference` umbrella graph. - -## Module ownership - -`curve.rs` native-domain evaluation, derivatives, jets, adaptive flattening; -`surface.rs` evaluation, partials, normals, jets, elementary inversion; -`arc_length.rs` arc-length evaluation of intrinsic curves and of the -plan-plus-elevation composition; -`frenet.rs` frame, point and tangent of a space curve from curvature AND -torsion, by Magnus expansion on SO(3); -`intrinsic_relation.rs` trim, offset and join over natural-equation space -curves; -`provider.rs` `ReferenceCurveEvaluator`, this crate's implementation of -the `CurveEvaluator` capability contract: point, tangent and placement -frame at a DISTANCE rather than a native parameter; -`nurbs.rs` shared private spline-axis machinery. - -## Invariants - -- `provider.rs` answers DISTANCE questions; `curve.rs` answers native - PARAMETER questions. Only `Line`, `Circle` and `Intrinsic` convert in - closed form. Never hand a parameter back as a distance. -- The placement frame is reference-up, never Frenet: the Frenet normal - flips sign at a vertical inflection and is undefined on a straight. - See ADR 0063. - -- A quadrature panel must never straddle a SEAM of a piecewise law: - Gauss-Legendre assumes a smooth integrand, and a joined curve was wrong - by 3.0e-3 until panels were split at seams. Use - `CurvatureLaw::seams_within` when adding any new integrator here. -- An offset of an arc-length curve is only arc-length again when curvature - is CONSTANT (speed is `|1 - d*k(s)|`). Offsetting a varying law is - refused, not refitted -- a refit would be a guess wearing an exact type - (ADR 0062). -- A 3D frame law does NOT integrate like a 2D one. In the plane, heading is - `theta_0 + int k` because angles commute. In space the frame obeys a matrix - ODE on SO(3) and `R(s) != exp(int Omega)` unless `tau/k` is constant. Use the - Magnus step in `frenet.rs`; dropping its commutator term costs ~5000x accuracy - on a clothoid-with-torsion (ADR 0061). -- A quadrature budget must come from TOTAL VARIATION of the law, never from its - signed integral: a zero-mean oscillation integrates to zero and would buy one - panel for a curve that swings through many radians (ADR 0061). -- An intrinsic curve's HEADING is exact in closed form; its POSITION is not - elementary and is Gauss-Legendre quadrature, subdivided by total turning and - bounded so a malformed law refuses rather than hangs. Pin any change to it - against an independent closed form (Fresnel for the clothoid, the elementary - arc for constant curvature) — never against another run of the quadrature - (ADR 0060). -- No feature gates. An oracle that varies by feature is not an oracle. -- No intrinsics or threading; stay obviously correct in preference to fast. -- `axiolid-reference` re-exports `curve` and `surface` unchanged. Renaming or - reshaping a public item here silently breaks `axiolid_reference::curve::*` - callers, so treat those paths as part of this package's public surface. diff --git a/crates/algorithms/parametric/evaluate/Cargo.toml b/crates/algorithms/parametric/evaluate/Cargo.toml index e5f6a5e9..3106882a 100644 --- a/crates/algorithms/parametric/evaluate/Cargo.toml +++ b/crates/algorithms/parametric/evaluate/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/algorithms/parametric/evaluate/README.md b/crates/algorithms/parametric/evaluate/README.md new file mode 100644 index 00000000..fb443da5 --- /dev/null +++ b/crates/algorithms/parametric/evaluate/README.md @@ -0,0 +1,19 @@ +# axiolid-evaluate + +The scalar evaluation oracle for parametric geometry (ADR 0012, ADR 0036): +native-domain evaluation of analytic and B-spline curves and surfaces, +derivatives, jets, adaptive flattening, elementary surface inversion, +arc-length evaluation of intrinsic (natural-equation) and elevated curves, +and `ReferenceCurveEvaluator`, the reference implementation of the +curve-evaluation contract (ADR 0063). It has no mesh, spatial, measure or +provider dependency, so a parametric consumer gets evaluation without the +`axiolid-reference` umbrella. It favours obvious correctness over speed: no +intrinsics, threading or feature gates. + +```bash +cargo add axiolid-evaluate +``` + +- API documentation: [docs.rs/axiolid-evaluate](https://docs.rs/axiolid-evaluate) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-evaluate) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/parametric/evaluate/src/arc_length.rs b/crates/algorithms/parametric/evaluate/src/arc_length.rs index a95433f0..00bc204d 100644 --- a/crates/algorithms/parametric/evaluate/src/arc_length.rs +++ b/crates/algorithms/parametric/evaluate/src/arc_length.rs @@ -17,6 +17,14 @@ //! precision on a single panel, versus roughly 1e-3 relative for a comparable //! trapezoid budget. Panels are subdivided by total turning so a tight spiral //! gets more of them. +//! +//! A panel must never straddle a seam of a piecewise law: Gauss-Legendre +//! assumes a smooth integrand, and a joined curve was wrong by 3.0e-3 until +//! panels were split at seams. Any new integrator splits at +//! `CurvatureLaw::seams_within` too. Pin a change to this quadrature against +//! an independent closed form (Fresnel for the clothoid, the elementary arc +//! for constant curvature), never against another run of the quadrature +//! (ADR 0060). use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation}; use axiolid_core::{Frame2, Point2, Point3, Scalar, Vec2, Vec3}; diff --git a/crates/algorithms/parametric/evaluate/src/lib.rs b/crates/algorithms/parametric/evaluate/src/lib.rs index 0823996c..ff691732 100644 --- a/crates/algorithms/parametric/evaluate/src/lib.rs +++ b/crates/algorithms/parametric/evaluate/src/lib.rs @@ -10,6 +10,10 @@ //! //! No intrinsics, no threading, no feature gates: it must stay obviously //! correct in preference to being fast. +//! +//! `axiolid-reference` re-exports [`curve`] and [`surface`] unchanged, so +//! `axiolid_reference::curve::*` paths are part of this package's public +//! surface too: renaming or reshaping an item here breaks those callers. pub mod arc_length; pub mod curve; diff --git a/crates/algorithms/parametric/nurbs/AGENTS.md b/crates/algorithms/parametric/nurbs/AGENTS.md deleted file mode 100644 index 9ee7b5ee..00000000 --- a/crates/algorithms/parametric/nurbs/AGENTS.md +++ /dev/null @@ -1,9 +0,0 @@ -# axiolid-nurbs - -L2 format-neutral algorithms over polynomial and rational B-spline values. - -- Reuse `axiolid-reference` as the correctness oracle; do not duplicate evaluation. -- Every tolerance-sensitive solver receives explicit bounded options. -- Shape-preserving transforms must be verified by independent evaluation samples. -- Closed metadata is not proof of periodicity; wrapping requires an explicit seam check. -- No importer, tessellator, file-format, or vendor vocabulary belongs here. diff --git a/crates/algorithms/parametric/nurbs/Cargo.toml b/crates/algorithms/parametric/nurbs/Cargo.toml index ce9bc361..29d2d945 100644 --- a/crates/algorithms/parametric/nurbs/Cargo.toml +++ b/crates/algorithms/parametric/nurbs/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/algorithms/parametric/nurbs/PLAN.md b/crates/algorithms/parametric/nurbs/PLAN.md deleted file mode 100644 index 77ef911a..00000000 --- a/crates/algorithms/parametric/nurbs/PLAN.md +++ /dev/null @@ -1,56 +0,0 @@ -# axiolid-nurbs plan - -Status: first general-kernel milestone implemented. - -## Implemented - -- Differential geometry from analytic second-order jets. -- Exact shape-preserving curve reversal, insertion, split, and Bézier decomposition. -- Exact tensor-product surface U/V insertion and reversal. -- Explicitly budgeted curve/surface inverse queries with honest local status. -- Outward-rounded globally certified clamped curve projection and curve-pair - minimum distance, including interval-aware homogeneous knot refinement. -- Bounded planar clamped curve/curve isolation for exact-sign lines and points, - and contractive transverse polynomial/rational Bézier boxes, with explicit - native-parameter resolution, distinct zero-length point contact, localized - structural endpoint tangency/overlap, compact parameter-only DFS work items, - allocation-safe work ceilings, and unresolved outcomes. -- Bounded clamped 3D curve/surface isolation with continuous internal span joins - (internal knot multiplicity `1..=degree`) for isolated transverse roots; valid - full-multiplicity internal knots remain unsupported by this certified query. - The path uses outward tensor rational-Bézier refinement, conservative native-span - surface partials, strict-interior 3×3 Krawczyk proofs, explicit `t/u/v` - resolution, retained partial certificates, compact parameter-only DFS work, - shared hard work ceilings, fallible allocations, and unresolved outcomes. -- Bounded clamped surface/surface patch-pair exclusion plus complete transverse - intersection segments for single-span polynomial affine patches. Affine identity - is exact over binary64 controls, normal transversality is outward-interval proved, - endpoints retain both native surface charts through strict curve/surface proofs, - and unsupported curved/degenerate cases remain unresolved. `axiolid-construct` - can promote the one-owner chord case into two closed trimmed faces plus an - explicit embedded pcurve on the containing face; dual-boundary ownership remains - unresolved. -- Verified closed-curve seam classification and parameter wrapping. -- Exact degree elevation, in homogeneous coordinates so rational curves keep - their weights. Result is in Bezier form and deliberately not knot-minimal. -- Knot removal and degree reduction, each measured against the original by - sampling and refused when the deviation exceeds the caller's tolerance. The - measured deviation is returned so a caller can apply its own budget. -- Chord-length cubic interpolation passing through its points, and lofting - that interpolates across sections so interior sections are reproduced - exactly rather than merely approached. -- Optional `axiolid/nurbs` facade feature and `parametric` bundle adoption. - -## Later - -- Rational knot removal and rational degree reduction: correct handling works - in homogeneous coordinates and needs its own weight-consistency proof, so - the current operations refuse rational input rather than approximating it. -- Ownership-aware boundary roots, full-multiplicity internal span joins, general - tangent/overlap classification, curved surface/surface tracing and multispan - stitching beyond the affine reference slice, and globally certified surface projection. -- Surface-periodic seam wrapping. -- Blending operations. -- Benchmarked optimized providers after scalar differential validation. - -No item is a capability claim until its public API, tests, and facade feature land. diff --git a/crates/algorithms/parametric/nurbs/README.md b/crates/algorithms/parametric/nurbs/README.md new file mode 100644 index 00000000..b60430b6 --- /dev/null +++ b/crates/algorithms/parametric/nurbs/README.md @@ -0,0 +1,19 @@ +# axiolid-nurbs + +Format-neutral algorithms over polynomial and rational B-spline curves and +surfaces: differential geometry, exact shape-preserving transforms (knot +insertion, reversal, splitting, Bezier decomposition, degree elevation), +tolerance-bounded knot removal and degree reduction, interpolation and +lofting, certified projection, inversion and intersection queries, exact +analytic curve and surface intersection, and verified periodic seams. +Lossy operations measure their deviation and refuse above the caller's +tolerance. It owns no importer, tessellator or file-format vocabulary, and +it uses `axiolid-evaluate` for evaluation rather than reimplementing it. + +```bash +cargo add axiolid-nurbs +``` + +- API documentation: [docs.rs/axiolid-nurbs](https://docs.rs/axiolid-nurbs) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-nurbs) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/parametric/nurbs/src/lib.rs b/crates/algorithms/parametric/nurbs/src/lib.rs index c4ba2fa7..fe25e508 100644 --- a/crates/algorithms/parametric/nurbs/src/lib.rs +++ b/crates/algorithms/parametric/nurbs/src/lib.rs @@ -3,9 +3,15 @@ //! General NURBS algorithms over Axiolid's format-neutral B-spline values. //! -//! This crate builds on the portable scalar oracle. It owns differential +//! This crate builds on the portable scalar oracle: evaluation comes from +//! `axiolid-evaluate` and is not reimplemented here. It owns differential //! geometry and exact shape-preserving transformations; importers and //! tessellators are consumers, not the capability boundary. +//! +//! Every tolerance-sensitive solver takes explicit, bounded options (work +//! ceilings, resolutions, tolerances) and reports an unresolved outcome or +//! refuses when they run out. A shape-preserving transform is tested against +//! independent evaluation samples of the original, not its own recurrence. mod axis; mod certified_bezier; diff --git a/crates/algorithms/parametric/nurbs/src/periodic.rs b/crates/algorithms/parametric/nurbs/src/periodic.rs index ce02740e..c49471c5 100644 --- a/crates/algorithms/parametric/nurbs/src/periodic.rs +++ b/crates/algorithms/parametric/nurbs/src/periodic.rs @@ -1,4 +1,8 @@ //! Verified periodic parameter semantics for closed NURBS curves. +//! +//! A `closed` flag is metadata, not proof of periodicity. Wrapping a +//! parameter goes through a view whose constructor has checked the seam +//! (at least positional continuity), never through the flag alone. use crate::{ axis::active_spans, diff --git a/crates/algorithms/parametric/nurbs/src/revolution_profile.rs.bak b/crates/algorithms/parametric/nurbs/src/revolution_profile.rs.bak deleted file mode 100644 index 740a57a7..00000000 --- a/crates/algorithms/parametric/nurbs/src/revolution_profile.rs.bak +++ /dev/null @@ -1,465 +0,0 @@ -//! Meridian profiles of surfaces of revolution. -//! -//! Plane, cylinder, cone, sphere and torus are all surfaces of -//! revolution. Each is generated by rotating a LINE or a CIRCLE in the -//! `(rho, z)` half-plane about the axis. Two coaxial such surfaces -//! therefore meet exactly where their profiles meet, and every profile -//! solution with `rho > 0` lifts to a full circle perpendicular to the -//! shared axis. -//! -//! This reduces ten separate surface-pair derivations to three profile -//! cases -- line/line, line/circle, circle/circle -- each closed form. - -use axiolid_core::Scalar; - -/// A meridian profile in the `(rho, z)` half-plane, `rho >= 0`. -#[derive(Debug, Clone, Copy, PartialEq)] -pub(crate) enum Profile { - /// `coefficient_rho * rho + coefficient_z * z = constant`. - Line { - /// Coefficient on the radial coordinate. - coefficient_rho: Scalar, - /// Coefficient on the axial coordinate. - coefficient_z: Scalar, - /// Right-hand side. - constant: Scalar, - }, - /// Circle of `radius` centred at `(centre_rho, centre_z)`. - Circle { - /// Radial coordinate of the centre. - centre_rho: Scalar, - /// Axial coordinate of the centre. - centre_z: Scalar, - /// Circle radius. - radius: Scalar, - }, -} - -/// A profile intersection point, which lifts to a circle of radius -/// `rho` at height `z` on the shared axis. -#[derive(Debug, Clone, Copy, PartialEq)] -pub(crate) struct ProfilePoint { - /// Radial distance from the axis. Always strictly positive: a - /// solution on the axis is a point, not a circle. - pub(crate) rho: Scalar, - /// Signed height along the axis. - pub(crate) z: Scalar, -} - -/// Why two coaxial profiles yield no liftable circle. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum ProfileFault { - /// The profiles do not meet. - Disjoint, - /// The profiles are the same curve, so the surfaces coincide. - Coincident, - /// The profiles touch without crossing, or meet only on the axis. - /// Neither yields a regular circle. - NotRegular, -} - -/// Intersect two coaxial meridian profiles. -/// -/// Returns the liftable points in deterministic order (ascending `z`, -/// then ascending `rho`). Points on or behind the axis are dropped -/// because they do not generate a circle; if that leaves nothing, the -/// result is a fault rather than an empty success, so a caller can -/// never mistake a degenerate touch for an absent intersection. -pub(crate) fn intersect_profiles( - first: Profile, - second: Profile, -) -> Result, ProfileFault> { - let raw = match (first, second) { - (Profile::Line { .. }, Profile::Line { .. }) => line_line(first, second)?, - (Profile::Line { .. }, Profile::Circle { .. }) => line_circle(first, second)?, - (Profile::Circle { .. }, Profile::Line { .. }) => line_circle(second, first)?, - (Profile::Circle { .. }, Profile::Circle { .. }) => circle_circle(first, second)?, - }; - let mut points: Vec = raw.into_iter().filter(|p| p.rho > 0.0).collect(); - if points.is_empty() { - return Err(ProfileFault::NotRegular); - } - points.sort_by(|a, b| a.z.total_cmp(&b.z).then_with(|| a.rho.total_cmp(&b.rho))); - Ok(points) -} - -/// Two profile lines meet in at most one point (Cramer's rule). -fn line_line(first: Profile, second: Profile) -> Result, ProfileFault> { - let (a1, b1, c1) = line_parts(first); - let (a2, b2, c2) = line_parts(second); - let determinant = a1 * b2 - a2 * b1; - if determinant == 0.0 { - // Parallel. Same line iff the constants agree under the same - // scaling, which is what the cross terms below compare. - let coincident = a1 * c2 - a2 * c1 == 0.0 && b1 * c2 - b2 * c1 == 0.0; - return Err(if coincident { - ProfileFault::Coincident - } else { - ProfileFault::Disjoint - }); - } - Ok(vec![ProfilePoint { - rho: (c1 * b2 - c2 * b1) / determinant, - z: (a1 * c2 - a2 * c1) / determinant, - }]) -} - -fn line_parts(profile: Profile) -> (Scalar, Scalar, Scalar) { - match profile { - Profile::Line { - coefficient_rho, - coefficient_z, - constant, - } => (coefficient_rho, coefficient_z, constant), - Profile::Circle { .. } => unreachable!("line_parts called on a circle profile"), - } -} - -/// A line meets a circle in zero, one or two points. -/// -/// Solved by projecting the centre onto the line: with `d` the distance -/// from centre to line, the chord half-length is `sqrt(r^2 - d^2)`. A -/// single tangential touch is refused because a tangency is not a -/// transversal intersection and lifting it would invent structure. -fn line_circle(line: Profile, circle: Profile) -> Result, ProfileFault> { - let (a, b, c) = line_parts(line); - let Profile::Circle { - centre_rho, - centre_z, - radius, - } = circle - else { - unreachable!("line_circle called with a non-circle profile"); - }; - let norm_squared = a * a + b * b; - if norm_squared == 0.0 { - return Err(ProfileFault::Disjoint); - } - let norm = norm_squared.sqrt(); - let signed = (a * centre_rho + b * centre_z - c) / norm; - let half_chord_squared = radius * radius - signed * signed; - if half_chord_squared < 0.0 { - return Err(ProfileFault::Disjoint); - } - if half_chord_squared == 0.0 { - return Err(ProfileFault::NotRegular); - } - let half_chord = half_chord_squared.sqrt(); - // Foot of the perpendicular from the centre to the line. - let foot_rho = centre_rho - a * signed / norm; - let foot_z = centre_z - b * signed / norm; - // Unit direction along the line. - let direction_rho = -b / norm; - let direction_z = a / norm; - Ok(vec![ - ProfilePoint { - rho: foot_rho - direction_rho * half_chord, - z: foot_z - direction_z * half_chord, - }, - ProfilePoint { - rho: foot_rho + direction_rho * half_chord, - z: foot_z + direction_z * half_chord, - }, - ]) -} - -/// Two profile circles meet in zero, one or two points. -/// -/// Same radical-line identity as sphere/sphere, one dimension lower: -/// with `d` the centre distance, the radical line sits at -/// `a = (d^2 + r1^2 - r2^2) / 2d` from the first centre, and the -/// intersection is `sqrt(r1^2 - a^2)` either side of it. -fn circle_circle(first: Profile, second: Profile) -> Result, ProfileFault> { - let Profile::Circle { - centre_rho: rho1, - centre_z: z1, - radius: r1, - } = first - else { - unreachable!("circle_circle called with a non-circle profile"); - }; - let Profile::Circle { - centre_rho: rho2, - centre_z: z2, - radius: r2, - } = second - else { - unreachable!("circle_circle called with a non-circle profile"); - }; - let delta_rho = rho2 - rho1; - let delta_z = z2 - z1; - let distance_squared = delta_rho * delta_rho + delta_z * delta_z; - if distance_squared == 0.0 { - return Err(if r1 == r2 { - ProfileFault::Coincident - } else { - ProfileFault::Disjoint - }); - } - let distance = distance_squared.sqrt(); - let along = (distance_squared + r1 * r1 - r2 * r2) / (2.0 * distance); - let half_chord_squared = r1 * r1 - along * along; - if half_chord_squared < 0.0 { - return Err(ProfileFault::Disjoint); - } - if half_chord_squared == 0.0 { - return Err(ProfileFault::NotRegular); - } - let half_chord = half_chord_squared.sqrt(); - let unit_rho = delta_rho / distance; - let unit_z = delta_z / distance; - let base_rho = rho1 + unit_rho * along; - let base_z = z1 + unit_z * along; - // Perpendicular to the centre line, in the (rho, z) plane. - Ok(vec![ - ProfilePoint { - rho: base_rho + unit_z * half_chord, - z: base_z - unit_rho * half_chord, - }, - ProfilePoint { - rho: base_rho - unit_z * half_chord, - z: base_z + unit_rho * half_chord, - }, - ]) -} - -use crate::exact_surface_intersection::{ - Derivation, ExactIntersectionCurve, ExactIntersectionRefusal, -}; -use axiolid_core::{Frame3, Point3, Vec3}; -use axiolid_curve::{Circle3, Curve3}; -use axiolid_surface::Surface; - -/// A surface of revolution reduced to its axis and meridian profile. -pub(crate) struct Revolution { - /// A point on the axis. - pub(crate) origin: Point3, - /// Unit axis direction. - pub(crate) axis: Vec3, - /// Meridian profile, in axis-local `(rho, z)` coordinates. - pub(crate) profile: Profile, -} - -/// Normalise a frame axis, refusing a degenerate one. -/// -/// Frames arrive with whatever `z` the caller stored. Nothing guarantees -/// it is unit length, and every downstream step -- the parallel test, the -/// axial/lateral split, the profile shift -- assumes it is. Normalising -/// here keeps that assumption true at the one place the axis enters. -fn unit_axis(axis: Vec3) -> Option { - let length = axis.length(); - if length == 0.0 || !length.is_finite() { - return None; - } - Some(axis / length) -} - -/// Describe an elementary surface as a surface of revolution. -/// -/// A plane qualifies only as the degenerate `z = 0` profile about its own -/// normal: any axis in the plane would also generate it, so the normal is -/// the single deterministic choice. -pub(crate) fn as_revolution(surface: &Surface) -> Option { - match surface { - Surface::Plane(plane) => Some(Revolution { - origin: plane.frame.origin, - axis: unit_axis(plane.frame.z)?, - profile: Profile::Line { - coefficient_rho: 0.0, - coefficient_z: 1.0, - constant: 0.0, - }, - }), - Surface::Cylinder(cylinder) => Some(Revolution { - origin: cylinder.frame.origin, - axis: unit_axis(cylinder.frame.z)?, - profile: Profile::Line { - coefficient_rho: 1.0, - coefficient_z: 0.0, - constant: cylinder.radius, - }, - }), - Surface::Cone(cone) => Some(Revolution { - origin: cone.frame.origin, - axis: unit_axis(cone.frame.z)?, - profile: Profile::Line { - coefficient_rho: 1.0, - coefficient_z: -cone.semi_angle.tan(), - constant: cone.radius, - }, - }), - Surface::Sphere(sphere) => Some(Revolution { - origin: sphere.frame.origin, - axis: unit_axis(sphere.frame.z)?, - profile: Profile::Circle { - centre_rho: 0.0, - centre_z: 0.0, - radius: sphere.radius, - }, - }), - Surface::Torus(torus) => Some(Revolution { - origin: torus.frame.origin, - axis: unit_axis(torus.frame.z)?, - profile: Profile::Circle { - centre_rho: torus.major_radius, - centre_z: 0.0, - radius: torus.minor_radius, - }, - }), - // A spline surface has no closed-form meridian, and a future - // variant is unknown here: neither can be claimed as a - // surface of revolution. - _ => None, - } -} - -/// Re-express `other`'s profile in `base`'s axis frame, when the two -/// surfaces are coaxial. -/// -/// Coaxial means the axes are parallel AND the origins differ only along -/// that shared axis. Anti-parallel axes are accepted by flipping the -/// profile's `z`, since a surface of revolution is unchanged by reversing -/// its axis. A lateral offset breaks the shared rotational symmetry and -/// the intersection is then generally a quartic, so it is rejected here. -pub(crate) fn align_profile(base: &Revolution, other: &Revolution) -> Option { - let dot = base.axis.dot(other.axis); - let flip = if dot > 0.0 { 1.0 } else { -1.0 }; - // Axes must be parallel: the rejected component of one along the - // other must vanish. - let parallel_error = (other.axis - base.axis * dot).length(); - if parallel_error > AXIS_TOLERANCE { - return None; - } - let between = other.origin - base.origin; - let axial = between.dot(base.axis); - let lateral = (between - base.axis * axial).length(); - if lateral > AXIS_TOLERANCE * profile_scale(base, other, between) { - return None; - } - Some(shift_profile(other.profile, axial, flip)) -} - -/// A characteristic length for the pair, used to make the coaxiality -/// test independent of modelling units. -/// -/// A fixed absolute threshold silently changes meaning with scale: the -/// same shape authored in millimetres rather than metres is a thousand -/// times larger in stored units, so an absolute bound rejects offsets it -/// accepted before. Comparing against a length drawn from the operands -/// themselves keeps the verdict tied to shape rather than units. -/// -/// The separation is included because two far-apart surfaces with small -/// radii still span a large model; falling back to `1.0` keeps the test -/// meaningful when every input length is zero, and never returns a scale -/// of zero, which would collapse the comparison to exact equality. -fn profile_scale(base: &Revolution, other: &Revolution, between: Vec3) -> Scalar { - let extent = profile_extent(base.profile) - .max(profile_extent(other.profile)) - .max(between.length()); - if extent.is_finite() && extent > 0.0 { - extent - } else { - 1.0 - } -} - -/// The largest length appearing in a profile, or zero when it has none. -/// -/// A profile line carries no intrinsic length except its offset from the -/// axis, so a cylinder contributes its radius and a plane through the -/// origin contributes nothing. -fn profile_extent(profile: Profile) -> Scalar { - match profile { - Profile::Line { constant, .. } => constant.abs(), - Profile::Circle { - centre_rho, - centre_z, - radius, - } => centre_rho.abs().max(centre_z.abs()).max(radius), - } -} - -/// Largest *relative* deviation still treated as a shared axis. -/// -/// Coaxiality is a structural precondition, not a measured quantity: it -/// decides which closed form applies at all. A tolerance is needed because -/// axes arrive as floating-point directions. -/// -/// This is a relative bound. The direction test compares unit vectors and -/// is already dimensionless; the lateral-offset test multiplies this by a -/// characteristic length from the operands, so the same shape gets the -/// same verdict whether it is modelled in metres or millimetres. -const AXIS_TOLERANCE: Scalar = 1.0e-12; - -/// Move a profile along the axis and optionally mirror it. -fn shift_profile(profile: Profile, axial: Scalar, flip: Scalar) -> Profile { - match profile { - Profile::Line { - coefficient_rho, - coefficient_z, - constant, - } => { - // Substituting z -> flip*(z - axial) keeps the same line. - let scaled = coefficient_z * flip; - Profile::Line { - coefficient_rho, - coefficient_z: scaled, - constant: constant + scaled * axial, - } - } - Profile::Circle { - centre_rho, - centre_z, - radius, - } => Profile::Circle { - centre_rho, - centre_z: axial + flip * centre_z, - radius, - }, - } -} - -/// Derive the exact intersection of two coaxial surfaces of revolution. -/// -/// Every result is a circle perpendicular to the shared axis, because a -/// profile solution at `(rho, z)` is swept by the shared rotational -/// symmetry. This one identity covers every coaxial combination of -/// plane, cylinder, cone, sphere and torus. -pub(crate) fn coaxial_revolution_intersection( - first: &Surface, - second: &Surface, -) -> Result { - let base = as_revolution(first).ok_or(ExactIntersectionRefusal::UnsupportedPair)?; - let other = as_revolution(second).ok_or(ExactIntersectionRefusal::UnsupportedPair)?; - let aligned = align_profile(&base, &other).ok_or(ExactIntersectionRefusal::UnsupportedPair)?; - let points = intersect_profiles(base.profile, aligned).map_err(|fault| match fault { - ProfileFault::Disjoint => ExactIntersectionRefusal::Disjoint, - ProfileFault::Coincident | ProfileFault::NotRegular => { - ExactIntersectionRefusal::NotRegularCurve - } - })?; - let mut branches = Vec::new(); - branches - .try_reserve_exact(points.len()) - .map_err(|_| ExactIntersectionRefusal::DegenerateFrame)?; - for point in points { - let centre = base.origin + base.axis * point.z; - let frame = frame_about_axis(centre, base.axis)?; - branches.push(Curve3::Circle(Circle3 { - frame, - radius: point.rho, - })); - } - Ok(ExactIntersectionCurve { - branches, - derivation: Derivation::CoaxialRevolutionCircles, - }) -} - -/// Build an orthonormal frame whose `z` is the given unit axis. -/// -/// Delegates to the same deterministic construction used by the other -/// exact derivations so identical geometry yields identical frames. -fn frame_about_axis(origin: Point3, axis: Vec3) -> Result { - crate::exact_surface_intersection::frame_from_normal(origin, axis) -} diff --git a/crates/algorithms/planar/AGENTS.md b/crates/algorithms/planar/AGENTS.md deleted file mode 100644 index c568f45e..00000000 --- a/crates/algorithms/planar/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Planar algorithms - -`overlay/` owns format-neutral planar overlay behavior. diff --git a/crates/algorithms/planar/arrangement/Cargo.toml b/crates/algorithms/planar/arrangement/Cargo.toml index 03e396ec..c21702c2 100644 --- a/crates/algorithms/planar/arrangement/Cargo.toml +++ b/crates/algorithms/planar/arrangement/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/algorithms/planar/arrangement/README.md b/crates/algorithms/planar/arrangement/README.md new file mode 100644 index 00000000..8c13f2e9 --- /dev/null +++ b/crates/algorithms/planar/arrangement/README.md @@ -0,0 +1,11 @@ +# axiolid-arrangement + +An editable planar subdivision: a doubly-connected edge list whose vertices, half-edges and faces keep stable handles across edits, so a caller can move a vertex or split a face without rebuilding the plane or losing track of which face is which. Orientation decisions use certified predicates, and the unbounded outer region is a real face. It is deliberately neutral: it exposes faces, boundaries, areas and adjacency, and leaves deciding that a face is a room to the caller. For one-shot polygon booleans with no retained structure, use `axiolid-overlay` instead. + +```bash +cargo add axiolid-arrangement +``` + +- API documentation: [docs.rs/axiolid-arrangement](https://docs.rs/axiolid-arrangement) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-arrangement) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/planar/overlay/AGENTS.md b/crates/algorithms/planar/overlay/AGENTS.md deleted file mode 100644 index 614d2fa3..00000000 --- a/crates/algorithms/planar/overlay/AGENTS.md +++ /dev/null @@ -1,46 +0,0 @@ -# axiolid-overlay - -Planar booleans and offsets over validated regions. - -## One exact core - -- Straight edges: `Region` / `overlay` / `union_soup` build one exact - subdivision of all rings (`src/exact_overlay.rs` over - `exact_arc/arrangement.rs`) and keep pieces by winding number and fill - rule (#173). `i_overlay` remains only for `offset.rs`. -- Arcs: `arc_overlay` on the in-tree exact core `src/exact_arc.rs` and `src/exact_arc/` - (ADR 0070). `point.rs` holds exact points `(a + b*sqrt(d)) / w` and their - filtered predicates; `edge.rs` holds segments and bulge arcs (circle in - exact dyadic conic form, half-angle parametrization for rational samples); - `mod.rs` splits, classifies, keeps, links and nests. - -## Rules - -- No decision in `exact_arc` may read a tolerance. The tolerance only - validates operands and cleans up output rounding (`presented` in - `arc_overlay.rs`). -- Every predicate goes through `point::sign`, which tries cached boxes, - then the `axiolid-exact` interval tier, then exact arithmetic. The only - exceptions are certified `f64` filters on input vertices - (`edge.rs::segments_quick`, `orient_f64`): each must return `None` when - its sign is not certain. `Sign` is - `non_exhaustive`: match `Positive`/`Negative` and treat the rest as zero. -- Output vertices are rounded once (`XPoint::rounded`: correctly rounded - for rational points). Never feed rounded output back into a decision. - -## Verification - -- `tests/arc_exact_oracle.rs`: area identities and point membership against - tessellated operands on random grid-snapped and decimal scenes. -- `python3 scripts/probe_arc_overlay_mutants.py` and - `python3 scripts/probe_exact_overlay_mutants.py`: every listed fault must - fail the suite. -- `src/exact_overlay.rs` tests the straight path against `i_overlay` by - point membership away from boundaries; `tests/exact_straight.rs` pins - bit-identical vertices and correctly rounded crossings. -- `cargo bench -p axiolid-overlay --bench arc_overlay`: per-call cost; - `SCALE=1` runs the edge-count scaling scenes instead. Quote it when - claiming a speed change. -- Bounding boxes (`edge.rs::Bounds`) only skip work and must contain the - whole edge. Changing how they are built needs the mutation probe: the - oracle tests include major arcs and many-edge rings for this. diff --git a/crates/algorithms/planar/overlay/Cargo.toml b/crates/algorithms/planar/overlay/Cargo.toml index bb2c86f5..11ec2022 100644 --- a/crates/algorithms/planar/overlay/Cargo.toml +++ b/crates/algorithms/planar/overlay/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/algorithms/planar/overlay/README.md b/crates/algorithms/planar/overlay/README.md new file mode 100644 index 00000000..d13aaf4e --- /dev/null +++ b/crates/algorithms/planar/overlay/README.md @@ -0,0 +1,19 @@ +# axiolid-overlay + +Validated, deterministic planar booleans (intersection, union, difference, xor) and offsets over regions with holes, plus the planar operations built on them: arc-aware booleans and arrangements, polyline strokes, Minkowski morphology bounds, minimum enclosing circles and rectangles, and visibility. Inputs are validated and refused with a typed error rather than repaired. It answers a query and keeps no structure; editable subdivisions with persistent identity live in `axiolid-arrangement`. + +```bash +cargo add axiolid-overlay +``` + +- API documentation: [docs.rs/axiolid-overlay](https://docs.rs/axiolid-overlay) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-overlay) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) + +## Design notes + +- Straight-edged booleans (`Region`, `overlay`, `union_soup`) and arc-aware ones (`arc_overlay`, + `ArcArrangement`) share one exact core in `src/exact_arc.rs` (ADR 0070, #173): every + topological decision is an exact sign, and output is rounded once, so an input vertex comes + back bit-identical. `i_overlay` remains only for offsets. The core's maintenance rules and + verification commands are in that module's docs. diff --git a/crates/algorithms/planar/overlay/src/exact_arc.rs b/crates/algorithms/planar/overlay/src/exact_arc.rs index 823fbd93..d55acd31 100644 --- a/crates/algorithms/planar/overlay/src/exact_arc.rs +++ b/crates/algorithms/planar/overlay/src/exact_arc.rs @@ -27,6 +27,36 @@ //! where several leave, which yields minimal rings. //! 6. Counter-clockwise rings are outer boundaries; each clockwise ring is //! a hole of the smallest outer that contains it. +//! +//! # Rules for changing it +//! +//! - No decision here may read a tolerance. The caller's tolerance only +//! validates operands and cleans up output rounding (`presented` in +//! `arc_overlay.rs`). +//! - Every predicate goes through `point::sign`: cached boxes first, then +//! the `axiolid-exact` interval tier, then exact arithmetic. The only +//! exceptions are certified `f64` filters on input vertices +//! (`edge.rs::segments_quick`, `orient_f64`), and each must return `None` +//! when its sign is not certain. `Sign` is `#[non_exhaustive]`; match +//! `Positive` and `Negative` and treat anything else as zero. +//! - Output vertices are rounded once (`XPoint::rounded`, correctly rounded +//! for rational points). Rounded output must never be fed back into a +//! decision. +//! +//! # Verification +//! +//! `tests/arc_exact_oracle.rs` checks area identities and point membership +//! against tessellated operands on random grid-snapped and decimal scenes. +//! `scripts/probe_arc_overlay_mutants.py` and +//! `scripts/probe_exact_overlay_mutants.py` (repository root) list faults +//! the suite must catch; run them after changing a decision or the edge +//! boxes. The straight path ([`crate::exact_overlay`]) is tested against +//! `i_overlay` by point membership away from boundaries, and +//! `tests/exact_straight.rs` pins bit-identical vertices and correctly +//! rounded crossings. +//! `cargo bench -p axiolid-overlay --bench arc_overlay` measures per-call +//! cost, and `SCALE=1` runs the edge-count scaling scenes instead; quote it +//! when claiming a speed change. pub(crate) mod arrangement; mod boxes; diff --git a/crates/algorithms/planar/overlay/src/exact_arc/edge.rs b/crates/algorithms/planar/overlay/src/exact_arc/edge.rs index c78d2ae2..beb99168 100644 --- a/crates/algorithms/planar/overlay/src/exact_arc/edge.rs +++ b/crates/algorithms/planar/overlay/src/exact_arc/edge.rs @@ -57,6 +57,11 @@ pub(crate) enum Carrier { /// contain the whole edge, never tightness; it is padded far beyond any /// rounding in its own computation, so a missed pair would need an error /// many orders of magnitude larger than `f64` arithmetic can make. +/// +/// A box too small makes crossings vanish without any test noticing unless +/// a scene exercises it, so a change to how boxes are built needs +/// `scripts/probe_arc_overlay_mutants.py`: `tests/arc_exact_oracle.rs` +/// includes major arcs and many-edge rings for exactly this. #[derive(Debug, Clone, Copy)] pub(crate) struct Bounds { x0: f64, diff --git a/crates/algorithms/planar/project/Cargo.toml b/crates/algorithms/planar/project/Cargo.toml index 8bba473a..122ec8b7 100644 --- a/crates/algorithms/planar/project/Cargo.toml +++ b/crates/algorithms/planar/project/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/algorithms/planar/project/README.md b/crates/algorithms/planar/project/README.md new file mode 100644 index 00000000..c149d291 --- /dev/null +++ b/crates/algorithms/planar/project/README.md @@ -0,0 +1,11 @@ +# axiolid-project + +Projection of triangle meshes onto a plane and intersection with prisms: the bridge between the kernel's 3D meshes and its planar booleans. `project_mesh` folds a mesh onto a plane, unions the result and keeps holes, and reports how many edge-on triangles it dropped instead of hiding them. It computes geometry, not a footprint: choosing the mesh, the reference plane and what to include is left to the consumer (ADR 0066). + +```bash +cargo add axiolid-project +``` + +- API documentation: [docs.rs/axiolid-project](https://docs.rs/axiolid-project) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-project) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/planar/route/Cargo.toml b/crates/algorithms/planar/route/Cargo.toml index 896b7c24..64d95985 100644 --- a/crates/algorithms/planar/route/Cargo.toml +++ b/crates/algorithms/planar/route/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-contracts.workspace = true diff --git a/crates/algorithms/planar/route/README.md b/crates/algorithms/planar/route/README.md new file mode 100644 index 00000000..6f8e63d4 --- /dev/null +++ b/crates/algorithms/planar/route/README.md @@ -0,0 +1,11 @@ +# axiolid-route + +Exact planar shortest paths over a visibility graph, plus distance maps, farthest points and forced walks built on the same graph. Which edges exist is decided with certified `orient2d`, so the combinatorics are exact; path lengths are sums of square roots in `f64` and carry ordinary rounding. Oversized input is refused with a proven lower bound rather than truncated, and the budget is a caller parameter. It reports routes and typed unreachable reasons, never whether a route is acceptable. For grid-sampled routing over layered fields, see `axiolid-field-ops`. + +```bash +cargo add axiolid-route +``` + +- API documentation: [docs.rs/axiolid-route](https://docs.rs/axiolid-route) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-route) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/planar/triangulate/Cargo.toml b/crates/algorithms/planar/triangulate/Cargo.toml index ed076f02..493016da 100644 --- a/crates/algorithms/planar/triangulate/Cargo.toml +++ b/crates/algorithms/planar/triangulate/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/algorithms/planar/triangulate/README.md b/crates/algorithms/planar/triangulate/README.md new file mode 100644 index 00000000..adb529a7 --- /dev/null +++ b/crates/algorithms/planar/triangulate/README.md @@ -0,0 +1,11 @@ +# axiolid-triangulate + +Constrained Delaunay triangulation with bounded quality refinement. Every constraint edge survives as a union of output edges, the result is Delaunay away from the constraints (decided by the certified `incircle` predicate), and optional Ruppert refinement drives interior angles toward a caller-chosen minimum. Refinement carries an explicit Steiner budget and reports when it was capped, so an unmet angle bound is never returned silently. + +```bash +cargo add axiolid-triangulate +``` + +- API documentation: [docs.rs/axiolid-triangulate](https://docs.rs/axiolid-triangulate) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-triangulate) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/predicates/AGENTS.md b/crates/algorithms/predicates/AGENTS.md deleted file mode 100644 index a458b362..00000000 --- a/crates/algorithms/predicates/AGENTS.md +++ /dev/null @@ -1,32 +0,0 @@ -# axiolid-predicates instructions - -Purpose: the single certified exact-arithmetic substrate (ADR 0012, ADR 0036). - -Allowed internal dependencies: `axiolid-core`, `axiolid-guarantees`. Nothing -else may be added — the value of this package is that a consumer acquires -certified signs without curves, surfaces, meshes, or execution. - -## Module ownership - -`expansion.rs` error-free transformations; `arithmetic.rs` arbitrary-length -expansions; `orientation.rs` `orient2d`; `orient3.rs` + `orient3_dyadic.rs` -`orient3d`; `sphere.rs` `incircle`/`insphere`; `static_filter.rs` precomputed -range bounds; `scene.rs` deterministic degeneracy-controlled scene generation -for tests and benchmarks. - -## Invariants - -A filter may return `Uncertain`; a public predicate may not. Escalation to -exact arithmetic is the contract, not an optimisation detail. A static filter -must never certify a sign the dynamic filter would reject, and must return -`None` when an input leaves its declared coordinate range. - -`Certified` is `#[non_exhaustive]`: an unrecognised variant must escalate, -never be treated as decisive. - -## Gates - -```bash -cargo test -p axiolid-predicates -cargo bench -p axiolid-predicates --bench predicates -``` diff --git a/crates/algorithms/predicates/Cargo.toml b/crates/algorithms/predicates/Cargo.toml index 412210d9..b3f70c70 100644 --- a/crates/algorithms/predicates/Cargo.toml +++ b/crates/algorithms/predicates/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [features] default = [] diff --git a/crates/algorithms/predicates/README.md b/crates/algorithms/predicates/README.md new file mode 100644 index 00000000..a801c49e --- /dev/null +++ b/crates/algorithms/predicates/README.md @@ -0,0 +1,18 @@ +# axiolid-predicates + +Certified geometric predicates: `orient2d`, `orient3d`, `incircle` and +`insphere`, built on error-free transformations and expansion arithmetic, +with static filters for callers that can bound their coordinates. Every +public predicate is a filtered cascade that escalates to exact arithmetic +instead of comparing against an epsilon, and returns a `Certified` sign. +The crate is deliberately narrow: no curve, surface, mesh, B-rep, provider +or big-integer dependency. `axiolid-reference` re-exports it unchanged +(ADR 0036); for signs of constructed values, see `axiolid-exact`. + +```bash +cargo add axiolid-predicates +``` + +- API documentation: [docs.rs/axiolid-predicates](https://docs.rs/axiolid-predicates) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-predicates) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/predicates/src/lib.rs b/crates/algorithms/predicates/src/lib.rs index 3ccc1778..e299e57a 100644 --- a/crates/algorithms/predicates/src/lib.rs +++ b/crates/algorithms/predicates/src/lib.rs @@ -10,8 +10,14 @@ //! B-rep, provider, or execution dependency, so a consumer that needs only //! certified signs — linear intersection, polygon orientation, NURBS root //! isolation, topology classification — pays for arithmetic and nothing else. -//! The broad `axiolid-reference` oracle re-exports these items unchanged +//! That includes big integers: the exact tier here is expansion arithmetic +//! (plus a fixed-size dyadic fallback for `orient3d`), and exact +//! *constructions* over big integers live in `axiolid-exact` instead. The +//! broad `axiolid-reference` oracle re-exports these items unchanged //! (ADR 0036). +//! +//! A `*_filter` function may return `Certified::Uncertain`; a public +//! predicate never does. pub mod arithmetic; pub mod expansion; diff --git a/crates/algorithms/query/AGENTS.md b/crates/algorithms/query/AGENTS.md deleted file mode 100644 index cf270ca1..00000000 --- a/crates/algorithms/query/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Query algorithms - -`spatial/` and `measure/` inspect geometry without owning representations or execution policy. diff --git a/crates/algorithms/query/collide/Cargo.toml b/crates/algorithms/query/collide/Cargo.toml index caa9e605..e3b30143 100644 --- a/crates/algorithms/query/collide/Cargo.toml +++ b/crates/algorithms/query/collide/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/algorithms/query/collide/README.md b/crates/algorithms/query/collide/README.md new file mode 100644 index 00000000..661bed76 --- /dev/null +++ b/crates/algorithms/query/collide/README.md @@ -0,0 +1,11 @@ +# axiolid-collide + +Convex collision queries by the separating axis theorem: whether two convex shapes overlap and, when they do not, how far apart they are and along which axis. It deliberately does not report penetration depth; callers who need EPA-style contact for physics want a physics engine. For clearance between triangle meshes, use `axiolid-inspect`. + +```bash +cargo add axiolid-collide +``` + +- API documentation: [docs.rs/axiolid-collide](https://docs.rs/axiolid-collide) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-collide) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/query/inspect/Cargo.toml b/crates/algorithms/query/inspect/Cargo.toml index 2280b6a0..80ab7015 100644 --- a/crates/algorithms/query/inspect/Cargo.toml +++ b/crates/algorithms/query/inspect/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/algorithms/query/inspect/README.md b/crates/algorithms/query/inspect/README.md new file mode 100644 index 00000000..2dd3d97d --- /dev/null +++ b/crates/algorithms/query/inspect/README.md @@ -0,0 +1,11 @@ +# axiolid-inspect + +Queries over triangle meshes: clearance between meshes, point containment and winding number, ray casting, line of sight, genus and per-component topology, plane detection, and intersection or difference volumes with a certified error bound. Containment reuses the exact ray-parity test of the mesh boolean, so the two cannot disagree. Every query reports a measurement or a typed refusal and leaves the verdict ("too close", "hidden") to the caller. + +```bash +cargo add axiolid-inspect +``` + +- API documentation: [docs.rs/axiolid-inspect](https://docs.rs/axiolid-inspect) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-inspect) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/query/intersection/linear/AGENTS.md b/crates/algorithms/query/intersection/linear/AGENTS.md deleted file mode 100644 index fd5f8eb4..00000000 --- a/crates/algorithms/query/intersection/linear/AGENTS.md +++ /dev/null @@ -1,41 +0,0 @@ -# axiolid-linear-intersection - -Certified 2D intersections for linear geometry (ADR 0036). - -Allowed internal dependencies: `axiolid-core`, `axiolid-guarantees`, -`axiolid-linear`, `axiolid-predicates`. Adding any other internal dependency -breaks the declared `linear-intersection-minimal` closure and fails -`cargo xtask architecture closure check`. - -## Invariants - -Topology comes from certified predicates, never from comparing a determinant to -an epsilon. `Tolerance` governs residual acceptance of the returned coordinate -only; it must never be able to change a `Parallel`/`Coincident`/`Point` branch. - -For unbounded lines, parallelism is a property of the DIRECTIONS. Sampling one -point from each line and comparing sides is wrong — two crossing lines can put -both samples on the same side. That bug was caught by `near_parallel_lines_still_cross`. - -Results classify; they are never `Option`. Crossing, endpoint contact, -parallel-disjoint, coincident, collinear-disjoint, and overlap are distinct -facts that topology and rule checking need. - -Invalid input is a typed refusal naming the operand (`InputSide`). A zero -direction, a collapsed segment, and a non-finite coordinate are refusals, not -degenerate answers. `Disjoint` is a successful classification, never a refusal. - -A certified endpoint orientation of exactly zero is stronger evidence than a -divided parameter, so segment parameters snap to exact `0.0`/`1.0` there. - -## Gates - -```bash -cargo test -p axiolid-linear-intersection -cargo xtask architecture closure check -``` - -The adversarial suite must keep asserting that every branch was generated; a -random suite that silently never produces a coincident pair proves nothing. -Operand-swap comparisons use a RELATIVE bound — the two orders evaluate -differently ordered float expressions, so bitwise equality fails spuriously. diff --git a/crates/algorithms/query/intersection/linear/Cargo.toml b/crates/algorithms/query/intersection/linear/Cargo.toml index eaf65f84..2dfbe21c 100644 --- a/crates/algorithms/query/intersection/linear/Cargo.toml +++ b/crates/algorithms/query/intersection/linear/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [features] default = [] diff --git a/crates/algorithms/query/intersection/linear/README.md b/crates/algorithms/query/intersection/linear/README.md new file mode 100644 index 00000000..c77df89a --- /dev/null +++ b/crates/algorithms/query/intersection/linear/README.md @@ -0,0 +1,11 @@ +# axiolid-linear-intersection + +Certified 2D intersections for lines and segments, with a minimal dependency closure so a line-query application does not pull in curves, surfaces, meshes or B-rep (ADR 0036). Results are classifications, not optional points: crossing, endpoint contact, parallel-disjoint, coincident, collinear-disjoint and overlap are distinct variants. Topology comes from certified predicates; the tolerance only governs acceptance of the computed coordinate. Invalid input is a typed refusal naming the operand. + +```bash +cargo add axiolid-linear-intersection +``` + +- API documentation: [docs.rs/axiolid-linear-intersection](https://docs.rs/axiolid-linear-intersection) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-linear-intersection) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/query/intersection/linear/src/lib.rs b/crates/algorithms/query/intersection/linear/src/lib.rs index 2c3b3a47..d3d9e3a2 100644 --- a/crates/algorithms/query/intersection/linear/src/lib.rs +++ b/crates/algorithms/query/intersection/linear/src/lib.rs @@ -3,9 +3,12 @@ //! Certified intersections for linear geometry. //! //! This package exists so a line-query application can compile intersection -//! logic with a three-package internal closure — `axiolid-core`, -//! `axiolid-linear`, `axiolid-predicates` — and no curves, surfaces, NURBS, -//! meshes, B-rep, topology, providers, or execution machinery (ADR 0036). +//! logic with a four-package internal closure — `axiolid-core`, +//! `axiolid-guarantees`, `axiolid-linear`, `axiolid-predicates` — and no +//! curves, surfaces, NURBS, meshes, B-rep, topology, providers, or execution +//! machinery (ADR 0036). The `linear-intersection-minimal` profile in +//! `docs/architecture/closure-profiles.toml` pins that closure, so any new +//! internal dependency fails `cargo xtask architecture closure check`. //! //! # Classification, not `Option` //! diff --git a/crates/algorithms/query/intersection/linear/tests/adversarial.rs b/crates/algorithms/query/intersection/linear/tests/adversarial.rs index 38083f36..276f0016 100644 --- a/crates/algorithms/query/intersection/linear/tests/adversarial.rs +++ b/crates/algorithms/query/intersection/linear/tests/adversarial.rs @@ -3,6 +3,12 @@ //! The oracle here is deliberately independent of the implementation: it uses //! rational arithmetic over exactly-representable inputs, so agreement is //! evidence rather than a restatement of the same float expression. +//! +//! A random suite must assert that it generated every branch it classifies: +//! a suite that silently never produces a coincident pair proves nothing +//! about coincidence. Operand-swap comparisons use a relative bound, because +//! the two orders evaluate differently ordered float expressions and bitwise +//! equality would fail spuriously. use axiolid_core::{Point2, Tolerance, Vec2}; use axiolid_linear::{Line2, Segment2}; diff --git a/crates/algorithms/query/intersection/ray-mesh/AGENTS.md b/crates/algorithms/query/intersection/ray-mesh/AGENTS.md deleted file mode 100644 index 447f6504..00000000 --- a/crates/algorithms/query/intersection/ray-mesh/AGENTS.md +++ /dev/null @@ -1,44 +0,0 @@ -# axiolid-ray-mesh - -Narrow-phase ray/triangle-mesh nearest-hit intersection (milestone v0.3, #41). - -Allowed internal dependencies: `axiolid-core`, `axiolid-guarantees`, -`axiolid-mesh`, `axiolid-predicates`. `axiolid-spatial` is a DEV dependency -only: the broad phase composes with this package, it is not required by it. - -## Boundary - -The kernel owns the intersection and the hit record. It does not own what a ray -*means* — sampling patterns, camera rigs, entity identity, or whether a hit -counts as an obstruction stay with the caller. - -## Invariants - -Front/back is a certified `orient3d` sign of the ray origin against the triangle -plane, never the sign of a floating-point dot product. A ray origin exactly in -the plane reports `Coplanar` rather than picking a side. - -Degenerate (zero-area) triangles and out-of-range triangle indices are typed -refusals. A degenerate triangle must never be silently skipped: that turns a -broken mesh into a plausible miss. - -`t` is expressed in units of the supplied direction vector, which is not assumed -normalized. Callers wanting metres normalize first. - -Coincident hits break ties by lowest triangle index, so candidate order from any -broad phase cannot change the answer. `nearest_hit_among` and `nearest_hit` -agree by construction. - -`Tolerance::linear()` bounds only the parallel/edge acceptance window; it never -decides the front/back branch. - -## Gates - -```bash -cargo test -p axiolid-ray-mesh -cargo xtask architecture check -``` - -Fixtures must keep covering edge-on, vertex-on, parallel-in-plane, back-face, -behind-origin, and BVH composition. An oracle suite that never produces an -edge-on hit proves nothing about tie-breaking. diff --git a/crates/algorithms/query/intersection/ray-mesh/CHANGELOG.md b/crates/algorithms/query/intersection/ray-mesh/CHANGELOG.md index f110a750..5940b64e 100644 --- a/crates/algorithms/query/intersection/ray-mesh/CHANGELOG.md +++ b/crates/algorithms/query/intersection/ray-mesh/CHANGELOG.md @@ -8,3 +8,13 @@ Pre-1.0: the minor version is the breaking-change slot, per Cargo's own caret rule for `0.x` versions. ## [Unreleased] + +### Changed + +- **Breaking:** a candidate triangle index at or beyond the mesh's + triangle count is refused with the new + `RayMeshError::TriangleIndexOutOfRange` instead of being skipped by + `nearest_hit_among`; a broad phase built over a different mesh would + otherwise report "no hit" for triangles it never tested. + `triangle_hit` refuses the same index instead of panicking in the + mesh view. diff --git a/crates/algorithms/query/intersection/ray-mesh/Cargo.toml b/crates/algorithms/query/intersection/ray-mesh/Cargo.toml index 809ee809..5abb1904 100644 --- a/crates/algorithms/query/intersection/ray-mesh/Cargo.toml +++ b/crates/algorithms/query/intersection/ray-mesh/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "axiolid-ray-mesh" description = "Narrow-phase ray/triangle-mesh nearest-hit intersection" -version = "0.3.0" +version = "0.4.0" edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [features] default = [] diff --git a/crates/algorithms/query/intersection/ray-mesh/README.md b/crates/algorithms/query/intersection/ray-mesh/README.md new file mode 100644 index 00000000..58228242 --- /dev/null +++ b/crates/algorithms/query/intersection/ray-mesh/README.md @@ -0,0 +1,11 @@ +# axiolid-ray-mesh + +Narrow-phase ray/triangle-mesh intersection: the nearest hit with its parameter, barycentric coordinates and a certified front/back/coplanar side. It composes with a broad phase such as `axiolid-spatial` by taking candidate triangle indices, but does not depend on one. Degenerate triangles are refused rather than silently missed. It owns the intersection only, not what a ray means to the caller. + +```bash +cargo add axiolid-ray-mesh +``` + +- API documentation: [docs.rs/axiolid-ray-mesh](https://docs.rs/axiolid-ray-mesh) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-ray-mesh) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/query/intersection/ray-mesh/src/lib.rs b/crates/algorithms/query/intersection/ray-mesh/src/lib.rs index 84492903..8797542d 100644 --- a/crates/algorithms/query/intersection/ray-mesh/src/lib.rs +++ b/crates/algorithms/query/intersection/ray-mesh/src/lib.rs @@ -34,6 +34,12 @@ //! because normalising a caller's ray silently changes the meaning of every //! distance they compare against. //! +//! # What the tolerance decides +//! +//! `Tolerance::linear()` bounds only the parallel-ray rejection and the +//! barycentric slack that keeps edge and vertex hits. It never decides the +//! front/back branch: [`FaceSide`] comes from the certified `orient3d` sign. +//! //! [`SpatialIndex::visit_ray`]: https://docs.rs/axiolid-spatial use core::fmt; @@ -96,6 +102,16 @@ pub enum RayMeshError { /// Offending triangle. triangle: usize, }, + /// A candidate triangle index is at or beyond the mesh's triangle count. + /// + /// Reported rather than skipped: a broad phase built over a different + /// mesh would otherwise answer "no hit" for triangles it never tested. + TriangleIndexOutOfRange { + /// Offending candidate index. + triangle: usize, + /// Triangles in the mesh. + triangle_count: usize, + }, } impl fmt::Display for RayMeshError { @@ -106,6 +122,13 @@ impl fmt::Display for RayMeshError { Self::InvalidTolerance => { formatter.write_str("ray/mesh tolerance must be finite and non-negative") } + Self::TriangleIndexOutOfRange { + triangle, + triangle_count, + } => write!( + formatter, + "triangle {triangle} is out of range for a mesh of {triangle_count} triangles" + ), Self::PositionIndexOutOfRange { triangle } => { write!( formatter, @@ -137,7 +160,10 @@ pub fn nearest_hit( /// /// This is the composition point with a broad phase: feed it the triangle /// indices a BVH walk produced. Candidates may repeat and may arrive in any -/// order; the result does not depend on that order. +/// order; the result does not depend on that order. A candidate index at or +/// beyond `mesh.triangle_count()` is refused with +/// [`RayMeshError::TriangleIndexOutOfRange`], and a triangle that references +/// a missing position with [`RayMeshError::PositionIndexOutOfRange`]. /// /// # Determinism /// @@ -155,9 +181,6 @@ pub fn nearest_hit_among( let mut best: Option = None; for triangle in candidates { - if triangle >= mesh.triangle_count() { - continue; - } let Some(hit) = triangle_hit(mesh, ray, tolerance, triangle)? else { continue; }; @@ -277,6 +300,13 @@ fn is_closer(candidate: &RayHit3, current: &RayHit3) -> bool { } fn corners(mesh: &impl TriangleMeshView, triangle: usize) -> Result<[Point3; 3], RayMeshError> { + let triangle_count = mesh.triangle_count(); + if triangle >= triangle_count { + return Err(RayMeshError::TriangleIndexOutOfRange { + triangle, + triangle_count, + }); + } let indices = mesh.triangle(triangle); let mut corners = [Point3::ZERO; 3]; for (slot, index) in corners.iter_mut().zip(indices) { diff --git a/crates/algorithms/query/intersection/ray-mesh/tests/nearest_hit.rs b/crates/algorithms/query/intersection/ray-mesh/tests/nearest_hit.rs index fe39e9b7..6d142944 100644 --- a/crates/algorithms/query/intersection/ray-mesh/tests/nearest_hit.rs +++ b/crates/algorithms/query/intersection/ray-mesh/tests/nearest_hit.rs @@ -2,13 +2,16 @@ //! //! Fixtures are chosen so every expected answer is checkable by hand: unit //! triangles at known offsets, axis-aligned rays, and exact edge/vertex hits. +//! Keep covering edge-on, vertex-on, parallel-in-plane, back-face, +//! behind-origin and BVH composition: a suite that never produces an edge-on +//! hit proves nothing about tie-breaking. use std::ops::ControlFlow; use axiolid_core::{Aabb, Point3, Ray3, Tolerance, Vec3}; use axiolid_mesh::TriMesh; use axiolid_ray_mesh::{ - intersect_triangle, nearest_hit, nearest_hit_among, FaceSide, RayMeshError, + intersect_triangle, nearest_hit, nearest_hit_among, triangle_hit, FaceSide, RayMeshError, }; use axiolid_spatial::{Bvh, SpatialIndex, SpatialItem}; @@ -288,6 +291,26 @@ fn an_out_of_range_triangle_index_is_reported_not_ignored() { ); } +#[test] +fn an_out_of_range_candidate_is_refused_not_skipped() { + let mesh = stacked_planes(); + let query = ray([0.5, 0.5, 0.0], [0.0, 0.0, 1.0]); + let count = axiolid_mesh::TriangleMeshView::triangle_count(&mesh); + let refused = Err(RayMeshError::TriangleIndexOutOfRange { + triangle: count, + triangle_count: count, + }); + // Even alongside a candidate that hits: a skipped index would hide it. + assert_eq!( + nearest_hit_among(&mesh, &query, Tolerance::METRE, [0, count]), + refused + ); + assert_eq!( + triangle_hit(&mesh, &query, Tolerance::METRE, count), + refused + ); +} + #[test] fn broad_phase_candidates_compose_with_the_narrow_phase() { let mesh = stacked_planes(); diff --git a/crates/algorithms/query/measure/AGENTS.md b/crates/algorithms/query/measure/AGENTS.md deleted file mode 100644 index d7ca858d..00000000 --- a/crates/algorithms/query/measure/AGENTS.md +++ /dev/null @@ -1,18 +0,0 @@ -# axiolid-measure instructions - -Purpose: Metric and mass-property contracts. - -Allowed internal dependencies: axiolid-core, axiolid-mesh. Follow parent `../AGENTS.md`. Do not read -`PLAN.md` unless assigned implementation or roadmap work. - -## Module ownership - -properties.rs; measure.rs. Split a module before unrelated data, validation, and algorithms grow -together. Add no empty placeholder files. - -## Invariants - -Reject undefined volume for open/non-manifold input. Carry signed and absolute values deliberately; do not return plausible zeros for unsupported geometry. - -Public values derive `Debug` and `Clone`; add other standard traits only when -semantically valid. Validate unsupported and unavailable paths in tests. diff --git a/crates/algorithms/query/measure/Cargo.toml b/crates/algorithms/query/measure/Cargo.toml index 74f47dc5..e7ff2162 100644 --- a/crates/algorithms/query/measure/Cargo.toml +++ b/crates/algorithms/query/measure/Cargo.toml @@ -6,7 +6,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true description = "Metric properties: area, volume, centroid, moments of inertia." diff --git a/crates/algorithms/query/measure/README.md b/crates/algorithms/query/measure/README.md new file mode 100644 index 00000000..0427376c --- /dev/null +++ b/crates/algorithms/query/measure/README.md @@ -0,0 +1,11 @@ +# axiolid-measure + +Metric properties of geometry: surface area, signed volume, centroids and second moments of triangle meshes, closest points and distances between segments, triangles and meshes, Frechet distance between polylines, and winding numbers. Undefined quantities are refused: an open or non-manifold mesh gets an error, not a plausible volume. The optional `exact` feature adds mass properties and certified boundary distance for exact B-reps without imposing them on mesh-only consumers. + +```bash +cargo add axiolid-measure +``` + +- API documentation: [docs.rs/axiolid-measure](https://docs.rs/axiolid-measure) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-measure) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/query/spatial/AGENTS.md b/crates/algorithms/query/spatial/AGENTS.md deleted file mode 100644 index 9847f67c..00000000 --- a/crates/algorithms/query/spatial/AGENTS.md +++ /dev/null @@ -1,18 +0,0 @@ -# axiolid-spatial instructions - -Purpose: Spatial acceleration contracts. - -Allowed internal dependencies: axiolid-core, axiolid-mesh. Follow parent `../AGENTS.md`. Do not read -`PLAN.md` unless assigned implementation or roadmap work. - -## Module ownership - -index.rs. Split a module before unrelated data, validation, and algorithms grow -together. Add no empty placeholder files. - -## Invariants - -Queries use callbacks to avoid mandatory allocation. Broad phase returns candidates only; do not label AABB overlap an exact clash. Output order must be deterministic when requested. - -Public values derive `Debug` and `Clone`; add other standard traits only when -semantically valid. Validate unsupported and unavailable paths in tests. diff --git a/crates/algorithms/query/spatial/Cargo.toml b/crates/algorithms/query/spatial/Cargo.toml index f3f80f94..4bfed4ad 100644 --- a/crates/algorithms/query/spatial/Cargo.toml +++ b/crates/algorithms/query/spatial/Cargo.toml @@ -6,7 +6,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true description = "Acceleration structures: BVH and uniform point grid, and their queries; barycentric and mean-value coordinates." diff --git a/crates/algorithms/query/spatial/PLAN.md b/crates/algorithms/query/spatial/PLAN.md deleted file mode 100644 index c67e145d..00000000 --- a/crates/algorithms/query/spatial/PLAN.md +++ /dev/null @@ -1,31 +0,0 @@ -# axiolid-spatial implementation plan - -Status: BVH and uniform point grid implemented; octree remains unimplemented -and is deliberately not claimed in the crate description or docs. - -## Standing invariants - -- Crate boundary and dependency direction are executable in the layering gate. -- [`Bvh`](src/bvh.rs) is a read-only, deterministic median-split broad-phase - provider. It rejects malformed bounds, preserves accepted input pair order, - supports callback AABB/ray traversal, pair candidates, and filtered nearest - queries. -- It is intentionally serial today. The public `SpatialIndex` callback seam - leaves room for parallel CPU and GPU providers without coupling the contract - to either execution strategy. - -## Shape of the work - -- Benchmark this BVH against an external reference implementation on - representative sparse, dense, and adversarial distributions before adding - parallel build/query code. -- `PointIndex` (uniform grid) is implemented for point KNN/radius queries. -- Add an octree only where a measured workload justifies it. The BVH covers - object queries and the grid covers point queries; an octree's advantage is - sparse volumetric subdivision, which no consumer needs yet. - -## Exit evidence - -Targeted differential tests, feature-isolated compile where applicable, -mutation-verified architecture/validation gates, and benchmarks before -performance claims. diff --git a/crates/algorithms/query/spatial/README.md b/crates/algorithms/query/spatial/README.md new file mode 100644 index 00000000..ebebdaad --- /dev/null +++ b/crates/algorithms/query/spatial/README.md @@ -0,0 +1,11 @@ +# axiolid-spatial + +Deterministic, callback-based spatial acceleration: a median-split BVH over bounded objects and a uniform grid for point KNN and radius search, both behind the `SpatialIndex` query contract. They return candidates only, never exact intersections. It also provides barycentric and mean-value coordinates for interpolating values given at triangle, tetrahedron and polygon corners. + +```bash +cargo add axiolid-spatial +``` + +- API documentation: [docs.rs/axiolid-spatial](https://docs.rs/axiolid-spatial) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-spatial) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/query/spatial/src/bvh.rs b/crates/algorithms/query/spatial/src/bvh.rs index fc9b6253..ec84fb1c 100644 --- a/crates/algorithms/query/spatial/src/bvh.rs +++ b/crates/algorithms/query/spatial/src/bvh.rs @@ -2,7 +2,8 @@ //! //! The tree stores only caller-owned keys and axis-aligned bounds. It is a //! broad-phase structure: overlap and ray results are candidates, never an -//! assertion about exact geometry. The immutable representation is deliberately +//! assertion about exact geometry. Build and queries are serial. The +//! immutable representation is deliberately //! provider-neutral; a parallel or GPU builder can implement the same //! [`crate::SpatialIndex`] contract later without exposing hardware concepts. diff --git a/crates/algorithms/reference/AGENTS.md b/crates/algorithms/reference/AGENTS.md deleted file mode 100644 index 2c7365f3..00000000 --- a/crates/algorithms/reference/AGENTS.md +++ /dev/null @@ -1,43 +0,0 @@ -# axiolid-reference instructions - -Purpose: portable scalar reference implementation and correctness oracle (ADR 0012). - -Allowed internal dependencies: axiolid-core, guarantees/common/operation contracts, axiolid-mesh, -axiolid-curve. Follow parent `../AGENTS.md`. Do not read -`PLAN.md` unless assigned implementation or roadmap work. - -## Module ownership - -expansion.rs; orientation.rs; boolean.rs (solid boolean oracle, ADR 0017); -curve.rs (curve evaluation and adaptive flattening, ADR 0018); section.rs -(exact-sign mesh plane-section oracle). Split a module -before unrelated data, validation, and algorithms grow -together. Add no empty placeholder files. - -## Invariants - -No intrinsics, no threading, no feature gates, no `unsafe`. This crate is the -differential oracle every optimized backend is validated against, so readability -outranks speed. Per ADR 0012 the scalar implementation of an operation lands -before any optimized implementation of it. - -Predicates return `Certified`, never a bare sign. A predicate that can escalate -must escalate: returning an uncertified sign for a topology decision is the one -failure this crate exists to prevent. Error bounds scale with operand magnitude; -a constant epsilon is a bug. - -Tests must include a differential gate against an oracle that shares no code -with the implementation, and must assert that the exact path was actually -reached -- a test suite that never escalates proves nothing about exactness. - -## Extracted packages (ADR 0036) - -Certified predicates live in `axiolid-predicates`; analytic and spline -curve/surface evaluation lives in `axiolid-evaluate`. Both are re-exported here -unchanged, so `axiolid_reference::orient2d` and `axiolid_reference::curve::*` -keep working. - -Do not re-inline either cluster. Their whole value is that a narrow consumer -(predicates, NURBS, CAD) can depend on the substrate without acquiring this -umbrella's mesh, spatial, and measure graph. Adding a dependency here is cheap; -adding one to an extracted package is an architectural decision. diff --git a/crates/algorithms/reference/Cargo.toml b/crates/algorithms/reference/Cargo.toml index 7eb071fe..59ea0b48 100644 --- a/crates/algorithms/reference/Cargo.toml +++ b/crates/algorithms/reference/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/algorithms/reference/PLAN.md b/crates/algorithms/reference/PLAN.md deleted file mode 100644 index 09a3982e..00000000 --- a/crates/algorithms/reference/PLAN.md +++ /dev/null @@ -1,30 +0,0 @@ -# axiolid-reference plan - -Design notes for the predicate suite. Status lives on GitHub, not here: -this file records *what the predicates must satisfy*, which does not -change when an item ships (kernel#25). - -## The predicate suite -Error-free transformations (`two_sum`, `two_diff`, `two_product`) and -arbitrary-length expansion arithmetic are the shared foundation. On top -of them: `orient2d` and `orient3d` (is a point above/on/below a line or -plane), `incircle` and `insphere` (is a point inside/on/outside a -circumcircle or circumsphere). - -Each is a filtered cascade: a fast floating-point path with a computed -error bound, escalating to exact expansion arithmetic only when the -bound cannot decide the sign. Static filters precompute bounds from a -coordinate magnitude limit, skipping the per-call permanent computation. - -## Gates -- Differential vs an independent exact oracle (i128 rational, integer inputs - bounded so the determinant cannot overflow). -- Measured escalation rate per degeneracy tier, asserted to stay in band. - The degeneracy benchmark harness reports throughput AND escalation rate - at 0%, 0.01%, 1%, 10% degenerate inputs. -- Mutation probes on every filter bound and every exact path. - -## Relationship to adopted predicates -`boolmesh` carries its own predicates and is MPL-2.0, so replacing them means -forking. See ADR 0016: ours serve our own algorithms and act as an independent -audit oracle for adopted ones, rather than trying to displace them. diff --git a/crates/algorithms/reference/README.md b/crates/algorithms/reference/README.md new file mode 100644 index 00000000..eafe81be --- /dev/null +++ b/crates/algorithms/reference/README.md @@ -0,0 +1,19 @@ +# axiolid-reference + +The portable scalar reference implementation that every optimized backend +is differentially tested against (ADR 0012): a solid boolean oracle, an +exact-sign mesh plane-section oracle, triangle/triangle and +segment/triangle relations, clash detection, convex hulls, polygon +triangulation and tessellation. It favours readability over speed: no +intrinsics, threading, feature gates or `unsafe`. It is also a convenience +umbrella that re-exports `axiolid-predicates` and `axiolid-evaluate` +unchanged (ADR 0036); a consumer that needs only certified signs or curve +evaluation should depend on those directly. + +```bash +cargo add axiolid-reference +``` + +- API documentation: [docs.rs/axiolid-reference](https://docs.rs/axiolid-reference) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-reference) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/reference/src/lib.rs b/crates/algorithms/reference/src/lib.rs index 99f9aa55..701fbf5b 100644 --- a/crates/algorithms/reference/src/lib.rs +++ b/crates/algorithms/reference/src/lib.rs @@ -10,7 +10,15 @@ //! now live in the focused `axiolid-predicates` package and are re-exported //! here unchanged, so an existing `axiolid_reference::orient2d` caller is //! unaffected while a narrow consumer can depend on the substrate directly -//! instead of acquiring this package's whole dependency graph. +//! instead of acquiring this package's whole dependency graph. Analytic and +//! spline evaluation moved to `axiolid-evaluate` the same way. Do not +//! re-inline either: adding a dependency here is cheap, adding one to an +//! extracted package is an architectural decision. +//! +//! Error bounds scale with operand magnitude; a constant epsilon is a bug. +//! Tests here include a differential gate against an oracle that shares no +//! code with the implementation, and assert that the exact path was actually +//! reached: a suite that never escalates proves nothing about exactness. pub mod assemble; pub mod boolean; diff --git a/crates/algorithms/repair/AGENTS.md b/crates/algorithms/repair/AGENTS.md deleted file mode 100644 index 9bb4cbe0..00000000 --- a/crates/algorithms/repair/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Repair algorithms - -`heal/` owns explicit mesh repair operations and diagnostics. diff --git a/crates/algorithms/repair/brep-audit/Cargo.toml b/crates/algorithms/repair/brep-audit/Cargo.toml index d3e0cd14..914ddce2 100644 --- a/crates/algorithms/repair/brep-audit/Cargo.toml +++ b/crates/algorithms/repair/brep-audit/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/algorithms/repair/brep-audit/README.md b/crates/algorithms/repair/brep-audit/README.md new file mode 100644 index 00000000..2c14a926 --- /dev/null +++ b/crates/algorithms/repair/brep-audit/README.md @@ -0,0 +1,11 @@ +# axiolid-brep-audit + +Geometric consistency auditing for exact B-reps. It evaluates curves and surfaces and checks that edge vertices lie on their 3D curves and that every pcurve, mapped through its face's surface, lands on the curve of the edge it trims. It complements the exact, tolerance-free topological audit in `axiolid-topology`; because it compares positions, it needs a tolerance and reports agreement to within it (ADR 0052). It diagnoses only and repairs nothing. + +```bash +cargo add axiolid-brep-audit +``` + +- API documentation: [docs.rs/axiolid-brep-audit](https://docs.rs/axiolid-brep-audit) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-brep-audit) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/repair/heal/AGENTS.md b/crates/algorithms/repair/heal/AGENTS.md deleted file mode 100644 index 22d1ae5c..00000000 --- a/crates/algorithms/repair/heal/AGENTS.md +++ /dev/null @@ -1,18 +0,0 @@ -# axiolid-heal instructions - -Purpose: Explicit diagnosis and opt-in repair. - -Allowed internal dependencies: axiolid-core. Follow parent `../AGENTS.md`. Do not read -`PLAN.md` unless assigned implementation or roadmap work. - -## Module ownership - -diagnosis.rs; repair.rs; traits.rs. Split a module before unrelated data, validation, and algorithms grow -together. Add no empty placeholder files. - -## Invariants - -Diagnosis never mutates. There is no repair-all switch. Every repair report records what changed and what remains. - -Public values derive `Debug` and `Clone`; add other standard traits only when -semantically valid. Validate unsupported and unavailable paths in tests. diff --git a/crates/algorithms/repair/heal/Cargo.toml b/crates/algorithms/repair/heal/Cargo.toml index 37ec3069..4d933760 100644 --- a/crates/algorithms/repair/heal/Cargo.toml +++ b/crates/algorithms/repair/heal/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/algorithms/repair/heal/PLAN.md b/crates/algorithms/repair/heal/PLAN.md deleted file mode 100644 index 5a3a49ac..00000000 --- a/crates/algorithms/repair/heal/PLAN.md +++ /dev/null @@ -1,47 +0,0 @@ -# axiolid-heal implementation plan - -Status: diagnosis implemented; repair not started. This is planning context, -not standing agent instruction. - -## Standing invariants - -- Crate boundary and dependency direction are executable in the layering gate. -- Public data/contracts compile. Behavior remains scaffold unless a test names it. - -## Implemented - -- `diagnose` produces a `Diagnosis` from a mesh: non-manifold edges, - inconsistent winding, boundary edges, degenerate triangles, and - self-intersecting triangle pairs. -- `self_intersections` decides triangle-triangle crossing exactly, through - certified `orient3d` with a BVH broad phase. `self_intersections_brute_force` - is the exhaustive reference the accelerated path is checked against. -- `Diagnosis::blocks_boolean` answers from measured defects rather than from - vocabulary. - -## Shape of the work - -Repair. Every repair must report what it changed, per defect, so a caller can -audit the difference rather than trust it. Diagnosis landed first deliberately: -a repair that cannot name what it fixed is not auditable. - -Coplanar overlapping triangles are currently reported conservatively as -intersecting; deciding them needs 2D region logic this crate does not own. - -## Exit evidence - -Targeted tests, feature-isolated compile where applicable, mutation-verified -architecture/validation gates, and benchmarks before performance claims. - -## Repair status (#74) - -- Implemented, all opt-in via `RepairPlan`: `WeldVertices`, - `DropDegenerateElements`, `UnifyOrientation`, `OrientOutward`. -- `OrientOutward` closes the gap `unify_orientation` left open: unify makes - neighbours agree, `OrientOutward` decides which way round the agreed shell - faces, using `axiolid-measure` for the volume convention. -- NOT implemented: stitching coincident boundary edges into shared edges, - and self-intersection removal. Detection of the latter exists; removal - changes topology and is deliberately deferred. -- No repair runs implicitly. Nothing in compilation or dispatch calls into - this crate. diff --git a/crates/algorithms/repair/heal/README.md b/crates/algorithms/repair/heal/README.md new file mode 100644 index 00000000..8fd652fd --- /dev/null +++ b/crates/algorithms/repair/heal/README.md @@ -0,0 +1,11 @@ +# axiolid-heal + +Explicit diagnosis and opt-in repair of triangle meshes. `diagnose` reports non-manifold edges, inconsistent winding, boundary edges, duplicate vertices, degenerate triangles and exact self-intersections without touching the mesh. Repairs (weld vertices, drop degenerate elements, unify orientation, orient outward) run only when a caller names them in a `RepairPlan`, and the `RepairReport` records what was applied, what was skipped and what happened to each attribute channel. There is no repair-everything mode, and no other operation heals implicitly. + +```bash +cargo add axiolid-heal +``` + +- API documentation: [docs.rs/axiolid-heal](https://docs.rs/axiolid-heal) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-heal) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/repair/heal/src/repair.rs b/crates/algorithms/repair/heal/src/repair.rs index f09c85f5..fa6a810a 100644 --- a/crates/algorithms/repair/heal/src/repair.rs +++ b/crates/algorithms/repair/heal/src/repair.rs @@ -1,6 +1,10 @@ //! Explicit repair plans and reports. /// One opt-in repair. There is deliberately no `All` variant. +/// +/// No action removes self-intersections. [`crate::self_intersections`] +/// detects them, but removing them cuts and re-triangulates faces, and that +/// is deliberately not offered as a repair. #[non_exhaustive] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum RepairAction { diff --git a/crates/algorithms/sampled/AGENTS.md b/crates/algorithms/sampled/AGENTS.md deleted file mode 100644 index 01147fe6..00000000 --- a/crates/algorithms/sampled/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Sampled algorithms - -`field/` operates on `axiolid-field` values. Application meaning remains outside Axiolid. diff --git a/crates/algorithms/sampled/field/AGENTS.md b/crates/algorithms/sampled/field/AGENTS.md deleted file mode 100644 index 42b5eb33..00000000 --- a/crates/algorithms/sampled/field/AGENTS.md +++ /dev/null @@ -1,63 +0,0 @@ -# axiolid-field-ops instructions - -Purpose: frame-neutral, deterministic algorithms over sampled layered fields. - -Allowed internal dependencies: `axiolid-core`, `axiolid-field`. Follow parent -`../AGENTS.md`. Do not read `PLAN.md` unless assigned implementation or roadmap -work. - -## Module ownership - -Field values, configuration, evidence, and invariant-preserving constructors -belong to `axiolid-field`. This crate owns `sample.rs` scalar CPU triangle -coverage; `morphology.rs` masks, metric -dilation/erosion, connected components; `clearance.rs` gap queries; -`navigate.rs` geometry-only traversal behind the `navigation` feature. - -## Invariants - -A cell holds **two separate channels**. `SurfaceHit`s are zero-thickness -crossings with a facing sign; occupancy is a set of strictly positive, -strictly disjoint intervals. Triangle coverage emits surface hits **only** — a -triangle has no thickness and must never be reported as occupied volume. -Occupancy is derived in a separate step from alternating enter/exit crossings -and requires a closed shell; an unbalanced sequence is -`UnbalancedCrossings`, and a tolerance-collapsed span is `DegenerateOccupancy`. -Neither is repaired silently. - -No world axis is assumed. The caller supplies a validated right-handed -`Frame3`; local `z` is the layering axis. Bounds, cell size, and radii are in -local units. A caller wanting Z-up passes the identity frame explicitly. - -There is no built-in resource cap. `FieldResourceBudget` is caller-owned, and -exhaustion is `CellBudgetExceeded` or `SampleBudgetExceeded` — never a silent -truncation. Tolerance is explicit per field; there is no global epsilon. - -Determinism is a contract: cells are row-major, surface hits sort by `w` then -facing, components are labelled in scan order with the lowest reachable index, -and route ties break by cost then node index. Repeated runs are bit-identical. - -Sampling never invents data. Parallel rays, degenerate triangles, out-of-bounds -crossings, edge/vertex contacts, and merged coincident hits from shared facet -edges are all counted in `SamplingEvidence` rather than dropped quietly. - -## Navigation boundary - -`navigate.rs` may report `route exists`, `no route under this envelope`, and -`clearance = X` with geometric rejection evidence. It must never name a domain -verdict: no accessibility, ADA, wheelchair, egress, escape-route, code -compliance, or vendor rule vocabulary in any type, field, variant, or doc. -Envelope values are geometry (`agent_radius`, `agent_height`, `max_step`, -`max_slope`), not policy. The feature is opt-in and stays that way until a -second independent consumer needs the same neutral contract. - -## Gates - -```bash -cargo test -p axiolid-field-ops --all-features -python3 scripts/probe_field_gate.py -``` - -Mutation probes must kill every defect before the suite is trusted. Probe -anchors are matched against rustfmt-normalised sources — re-read the formatted -file after `cargo fmt`, or anchors silently miss and probes report as leaked. diff --git a/crates/algorithms/sampled/field/Cargo.toml b/crates/algorithms/sampled/field/Cargo.toml index d8c91a18..66a43c6e 100644 --- a/crates/algorithms/sampled/field/Cargo.toml +++ b/crates/algorithms/sampled/field/Cargo.toml @@ -6,7 +6,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true description = "Sampling, morphology, clearance, and navigation over Axiolid layered fields." diff --git a/crates/algorithms/sampled/field/PLAN.md b/crates/algorithms/sampled/field/PLAN.md deleted file mode 100644 index 79b9d0c8..00000000 --- a/crates/algorithms/sampled/field/PLAN.md +++ /dev/null @@ -1,44 +0,0 @@ -# axiolid-field implementation plan - -Status: coverage, representation, morphology, clearance, and opt-in traversal -are implemented and mutation-verified. This is planning context, not standing -agent instruction. - -## Standing invariants - -- Explicit `Frame3` / `FieldBounds` / cell size / `Tolerance` / - `FieldResourceBudget` configuration, validated together. -- Two-channel `LayeredCell`: ordered `SurfaceHit`s and disjoint occupancy. -- Deterministic scalar CPU triangle coverage emitting surface hits only. -- Separate closed-shell occupancy derivation with typed failure evidence. -- Planar masks, metric dilation/erosion, connected components. -- Clearance queries along the local layering axis. -- Geometry-only traversal behind the `navigation` feature. -- 10/10 mutation probes killed by `scripts/probe_field_gate.py`. - -## Deliberately not done - -**No GPU provider.** The constraint is explicit: a GPU batch provider requires -representative benchmarks demonstrating a meaningful advantage over the CPU -provider on an agreed workload. No such workload or threshold has been agreed, -so adding GPU code now would be unjustified complexity. The CPU sampler is -structured for it — per-cell independent work over a flat triangle slice — but -the seam stays unimplemented until evidence exists. - -**No navigation promotion to a shared contract.** The rule is at least two -consumers needing the same neutral contract. One exists today. Until a second -appears, `navigation` stays an opt-in feature of this crate rather than a -kernel-level trait. - -## Shape of the work - -Only when driven by a real consumer: - -- BVH-accelerated candidate triangle lookup for large scenes (measure first). -- Parallel per-row sampling via an explicit context-local pool. -- Sparse cell storage if profiling shows empty-cell waste dominates. - -## Exit evidence - -Targeted tests, feature-isolated compile, mutation-verified gates, and -benchmarks before any performance claim. diff --git a/crates/algorithms/sampled/field/README.md b/crates/algorithms/sampled/field/README.md new file mode 100644 index 00000000..71179610 --- /dev/null +++ b/crates/algorithms/sampled/field/README.md @@ -0,0 +1,17 @@ +# axiolid-field-ops + +Deterministic algorithms over `axiolid-field` layered fields: scalar CPU triangle coverage, planar masks with metric dilation, erosion and connected components, clearance along the layering axis, and, behind the opt-in `navigation` feature, geometry-only route finding under an explicit agent envelope. It reports coverage, spans, components and route existence, never an application verdict such as accessibility or compliance. + +```bash +cargo add axiolid-field-ops +``` + +- API documentation: [docs.rs/axiolid-field-ops](https://docs.rs/axiolid-field-ops) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-field-ops) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) + +## Design notes + +There is no GPU coverage provider. The CPU sampler is per-cell independent work over a flat +triangle slice, so a batch provider can be added once a benchmark on an agreed workload shows it +pays; without that evidence it would be complexity with no measured benefit. diff --git a/crates/algorithms/sampled/levelset/Cargo.toml b/crates/algorithms/sampled/levelset/Cargo.toml index f3b32c07..824034cc 100644 --- a/crates/algorithms/sampled/levelset/Cargo.toml +++ b/crates/algorithms/sampled/levelset/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] ahash.workspace = true diff --git a/crates/algorithms/sampled/levelset/README.md b/crates/algorithms/sampled/levelset/README.md new file mode 100644 index 00000000..771e1710 --- /dev/null +++ b/crates/algorithms/sampled/levelset/README.md @@ -0,0 +1,11 @@ +# axiolid-levelset + +Level-set extraction: a closed two-manifold triangle mesh from a scalar field sampled on a grid. Cells are split into Kuhn tetrahedra rather than marched as cubes, so shared faces always split the same way and the result is watertight by construction; exact grid tangency is resolved by simulation of simplicity. The mesh interpolates the field linearly along cell edges, so it is an approximation whose error shrinks with the grid, not a certified surface. + +```bash +cargo add axiolid-levelset +``` + +- API documentation: [docs.rs/axiolid-levelset](https://docs.rs/axiolid-levelset) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-levelset) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/algorithms/sampled/levelset/src/lib.rs b/crates/algorithms/sampled/levelset/src/lib.rs index 5d3ed35b..d29b29f2 100644 --- a/crates/algorithms/sampled/levelset/src/lib.rs +++ b/crates/algorithms/sampled/levelset/src/lib.rs @@ -46,41 +46,6 @@ //! so no sample sits exactly at the level and every crossing is strictly //! interior to its edge. See `SOS_DELTA` for the numeric stand-in and why //! its magnitude matters. -//! -//! # What this is not -//! -//! Extraction is an approximation. The mesh interpolates the field -//! linearly along each edge, so a curved surface is faceted and the error -//! shrinks with the grid, it does not vanish. This is not a certified -//! path and does not claim to be. -//! -//! # Known limitation: grid tangency -//! -//! The closed-manifold guarantee holds when the surface passes BETWEEN -//! grid samples. It does not currently hold when the level set is exactly -//! tangent to a grid plane -- a sphere of radius 1 with samples landing -//! exactly on 1.0, for instance. Two edges of the same tetrahedron then -//! interpolate to the same point, the triangle between them has zero area, -//! and the surface is left with unmatched edges. -//! -//! Measured, so the boundary of the guarantee is known rather than -//! assumed: a unit sphere in bounds of half-extent 1.45 is closed at edge -//! lengths 0.4, 0.2 and 0.1, while the same sphere in half-extent 1.4 is -//! closed at 0.4 and open at 0.2 and 0.1 -- exactly the resolutions whose -//! samples land on the radius. -//! -//! Two fixes were tried and rejected. Offsetting an exactly-zero sample by -//! `Scalar::MIN_POSITIVE` produced vertices a SUBNORMAL distance apart: -//! distinct in bits, identical in geometry, so it reproduced the -//! degeneracy it was meant to remove. Offsetting by a fraction of the cell -//! instead flips the sample's side and changes the topology, which broke -//! the general case to patch the special one. The remaining candidate is -//! symbolic perturbation (simulation of simplicity), which decides ties by -//! index rather than by value; that is a larger change and is not done -//! here. -//! -//! A caller who needs the guarantee unconditionally should offset the -//! bounds so no grid plane is tangent to the surface. use ahash::AHashMap; diff --git a/crates/contracts/AGENTS.md b/crates/contracts/AGENTS.md deleted file mode 100644 index 5aa96425..00000000 --- a/crates/contracts/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Contracts - -Provider-neutral capability schemas and guarantees. `common/` is temporary mixed ownership; `operations/` owns typed operation contracts. diff --git a/crates/contracts/common/AGENTS.md b/crates/contracts/common/AGENTS.md deleted file mode 100644 index 0f3a561d..00000000 --- a/crates/contracts/common/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Common contracts - -`base/` owns execution/diagnostic vocabulary; `mesh/` owns shared mesh admissibility. Neither selects providers. diff --git a/crates/contracts/common/base/AGENTS.md b/crates/contracts/common/base/AGENTS.md deleted file mode 100644 index ccb74150..00000000 --- a/crates/contracts/common/base/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Common base contracts - -Owns backend identity, cancellation, diagnostics, output bounds, and execution options. Depends only on core and guarantees; never operation schemas, providers, or dispatch. diff --git a/crates/contracts/common/base/Cargo.toml b/crates/contracts/common/base/Cargo.toml index 249dfe7e..6510383a 100644 --- a/crates/contracts/common/base/Cargo.toml +++ b/crates/contracts/common/base/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/contracts/common/base/PLAN.md b/crates/contracts/common/base/PLAN.md deleted file mode 100644 index 33dfab0f..00000000 --- a/crates/contracts/common/base/PLAN.md +++ /dev/null @@ -1,20 +0,0 @@ -# Common base contracts plan - -Status: superseded by ADR 0035. Common vocabulary remains here; operation schemas and execution policy were extracted to sibling packages. -not standing agent instruction. - -## Standing invariants - -- Crate boundary and dependency direction are executable in the layering gate. -- Public operation traits compile; the mesh-boolean registry stores executable - trait objects rather than capability metadata. - -## Shape of the work - -Add a narrow batch trait only when a real implementation needs it; add an -operation-specific executable registry only when more than one provider exists. - -## Exit evidence - -Targeted tests, feature-isolated compile where applicable, mutation-verified -architecture/validation gates, and benchmarks before performance claims. diff --git a/crates/contracts/common/base/README.md b/crates/contracts/common/base/README.md new file mode 100644 index 00000000..e8892863 --- /dev/null +++ b/crates/contracts/common/base/README.md @@ -0,0 +1,16 @@ +# axiolid-contracts + +Common, provider-neutral contracts shared by every operation: backend +identity and descriptors, cancellation, diagnostics and `GeomError`, output +bounds and execution options, operation plans, and the integration profiles +a downstream application checks against. It defines no operation schema of +its own (those are the sibling `axiolid-*-contract` packages) and does no +provider selection or fallback, which belong to `axiolid-dispatch`. + +```bash +cargo add axiolid-contracts +``` + +- API documentation: [docs.rs/axiolid-contracts](https://docs.rs/axiolid-contracts) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-contracts) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/contracts/common/mesh/Cargo.toml b/crates/contracts/common/mesh/Cargo.toml index 50751cb1..c0ef1bdb 100644 --- a/crates/contracts/common/mesh/Cargo.toml +++ b/crates/contracts/common/mesh/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-contracts.workspace = true diff --git a/crates/contracts/common/mesh/README.md b/crates/contracts/common/mesh/README.md new file mode 100644 index 00000000..4dbeb4e3 --- /dev/null +++ b/crates/contracts/common/mesh/README.md @@ -0,0 +1,14 @@ +# axiolid-mesh-contracts + +Shared admissibility rules for mesh-valued operations: what a triangle mesh +must satisfy to be accepted as a solid operand, with a typed rejection when +it does not. Axiolid owns this definition so that every provider accepts +exactly the same inputs; it does not select or run a provider. + +```bash +cargo add axiolid-mesh-contracts +``` + +- API documentation: [docs.rs/axiolid-mesh-contracts](https://docs.rs/axiolid-mesh-contracts) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-mesh-contracts) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/contracts/guarantees/AGENTS.md b/crates/contracts/guarantees/AGENTS.md deleted file mode 100644 index 8de5ebc1..00000000 --- a/crates/contracts/guarantees/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Guarantees - -Certified results, indeterminate states, and escalation vocabulary. No representation or provider dependencies. diff --git a/crates/contracts/guarantees/Cargo.toml b/crates/contracts/guarantees/Cargo.toml index 20618bd0..04c96ba5 100644 --- a/crates/contracts/guarantees/Cargo.toml +++ b/crates/contracts/guarantees/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [package.metadata.axiolid] architecture-version = 1 diff --git a/crates/contracts/guarantees/README.md b/crates/contracts/guarantees/README.md new file mode 100644 index 00000000..207809ec --- /dev/null +++ b/crates/contracts/guarantees/README.md @@ -0,0 +1,14 @@ +# axiolid-guarantees + +Provider-neutral vocabulary for what a geometric answer is worth: certified +values, signs that may be indeterminate, reported precision, and the +escalation ladder a provider climbs when a fast answer is not certain. It +depends on no representation or provider, so any contract can use it. + +```bash +cargo add axiolid-guarantees +``` + +- API documentation: [docs.rs/axiolid-guarantees](https://docs.rs/axiolid-guarantees) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-guarantees) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/contracts/operations/AGENTS.md b/crates/contracts/operations/AGENTS.md deleted file mode 100644 index f542c63a..00000000 --- a/crates/contracts/operations/AGENTS.md +++ /dev/null @@ -1,16 +0,0 @@ -# Operation contracts - -Each child owns one portable request/result/evidence schema. Provider selection and fallback live in `crates/execution/dispatch`. - -## Naming - -A crate here is named `axiolid--contract`, where `` is the -package's own `metadata.axiolid.domain` with dots replaced by hyphens. An -implementation NEVER takes the bare `axiolid-` name: most -capabilities here have more than one implementation, and the bare name -would let one claim to be the only one. Providers add an engine suffix -(`axiolid-mesh-boolean-boolmesh`). - -`cargo xtask architecture check` enforces this; see ADR 0064. One -grandfathered exception (`axiolid-mesh-compile`, already published) is -listed explicitly in `tools/xtask/src/architecture/naming.rs`. diff --git a/crates/contracts/operations/compile/Cargo.toml b/crates/contracts/operations/compile/Cargo.toml index 5b0a3e4b..c2c3b2b5 100644 --- a/crates/contracts/operations/compile/Cargo.toml +++ b/crates/contracts/operations/compile/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-contracts.workspace = true diff --git a/crates/contracts/operations/compile/README.md b/crates/contracts/operations/compile/README.md new file mode 100644 index 00000000..8a203066 --- /dev/null +++ b/crates/contracts/operations/compile/README.md @@ -0,0 +1,15 @@ +# axiolid-mesh-compile-contract + +The contract for compiling an `axiolid-model` geometry graph into a +triangle mesh. The outcome says whether the mesh is closed, and the +contract never claims to preserve an exact B-rep. It is a contract only: +`axiolid-mesh-compile` implements it, and `axiolid-exact-compile-contract` +is the exact counterpart. + +```bash +cargo add axiolid-mesh-compile-contract +``` + +- API documentation: [docs.rs/axiolid-mesh-compile-contract](https://docs.rs/axiolid-mesh-compile-contract) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-mesh-compile-contract) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/contracts/operations/curve-evaluate/AGENTS.md b/crates/contracts/operations/curve-evaluate/AGENTS.md deleted file mode 100644 index 85f39e63..00000000 --- a/crates/contracts/operations/curve-evaluate/AGENTS.md +++ /dev/null @@ -1,25 +0,0 @@ -# axiolid-curve-evaluate-contract - -Names curve evaluation as a capability so a consumer can request it -without depending on an engine. Shaped like the mesh contracts. - -- `contract.rs` the `CurveEvaluator: Backend` trait: `point_at`, - `tangent_at`, `frame_at`, `distance_convention`. -- `convention.rs` `DistanceConvention`: which distance a provider - measures, or `Unsupported`. -- `measure.rs` `CurveMeasure`: whether the caller's number is a length or - a native parameter. Maps 1:1 to `IfcCurveMeasureSelect`. -- `conformance.rs` the suite every provider must pass. - -## Pitfalls - -- Distance is NOT the native curve parameter, and the two axes are - separate: `DistanceConvention` says what a distance measures, - `CurveMeasure` says whether the value IS a distance. Only `Line`, - `Circle` and `Intrinsic` recover distance in closed form; the rest report - `Unsupported` for distance but still answer `CurveMeasure::Parameter`. -- `frame_at` is reference-up, not Frenet. The Frenet normal flips at a - vertical inflection and is undefined on a straight, which would - silently invert a placement. See ADR 0063. -- `Elevated` reports `PlanDistance`, which is shorter than 3D arc length - by the grade factor. Do not convert silently. diff --git a/crates/contracts/operations/curve-evaluate/Cargo.toml b/crates/contracts/operations/curve-evaluate/Cargo.toml index f5dac05c..927ee220 100644 --- a/crates/contracts/operations/curve-evaluate/Cargo.toml +++ b/crates/contracts/operations/curve-evaluate/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-contracts.workspace = true diff --git a/crates/contracts/operations/curve-evaluate/README.md b/crates/contracts/operations/curve-evaluate/README.md new file mode 100644 index 00000000..3b464d64 --- /dev/null +++ b/crates/contracts/operations/curve-evaluate/README.md @@ -0,0 +1,17 @@ +# axiolid-curve-evaluate-contract + +The curve-evaluation capability as a contract: point, tangent and oriented +frame at a place on a `Curve3`, plus a conformance suite every provider must +pass. A caller says whether its number is a distance or a native parameter +(`CurveMeasure`), and a provider says which distance it measures for each +curve (`DistanceConvention`) or that it cannot. Frames are reference-up, not +Frenet. This crate evaluates nothing itself; `axiolid-evaluate` provides the +scalar implementation. See ADR 0063. + +```bash +cargo add axiolid-curve-evaluate-contract +``` + +- API documentation: [docs.rs/axiolid-curve-evaluate-contract](https://docs.rs/axiolid-curve-evaluate-contract) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-curve-evaluate-contract) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/contracts/operations/exact-compile/Cargo.toml b/crates/contracts/operations/exact-compile/Cargo.toml index 9cca8528..1a53b5b9 100644 --- a/crates/contracts/operations/exact-compile/Cargo.toml +++ b/crates/contracts/operations/exact-compile/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-contracts.workspace = true diff --git a/crates/contracts/operations/exact-compile/README.md b/crates/contracts/operations/exact-compile/README.md new file mode 100644 index 00000000..f89d3530 --- /dev/null +++ b/crates/contracts/operations/exact-compile/README.md @@ -0,0 +1,15 @@ +# axiolid-exact-compile-contract + +The contract for compiling an `axiolid-model` geometry graph into an exact +B-rep. An implementation either preserves analytic supports and trims or +refuses; there is deliberately no variant that returns a mesh, so a caller +that asked for exactness is never handed an approximation. It is a contract +only; `axiolid-mesh-compile` provides an implementation. + +```bash +cargo add axiolid-exact-compile-contract +``` + +- API documentation: [docs.rs/axiolid-exact-compile-contract](https://docs.rs/axiolid-exact-compile-contract) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-exact-compile-contract) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/contracts/operations/mesh-boolean/Cargo.toml b/crates/contracts/operations/mesh-boolean/Cargo.toml index 9d5392e7..1821e502 100644 --- a/crates/contracts/operations/mesh-boolean/Cargo.toml +++ b/crates/contracts/operations/mesh-boolean/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-contracts.workspace = true diff --git a/crates/contracts/operations/mesh-boolean/README.md b/crates/contracts/operations/mesh-boolean/README.md new file mode 100644 index 00000000..742d2f8c --- /dev/null +++ b/crates/contracts/operations/mesh-boolean/README.md @@ -0,0 +1,15 @@ +# axiolid-mesh-boolean-contract + +The portable mesh-boolean contract: the `MeshBoolean` provider trait, the +evidence a provider must report with its result, and a conformance suite. +Operand admissibility comes from `axiolid-mesh-contracts`. It performs no +booleans itself; providers such as `axiolid-mesh-boolean-boolmesh` +implement it, and `axiolid-dispatch` chooses between them. + +```bash +cargo add axiolid-mesh-boolean-contract +``` + +- API documentation: [docs.rs/axiolid-mesh-boolean-contract](https://docs.rs/axiolid-mesh-boolean-contract) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-mesh-boolean-contract) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/contracts/operations/mesh-section/Cargo.toml b/crates/contracts/operations/mesh-section/Cargo.toml index e675be4a..a360e022 100644 --- a/crates/contracts/operations/mesh-section/Cargo.toml +++ b/crates/contracts/operations/mesh-section/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-contracts.workspace = true diff --git a/crates/contracts/operations/mesh-section/README.md b/crates/contracts/operations/mesh-section/README.md new file mode 100644 index 00000000..8d28c0c4 --- /dev/null +++ b/crates/contracts/operations/mesh-section/README.md @@ -0,0 +1,14 @@ +# axiolid-mesh-section-contract + +The portable contract for cutting a triangle mesh with a plane: limits, +section contours, evidence, and a conformance suite. It computes nothing +itself; providers implement `MeshPlaneSection`, and `axiolid-dispatch` +selects one. See ADR 0033. + +```bash +cargo add axiolid-mesh-section-contract +``` + +- API documentation: [docs.rs/axiolid-mesh-section-contract](https://docs.rs/axiolid-mesh-section-contract) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-mesh-section-contract) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/contracts/operations/pointcloud-reconstruction/Cargo.toml b/crates/contracts/operations/pointcloud-reconstruction/Cargo.toml index 473cf0b8..e08481f8 100644 --- a/crates/contracts/operations/pointcloud-reconstruction/Cargo.toml +++ b/crates/contracts/operations/pointcloud-reconstruction/Cargo.toml @@ -6,7 +6,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true description = "Portable pointcloud-to-surface reconstruction contract, evidence, and conformance suite." diff --git a/crates/contracts/operations/pointcloud-reconstruction/README.md b/crates/contracts/operations/pointcloud-reconstruction/README.md new file mode 100644 index 00000000..002a0a34 --- /dev/null +++ b/crates/contracts/operations/pointcloud-reconstruction/README.md @@ -0,0 +1,16 @@ +# axiolid-pointcloud-reconstruction-contract + +The portable contract for reconstructing a surface from an +`axiolid-pointcloud`: request, result, evidence, typed refusal, and a +conformance suite. A reconstruction is an estimate, so the contract makes a +provider report interpolated surface and resolved sample spacing, and +refuse rather than return an empty or fabricated mesh. Providers such as +`axiolid-pointcloud-reconstruction-sdf` implement it. See ADR 0044. + +```bash +cargo add axiolid-pointcloud-reconstruction-contract +``` + +- API documentation: [docs.rs/axiolid-pointcloud-reconstruction-contract](https://docs.rs/axiolid-pointcloud-reconstruction-contract) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-pointcloud-reconstruction-contract) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/contracts/operations/tessellate/AGENTS.md b/crates/contracts/operations/tessellate/AGENTS.md deleted file mode 100644 index 71e83d35..00000000 --- a/crates/contracts/operations/tessellate/AGENTS.md +++ /dev/null @@ -1,18 +0,0 @@ -# axiolid-tessellation-contract instructions - -Purpose: Exact-to-discrete conversion contracts. - -Allowed internal dependencies: all needed L1 representations. Follow parent `../AGENTS.md`. Do not read -`PLAN.md` unless assigned implementation or roadmap work. - -## Module ownership - -options.rs; tessellator.rs. Split a module before unrelated data, validation, and algorithms grow -together. Add no empty placeholder files. - -## Invariants - -Tolerance and chord error are explicit. Shared topological edges are discretized once and reused; independent per-face tessellation is not watertight. - -Public values derive `Debug` and `Clone`; add other standard traits only when -semantically valid. Validate unsupported and unavailable paths in tests. diff --git a/crates/contracts/operations/tessellate/Cargo.toml b/crates/contracts/operations/tessellate/Cargo.toml index d5a61af7..42d33101 100644 --- a/crates/contracts/operations/tessellate/Cargo.toml +++ b/crates/contracts/operations/tessellate/Cargo.toml @@ -6,7 +6,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true description = "Curves, surfaces and B-reps to triangles under an explicit tolerance." diff --git a/crates/contracts/operations/tessellate/README.md b/crates/contracts/operations/tessellate/README.md new file mode 100644 index 00000000..534445a4 --- /dev/null +++ b/crates/contracts/operations/tessellate/README.md @@ -0,0 +1,17 @@ +# axiolid-tessellation-contract + +The contract for turning exact geometry from an `axiolid-model` graph into +triangles under an explicit tolerance: `TessellationOptions` (which has no +default chord error), the `Tessellator` trait, and a `TessellatedMesh` that +carries the tolerance it was built to. Adjacent faces must share one +discretisation of each topological edge, because tessellating faces +independently is not watertight. It is a contract only; providers implement +it. + +```bash +cargo add axiolid-tessellation-contract +``` + +- API documentation: [docs.rs/axiolid-tessellation-contract](https://docs.rs/axiolid-tessellation-contract) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-tessellation-contract) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/execution/AGENTS.md b/crates/execution/AGENTS.md deleted file mode 100644 index 91fac648..00000000 --- a/crates/execution/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Execution - -Runtime orchestration and execution contexts. Plans are internal policy, never portable public capability schemas. diff --git a/crates/execution/compile/AGENTS.md b/crates/execution/compile/AGENTS.md deleted file mode 100644 index a4954129..00000000 --- a/crates/execution/compile/AGENTS.md +++ /dev/null @@ -1,89 +0,0 @@ -# axiolid-mesh-compile instructions - -Purpose: the reference mesh and exact compilers. `ReferenceMeshCompiler` orchestrates -`GeometryGraph` to `TriMesh`; `ReferenceExactCompiler` owns a separate per-batch -`NodeId -> ExactBRep` cache and never accepts discrete values. Graph traversal, -transform composition, and operation dispatch live here; construction algorithms -and their local invariants remain owned by `axiolid-construct`. - -## Invariants - -Extrusion output must be **closed, edge-manifold, and outward-oriented**, -because that is exactly `axiolid-mesh-boolean-boolmesh`'s input precondition. Volume alone does -NOT verify this: a cap lying in the z = 0 plane contributes nothing to the -divergence integral, so a flipped bottom cap is invisible to a volume check. -Use the directed-edge parity gate in -`crates/algorithms/construction/construct/tests/extrusion.rs` — every directed -edge exactly once, every edge with exactly one opposing half-edge. - -Unsupported profile and solid families return `GeomError::UnsupportedInput` naming -the family, never a silent approximation. Exact compilation currently accepts only -supported extrusion roots and must never delegate to mesh compilation. - -No default tolerance or chord budget. The caller supplies both, because -acceptable error depends on source units and downstream use. - -**Channels ride with the geometry (#115).** The cache holds -`channels::Built` (mesh + per-channel fates), not a bare `TriMesh`, so every -node kind must say what it did to each channel. `Instance` and `Collection` -never create surface points and so never derive a value; do not rebuild a -mesh from positions and indices on those paths -- that is the #115 bug. New -graph paths go through `channels::{transform, merge, after_boolean}` or wrap -a freshly made mesh in `Built::leaf`. Gate: `tests/graph_channels.rs`. - -**Closure rides with the geometry too (#161).** `Built::leaf` claims a -solid; a mesh that may not be one (a B-rep with shells but no solid, an -authored mesh) goes through `Built::with_closure`. A surface model's mesh can -be watertight, so never infer "solid" from a closed mesh on a B-rep path -- -only a declared solid is one. A collection is a solid only if every member -is, and a boolean refuses a surface operand. Callers read volume through -`CompileOutcome::solid_mesh`. Gate: `tests/surface_models.rs`. - -**Authored polygons are triangulated here (#160).** `PolygonMesh` faces that -are not plain triangles (n-gons, concave, with holes) go through -`planar::triangulate_polygon`, shared with planar B-rep faces: Newell plane, -planarity refusal beyond the linear tolerance, earcut, and an area -cross-check because earcut returns a partial result on crossing rings instead -of failing. Authored positions are never moved or added. Gate: -`tests/authored_polygons.rs`. - -Curve flattening is **not owned here**. `segment_points`, `circle_rings`, and -`ellipse_rings` all delegate to `axiolid_reference::curve::flatten2` (ADR 0018), -which subdivides adaptively on measured sagitta. The old private -`circle_segments`/`circle_ring` pair is gone — do not reintroduce a -closed-form segment count, it only models circles and cannot express an -ellipse or a rational spline. - -`crates/algorithms/construction/construct/tests/extrusion_volume.rs` pins the -identity `volume == area * depth` for every supported profile family and asserts -the chord budget actually bounds the volume error (measured: error is O(chord), -constant under 5). Volume and area come from `axiolid-measure`, never a local -divergence sum: that crate audits closed-two-manifold first, so a hand-rolled -integral would silently measure a torn shell. - -**Tolerance must scale with the chord budget.** `audit_mesh` calls a triangle -degenerate when `2A <= tolerance.linear()^2`. A cylinder flattened at chord -`c` has side quads about `sqrt(8*r*c)` wide and cap slivers far smaller, so a -fixed `Tolerance::MILLIMETRE` rejects perfectly correct geometry as soon as a -caller asks for sub-millimetre accuracy. Use `tolerance_for(chord)` in tests; -in production pass a tolerance derived from the same budget that drove -flattening. This is not a test artefact -- it is a real API contract. - -## Adopted dependencies - -`earcut` (ADR 0015) is owned by -`crates/algorithms/construction/construct/src/profile.rs` and is not re-exported. -This crate uses it directly for planar faces in `src/planar.rs` (one call -site) and for curved-face trims in `src/brep.rs`; both check its output -rather than trusting it. -`axiolid_reference::triangulate_simple` audits it differentially on hole-free -polygons in `crates/algorithms/construction/construct/tests/oracle.rs` — the -adopted crate is verified, not trusted. This crate's graph-level integration -coverage lives in `tests/pipeline.rs`, `tests/generation_boolean.rs`, -`tests/brep_tessellation.rs`, and the other current `tests/*.rs` targets. - -## Layer - -L3, an implementation crate alongside `axiolid-backend-cpu` and `axiolid-mesh-boolean-boolmesh`. -It may depend on representation and contract crates; nothing in L0–L2 may -depend on it. diff --git a/crates/execution/compile/Cargo.toml b/crates/execution/compile/Cargo.toml index 0a600f87..e3a9ff4f 100644 --- a/crates/execution/compile/Cargo.toml +++ b/crates/execution/compile/Cargo.toml @@ -6,7 +6,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true description = "Scalar reference MeshCompiler: profiles, extrusion, transforms, boolean dispatch." diff --git a/crates/execution/compile/PLAN.md b/crates/execution/compile/PLAN.md deleted file mode 100644 index 321a984a..00000000 --- a/crates/execution/compile/PLAN.md +++ /dev/null @@ -1,26 +0,0 @@ -# axiolid-mesh-compile plan - -Design notes for graph compilation. -Status lives on GitHub, not here (kernel#25). - -## Standing invariants - -- Outer rings CCW, holes CW. Mirrored placements are re-oriented, never - passed through: a negative-determinant transform silently inverts a solid. -- Volume alone cannot gate winding. A cap in the z=0 plane contributes - nothing to the divergence integral, so a flipped cap is invisible to it. - Directed-edge parity is the winding-sensitive gate. -- Unsupported families return `Unsupported` naming the capability needed. - -## Design shape - -- Profile flattening covers rectangle, circle, ellipse, hollow variants, - contours, and `Derived` (2D placement), which every real IFC profile uses. -- `earcut` triangulation with holes (ADR 0015); `axiolid-reference` audits it. -- Linear extrusion with caps and sides; edge-parity verified. -- `ReferenceMeshCompiler` walks post-order iteratively, memoised, dispatching - booleans through the registry. - -## Families not yet modelled - -Revolution (seam handling), swept disk, B-rep, and tessellated face sets. diff --git a/crates/execution/compile/README.md b/crates/execution/compile/README.md new file mode 100644 index 00000000..1067ce40 --- /dev/null +++ b/crates/execution/compile/README.md @@ -0,0 +1,27 @@ +# axiolid-mesh-compile + +The scalar reference compilers for an `axiolid-model` geometry graph. +`ReferenceMeshCompiler` walks the graph and produces one `TriMesh` per root: +it resolves instances and collections, composes transforms, tessellates +profiles, sweeps, B-reps and authored meshes, and hands booleans to whichever +`MeshBoolean` provider it is given. `ReferenceExactCompiler` compiles the +families it supports to exact B-reps and refuses the rest by name. This crate +owns graph traversal and dispatch; the construction algorithms themselves +(profile flattening, extrusion, revolution, sweeps) belong to +`axiolid-construct`. + +```bash +cargo add axiolid-mesh-compile +``` + +- API documentation: [docs.rs/axiolid-mesh-compile](https://docs.rs/axiolid-mesh-compile) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-mesh-compile) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) + +## Design notes + +- The caller always supplies the tolerance through `ExecutionOptions`; + there is no built-in default. Without an explicit chord budget, curves are + flattened to the linear tolerance. +- Read volume through `CompileOutcome::solid_mesh`: a surface model can + compile to a closed mesh without bounding a solid. diff --git a/crates/execution/compile/src/brep.rs b/crates/execution/compile/src/brep.rs index e6da739c..2048458c 100644 --- a/crates/execution/compile/src/brep.rs +++ b/crates/execution/compile/src/brep.rs @@ -331,15 +331,11 @@ fn append_face( total_curved_records: &mut usize, ) -> GeomResult<()> { let (brep, graph) = (ctx.brep, ctx.graph); - // A face carrying a curved support surface cannot be tessellated by - // projecting its boundary onto a plane: the interior curves away from - // that plane and the error is invisible in the output. Refuse instead. - // `axiolid-mesh-compile/AGENTS.md`: a missing wall is cheap, a wrong wall - // corrupts every downstream quantity. // A curved support cannot be tessellated by projecting the boundary onto // a plane: the interior curves away from it and the error is invisible in // the output. Sample the surface itself when the face states its boundary - // in surface parameters, and refuse when it does not. + // in surface parameters, and refuse when it does not: a missing wall is + // cheap, a wrong wall corrupts every downstream quantity. if let Some(surface) = face_surface(graph, face)? { if !surface_is_planar(surface) { return with_curved_face_transaction( diff --git a/crates/execution/compile/src/channels.rs b/crates/execution/compile/src/channels.rs index 2dff1725..52946398 100644 --- a/crates/execution/compile/src/channels.rs +++ b/crates/execution/compile/src/channels.rs @@ -6,6 +6,23 @@ //! here derives one. The only loss is a channel name that two inputs define //! incompatibly (different width or blend): that is dropped by name and //! reported, never guessed at. +//! +//! The compiler caches a [`Built`], never a bare `TriMesh`, so every node +//! kind has to state what it did to each channel. A new graph path goes +//! through [`transform()`], [`merge()`] or [`after_boolean`], or wraps a mesh it +//! made itself in [`Built::leaf`] / [`Built::with_closure`]. Rebuilding a +//! mesh from positions and indices on an `Instance` or `Collection` path is +//! the #115 bug: it silently drops every channel. Gate: +//! `tests/graph_channels.rs`. +//! +//! # Closure rides along too (#161) +//! +//! [`Built::leaf`] claims a solid. Anything that may not bound one goes +//! through [`Built::with_closure`]: a B-rep is a solid only when it declares +//! one, never because its shells happen to be watertight, since a surface +//! model's mesh can be closed. A collection is a solid only if every member +//! is ([`combined_closure`]), and a boolean refuses a surface operand. +//! Gate: `tests/surface_models.rs`. mod boolean; mod merge; diff --git a/crates/execution/compile/src/exact.rs b/crates/execution/compile/src/exact.rs index 014435d7..d80c3f30 100644 --- a/crates/execution/compile/src/exact.rs +++ b/crates/execution/compile/src/exact.rs @@ -2,6 +2,10 @@ //! //! Exact and mesh compilation are separate result domains. One exact batch owns //! a `NodeId -> ExactBRep` memo table; a discrete value cannot enter that cache. +//! +//! It never falls back to mesh compilation. Extrusions, revolutions and +//! booleans of sharp rectangle prisms along +z are compiled exactly; every +//! other family is refused with `GeomError::UnsupportedInput` naming it. use std::collections::{HashMap, HashSet}; diff --git a/crates/execution/compile/tests/generation_boolean.rs b/crates/execution/compile/tests/generation_boolean.rs index 28cf8f72..722996b9 100644 --- a/crates/execution/compile/tests/generation_boolean.rs +++ b/crates/execution/compile/tests/generation_boolean.rs @@ -25,6 +25,13 @@ fn volume_at(mesh: &TriMesh, tolerance: Tolerance) -> Scalar { } /// A tolerance proportional to a chord budget, floored at f64 sanity. +/// +/// The mesh audit calls a triangle degenerate when its doubled area is at +/// most `tolerance.linear()²`. A cylinder flattened at chord `c` has side +/// quads about `sqrt(8 r c)` wide and cap slivers far smaller, so a fixed +/// `Tolerance::MILLIMETRE` rejects correct geometry as soon as the chord +/// budget goes sub-millimetre. The measuring tolerance has to follow the +/// budget that drove flattening. fn tolerance_for(chord: Scalar) -> Tolerance { Tolerance::new((chord * 1e-3).max(1e-12), 1e-9).expect("valid tolerance") } diff --git a/crates/execution/cpu/AGENTS.md b/crates/execution/cpu/AGENTS.md deleted file mode 100644 index 93f3bf60..00000000 --- a/crates/execution/cpu/AGENTS.md +++ /dev/null @@ -1,56 +0,0 @@ -# axiolid-backend-cpu instructions - -Purpose: Portable/runtime-specialized CPU execution context. - -Allowed internal dependencies: `axiolid-contracts` and operation contracts plus L2 algorithms. Follow parent `../AGENTS.md`. Do not read -`PLAN.md` unless assigned implementation or roadmap work. - -## Module ownership - -features.rs; topology.rs; config.rs; execution.rs. Split a module before unrelated data, validation, and algorithms grow -together. Add no empty placeholder files. - -## Invariants - -This crate is an execution **context** (ISA detection, worker pool, policy). It -is explicitly **not** the correctness oracle: per `docs/adr/0012` the scalar -reference implementation is owned by `axiolid-reference`, and the scalar -implementation of an operation lands before any optimized implementation of it. - -Portable path is the differential oracle's target, not its owner. SIMD requires runtime detection. Optional Rayon uses a -local bounded pool. Operation providers compose this context and implement a -capability trait only when the algorithm works. Feature-gated tests must prove -default scalar selection, SIMD runtime selection, disabled-parallel rejection, -and configured local-pool worker counts. Never compile the whole workspace for -the build host only. - -Public values derive `Debug` and `Clone`; add other standard traits only when -semantically valid. Validate unsupported and unavailable paths in tests. - -## topology.rs — tuning inputs, not capability gates - -`CpuFeatures` answers *what can this machine execute* (a correctness -question: running AVX-512 where it is absent faults). `CpuTopology` answers -*what shape is this machine* -- cache sizes, line size, sharing, logical CPU -count, heterogeneous cores. Getting topology wrong costs speed, never -correctness, so the two must not be merged. - -Every field is `Option`. An undetectable value stays `None` rather than -defaulting: a fabricated 32 KiB L1 would silently mistune a strategy choice, -and `None` lets a caller apply its own policy explicitly. `heterogeneous()` -returns `Option` for the same reason -- on this Xeon it is `None` -(neither signal present), which is *undetermined*, not "homogeneous". - -Detection uses sysfs only: no dependency, no `unsafe`, no CPUID, and it -works identically on x86_64 and aarch64 Linux. `logical_cpus` comes from -`available_parallelism`, so cgroup limits and CPU affinity are honoured -- -a container pinned to 2 cores must not be told it has 20. - -Intended use is **strategy selection**: pick the algorithm whose working set -fits the measured cache, rather than assuming one path is universally best. -`fits_in_l1/l2/llc(bytes)` exist for exactly that query. A named machine -preset may only ever *narrow* what detection reports, never widen it, so a -preset written for one machine cannot claim capabilities on another. - -Run `cargo run --release -p axiolid-backend-cpu --example host_profile` to -print what the current host actually reports. diff --git a/crates/execution/cpu/Cargo.toml b/crates/execution/cpu/Cargo.toml index 78ce4b2c..4ed13be1 100644 --- a/crates/execution/cpu/Cargo.toml +++ b/crates/execution/cpu/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [features] default = [] diff --git a/crates/execution/cpu/README.md b/crates/execution/cpu/README.md new file mode 100644 index 00000000..ee17ddee --- /dev/null +++ b/crates/execution/cpu/README.md @@ -0,0 +1,24 @@ +# axiolid-backend-cpu + +A CPU execution context for Axiolid providers: runtime instruction-set +detection (`CpuFeatures`), measured cache and core topology for tuning +(`CpuTopology`), and an optional context-owned Rayon pool. The default build +is portable and single-threaded; the `simd` feature lets providers select an +instruction set at run time, and `parallel` adds a bounded local pool instead +of touching Rayon's global one. It bundles no geometry algorithm and is not +the correctness oracle: that is `axiolid-reference` (ADR 0012). Operation +providers compose this context. + +```bash +cargo add axiolid-backend-cpu +``` + +- API documentation: [docs.rs/axiolid-backend-cpu](https://docs.rs/axiolid-backend-cpu) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-backend-cpu) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) + +To see what the current host reports: + +```bash +cargo run --release -p axiolid-backend-cpu --example host_profile +``` diff --git a/crates/execution/cpu/src/topology.rs b/crates/execution/cpu/src/topology.rs index 59a1c282..1af951b6 100644 --- a/crates/execution/cpu/src/topology.rs +++ b/crates/execution/cpu/src/topology.rs @@ -15,6 +15,10 @@ //! tuning decision made on a number nobody measured. Callers must supply //! their own fallback explicitly, so the guess is visible at the call site. //! +//! Detection is Linux sysfs plus `available_parallelism`: no dependency, no +//! `unsafe`, no CPUID, and the same code on x86_64 and aarch64. Other +//! targets report no caches and undetermined core heterogeneity. +//! //! # Evidence this matters //! //! A radix sort in the mesh audit executed 18.9% fewer instructions than the diff --git a/crates/execution/dispatch/AGENTS.md b/crates/execution/dispatch/AGENTS.md deleted file mode 100644 index fce27c92..00000000 --- a/crates/execution/dispatch/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Dispatch - -Optional runtime registries for operation providers. Owns ordering, device matching, fallback, and budget admission; never portable schemas. diff --git a/crates/execution/dispatch/Cargo.toml b/crates/execution/dispatch/Cargo.toml index 96de3527..f07770ba 100644 --- a/crates/execution/dispatch/Cargo.toml +++ b/crates/execution/dispatch/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [features] default = [] diff --git a/crates/execution/dispatch/README.md b/crates/execution/dispatch/README.md new file mode 100644 index 00000000..8d98138c --- /dev/null +++ b/crates/execution/dispatch/README.md @@ -0,0 +1,17 @@ +# axiolid-dispatch + +Runtime registries for operation providers: `MeshBooleanRegistry`, +`MeshPlaneSectionRegistry` and `PointcloudReconstructionRegistry`, each +behind its own feature. A registry owns provider ordering, device matching, +fallback to the next provider, and memory-budget admission. It defines no +request or result types: those live in the operation-contract crates, and +providers implement them without depending on this crate. The `parallel` +feature scopes each dispatched call to a caller-owned CPU pool. + +```bash +cargo add axiolid-dispatch --features mesh-boolean +``` + +- API documentation: [docs.rs/axiolid-dispatch](https://docs.rs/axiolid-dispatch) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-dispatch) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/execution/gpu/AGENTS.md b/crates/execution/gpu/AGENTS.md deleted file mode 100644 index f15f0095..00000000 --- a/crates/execution/gpu/AGENTS.md +++ /dev/null @@ -1,33 +0,0 @@ -# axiolid-backend-gpu instructions - -Purpose: API-neutral GPU executor adapter. - -Allowed internal dependencies: axiolid-contracts, axiolid-mesh-compile-contract, axiolid-model, axiolid-mesh. Follow parent `../AGENTS.md`. Do not read -`PLAN.md` unless assigned implementation or roadmap work. - -## Module ownership - -device.rs; executor.rs; adapter.rs. Split a module before unrelated data, validation, and algorithms grow -together. Add no empty placeholder files. - -## Invariants - -No fake device or claimed operation. Concrete API crates implement -`GpuGraphExecutor` or another narrow executor. Batch boundaries amortize -transfer. The adapter validates device preference, f32/f64 policy, graph-root -ownership, and result cardinality, then invokes the executor's required -operation-specific option-validation hook before submission. Result residency is -validated before dispatch: a device cannot deliver into another device's memory, -and an unrecognized future residency is refused rather than assumed. The adapter overrides `compile_mesh_batch_into` (not `compile_mesh_batch`) so both batch -call shapes reach the device in one submission. Executor output -contract violations use `BackendContractViolation`; they are not caller input -errors. Concrete executors must honor forwarded determinism and memory-budget -requirements. - -The seam must stay satisfiable by an out-of-tree crate using published items -only; `tests/out_of_tree_executor.rs` proves this with a simulated native -backend. A native (CUDA/HIP) implementor must contain unwinds at its FFI -boundary and report faults as `BackendContractViolation`. See `docs/adr/0011`. - -Public values derive `Debug` and `Clone`; add other standard traits only when -semantically valid. Validate unsupported and unavailable paths in tests. diff --git a/crates/execution/gpu/Cargo.toml b/crates/execution/gpu/Cargo.toml index a7c54d5d..33ed8ee6 100644 --- a/crates/execution/gpu/Cargo.toml +++ b/crates/execution/gpu/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [features] default = [] diff --git a/crates/execution/gpu/PLAN.md b/crates/execution/gpu/PLAN.md deleted file mode 100644 index fbf97ea5..00000000 --- a/crates/execution/gpu/PLAN.md +++ /dev/null @@ -1,26 +0,0 @@ -# axiolid-backend-gpu plan - -Design constraints for GPU execution. Status lives on GitHub, not here -(kernel#25). - -## Standing invariants - -The generic adapter validates device and precision policy, graph-owned -roots, and one-result-per-root cardinality before accepting executor -output. An executor that cannot satisfy those is refused rather than -trusted. - -## Shape of the work - -A wgpu graph compiler stays separately feature-gated, and only earns its -place with real batched compute kernels plus CPU differential tests -- -a GPU path that cannot be differentially checked against the CPU one is -not evidence of anything. - -Further GPU operation executors arrive as separate traits and adapters, -never as methods on one god backend. - -## Exit evidence - -Targeted tests, feature-isolated compile where applicable, mutation-verified -architecture/validation gates, and benchmarks before performance claims. diff --git a/crates/execution/gpu/README.md b/crates/execution/gpu/README.md new file mode 100644 index 00000000..20ef76d1 --- /dev/null +++ b/crates/execution/gpu/README.md @@ -0,0 +1,25 @@ +# axiolid-backend-gpu + +An API-neutral seam for GPU graph compilation. `GpuCompiler` adapts any +`GpuGraphExecutor` to the `MeshCompiler` contract: it validates device, +precision, residency and roots before submitting one batch, and checks the +executor's results afterwards. The crate chooses no GPU API (CUDA, Metal, +Vulkan, WebGPU) and ships no executor, so default builds carry no driver +stack. Concrete executors live in their own crates, including out of tree +(ADR 0011). + +```bash +cargo add axiolid-backend-gpu +``` + +- API documentation: [docs.rs/axiolid-backend-gpu](https://docs.rs/axiolid-backend-gpu) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-backend-gpu) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) + +## Design notes + +- Each further GPU operation gets its own narrow executor trait and + adapter, never a method on one catch-all backend. +- A GPU path is evidence only if a CPU differential test checks it. Maturing + the executor is tracked in + [#22](https://github.com/axiolid/kernel/issues/22). diff --git a/crates/execution/gpu/src/adapter.rs b/crates/execution/gpu/src/adapter.rs index e44dc4a6..267026ca 100644 --- a/crates/execution/gpu/src/adapter.rs +++ b/crates/execution/gpu/src/adapter.rs @@ -1,4 +1,13 @@ //! Adapter from an API-specific GPU executor to graph compilation. +//! +//! Before anything is submitted, the adapter checks what it can check +//! without the device: device preference, f32/f64 policy, and result +//! residency (a device cannot deliver into another device's memory, and an +//! unrecognized future residency is refused rather than assumed), then that +//! every root belongs to the graph, then the executor's own +//! option-validation hook. After the executor returns, it checks one result +//! per root. Caller faults are `Unsupported` or `InvalidInput`; executor +//! output that breaks the seam's contract is `BackendContractViolation`. use axiolid_contracts::{ Backend, BackendDescriptor, BackendId, DevicePreference, ExecutionOptions, ExecutionTarget, @@ -123,9 +132,6 @@ impl MeshCompiler for GpuCompiler { }) } - /// Overriding the `_into` seam keeps *both* batch call shapes on the - /// single-dispatch GPU path; overriding only `compile_mesh_batch` would leave - /// `compile_mesh_batch_into` silently falling back to one submission per root. /// Overriding the `_into` seam keeps *both* batch call shapes on the /// single-dispatch GPU path; overriding only `compile_mesh_batch` would leave /// `compile_mesh_batch_into` silently falling back to one submission per root. diff --git a/crates/facade/AGENTS.md b/crates/facade/AGENTS.md deleted file mode 100644 index 28f2c7d3..00000000 --- a/crates/facade/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Facade - -Public feature-gated entry point. `axiolid/` re-exports packages without owning geometry semantics or providers. diff --git a/crates/facade/axiolid-capi/AGENTS.md b/crates/facade/axiolid-capi/AGENTS.md deleted file mode 100644 index 71b6b00b..00000000 --- a/crates/facade/axiolid-capi/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Axiolid C ABI - -This crate is the only in-tree unsafe language boundary. It wraps the supported `axiolid::application` facade with versioned C symbols, scalar handles, `#[repr(C)]` data, bounded copies, and panic containment. Keep C/C++ and downstream-format types out of kernel crates. Generate `include/axiolid.h` from the Rust surface; never hand-maintain a second ABI schema. diff --git a/crates/facade/axiolid-capi/Cargo.toml b/crates/facade/axiolid-capi/Cargo.toml index 0aed46e9..735acd8d 100644 --- a/crates/facade/axiolid-capi/Cargo.toml +++ b/crates/facade/axiolid-capi/Cargo.toml @@ -6,7 +6,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true description = "Versioned, memory-safe C ABI for the Axiolid application facade." diff --git a/crates/facade/axiolid-capi/README.md b/crates/facade/axiolid-capi/README.md new file mode 100644 index 00000000..63aefb63 --- /dev/null +++ b/crates/facade/axiolid-capi/README.md @@ -0,0 +1,18 @@ +# axiolid-capi + +A versioned C ABI over the `axiolid::application` facade, for C and C++ +applications. Every symbol carries the `axiolid_v0_4_` prefix; results are +reached through scalar handles owned by a context, data is copied into +caller-sized buffers so no Rust allocation crosses the boundary, and no +function unwinds. Exact and triangle-mesh results are distinguishable, and +an unsupported exact operation is refused rather than tessellated. The C +header is generated from the Rust surface into `include/axiolid.h`. This is +the only Axiolid crate that contains `unsafe` code. See ADR 0040. + +```bash +cargo add axiolid-capi +``` + +- API documentation: [docs.rs/axiolid-capi](https://docs.rs/axiolid-capi) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-capi) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/facade/axiolid/AGENTS.md b/crates/facade/axiolid/AGENTS.md deleted file mode 100644 index 00fc3ab7..00000000 --- a/crates/facade/axiolid/AGENTS.md +++ /dev/null @@ -1,18 +0,0 @@ -# axiolid instructions - -Purpose: Feature-gated end-user facade. - -Allowed internal dependencies: optional dependencies on all geometry crates. Follow parent `../AGENTS.md`. Do not read -`PLAN.md` unless assigned implementation or roadmap work. - -## Module ownership - -lib.rs and Cargo feature table. Split a module before unrelated data, validation, and algorithms grow -together. Add no empty placeholder files. - -## Invariants - -Keep default small and portable. Features are additive capabilities; bundles never mention IFC or vendor names. Leaf crates remain directly usable. - -Public values derive `Debug` and `Clone`; add other standard traits only when -semantically valid. Validate unsupported and unavailable paths in tests. diff --git a/crates/facade/axiolid/CHANGELOG.md b/crates/facade/axiolid/CHANGELOG.md index f110a750..ccb75b60 100644 --- a/crates/facade/axiolid/CHANGELOG.md +++ b/crates/facade/axiolid/CHANGELOG.md @@ -8,3 +8,11 @@ Pre-1.0: the minor version is the breaking-change slot, per Cargo's own caret rule for `0.x` versions. ## [Unreleased] + +### Changed + +- **Breaking:** requires `axiolid-ray-mesh` 0.4, so `axiolid::ray_mesh` + (re-exported under the ray features) carries the new + `RayMeshError::TriangleIndexOutOfRange`, and `RayIndex` queries refuse an + out-of-range candidate instead of skipping it. An exhaustive `match` on + `RayMeshError` needs the new arm. diff --git a/crates/facade/axiolid/Cargo.toml b/crates/facade/axiolid/Cargo.toml index 52dbe7ec..2f372aea 100644 --- a/crates/facade/axiolid/Cargo.toml +++ b/crates/facade/axiolid/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "axiolid" description = "Feature-gated facade for Axiolid's format-neutral geometry stack" -version = "0.3.0" +version = "0.4.0" edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [features] # Pay-for-what-you-use: naming no feature compiles no geometry (kernel#9). diff --git a/crates/facade/axiolid/README.md b/crates/facade/axiolid/README.md new file mode 100644 index 00000000..b8a8b74b --- /dev/null +++ b/crates/facade/axiolid/README.md @@ -0,0 +1,20 @@ +# axiolid + +The feature-gated entry point to Axiolid's format-neutral geometry stack. +It re-exports the representation, algorithm, contract and provider packages +behind Cargo features and adds a small application layer over them; it owns +no geometry semantics of its own. `default = []`, so a consumer names the +capabilities it needs (for example `mesh`, `brep`, `nurbs`, +`tessellation`) or a bundle (`standard`, `discrete`, `parametric`, +`advanced`, `full`) and compiles nothing else. Exact geometry stays exact: +nothing here converts to a mesh unless the caller asked for a mesh and +supplied a tolerance. Every package behind a feature can also be used +directly. + +```bash +cargo add axiolid +``` + +- API documentation: [docs.rs/axiolid](https://docs.rs/axiolid) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/facade/axiolid/src/lib.rs b/crates/facade/axiolid/src/lib.rs index 5bc841cf..637976de 100644 --- a/crates/facade/axiolid/src/lib.rs +++ b/crates/facade/axiolid/src/lib.rs @@ -9,6 +9,10 @@ //! `standard` reproduces the pre-0.4 default (`mesh + cpu + integration`) for //! consumers who want the old behaviour in one line. //! +//! Features are additive and named for geometry capabilities. No feature or +//! bundle is named after a source format, vendor or downstream product, and +//! every package behind a feature stays usable as a direct dependency. +//! //! # Exact geometry is the primary currency //! //! No entry point in this facade converts exact geometry into a mesh diff --git a/crates/foundation/AGENTS.md b/crates/foundation/AGENTS.md deleted file mode 100644 index 28548448..00000000 --- a/crates/foundation/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Foundation - -Lowest dependency root: numeric policy, identity, errors, and proof primitives. `core/` must never depend on another Axiolid package. diff --git a/crates/foundation/core/AGENTS.md b/crates/foundation/core/AGENTS.md deleted file mode 100644 index 17cdbebe..00000000 --- a/crates/foundation/core/AGENTS.md +++ /dev/null @@ -1,18 +0,0 @@ -# axiolid-core instructions - -Purpose: Dependency-root geometry values. - -Allowed internal dependencies: none. Follow parent `../AGENTS.md`. Do not read -`PLAN.md` unless assigned implementation or roadmap work. - -## Module ownership - -bounds.rs; primitives.rs; scalar.rs. Split a module before unrelated data, validation, and algorithms grow -together. Add no empty placeholder files. - -## Invariants - -Keep tolerance explicit and validated. Keep f64 storage and format-neutral units. No algorithms, serialization, or source identifiers. - -Public values derive `Debug` and `Clone`; add other standard traits only when -semantically valid. Tests must exercise invalid input as well as happy paths. diff --git a/crates/foundation/core/Cargo.toml b/crates/foundation/core/Cargo.toml index 21069dc4..ec935e4c 100644 --- a/crates/foundation/core/Cargo.toml +++ b/crates/foundation/core/Cargo.toml @@ -6,7 +6,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true description = "Geometry data types and tolerance policy. No algorithms, no backends." diff --git a/crates/foundation/core/README.md b/crates/foundation/core/README.md new file mode 100644 index 00000000..f5f05290 --- /dev/null +++ b/crates/foundation/core/README.md @@ -0,0 +1,17 @@ +# axiolid-core + +The dependency root of Axiolid: points, vectors, frames, transforms, +intervals, bounding boxes, simple 2D and 3D primitives, and the explicit +`Tolerance` policy every tolerance-sensitive operation takes. It holds data +only. There are no algorithms, no serialization, no source-format +identifiers, and no hardware backends here, and it depends on no other +Axiolid package. Coordinates are `f64` in whatever length unit the caller's +model uses. + +```bash +cargo add axiolid-core +``` + +- API documentation: [docs.rs/axiolid-core](https://docs.rs/axiolid-core) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-core) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/providers/AGENTS.md b/crates/providers/AGENTS.md deleted file mode 100644 index 1489eb38..00000000 --- a/crates/providers/AGENTS.md +++ /dev/null @@ -1,16 +0,0 @@ -# Providers - -Concrete capability implementations and optional heavyweight dependencies. Provider policy must not leak into contracts. - -## Naming - -A provider is named `axiolid--`: the capability it -implements, then the engine it wraps (`axiolid-mesh-boolean-boolmesh`, -`axiolid-pointcloud-reconstruction-sdf`). The engine suffix is required, -not decorative -- `MeshBoolean` has two implementations in this -workspace, so a crate named `axiolid-mesh-boolean` would claim to be the -only one. - -`cargo xtask architecture check` derives the expected name from the -package's own `role` and `domain` metadata and fails on a mismatch. See -ADR 0064. diff --git a/crates/providers/mesh/AGENTS.md b/crates/providers/mesh/AGENTS.md deleted file mode 100644 index 0a8e4630..00000000 --- a/crates/providers/mesh/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Mesh providers - -`boolmesh/` is the concrete boolmesh-backed implementation of the portable mesh-boolean contract. diff --git a/crates/providers/mesh/boolmesh/AGENTS.md b/crates/providers/mesh/boolmesh/AGENTS.md deleted file mode 100644 index 8b9db818..00000000 --- a/crates/providers/mesh/boolmesh/AGENTS.md +++ /dev/null @@ -1,130 +0,0 @@ -# axiolid-mesh-boolean-boolmesh instructions - -Purpose: adapt the adopted `boolmesh` crate to `axiolid_contracts::MeshBoolean` (ADR 0014). -This crate owns conversion and contract enforcement; the algorithm is upstream's. - -## Module ownership - -convert.rs (TriMesh <-> Manifold, orientation gate); provider.rs (the trait impl, -result contract); box_detect.rs (axis-aligned box recognition); cellular.rs (the -analytic subtraction construction). Split before unrelated concerns grow together. - -## Invariants - -Orientation is checked on the way IN, per argument, naming which argument failed. -An inside-out mesh is structurally valid and manifold, so nothing else catches it; -`Difference` then behaves as `Union` and returns a LARGER mesh with no error. This -happened for real during the ADR 0014 evaluation. - -Input faults are `InvalidInput`/`Degenerate`/`NotManifold` (caller's fault). -Result faults are `BackendContractViolation` (upstream's fault). Never blame the -caller for an upstream defect. - -Scratch is `Unbounded`: `boolmesh` exposes no bound, so a caller with a hard -budget is refused rather than silently allowed past it. - -Results carry no normals. `boolmesh` computes face normals; re-exporting them as -vertex normals would misrepresent the hard edges a cut creates. - -## Verification - -Volume conservation (`vol(a\b) + vol(a^b) == vol(a)`) is the gate, not index -comparison: it is triangulation-invariant, so it tests geometry rather than an -output buffer we do not control. Test helpers compute volume independently of the -crate's own helper, or the test would confirm the implementation with itself. - -`boolmesh` must not be re-exported. It is MPL-2.0 and swappable; leaking its types -would make the adoption visible to consumers and defeat the seam. - -## Batch override - -`subtract_many` groups mutually disjoint cutters (AABB overlap graph, greedy -first-fit colouring) and removes each group with one boolean. Measured 9.2x at -n=64 on the IFC-dominant layout; 0.99x worst case, so it is unconditional. - -Invariants, each mutation-proven in `tests/batch.rs`: - -- **Only disjoint cutters may be fused.** Concatenating overlapping solids - yields a self-intersecting mesh; subtracting it gives a wrong answer that - still looks like a valid result. The disjointness check is load-bearing. -- **`fuse` must rebase indices.** Forgetting the offset silently duplicates the - first mesh's triangles. -- **Every group must be subtracted**, and the single-member fast path must use - that group's tool, not `tools[0]`. - -`union_many` reduces in a BALANCED TREE rather than folding left. Same number -of booleans (n-1); the win is operand SIZE, since a fold makes step `i` union -an accumulator already holding `i` solids. Measured on a k^3 box grid -(`benches/union_many.rs`): - -| n | fold | tree | speedup | -|---|---|---|---| -| 8 | 0.38 ms | 0.23 ms | 1.6x | -| 27 | 4.30 ms | 1.60 ms | 2.7x | -| 64 | 21.69 ms | 3.97 ms | 5.5x | -| 125 | 82.80 ms | 10.33 ms | 8.0x (7.5-9.5x across three runs) | - -The ratio GROWS with n, which is what makes it a complexity difference rather -than a constant factor. On OVERLAPPING grids the win is smaller (1.1x at n=8, -1.9x at n=64): operands there merge into one growing solid, so the tree has -less small-operand advantage to exploit. Both numbers are reported. - -Unlike `subtract_many` this has NO correctness cliff: union is associative and -commutative, so any reduction order yields the same solid, with no disjointness -precondition and no fusing. Invariants, mutation-proven in `tests/union_batch.rs`: - -- **The odd trailing solid must ride to the next level.** Dropping it is - invisible at even counts; `odd_counts_do_not_drop_the_trailing_solid` sweeps - n=1,3,5,7,9,11. Deleting the `remainder()` push was verified to fail 4 gates. -- **Evidence must report n-1 sub-operations.** The tree does not save calls, - and evidence claiming otherwise would misrepresent where the win comes from. -- **Reversing the operands must not change the answer** — the commutativity the - regrouping relies on. - -⚠️ A 125-box OVERLAPPING grid trips an assert inside the absorbed kernel -(`boolean45.rs`'s `pair_up`: odd edge-point count). **Pre-existing and unrelated -to reduction order** — reproduced on the sequential fold, which `union_many` -does not touch. Same class as the `hmesh.rs:67` Menger panic: an upstream assert -firing on hard geometry instead of returning a typed error. The bench caps the -overlapping sweep at n=64 to measure up to the cliff without pretending it is -absent. - -Volume comparisons between the grouped and sequential paths use a RELATIVE -tolerance: the two sum a differently ordered triangle list, so the last bits -legitimately differ. Bitwise equality fails spuriously. - -## Analytic box path (opt-in) - -`subtract_boxes_analytic` cuts axis-aligned boxes out of an axis-aligned box in -closed form. ~25x faster than the general solver at n=64 openings. - -**Opt-in, never auto-dispatched.** Unlike the batch override above (unconditional -because its worst case is 0.99x), this path changes the OUTPUT TOPOLOGY, not just -the schedule. Dispatching on shape would make triangle counts depend on whether a -wall's openings happened to be axis-aligned. The caller asks, and handles -`Ok(None)`. - -Invariants, each mutation-proven in `tests/analytic_boxes.rs`: - -- **Recognition is structural, never by bounding box.** Every mesh has a bounding - box; a sphere and its enclosing cube share one. Acceptance requires an exact - index count, all corners on the min/max lattice, and exactly 2 triangles per - face plane. -- **The index-count check is not redundant with the plane check.** `chunks_exact(3)` - silently drops a trailing partial triangle, so a malformed 38-index buffer - presents a perfect box to the plane loop. Only the length check sees it. -- **The lattice check is not redundant either**, for one reason: it walks ALL - positions, while the plane check only sees REFERENCED ones. An unused - off-lattice vertex is invisible to the latter. -- **Refusal must stay a refusal.** Returning a wrong solid is worse than - returning nothing; every decline case has a test. - -Three independent oracles are needed, because each is blind to a different -defect: - -- signed volume misses cancelling errors (an inverted face pair sums to zero); -- edge pairing misses coincident duplicate faces (each edge still balances); -- duplicate-face detection is the only one that catches an emitted interior face. - -The interior-face mutant produced 96 triangles instead of 64 with IDENTICAL -volume and ZERO edge-pairing defects. Volume alone would have passed it. diff --git a/crates/providers/mesh/boolmesh/CHANGELOG.md b/crates/providers/mesh/boolmesh/CHANGELOG.md index 78e36008..bb611f07 100644 --- a/crates/providers/mesh/boolmesh/CHANGELOG.md +++ b/crates/providers/mesh/boolmesh/CHANGELOG.md @@ -9,6 +9,16 @@ caret rule for `0.x` versions. ## [Unreleased] +### Changed + +- **Behaviour change:** a refusal inside the solve (an odd edge-point + count in `pair_up`, #101) is reported as + `GeomError::BackendContractViolation` naming `boolmesh`, not + `Degenerate`. The operands passed every input gate, so the failure is + this provider's defect and no longer reads as the caller's. + `tests/solve_failure.rs` pins it on a grid union that still reaches the + refusal (#203). + ## [0.3.1] - 2026-09-27 ### Fixed diff --git a/crates/providers/mesh/boolmesh/Cargo.toml b/crates/providers/mesh/boolmesh/Cargo.toml index 38d8e955..400377ed 100644 --- a/crates/providers/mesh/boolmesh/Cargo.toml +++ b/crates/providers/mesh/boolmesh/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true @@ -23,9 +23,9 @@ rayon = { version = "1.11", optional = true } [features] # Upstream multi-threading. OFF by default: the parallel path has not been -# audited for run-to-run byte stability, and `determinism()` drops to -# `BestEffort` when it is on, so `Plan::admit` refuses a stronger request -# rather than silently returning a schedule-dependent result. +# audited for run-to-run byte stability. `determinism()` is `Topological` +# either way (the general path is not bitwise-stable even single-threaded), +# and `tests/determinism.rs` holds that enabling threads cannot weaken it. parallel = ["dep:rayon"] # Inter-boolean batch parallelism: independent tree nodes in `union_many` diff --git a/crates/providers/mesh/boolmesh/PLAN.md b/crates/providers/mesh/boolmesh/PLAN.md deleted file mode 100644 index a472e8f5..00000000 --- a/crates/providers/mesh/boolmesh/PLAN.md +++ /dev/null @@ -1,31 +0,0 @@ -# axiolid-mesh-boolean-boolmesh plan - -Owner: geometry -Depends on: axiolid-mesh-boolean-contract, axiolid-mesh-contracts, axiolid-contracts, axiolid-mesh, axiolid-core - -Design notes and standing constraints. Status lives on GitHub, not here -(kernel#25). - -## What this provider owns - -TriMesh <-> Manifold conversion with an orientation gate on input, and -`MeshBoolean` for union/intersection/difference. Registry integration -honours budget refusal: an over-budget provider is never invoked. - -`subtract_many` unions disjoint cutters before subtracting rather than -removing one cutter per boolean. The standing rule for that optimisation: -it must beat the sequential baseline recorded in ADR 0014 (n=16: 6.95 ms, -n=64: 48.68 ms). If it ever stops beating it, it does not earn its -complexity and should go. - -## Gates - -- Volume-conservation and winding gates; fixture issue_2019 regression. -- Fixture issue_1155 (near-degenerate halfspace). The half-space is still - bounded in the test; moving that bounding into axiolid-model remains a - separate concern. -- Differential test against certified `axiolid-predicates` (`orient3d`). - Convexity, inside/outside and winding are re-decided exactly where those - invariants hold; non-convex results stay covered by the conservation and - structural gates instead, because "wound away from one interior point" is - only true for convex solids. diff --git a/crates/providers/mesh/boolmesh/README.md b/crates/providers/mesh/boolmesh/README.md new file mode 100644 index 00000000..40f82f07 --- /dev/null +++ b/crates/providers/mesh/boolmesh/README.md @@ -0,0 +1,29 @@ +# axiolid-mesh-boolean-boolmesh + +A `MeshBoolean` provider for closed, outward-oriented triangle meshes: union, +intersection, difference, and symmetric difference composed from those. The +algorithm is absorbed from the `boolmesh` crate into a private module (ADR +0014, ADR 0047); this crate adds the conversion, an orientation gate on every +input, result checks, and batch overrides (`subtract_many` fuses disjoint +cutters, `union_many` reduces as a balanced tree). It also offers +`subtract_boxes_analytic`, an opt-in closed-form path for axis-aligned box +cutters in an axis-aligned box. Register it with `axiolid-dispatch` or call it +directly. + +```bash +cargo add axiolid-mesh-boolean-boolmesh +``` + +- API documentation: [docs.rs/axiolid-mesh-boolean-boolmesh](https://docs.rs/axiolid-mesh-boolean-boolmesh) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-mesh-boolean-boolmesh) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) + +## Design notes + +- Results carry attribute channels but no normals; derive normals from the + topology you want. +- The general path is `Determinism::Topological`. Use the analytic box path + when you need byte-identical output across processes. +- The `parallel` feature threads inside one solve and is off by default; + `parallel-batch` runs independent `union_many` pairs concurrently with + output identical to the sequential path. diff --git a/crates/providers/mesh/boolmesh/benches/union_many.rs b/crates/providers/mesh/boolmesh/benches/union_many.rs index 724e5521..0bcda0ef 100644 --- a/crates/providers/mesh/boolmesh/benches/union_many.rs +++ b/crates/providers/mesh/boolmesh/benches/union_many.rs @@ -153,12 +153,11 @@ fn main() { // 3.0 leaves clear air between boxes; 0.6 makes every neighbour // overlap, so each union does real cutting work. // Sizes are per-case. The overlapping sweep stops at 64: a 125-box - // overlapping grid trips an assert inside the absorbed kernel - // (`boolean45.rs`'s `pair_up`, odd edge-point count). That fault is - // PRE-EXISTING and unrelated to reduction order -- verified by - // reproducing it on the sequential fold, which this override does not - // touch. Benching up to the cliff measures what is measurable without - // pretending the cliff is not there. + // overlapping grid tripped an assert inside the absorbed kernel + // (`boolean45.rs`'s `pair_up`, odd edge-point count), reproduced on the + // sequential fold too, so unrelated to reduction order. Since #101 + // `pair_up` refuses with a typed error instead of panicking; the sweep + // has not been extended past the old cliff. let cases: [(&str, f64, &[usize]); 2] = [ ("disjoint grid (multi-component result)", 3.0, &[2, 3, 4, 5]), ("overlapping grid (fuses into one solid)", 0.6, &[2, 3, 4]), diff --git a/crates/providers/mesh/boolmesh/src/box_detect.rs b/crates/providers/mesh/boolmesh/src/box_detect.rs index f2c1b0d8..0c3512ee 100644 --- a/crates/providers/mesh/boolmesh/src/box_detect.rs +++ b/crates/providers/mesh/boolmesh/src/box_detect.rs @@ -41,15 +41,22 @@ impl AlignedBox { /// the cube around it share a bounding box and must not share an answer. /// /// Requirements, all necessary: -/// * exactly 8 distinct corner positions, each a combination of the min/max -/// coordinate on every axis -/// * exactly 12 triangles +/// * exactly 36 indices (12 triangles) +/// * every position, referenced or not, lies on the min/max lattice of each +/// axis /// * every triangle lies in one of the 6 axis-aligned face planes /// * each face plane carries exactly 2 triangles /// /// Together these exclude a box with a dent (wrong triangle count), a sheared /// box (corners off the min/max lattice), and a box with an interior void /// (extra triangles). +/// +/// None of the checks is redundant. `chunks_exact(3)` drops a trailing +/// partial triangle, so a malformed 38-index buffer presents a perfect box +/// to the plane loop and only the length check sees it. The plane loop sees +/// only referenced positions, so an unused off-lattice vertex is caught only +/// by the lattice walk. Each decline case is pinned in +/// `tests/analytic_boxes.rs`. pub fn recognise(mesh: &TriMesh, eps: f64) -> Option { if mesh.indices.len() != 36 { return None; @@ -77,8 +84,8 @@ pub fn recognise(mesh: &TriMesh, eps: f64) -> Option { return None; } - // Every referenced position must sit on a corner of the lattice: each - // coordinate equals that axis's min or max. This is what rejects a sphere, + // Every position, referenced or not, must sit on a corner of the + // lattice: each coordinate equals that axis's min or max. This is what rejects a sphere, // a sheared box, or any mesh that merely happens to span the same extent. for v in p { let c = [v.x, v.y, v.z]; diff --git a/crates/providers/mesh/boolmesh/src/grouping.rs b/crates/providers/mesh/boolmesh/src/grouping.rs index 94d97c39..9ed8372c 100644 --- a/crates/providers/mesh/boolmesh/src/grouping.rs +++ b/crates/providers/mesh/boolmesh/src/grouping.rs @@ -17,6 +17,19 @@ //! in a group overlap. Optimal colouring is NP-hard, so a greedy first-fit is //! used; the result only needs to be good, not optimal, and any partition is //! CORRECT because each group is verified disjoint before fusing. +//! +//! Three things are load-bearing, each gated in `tests/batch.rs`: +//! only disjoint cutters may be fused (concatenated overlapping solids are +//! self-intersecting, and subtracting them gives a wrong answer that still +//! looks valid); [`fuse`] must rebase indices (forgetting the offset +//! silently duplicates the first mesh's triangles); and every group must be +//! subtracted, the single-member fast path with that group's own tool. +//! +//! The override runs unconditionally because its worst case (a complete +//! overlap graph) costs no more than the sequential loop (0.99x in ADR 0014). +//! It stays only while it beats the sequential baseline ADR 0014 records; +//! `benches/subtract_many.rs` measures both in one run. If it stops +//! winning, it no longer earns its complexity and should be removed. use axiolid_core::Aabb; use axiolid_mesh::{AttributeChannel, TriMesh}; diff --git a/crates/providers/mesh/boolmesh/src/provider.rs b/crates/providers/mesh/boolmesh/src/provider.rs index 49b5190a..d5118603 100644 --- a/crates/providers/mesh/boolmesh/src/provider.rs +++ b/crates/providers/mesh/boolmesh/src/provider.rs @@ -1,4 +1,16 @@ //! The `MeshBoolean` implementation. +//! +//! # Whose fault an error is +//! +//! Input faults are the caller's: an operand that fails the orientation +//! gate (`InvalidInput`, `Degenerate`, naming which argument) or that the +//! kernel cannot build as a manifold (`NotManifold`). A result that fails +//! [`check_result`] is `BackendContractViolation`: the inputs were already +//! validated, so a bad result is this provider's defect, never blamed on +//! the caller. A failure inside the kernel's own solve (for example an odd +//! edge-point count in `pair_up`, #101) is `BackendContractViolation` for +//! the same reason: the operands passed every input gate, so the solve +//! failing on them is this provider's defect. use crate::csg::{compute_boolean, OpType}; use axiolid_contracts::{ @@ -14,10 +26,13 @@ use axiolid_mesh_boolean_contract::{ use crate::attributes::{carry, sources_by_plane, FaceSource}; use crate::convert::{from_boolean_mesh, six_signed_volume, to_manifold}; -/// Mesh boolean backed by `boolmesh` (pure Rust, `glam`-only, MPL-2.0). +/// Mesh boolean built on the algorithm absorbed from `boolmesh` (pure Rust, +/// `glam`-only, MPL-2.0). /// -/// Adopted rather than written: see `docs/adr/0014`. This type owns the -/// conversion and contract enforcement; the algorithm itself is upstream's. +/// Adopted in `docs/adr/0014` and absorbed into this crate's private `csg` +/// module by `docs/adr/0047`. This type owns conversion and contract +/// enforcement; no `csg` type is part of the public API, so the kernel can +/// be replaced without a consumer noticing. #[derive(Debug, Clone, Copy, Default)] pub struct BoolmeshBoolean; @@ -196,13 +211,6 @@ impl MeshBoolean for BoolmeshBoolean { } } - /// Group mutually disjoint cutters and remove each group with one - /// boolean, instead of one boolean per cutter. - /// - /// Rests on `(S \ A) \ B == S \ (A union B)` and on a concatenation of - /// disjoint solids being their union. Bounding-box grouping over-separates - /// but never wrongly fuses, so the result is identical to the sequential - /// default -- gated by volume equality in `tests/batch.rs`. /// `boolmesh` takes no cancellation handle, so nothing can interrupt a /// single boolean once it starts. Declared honestly: the batch override /// polls between groups, which is the only real poll point available. @@ -210,6 +218,13 @@ impl MeshBoolean for BoolmeshBoolean { CancellationGranularity::BetweenOperations } + /// Group mutually disjoint cutters and remove each group with one + /// boolean, instead of one boolean per cutter. + /// + /// Rests on `(S \ A) \ B == S \ (A union B)` and on a concatenation of + /// disjoint solids being their union. Bounding-box grouping over-separates + /// but never wrongly fuses, so the result is identical to the sequential + /// default -- gated by volume equality in `tests/batch.rs`. fn subtract_many( &self, subject: &TriMesh, @@ -318,9 +333,10 @@ impl BoolmeshBoolean { return Ok(BooleanOutcome::new(empty, evidence)); } Err(reason) => { - return Err(GeomError::Degenerate(format!( - "boolmesh {operation:?} failed: {reason}" - ))) + return Err(GeomError::BackendContractViolation { + backend: BoolmeshBoolean::ID, + detail: format!("{operation:?} failed inside the solve: {reason}"), + }) } }; diff --git a/crates/providers/mesh/boolmesh/tests/fixture_issue_1155.rs b/crates/providers/mesh/boolmesh/tests/fixture_issue_1155.rs index 670997d5..650ece30 100644 --- a/crates/providers/mesh/boolmesh/tests/fixture_issue_1155.rs +++ b/crates/providers/mesh/boolmesh/tests/fixture_issue_1155.rs @@ -72,9 +72,9 @@ fn bounds(mesh: &TriMesh) -> (Point3, Point3) { /// A bounded stand-in for the half-space whose boundary plane is `x = c - t*y`. /// -/// A true `IfcHalfSpaceSolid` is unbounded; mesh booleans need it bounded, and -/// PLAN.md tracks moving that bounding into `axiolid-model`. Until it lives -/// there the bound is constructed here. +/// A true `IfcHalfSpaceSolid` is unbounded; mesh booleans need it bounded. +/// Graph compilation bounds one in `axiolid_construct::half_space`; this +/// provider-level fixture has no graph, so it builds its own bound. /// /// Sizing matters and is easy to get wrong. The bound must comfortably cover /// the column so a flyaway cannot be masked by a tight cutter, but it must NOT diff --git a/crates/providers/mesh/boolmesh/tests/solve_failure.rs b/crates/providers/mesh/boolmesh/tests/solve_failure.rs new file mode 100644 index 00000000..4d8a9d8b --- /dev/null +++ b/crates/providers/mesh/boolmesh/tests/solve_failure.rs @@ -0,0 +1,58 @@ +//! A failure inside the solve is the provider's defect, not the caller's. +//! +//! Every operand here passes the input gates: each box is a closed, +//! outward, manifold solid, and so is every partial union. When the solve +//! still refuses (an odd edge-point count in `pair_up`, #101), the error +//! must say so as `BackendContractViolation`, never `Degenerate`, which +//! would blame operands that were admissible. + +mod support; + +use axiolid_contracts::{ExecutionOptions, GeomError}; +use axiolid_core::{BooleanOperator, Tolerance}; +use axiolid_mesh_boolean_boolmesh::BoolmeshBoolean; +use axiolid_mesh_boolean_contract::MeshBoolean; +use support::boxx; + +#[test] +fn a_refusal_inside_the_solve_is_a_backend_contract_violation() { + let provider = BoolmeshBoolean::new(); + let options = ExecutionOptions::new(Tolerance::MILLIMETRE); + // A 5 x 5 x 5 grid of overlapping unit boxes at pitch 0.8: the + // sequential fold reaches the `pair_up` refusal (#101) at its sixth + // union (#203). Pitches 0.65 and 0.7 fail too; 0.6 no longer does. + let pitch = 0.8; + let mut current: Option = None; + let mut refusal = None; + 'fold: for i in 0..5 { + for j in 0..5 { + for l in 0..5 { + let (x, y, z) = (i as f64 * pitch, j as f64 * pitch, l as f64 * pitch); + let solid = boxx(x, y, z - 0.5, 1.0, 1.0, 1.0, 0.0); + let Some(acc) = current.take() else { + current = Some(solid); + continue; + }; + match provider.boolean(&acc, &solid, BooleanOperator::Union, &options) { + Ok(outcome) => current = Some(outcome.mesh), + Err(error) => { + refusal = Some(error); + break 'fold; + } + } + } + } + } + let error = refusal.expect( + "the overlapping grid no longer reaches a solve refusal; \ + pick a new reproducer so this mapping stays tested", + ); + assert!( + matches!( + &error, + GeomError::BackendContractViolation { backend, detail } + if *backend == BoolmeshBoolean::ID && detail.contains("inside the solve") + ), + "{error:?}" + ); +} diff --git a/crates/providers/pointcloud/sdf/Cargo.toml b/crates/providers/pointcloud/sdf/Cargo.toml index 41ee89a2..bb8e30c1 100644 --- a/crates/providers/pointcloud/sdf/Cargo.toml +++ b/crates/providers/pointcloud/sdf/Cargo.toml @@ -6,7 +6,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true description = "Reference pointcloud reconstruction: signed-distance field from samples, extracted as a level set." diff --git a/crates/providers/pointcloud/sdf/README.md b/crates/providers/pointcloud/sdf/README.md new file mode 100644 index 00000000..de501124 --- /dev/null +++ b/crates/providers/pointcloud/sdf/README.md @@ -0,0 +1,18 @@ +# axiolid-pointcloud-reconstruction-sdf + +The reference `PointcloudReconstruction` provider. `SdfReconstruction` builds +a signed-distance field from the samples (nearest neighbours through +`axiolid-spatial`) and extracts its zero level set with `axiolid-levelset`. +With normals the surface passes through the samples; without them it wraps +around them, and the evidence says which. It is not a hole filler: where the +capture has no data the surface is extrapolated, and those triangles are +counted. It has no external dependency, so a better reconstruction can +replace it behind the same contract. + +```bash +cargo add axiolid-pointcloud-reconstruction-sdf +``` + +- API documentation: [docs.rs/axiolid-pointcloud-reconstruction-sdf](https://docs.rs/axiolid-pointcloud-reconstruction-sdf) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-pointcloud-reconstruction-sdf) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/representations/AGENTS.md b/crates/representations/AGENTS.md deleted file mode 100644 index 59390bf0..00000000 --- a/crates/representations/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Representations - -Portable geometry values only. Children: `analytic/`, `region/`, `topology/`, `brep/`, `discrete/`, `sampled/`, and `modeling/`. diff --git a/crates/representations/analytic/AGENTS.md b/crates/representations/analytic/AGENTS.md deleted file mode 100644 index ae3557a9..00000000 --- a/crates/representations/analytic/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Analytic representations - -`curve/`, `surface/`, and `primitive/` own exact analytic value families. No algorithms or provider policy. diff --git a/crates/representations/analytic/curve/AGENTS.md b/crates/representations/analytic/curve/AGENTS.md deleted file mode 100644 index b303740a..00000000 --- a/crates/representations/analytic/curve/AGENTS.md +++ /dev/null @@ -1,33 +0,0 @@ -# axiolid-curve instructions - -Purpose: Atomic exact curves and evaluation seams. - -Allowed internal dependencies: axiolid-core. Follow parent `../AGENTS.md`. Do not read -`PLAN.md` unless assigned implementation or roadmap work. - -## Module ownership - -linear.rs; conic.rs; spline.rs; intrinsic.rs; intrinsic3.rs; elevation.rs; evaluate.rs. Split a module before unrelated data, validation, and algorithms grow -together. Add no empty placeholder files. - -## Invariants - -Composite/trim/offset/surface relations belong in axiolid-model to avoid curve-surface cycles. Preserve knots, multiplicities, weights, and domains. - -An `ElevationLaw` is written against **plan distance**, never 3D arc length: -the two diverge by `sqrt(1 + g^2)` wherever grade is non-zero. `Elevated3` -pairs one with a `Curve2`, and only arc-length parameterisations (line, -circle, intrinsic) may be paired — a B-spline parameter is not distance, so -`axiolid-evaluate` refuses it rather than reinterpreting the law (ADR 0060). - -This crate is representation only: it declares `CurveEvaluator` but implements -no evaluation. The scalar implementation is `axiolid_reference::curve` -(ADR 0018) — analytic per family, de Boor for splines, adaptive flattening on -measured sagitta. Do not add an evaluator here; L1 is data, L2 solves. - -A polyline's parameter is **one unit per segment**, so a closed n-point ring -has domain `(0, n)`. A `ProfileSegment::domain` of `(0, 1)` on a multi-segment -polyline is rejected rather than silently truncated. - -Public values derive `Debug` and `Clone`; add other standard traits only when -semantically valid. Tests must exercise invalid input as well as happy paths. diff --git a/crates/representations/analytic/curve/Cargo.toml b/crates/representations/analytic/curve/Cargo.toml index 16f03fc7..a633d142 100644 --- a/crates/representations/analytic/curve/Cargo.toml +++ b/crates/representations/analytic/curve/Cargo.toml @@ -6,9 +6,9 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true -description = "Curve evaluation: lines, conics, composite curves, NURBS, trimming." +description = "Exact, format-neutral curve values: lines, conics, B-splines, natural-equation and elevated curves." [dependencies] axiolid-core.workspace = true diff --git a/crates/representations/analytic/curve/README.md b/crates/representations/analytic/curve/README.md new file mode 100644 index 00000000..5fdc9e6b --- /dev/null +++ b/crates/representations/analytic/curve/README.md @@ -0,0 +1,18 @@ +# axiolid-curve + +Exact, format-neutral curve values: lines and polylines (from +`axiolid-linear`), conics, rational and polynomial B-splines, +natural-equation (intrinsic) curves, elevated alignment curves, and the +curves where surfaces meet. Knots, multiplicities, weights and domains are +kept as authored. It declares the `CurveEvaluator` seam but evaluates +nothing; `axiolid-evaluate` does that. Composite, trimmed, offset and +surface-bound curves are relations in `axiolid-model`, which keeps curves +and surfaces free of a dependency cycle. + +```bash +cargo add axiolid-curve +``` + +- API documentation: [docs.rs/axiolid-curve](https://docs.rs/axiolid-curve) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-curve) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/representations/analytic/curve/src/elevation.rs b/crates/representations/analytic/curve/src/elevation.rs index d246c1ec..6eab6b1b 100644 --- a/crates/representations/analytic/curve/src/elevation.rs +++ b/crates/representations/analytic/curve/src/elevation.rs @@ -201,6 +201,11 @@ fn piece_at<'a>( /// Composition rather than re-encoding: a `Curve3::BSpline` fitted through the /// pair would lose both. Storing the two laws keeps each one's own exactness /// and lets a consumer recover either half unchanged. +/// +/// Only a plan whose parameter is arc length (line, circle, intrinsic) can +/// carry a law. A B-spline's parameter is not a distance, so evaluators +/// refuse an elevated B-spline plan rather than reinterpret the law +/// (ADR 0060). #[derive(Debug, Clone, PartialEq)] pub struct Elevated3 { /// Horizontal layout. Boxed to keep [`Curve3`](crate::Curve3) small. diff --git a/crates/representations/analytic/curve/src/evaluate.rs b/crates/representations/analytic/curve/src/evaluate.rs index 61ac92af..6299d339 100644 --- a/crates/representations/analytic/curve/src/evaluate.rs +++ b/crates/representations/analytic/curve/src/evaluate.rs @@ -1,4 +1,9 @@ //! Backend-open curve evaluation capability. +//! +//! This crate declares the trait and implements it for nothing: it holds +//! curve data, and evaluation is an algorithm. The scalar implementation is +//! `axiolid_evaluate::curve::ScalarCurve` (ADR 0018), also reachable as +//! `axiolid_reference::curve`. Do not add an evaluator here. use axiolid_core::{Interval, Scalar, Tolerance}; diff --git a/crates/representations/analytic/linear/AGENTS.md b/crates/representations/analytic/linear/AGENTS.md deleted file mode 100644 index 985905ae..00000000 --- a/crates/representations/analytic/linear/AGENTS.md +++ /dev/null @@ -1,14 +0,0 @@ -# axiolid-linear - -L1 linear representation vocabulary: `Line`, `Ray2`, `Segment`, `Polyline`. - -- Data only. No evaluation, no tolerance policy, no algorithms, no features. -- `Ray3` is re-exported from `axiolid-core`, never redefined — a second `Ray3` - would split the vocabulary for existing consumers. -- Directions are stored as authored; do not normalise on construction, because - that silently reparameterises the caller's geometry. -- `axiolid-curve` depends on this package and re-exports it. Both - `axiolid_linear::Line2` and `axiolid_curve::Line2` must keep naming this type. -- This package exists so a line-query consumer compiles without curves, - surfaces, meshes, topology, providers, or execution. Adding any dependency - beyond `axiolid-core` defeats its reason to exist. diff --git a/crates/representations/analytic/linear/Cargo.toml b/crates/representations/analytic/linear/Cargo.toml index 1268d090..22188d75 100644 --- a/crates/representations/analytic/linear/Cargo.toml +++ b/crates/representations/analytic/linear/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true [features] diff --git a/crates/representations/analytic/linear/README.md b/crates/representations/analytic/linear/README.md new file mode 100644 index 00000000..b7ecb05a --- /dev/null +++ b/crates/representations/analytic/linear/README.md @@ -0,0 +1,16 @@ +# axiolid-linear + +Format-neutral linear values: lines, rays, segments and polylines in 2D and +3D. It holds data only (no evaluation, tolerance policy or algorithms) and +depends only on `axiolid-core`, so an application that needs lines alone +does not compile curves, surfaces, meshes or topology. `axiolid-curve` +re-exports these types unchanged; use this crate when lines are all you +need. + +```bash +cargo add axiolid-linear +``` + +- API documentation: [docs.rs/axiolid-linear](https://docs.rs/axiolid-linear) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-linear) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/representations/analytic/linear/src/polyline.rs b/crates/representations/analytic/linear/src/polyline.rs index eb7f2084..3b5e7af9 100644 --- a/crates/representations/analytic/linear/src/polyline.rs +++ b/crates/representations/analytic/linear/src/polyline.rs @@ -3,6 +3,13 @@ use axiolid_core::{Point2, Point3}; /// Piecewise-linear curve preserving source vertex order. +/// +/// Its native parameter is one unit per segment, not arc length and not a +/// normalised `(0, 1)`: an open n-point polyline spans `(0, n - 1)` and a +/// closed one `(0, n)`. A trim or profile-segment domain of `(0, 1)` on a +/// multi-segment polyline therefore selects only its first edge; the scalar +/// flattener in `axiolid-evaluate` refuses that rather than silently dropping +/// the remaining vertices. #[derive(Debug, Clone, Default, PartialEq)] pub struct Polyline

{ /// Ordered control points. diff --git a/crates/representations/analytic/primitive/AGENTS.md b/crates/representations/analytic/primitive/AGENTS.md deleted file mode 100644 index bd864144..00000000 --- a/crates/representations/analytic/primitive/AGENTS.md +++ /dev/null @@ -1,18 +0,0 @@ -# axiolid-primitive instructions - -Purpose: Exact CSG leaf solids and half-spaces. - -Allowed internal dependencies: axiolid-core. Follow parent `../AGENTS.md`. Do not read -`PLAN.md` unless assigned implementation or roadmap work. - -## Module ownership - -solid.rs; half_space.rs. Split a module before unrelated data, validation, and algorithms grow -together. Add no empty placeholder files. - -## Invariants - -Values are exact intent, not meshes. Finite clipping policy is explicit. Do not hide tessellation or boolean work in constructors. - -Public values derive `Debug` and `Clone`; add other standard traits only when -semantically valid. Tests must exercise invalid input as well as happy paths. diff --git a/crates/representations/analytic/primitive/Cargo.toml b/crates/representations/analytic/primitive/Cargo.toml index 62bdc1db..d1f72cf0 100644 --- a/crates/representations/analytic/primitive/Cargo.toml +++ b/crates/representations/analytic/primitive/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/representations/analytic/primitive/README.md b/crates/representations/analytic/primitive/README.md new file mode 100644 index 00000000..8337e2bf --- /dev/null +++ b/crates/representations/analytic/primitive/README.md @@ -0,0 +1,15 @@ +# axiolid-primitive + +Exact parametric primitive solids and half-spaces, used as CSG leaves. The +values stay exact until someone explicitly tessellates them: constructors do +no tessellation or boolean work, and the finite margin used when a +half-space has to be clipped for meshing is an explicit parameter. It has no +mesh or kernel dependency. + +```bash +cargo add axiolid-primitive +``` + +- API documentation: [docs.rs/axiolid-primitive](https://docs.rs/axiolid-primitive) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-primitive) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/representations/analytic/surface/AGENTS.md b/crates/representations/analytic/surface/AGENTS.md deleted file mode 100644 index 17a26490..00000000 --- a/crates/representations/analytic/surface/AGENTS.md +++ /dev/null @@ -1,18 +0,0 @@ -# axiolid-surface instructions - -Purpose: Atomic exact surfaces and evaluation seams. - -Allowed internal dependencies: axiolid-core, axiolid-curve. Follow parent `../AGENTS.md`. Do not read -`PLAN.md` unless assigned implementation or roadmap work. - -## Module ownership - -elementary.rs; spline.rs; evaluate.rs. Split a module before unrelated data, validation, and algorithms grow -together. Add no empty placeholder files. - -## Invariants - -Bounded/swept/offset relationships belong in axiolid-model. Preserve tensor-product knot grids and rational weights exactly. - -Public values derive `Debug` and `Clone`; add other standard traits only when -semantically valid. Tests must exercise invalid input as well as happy paths. diff --git a/crates/representations/analytic/surface/Cargo.toml b/crates/representations/analytic/surface/Cargo.toml index 675b81cf..efb02d5c 100644 --- a/crates/representations/analytic/surface/Cargo.toml +++ b/crates/representations/analytic/surface/Cargo.toml @@ -6,9 +6,9 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true -description = "Surface evaluation: elementary surfaces, swept surfaces, NURBS patches." +description = "Exact, format-neutral surface values: planes, quadrics, tori, and B-spline patches." [dependencies] axiolid-core.workspace = true diff --git a/crates/representations/analytic/surface/README.md b/crates/representations/analytic/surface/README.md new file mode 100644 index 00000000..fab3985b --- /dev/null +++ b/crates/representations/analytic/surface/README.md @@ -0,0 +1,16 @@ +# axiolid-surface + +Exact, format-neutral surface values: planes, circular and elliptical +cylinders, cones, spheres, tori, and rational or polynomial B-spline +surfaces with their knot grids and weights kept as authored. It declares the +`SurfaceEvaluator` seam but evaluates nothing. Bounded, swept, offset and +curve-on-surface relationships are nodes in `axiolid-model`, not types +here. + +```bash +cargo add axiolid-surface +``` + +- API documentation: [docs.rs/axiolid-surface](https://docs.rs/axiolid-surface) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-surface) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/representations/analytic/surface/tests/reexport.rs b/crates/representations/analytic/surface/tests/reexport.rs index a1ce4818..4b8f6ed1 100644 --- a/crates/representations/analytic/surface/tests/reexport.rs +++ b/crates/representations/analytic/surface/tests/reexport.rs @@ -1,5 +1,5 @@ //! `BSplineSurface` still resolves from both of its published paths, with -//! every published field (architecture/semver-exceptions.toml). +//! every published field (docs/architecture/semver-exceptions.toml). use axiolid_core::Point3; use axiolid_curve::KnotSpec; diff --git a/crates/representations/brep/AGENTS.md b/crates/representations/brep/AGENTS.md deleted file mode 100644 index 4e981ad4..00000000 --- a/crates/representations/brep/AGENTS.md +++ /dev/null @@ -1,20 +0,0 @@ -# `axiolid-brep` - -Owns the strict, analytic B-rep **result** contract: typed 3D-curve, 2D-pcurve, -and surface catalogs plus an owned `axiolid-topology` graph. - -`axiolid-topology` remains generic and format-neutral. This crate binds it to -Axiolid's neutral `Curve2`, `Curve3`, and `Surface` values without a model, -compiler, mesh, or source-format dependency. - -## Invariants - -- An exact result has at least one face. -- Every edge has a finite, non-zero native 3D curve interval. -- Every edge use has a finite, non-zero pcurve interval. -- Every face has a support surface. -- All typed support handles resolve in their matching catalog. -- Structural topology must pass `audit_brep`; closure is required only by an - eventual exact-solid result, not by an exact sheet. - -Do not add evaluation, intersection, tessellation, or graph traversal here. diff --git a/crates/representations/brep/Cargo.toml b/crates/representations/brep/Cargo.toml index 4afe6ceb..154f4d30 100644 --- a/crates/representations/brep/Cargo.toml +++ b/crates/representations/brep/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/representations/brep/README.md b/crates/representations/brep/README.md new file mode 100644 index 00000000..51c8dd39 --- /dev/null +++ b/crates/representations/brep/README.md @@ -0,0 +1,16 @@ +# axiolid-brep + +The strict exact B-rep result: an `axiolid-topology` graph bound to owned +catalogs of `Curve3` edge supports, `Curve2` pcurves and `Surface` face +supports, with every edge and pcurve interval stated explicitly and checked +before the value can exist. Faces and edges can carry persistent structural +names that survive rebuilds. It does not evaluate, intersect, tessellate or +traverse geometry. + +```bash +cargo add axiolid-brep +``` + +- API documentation: [docs.rs/axiolid-brep](https://docs.rs/axiolid-brep) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-brep) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/representations/brep/src/lib.rs b/crates/representations/brep/src/lib.rs index 20ea1684..eea687bf 100644 --- a/crates/representations/brep/src/lib.rs +++ b/crates/representations/brep/src/lib.rs @@ -227,7 +227,6 @@ impl ExactBRepBuilder { self.pcurve_intervals.insert((loop_id, use_index), interval); } - /// Validate and freeze the exact B-rep result. /// Copy every vertex, edge, loop, face and shell of `other` into this /// builder, with its curves, surfaces, intervals and names, and return /// the new handles of `other`'s shells in order. @@ -349,6 +348,13 @@ impl ExactBRepBuilder { .collect() } + /// Validate and freeze the exact B-rep result. + /// + /// Refuses a result with no faces, topology that fails + /// [`axiolid_topology::audit_brep`], and any edge, edge use or face whose + /// support or finite non-zero interval is missing or does not resolve in + /// its catalog (see [`ExactBRepError`]). Closure is not required: an open + /// sheet is a valid exact result. pub fn finish(self) -> Result { validate(&self)?; Ok(ExactBRep { diff --git a/crates/representations/discrete/AGENTS.md b/crates/representations/discrete/AGENTS.md deleted file mode 100644 index e5f4e6bd..00000000 --- a/crates/representations/discrete/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Discrete representations - -`mesh/` owns triangle mesh values and invariant-preserving views, not mesh operations. diff --git a/crates/representations/discrete/mesh/AGENTS.md b/crates/representations/discrete/mesh/AGENTS.md deleted file mode 100644 index 6d296dcd..00000000 --- a/crates/representations/discrete/mesh/AGENTS.md +++ /dev/null @@ -1,18 +0,0 @@ -# axiolid-mesh instructions - -Purpose: Discrete mesh exchange representations. - -Allowed internal dependencies: axiolid-core. Follow parent `../AGENTS.md`. Do not read -`PLAN.md` unless assigned implementation or roadmap work. - -## Module ownership - -triangle.rs; polygon.rs; view.rs; error.rs; audit.rs. Split a module before unrelated data, validation, and algorithms grow -together. Add no empty placeholder files. - -## Invariants - -Preserve n-gons/holes until triangulation. Keep indices u32 and validate before indexing. MeshView must permit zero-copy foreign meshes. Rendering materials are not geometry. Budgeted callers use `try_audit_mesh` plus `audit_mesh_scratch_bytes`; never allocate before admitting that checked bound. - -Public values derive `Debug` and `Clone`; add other standard traits only when -semantically valid. Tests must exercise invalid input as well as happy paths. diff --git a/crates/representations/discrete/mesh/Cargo.toml b/crates/representations/discrete/mesh/Cargo.toml index fc784e9d..57bd63e9 100644 --- a/crates/representations/discrete/mesh/Cargo.toml +++ b/crates/representations/discrete/mesh/Cargo.toml @@ -6,7 +6,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true description = "Triangle meshes: the discrete representation every backend consumes." diff --git a/crates/representations/discrete/mesh/PLAN.md b/crates/representations/discrete/mesh/PLAN.md deleted file mode 100644 index 479dd6a6..00000000 --- a/crates/representations/discrete/mesh/PLAN.md +++ /dev/null @@ -1,20 +0,0 @@ -# axiolid-mesh implementation plan - -Status: structural audit implemented; repair and richer topology remain planned. - -## Standing invariants - -- Crate boundary and dependency direction are executable in the layering gate. -- `TriangleMeshView` adapts foreign index storage without ownership conversion. -- `audit_mesh` reports malformed input, non-finite coordinates, tolerance-aware - degenerate faces, boundary edges, and non-manifold edges deterministically. - It does not mutate or reject dirty source geometry. - -## Shape of the work - -Add attribute channels, explicit repair plans, and richer topology diagnostics. - -## Exit evidence - -Targeted tests, feature-isolated compile where applicable, mutation-verified -architecture/validation gates, and benchmarks before performance claims. diff --git a/crates/representations/discrete/mesh/README.md b/crates/representations/discrete/mesh/README.md new file mode 100644 index 00000000..78116b42 --- /dev/null +++ b/crates/representations/discrete/mesh/README.md @@ -0,0 +1,23 @@ +# axiolid-mesh + +Triangle and polygon mesh values: `TriMesh` as the compact exchange type, +`PolygonMesh` keeping n-gons and holes until explicit triangulation, u32 +indices, named per-vertex and per-corner attribute channels, derived edge +adjacency, connected components, and a deterministic structural audit that +reports defects instead of rejecting dirty input. `MeshView` and +`TriangleMeshView` let a foreign mesh be read without copying it. Mesh +operations such as booleans, sections and repair live in other crates. + +```bash +cargo add axiolid-mesh +``` + +- API documentation: [docs.rs/axiolid-mesh](https://docs.rs/axiolid-mesh) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-mesh) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) + +## Design notes + +Rendering appearance (materials, shaders, colours as styling) is not part of a +mesh value. Data that must follow the geometry, such as a material id per +vertex, travels as an attribute channel. diff --git a/crates/representations/discrete/pointcloud/Cargo.toml b/crates/representations/discrete/pointcloud/Cargo.toml index 7addfc5f..8e3d4259 100644 --- a/crates/representations/discrete/pointcloud/Cargo.toml +++ b/crates/representations/discrete/pointcloud/Cargo.toml @@ -6,7 +6,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true description = "Portable point-sampled geometry values with optional per-point channels." diff --git a/crates/representations/discrete/pointcloud/README.md b/crates/representations/discrete/pointcloud/README.md new file mode 100644 index 00000000..21872d11 --- /dev/null +++ b/crates/representations/discrete/pointcloud/README.md @@ -0,0 +1,17 @@ +# axiolid-pointcloud + +Point-sampled geometry: an ordered set of 3D points with optional normal, +colour and intensity channels, each validated to have exactly one entry per +point. A pointcloud is a sample of a surface, with no topology or +adjacency. It is not a file format (LAS, E57 and similar are parsed outside +the kernel) and not an algorithm: queries live in `axiolid-spatial` +and reconstruction behind `axiolid-pointcloud-reconstruction-contract`. See +ADR 0044. + +```bash +cargo add axiolid-pointcloud +``` + +- API documentation: [docs.rs/axiolid-pointcloud](https://docs.rs/axiolid-pointcloud) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-pointcloud) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/representations/fixtures/AGENTS.md b/crates/representations/fixtures/AGENTS.md deleted file mode 100644 index ad6dd448..00000000 --- a/crates/representations/fixtures/AGENTS.md +++ /dev/null @@ -1,40 +0,0 @@ -# tools/fixtures - -The shared adversarial and degenerate geometry corpus (#18). Consumed by the -differential tests in `axiolid-mesh-boolean-boolmesh` and by any crate that -needs a case known to break naive implementations. - -## Why constructed, not stored - -A degenerate case is usually a *number*, not a file: a 2e-9 plane tilt, a -sliver a fraction of a millimetre wide, two vertices that coincide exactly. -Round-tripping those through a mesh file format invites an exporter to round a -coordinate, at which point the fixture silently stops being degenerate and the -test keeps passing while covering nothing. - -Constructing in code keeps the exact bit pattern under version control. - -## Adding a fixture - -1. Write a constructor returning `Fixture`. -2. Fill in every `Provenance` field. `source` must be specific enough that a - reader can go and verify the claim -- an issue number, an ADR, or the - geometric principle involved. -3. State `expectation` as what an implementation *must* do. Never record what - it currently does: a fixture that encodes present behaviour cannot detect a - regression, because the regression just becomes the new expectation. -4. Add it to `corpus()` so existing differential tests pick it up without - being edited. - -## Licence - -Every fixture here is original work, reconstructed from published bug -descriptions or first principles, and carries the repository licence. Nothing -is copied from an external corpus, which keeps redistribution unencumbered. -If that ever changes, the `licence` field must say so. - -## Verify - -```bash -cargo test -p axiolid-fixtures -``` diff --git a/crates/representations/fixtures/Cargo.toml b/crates/representations/fixtures/Cargo.toml index 3371af47..80693c37 100644 --- a/crates/representations/fixtures/Cargo.toml +++ b/crates/representations/fixtures/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/representations/fixtures/README.md b/crates/representations/fixtures/README.md new file mode 100644 index 00000000..885944af --- /dev/null +++ b/crates/representations/fixtures/README.md @@ -0,0 +1,18 @@ +# axiolid-fixtures + +A shared corpus of adversarial and degenerate mesh fixtures (sliver and +zero-area triangles, open shells, extreme scale disparity, coplanar contact +between operands, and the like), each with a `Provenance` saying where the +case comes from, its licence, and what an implementation must do with it. +Fixtures are built in code rather than stored as files, so the exact bit +patterns that make them degenerate cannot be rounded away by an exporter. +Differential tests, for example in `axiolid-mesh-boolean-boolmesh`, iterate +`corpus()`. Every fixture is original work under the repository licence. + +```bash +cargo add axiolid-fixtures +``` + +- API documentation: [docs.rs/axiolid-fixtures](https://docs.rs/axiolid-fixtures) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-fixtures) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/representations/fixtures/src/lib.rs b/crates/representations/fixtures/src/lib.rs index 49123f1e..85f5c3dc 100644 --- a/crates/representations/fixtures/src/lib.rs +++ b/crates/representations/fixtures/src/lib.rs @@ -35,6 +35,10 @@ pub struct Provenance { /// principles. Specific enough that a reader can go and check it. pub source: &'static str, /// Licence covering redistribution of this fixture's data. + /// + /// Every fixture so far is original work under the repository licence. A + /// fixture taken from an external corpus must name that corpus's licence + /// here. pub licence: &'static str, /// What an implementation must do with it, stated as a requirement. pub expectation: &'static str, diff --git a/crates/representations/modeling/AGENTS.md b/crates/representations/modeling/AGENTS.md deleted file mode 100644 index 4d926da0..00000000 --- a/crates/representations/modeling/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Modeling representations - -`graph/` owns the immutable typed geometry DAG. Evaluation and execution live elsewhere. diff --git a/crates/representations/modeling/graph/AGENTS.md b/crates/representations/modeling/graph/AGENTS.md deleted file mode 100644 index 85c8c6b2..00000000 --- a/crates/representations/modeling/graph/AGENTS.md +++ /dev/null @@ -1,33 +0,0 @@ -# axiolid-model instructions - -Purpose: Immutable format-neutral geometry DAG. - -Allowed internal dependencies: all L1 representation crates. Follow parent `../AGENTS.md`. Do not read -`PLAN.md` unless assigned implementation or roadmap work. - -## Module ownership - -id.rs; graph.rs; node.rs; value.rs; validation.rs; curve_relation.rs; surface_relation.rs; -solid_operation.rs. `value.rs` owns sealed built-in-to-node conversions only; -`validation.rs` owns graph-reference family checks; graph ownership stays in `id.rs`/`graph.rs`. -execution/provider traits must remain open in operation-contract packages. Split a module before -unrelated data, validation, and algorithms grow together. Add no empty -placeholder files. - -## Invariants - -Every reference points to a prior node in the same graph and satisfies the -edge's accepted reference family, including Curve2/Curve3 dimensionality through -instances and curve-relation chains. An instance preserves its source node's -reference family; it must not erase dimensional semantics. Keep source IDs -outside the graph. Preserve instancing and exact operations; never lower to -meshes here. - -Public payload types stay owned plain data so a future native (CUDA/HIP) -backend can copy them across FFI: no trait objects, callables, borrowed -references, raw pointers, shared-ownership handles, or interior mutability. -Error types are exempt as diagnostics. Enforced by -`tests/native_backend_readiness.rs`; see `docs/adr/0011`. - -Public values derive `Debug` and `Clone`; add other standard traits only when -semantically valid. Tests must exercise invalid input as well as happy paths. diff --git a/crates/representations/modeling/graph/Cargo.toml b/crates/representations/modeling/graph/Cargo.toml index d3835ff6..759bb210 100644 --- a/crates/representations/modeling/graph/Cargo.toml +++ b/crates/representations/modeling/graph/Cargo.toml @@ -6,7 +6,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true description = "Format-neutral geometry item tree. The currency between a format reader and a kernel." diff --git a/crates/representations/modeling/graph/PLAN.md b/crates/representations/modeling/graph/PLAN.md deleted file mode 100644 index 3223ff7a..00000000 --- a/crates/representations/modeling/graph/PLAN.md +++ /dev/null @@ -1,22 +0,0 @@ -# axiolid-model plan - -Design notes for the immutable geometry DAG. -Status lives on GitHub, not here (kernel#25). - -## Standing invariants - -Node handles carry a graph-owner brand. Insertion rejects foreign, -forward, and semantically invalid reference families before an immutable -graph can exist -- the brand is what makes a handle from one graph -unusable in another, so an invalid graph is unrepresentable rather than -merely undetected. - -## Shape of the work - -Graph visitors, budgets, provenance side tables, and complete compiler -coverage. - -## Exit evidence - -Targeted tests, feature-isolated compile where applicable, mutation-verified -architecture/validation gates, and benchmarks before performance claims. diff --git a/crates/representations/modeling/graph/README.md b/crates/representations/modeling/graph/README.md new file mode 100644 index 00000000..1393f16a --- /dev/null +++ b/crates/representations/modeling/graph/README.md @@ -0,0 +1,18 @@ +# axiolid-model + +The format-neutral geometry graph that source adapters lower into and +kernels consume: an immutable, append-only DAG of typed nodes preserving +exact curves, surfaces, profiles, primitives, topology, curve and surface +relations, CSG instructions and instancing, alongside source meshes. Handles +are branded per graph and references must point to earlier nodes of the +right family, so cycles, dangling references and cross-graph handles cannot +be built. It evaluates, tessellates and compiles nothing, and keeps source +identifiers outside the graph. + +```bash +cargo add axiolid-model +``` + +- API documentation: [docs.rs/axiolid-model](https://docs.rs/axiolid-model) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-model) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/representations/modeling/graph/src/id.rs b/crates/representations/modeling/graph/src/id.rs index cfde8203..ffbeef1e 100644 --- a/crates/representations/modeling/graph/src/id.rs +++ b/crates/representations/modeling/graph/src/id.rs @@ -20,6 +20,12 @@ impl GraphId { } /// Stable index owned by one immutable [`crate::GeometryGraph`]. +/// +/// A handle carries its graph's brand. Another builder refuses it +/// ([`crate::GraphError::ForeignReference`]) and another graph's +/// [`crate::GeometryGraph::get`] returns `None`, so a graph that references +/// a node it does not own cannot be built at all rather than being detected +/// later. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct NodeId { graph: GraphId, diff --git a/crates/representations/modeling/graph/src/lib.rs b/crates/representations/modeling/graph/src/lib.rs index ce2d1448..2d2bd6a8 100644 --- a/crates/representations/modeling/graph/src/lib.rs +++ b/crates/representations/modeling/graph/src/lib.rs @@ -6,6 +6,10 @@ //! exact curve, surface, topology, instancing, and construction intent; kernels //! choose how to evaluate or tessellate them. Typed append-only handles replace //! recursive `Box` trees and make mapped-item/CSG cycles impossible. +//! +//! Source identifiers stay outside the graph: an adapter keeps its own map +//! from source entities to [`NodeId`]s. The graph never lowers anything to a +//! mesh; compilation lives in the execution tier. pub mod curve_relation; pub mod graph; diff --git a/crates/representations/modeling/graph/src/validation.rs b/crates/representations/modeling/graph/src/validation.rs index a796722c..e8a9ec37 100644 --- a/crates/representations/modeling/graph/src/validation.rs +++ b/crates/representations/modeling/graph/src/validation.rs @@ -1,4 +1,10 @@ //! Semantic validation for graph references. +//! +//! Every reference must name a node of the family its edge accepts. Curve +//! dimensionality is followed through `Instance` nodes and trimmed, offset +//! and composite curve relations, so an instance keeps its source's +//! `Curve2`/`Curve3` family and cannot be used to smuggle a 3D curve into a +//! 2D slot. use std::collections::HashSet; diff --git a/crates/representations/region/AGENTS.md b/crates/representations/region/AGENTS.md deleted file mode 100644 index ee900c81..00000000 --- a/crates/representations/region/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Region representations - -`profile/` owns bounded planar region values used by construction algorithms. diff --git a/crates/representations/region/profile/AGENTS.md b/crates/representations/region/profile/AGENTS.md deleted file mode 100644 index 748fae7c..00000000 --- a/crates/representations/region/profile/AGENTS.md +++ /dev/null @@ -1,18 +0,0 @@ -# axiolid-profile instructions - -Purpose: Exact 2D sweep sections. - -Allowed internal dependencies: axiolid-core, axiolid-curve. Follow parent `../AGENTS.md`. Do not read -`PLAN.md` unless assigned implementation or roadmap work. - -## Module ownership - -contour.rs; parameterized.rs; validate.rs. Split a module before unrelated data, validation, and algorithms grow -together. Add no empty placeholder files. - -## Invariants - -Keep circles/sections parameterized. A contour segment is bounded; never store an infinite line as a closed edge. Profile placement uses Derived, not baked tessellation. - -Public values derive `Debug` and `Clone`; add other standard traits only when -semantically valid. Tests must exercise invalid input as well as happy paths. diff --git a/crates/representations/region/profile/Cargo.toml b/crates/representations/region/profile/Cargo.toml index 2749fc59..091c9b3f 100644 --- a/crates/representations/region/profile/Cargo.toml +++ b/crates/representations/region/profile/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/representations/region/profile/README.md b/crates/representations/region/profile/README.md new file mode 100644 index 00000000..710f4505 --- /dev/null +++ b/crates/representations/region/profile/README.md @@ -0,0 +1,16 @@ +# axiolid-profile + +Exact 2D profiles for sweeps and sectioned solids: parameterised +rectangles, circles, ellipses and structural sections, closed contours of +bounded exact curve segments with holes, centre-line profiles, and +transformed or composite profiles. It stores profile intent only. Boolean +cleanup, offsetting and triangulation are algorithms in higher tiers, so a +consumer can read profiles without them. + +```bash +cargo add axiolid-profile +``` + +- API documentation: [docs.rs/axiolid-profile](https://docs.rs/axiolid-profile) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-profile) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/representations/region/profile/src/contour.rs b/crates/representations/region/profile/src/contour.rs index ebf8e93c..3da1d704 100644 --- a/crates/representations/region/profile/src/contour.rs +++ b/crates/representations/region/profile/src/contour.rs @@ -9,6 +9,9 @@ pub struct ProfileSegment { /// Exact supporting curve. pub curve: Curve2, /// Parameter interval on `curve`. + /// + /// Always a finite span: a contour edge is bounded, so an unbounded + /// support such as a line contributes only this interval, never itself. pub domain: Interval, /// Whether parameter direction follows contour orientation. pub same_sense: bool, diff --git a/crates/representations/region/profile/src/lib.rs b/crates/representations/region/profile/src/lib.rs index ebd1fbcf..a4c2549c 100644 --- a/crates/representations/region/profile/src/lib.rs +++ b/crates/representations/region/profile/src/lib.rs @@ -32,6 +32,9 @@ pub enum Profile { /// Arbitrary exact contour with holes. Contour(ContourProfile), /// Profile transformed from another profile. + /// + /// Placement is recorded as a transform over the exact basis, not by + /// transforming tessellated points, so a placed circle stays a circle. Derived { /// Base profile. basis: Box, diff --git a/crates/representations/sampled/AGENTS.md b/crates/representations/sampled/AGENTS.md deleted file mode 100644 index 7bdbaa16..00000000 --- a/crates/representations/sampled/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Sampled representations - -`field/` owns layered spatial-field values. Sampling and analysis belong under `crates/algorithms/sampled/`. diff --git a/crates/representations/sampled/field/AGENTS.md b/crates/representations/sampled/field/AGENTS.md deleted file mode 100644 index 2d59c06b..00000000 --- a/crates/representations/sampled/field/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Sampled field values - -`axiolid-field` owns frame-neutral field cells, configuration, evidence, and invariant-preserving constructors. Algorithms live in `crates/algorithms/sampled/field`. diff --git a/crates/representations/sampled/field/Cargo.toml b/crates/representations/sampled/field/Cargo.toml index b9d22ed7..1efc2520 100644 --- a/crates/representations/sampled/field/Cargo.toml +++ b/crates/representations/sampled/field/Cargo.toml @@ -6,7 +6,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true description = "Frame-neutral layered spatial-field values and validated sampling configuration." diff --git a/crates/representations/sampled/field/README.md b/crates/representations/sampled/field/README.md new file mode 100644 index 00000000..8264e015 --- /dev/null +++ b/crates/representations/sampled/field/README.md @@ -0,0 +1,16 @@ +# axiolid-field + +Frame-neutral, deterministic layered spatial-field values: row-major cells +in an explicit frame, with surface crossings and positive-length occupancy +spans kept in separate channels so a zero-thickness facet never reads as +filled space. It owns the values, their validation and caller-supplied +configuration and budgets. Sampling, morphology, clearance and navigation +live in `axiolid-field-ops`. + +```bash +cargo add axiolid-field +``` + +- API documentation: [docs.rs/axiolid-field](https://docs.rs/axiolid-field) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-field) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/crates/representations/topology/AGENTS.md b/crates/representations/topology/AGENTS.md deleted file mode 100644 index 7f942ed5..00000000 --- a/crates/representations/topology/AGENTS.md +++ /dev/null @@ -1,18 +0,0 @@ -# axiolid-topology instructions - -Purpose: Typed-handle B-rep topology. - -Allowed internal dependencies: axiolid-core. Follow parent `../AGENTS.md`. Do not read -`PLAN.md` unless assigned implementation or roadmap work. - -## Module ownership - -id.rs; entity.rs; brep.rs. Split a module before unrelated data, validation, and algorithms grow -together. Add no empty placeholder files. - -## Invariants - -Never use bare usize across topology kinds. Geometry support is generic G; do not depend on one curve/surface model. Keep orientation explicit. - -Public values derive `Debug` and `Clone`; add other standard traits only when -semantically valid. Tests must exercise invalid input as well as happy paths. diff --git a/crates/representations/topology/Cargo.toml b/crates/representations/topology/Cargo.toml index 19bc6c93..15ce3580 100644 --- a/crates/representations/topology/Cargo.toml +++ b/crates/representations/topology/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" [dependencies] axiolid-core.workspace = true diff --git a/crates/representations/topology/README.md b/crates/representations/topology/README.md new file mode 100644 index 00000000..169f5190 --- /dev/null +++ b/crates/representations/topology/README.md @@ -0,0 +1,17 @@ +# axiolid-topology + +Exact B-rep topology with typed handles: vertices, edges, edge uses, loops, +faces, shells and solids, each with its own handle type and explicit +orientation, plus a structural audit. Geometry is linked through a +caller-chosen handle type (`BRep`), so the graph does not depend on any +curve or surface model and serves exact kernels, mesh converters and import +adapters alike. `axiolid-brep` binds it to Axiolid's own curves and +surfaces. + +```bash +cargo add axiolid-topology +``` + +- API documentation: [docs.rs/axiolid-topology](https://docs.rs/axiolid-topology) +- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-topology) +- Source and issues: [github.com/axiolid/kernel](https://github.com/axiolid/kernel) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 51573250..279fdf86 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,3 +1,4 @@ +import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { defineConfig } from "vitepress"; @@ -50,6 +51,34 @@ const adrs = [ ["0044-pointcloud-representation-and-reconstruction", 44, "Pointcloud representation and reconstruction"], ] as const; +// Generated by `cargo xtask docs` from the crate manifests; the gate fails +// when it is stale, so a new crate or layer reaches the sidebar with no edit +// here. +const facts: { + crates: { groups: { key: string; title: string; crates: string[] }[] }; +} = JSON.parse(readFileSync(new URL("./data/facts.json", import.meta.url), "utf8")); + +function referenceSidebar() { + const groups = facts.crates.groups + .filter((group) => group.crates.length > 0) + .map((group) => ({ + text: group.title, + collapsed: group.key !== "facade", + items: group.crates.map((name) => ({ text: name, link: `/reference/crates/${name}` })), + })); + return [ + { + text: "Crate reference", + items: [ + { text: "All crates", link: "/reference/" }, + { text: "Selecting a package", link: "/reference/selecting-packages" }, + { text: "Per-crate changelog", link: "/reference/changelog" }, + ], + }, + ...groups, + ]; +} + const adrIcon = ``; function adrSidebarItem([file, number, title]: (typeof adrs)[number]) { @@ -64,6 +93,10 @@ export default defineConfig({ description: "A pure-Rust, format-agnostic geometry kernel.", base: docsBase, srcExclude: ["adr/_template.md"], + // rustdoc is built by `cargo doc` in the docs workflow and copied into + // dist/api/rustdoc/ after this build, so VitePress cannot see it here. + // Scoped to that prefix: every other dead link still fails the build. + ignoreDeadLinks: [/^\/api\/rustdoc\//], markdown: { html: false, math: true, @@ -87,51 +120,55 @@ export default defineConfig({ { text: "Architecture", link: "/architecture" }, { text: "Capabilities", link: "/capabilities" }, { text: "Support map", link: "/support-map" }, + { text: "Reference", link: "/reference/" }, { text: "API", link: "https://docs.rs/axiolid" }, { text: "GitHub", link: "https://github.com/axiolid/kernel" }, ], - sidebar: [ - { - text: "Start here", - items: [ - { text: "Overview", link: "/" }, - { text: "Getting started", link: "/guide/getting-started" }, - { text: "Downstream integration", link: "/guide/downstream-integration" }, - { text: "Geometry concepts", link: "/guide/geometry-concepts" }, - { text: "Glossary", link: "/glossary" }, - { text: "Capabilities", link: "/capabilities" }, - { text: "Support map", link: "/support-map" }, - { text: "Architecture", link: "/architecture" }, - { text: "Crate map", link: "/architecture/crate-map" }, - { text: "Closure profiles", link: "/architecture/closure-profiles" }, - { text: "2D-only consumers", link: "/architecture/2d-only-consumers" }, - { text: "C ABI v0.4", link: "/architecture/c-abi-v0.4" }, - { text: "Native distribution", link: "/architecture/native-distribution" }, - { text: "Dependency graph", link: "/architecture/dependency-graph" }, - { text: "Threading", link: "/architecture/threading" }, - { text: "openbim.geometry boundary", link: "/architecture/openbim-geometry-boundary" }, - { text: "Public crate reference", link: "/reference/crates" }, - ], - }, - { - text: "About", - items: [ - { text: "Roadmap", link: "/ROADMAP" }, - { text: "Changelog", link: "/CHANGELOG" }, - { text: "Per-crate changelog", link: "/reference/changelog" }, - { text: "Research", link: "/research/geometry-capability-comparison-occt-cgal" }, - { text: "Licensing", link: "/guide/licensing" }, - { text: "Contributing", link: "/guide/contributing" }, - { text: "Where things go", link: "/contributing/where-things-go" }, - { text: "Crate migration", link: "/contributing/crate-migration" }, - { text: "Breaking changes", link: "/contributing/breaking-changes" }, - ], - }, - { - text: "Architecture decisions", - items: adrs.map(adrSidebarItem), - }, - ], + sidebar: { + "/reference/": referenceSidebar(), + "/": [ + { + text: "Start here", + items: [ + { text: "Overview", link: "/" }, + { text: "Getting started", link: "/guide/getting-started" }, + { text: "Downstream integration", link: "/guide/downstream-integration" }, + { text: "Geometry concepts", link: "/guide/geometry-concepts" }, + { text: "Glossary", link: "/glossary" }, + { text: "Capabilities", link: "/capabilities" }, + { text: "Support map", link: "/support-map" }, + { text: "Architecture", link: "/architecture" }, + { text: "Crate map", link: "/architecture/crate-map" }, + { text: "Closure profiles", link: "/architecture/closure-profiles" }, + { text: "2D-only consumers", link: "/architecture/2d-only-consumers" }, + { text: "C ABI v0.4", link: "/architecture/c-abi-v0.4" }, + { text: "Native distribution", link: "/architecture/native-distribution" }, + { text: "Dependency graph", link: "/architecture/dependency-graph" }, + { text: "Threading", link: "/architecture/threading" }, + { text: "openbim.geometry boundary", link: "/architecture/openbim-geometry-boundary" }, + { text: "Crate reference", link: "/reference/" }, + ], + }, + { + text: "About", + items: [ + { text: "Roadmap", link: "/ROADMAP" }, + { text: "Changelog", link: "/CHANGELOG" }, + { text: "Per-crate changelog", link: "/reference/changelog" }, + { text: "Research", link: "/research/geometry-capability-comparison-occt-cgal" }, + { text: "Licensing", link: "/guide/licensing" }, + { text: "Contributing", link: "/guide/contributing" }, + { text: "Where things go", link: "/contributing/where-things-go" }, + { text: "Crate migration", link: "/contributing/crate-migration" }, + { text: "Breaking changes", link: "/contributing/breaking-changes" }, + ], + }, + { + text: "Architecture decisions", + items: adrs.map(adrSidebarItem), + }, + ], + }, socialLinks: [{ icon: "github", link: "https://github.com/axiolid/kernel" }], footer: { message: "Released under the Mozilla Public License 2.0.", diff --git a/docs/.vitepress/data/facts.json b/docs/.vitepress/data/facts.json new file mode 100644 index 00000000..e3b03bf5 --- /dev/null +++ b/docs/.vitepress/data/facts.json @@ -0,0 +1,621 @@ +{ + "crate": { + "axiolid": { + "description": "Feature-gated facade for Axiolid's format-neutral geometry stack", + "layer": "facade", + "path": "crates/facade/axiolid", + "released": null, + "released_date": null, + "role": "facade", + "version": "0.4.0" + }, + "axiolid-arrangement": { + "description": "Editable planar subdivision with persistent half-edge topology", + "layer": "algorithms", + "path": "crates/algorithms/planar/arrangement", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "algorithm.planar", + "version": "0.3.0" + }, + "axiolid-backend-cpu": { + "description": "Portable and runtime-optimized CPU execution context for Axiolid geometry", + "layer": "execution", + "path": "crates/execution/cpu", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "execution.context", + "version": "0.3.0" + }, + "axiolid-backend-gpu": { + "description": "GPU executor adapter contract for batched Axiolid geometry", + "layer": "execution", + "path": "crates/execution/gpu", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "execution.context", + "version": "0.3.0" + }, + "axiolid-brep": { + "description": "Exact analytic B-rep result contracts over neutral topology", + "layer": "representations", + "path": "crates/representations/brep", + "released": "0.3.1", + "released_date": "2026-09-27", + "role": "representation.composed", + "version": "0.3.1" + }, + "axiolid-brep-audit": { + "description": "Geometric consistency auditing for exact boundary representations", + "layer": "algorithms", + "path": "crates/algorithms/repair/brep-audit", + "released": "0.3.1", + "released_date": "2026-09-27", + "role": "algorithm.repair", + "version": "0.3.1" + }, + "axiolid-brep-boolean": { + "description": "General exact B-rep booleans over analytic faces (ADR 0075)", + "layer": "algorithms", + "path": "crates/algorithms/construction/brep-boolean", + "released": "0.1.0", + "released_date": "2026-09-27", + "role": "algorithm.construction", + "version": "0.1.0" + }, + "axiolid-capi": { + "description": "Versioned, memory-safe C ABI for the Axiolid application facade.", + "layer": "facade", + "path": "crates/facade/axiolid-capi", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "facade.native-c", + "version": "0.3.0" + }, + "axiolid-collide": { + "description": "Convex collision queries: separating axis, overlap, and separation distance", + "layer": "algorithms", + "path": "crates/algorithms/query/collide", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "algorithm.query", + "version": "0.3.0" + }, + "axiolid-construct": { + "description": "Solid generation: profiles, lofts, sweeps, revolutions and half-space clipping", + "layer": "algorithms", + "path": "crates/algorithms/construction/construct", + "released": "0.3.5", + "released_date": "2026-09-28", + "role": "algorithm.construction", + "version": "0.3.5" + }, + "axiolid-contracts": { + "description": "Common backend-neutral execution and diagnostic contracts", + "layer": "contracts", + "path": "crates/contracts/common/base", + "released": "0.3.1", + "released_date": "2026-09-25", + "role": "contract.common", + "version": "0.3.1" + }, + "axiolid-core": { + "description": "Geometry data types and tolerance policy. No algorithms, no backends.", + "layer": "foundation", + "path": "crates/foundation/core", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "foundation.values", + "version": "0.3.0" + }, + "axiolid-curve": { + "description": "Exact, format-neutral curve values: lines, conics, B-splines, natural-equation and elevated curves.", + "layer": "representations", + "path": "crates/representations/analytic/curve", + "released": "0.3.1", + "released_date": "2026-09-27", + "role": "representation.atomic", + "version": "0.3.1" + }, + "axiolid-curve-evaluate-contract": { + "description": "Portable curve evaluation capability contract: point, tangent and oriented frame at a distance", + "layer": "contracts", + "path": "crates/contracts/operations/curve-evaluate", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "contract.operation", + "version": "0.3.0" + }, + "axiolid-decimate": { + "description": "Edge-collapse mesh decimation with a bounded, reported deviation", + "layer": "algorithms", + "path": "crates/algorithms/discrete/decimate", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "algorithm.discrete", + "version": "0.3.0" + }, + "axiolid-decompose": { + "description": "Convex decomposition of a solid, exact or approximate and always labelled", + "layer": "algorithms", + "path": "crates/algorithms/discrete/decompose", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "algorithm.discrete", + "version": "0.3.0" + }, + "axiolid-dispatch": { + "description": "Provider registration, ordering, fallback, and execution policy", + "layer": "execution", + "path": "crates/execution/dispatch", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "execution.dispatch", + "version": "0.3.0" + }, + "axiolid-evaluate": { + "description": "Analytic and spline curve/surface evaluation, jets, and inversion", + "layer": "algorithms", + "path": "crates/algorithms/parametric/evaluate", + "released": "0.3.1", + "released_date": "2026-09-27", + "role": "algorithm.parametric", + "version": "0.3.1" + }, + "axiolid-exact": { + "description": "Filtered exact arithmetic: interval filter, dyadic big integers, a + b*sqrt(c)", + "layer": "algorithms", + "path": "crates/algorithms/exact", + "released": "0.1.1", + "released_date": "2026-09-28", + "role": "algorithm.reference", + "version": "0.1.1" + }, + "axiolid-exact-compile-contract": { + "description": "Portable graph-to-exact-B-rep compilation contract", + "layer": "contracts", + "path": "crates/contracts/operations/exact-compile", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "contract.operation", + "version": "0.3.0" + }, + "axiolid-field": { + "description": "Frame-neutral layered spatial-field values and validated sampling configuration.", + "layer": "representations", + "path": "crates/representations/sampled/field", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "representation.sampled", + "version": "0.3.0" + }, + "axiolid-field-ops": { + "description": "Sampling, morphology, clearance, and navigation over Axiolid layered fields.", + "layer": "algorithms", + "path": "crates/algorithms/sampled/field", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "algorithm.sampled", + "version": "0.3.0" + }, + "axiolid-fixtures": { + "description": "Shared adversarial and degenerate geometry fixtures with provenance", + "layer": "representations", + "path": "crates/representations/fixtures", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "representation.fixtures", + "version": "0.3.0" + }, + "axiolid-guarantees": { + "description": "Certified-value and escalation vocabulary for geometry contracts", + "layer": "contracts", + "path": "crates/contracts/guarantees", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "contract.guarantees", + "version": "0.3.0" + }, + "axiolid-heal": { + "description": "Explicit diagnosis and opt-in repair contracts for dirty geometry", + "layer": "algorithms", + "path": "crates/algorithms/repair/heal", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "algorithm.repair", + "version": "0.3.0" + }, + "axiolid-inspect": { + "description": "Mesh queries: clearance, containment, ray casting, and genus", + "layer": "algorithms", + "path": "crates/algorithms/query/inspect", + "released": "0.3.3", + "released_date": "2026-09-27", + "role": "algorithm.query", + "version": "0.3.3" + }, + "axiolid-levelset": { + "description": "Level-set extraction: a closed manifold mesh from a sampled scalar field", + "layer": "algorithms", + "path": "crates/algorithms/sampled/levelset", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "algorithm.sampled", + "version": "0.3.0" + }, + "axiolid-linear": { + "description": "Format-neutral line, ray, segment, and polyline representations", + "layer": "representations", + "path": "crates/representations/analytic/linear", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "representation.atomic", + "version": "0.3.0" + }, + "axiolid-linear-intersection": { + "description": "Portable, deterministic intersections for linear geometry", + "layer": "algorithms", + "path": "crates/algorithms/query/intersection/linear", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "algorithm.query", + "version": "0.3.0" + }, + "axiolid-measure": { + "description": "Metric properties: area, volume, centroid, moments of inertia.", + "layer": "algorithms", + "path": "crates/algorithms/query/measure", + "released": "0.3.2", + "released_date": "2026-09-27", + "role": "algorithm.query", + "version": "0.3.2" + }, + "axiolid-mesh": { + "description": "Triangle meshes: the discrete representation every backend consumes.", + "layer": "representations", + "path": "crates/representations/discrete/mesh", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "representation.discrete", + "version": "0.3.0" + }, + "axiolid-mesh-boolean-boolmesh": { + "description": "boolmesh-backed MeshBoolean provider", + "layer": "providers", + "path": "crates/providers/mesh/boolmesh", + "released": "0.3.1", + "released_date": "2026-09-27", + "role": "provider.mesh", + "version": "0.3.1" + }, + "axiolid-mesh-boolean-contract": { + "description": "Portable mesh boolean request, result, evidence, and conformance contract", + "layer": "contracts", + "path": "crates/contracts/operations/mesh-boolean", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "contract.operation", + "version": "0.3.0" + }, + "axiolid-mesh-compile": { + "description": "Scalar reference MeshCompiler: profiles, extrusion, transforms, boolean dispatch.", + "layer": "execution", + "path": "crates/execution/compile", + "released": "0.3.5", + "released_date": "2026-09-28", + "role": "execution.orchestration", + "version": "0.3.5" + }, + "axiolid-mesh-compile-contract": { + "description": "Portable graph-to-mesh compilation contract", + "layer": "contracts", + "path": "crates/contracts/operations/compile", + "released": "0.3.1", + "released_date": "2026-09-24", + "role": "contract.operation", + "version": "0.3.1" + }, + "axiolid-mesh-contracts": { + "description": "Shared mesh admissibility contracts", + "layer": "contracts", + "path": "crates/contracts/common/mesh", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "contract.common.mesh", + "version": "0.3.0" + }, + "axiolid-mesh-section-contract": { + "description": "Portable mesh plane-section request, result, and evidence contract", + "layer": "contracts", + "path": "crates/contracts/operations/mesh-section", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "contract.operation", + "version": "0.3.0" + }, + "axiolid-minkowski": { + "description": "Minkowski sum and difference of planar-faced solids", + "layer": "algorithms", + "path": "crates/algorithms/discrete/minkowski", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "algorithm.discrete", + "version": "0.3.0" + }, + "axiolid-model": { + "description": "Format-neutral geometry item tree. The currency between a format reader and a kernel.", + "layer": "representations", + "path": "crates/representations/modeling/graph", + "released": "0.3.2", + "released_date": "2026-09-27", + "role": "representation.graph", + "version": "0.3.2" + }, + "axiolid-nurbs": { + "description": "General polynomial and rational B-spline analysis and transformation algorithms", + "layer": "algorithms", + "path": "crates/algorithms/parametric/nurbs", + "released": "0.3.3", + "released_date": "2026-09-28", + "role": "algorithm.parametric", + "version": "0.3.3" + }, + "axiolid-overlay": { + "description": "Deterministic validated planar overlay contract", + "layer": "algorithms", + "path": "crates/algorithms/planar/overlay", + "released": "0.3.5", + "released_date": "2026-09-28", + "role": "algorithm.planar", + "version": "0.3.5" + }, + "axiolid-pointcloud": { + "description": "Portable point-sampled geometry values with optional per-point channels.", + "layer": "representations", + "path": "crates/representations/discrete/pointcloud", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "representation.discrete", + "version": "0.3.0" + }, + "axiolid-pointcloud-reconstruction-contract": { + "description": "Portable pointcloud-to-surface reconstruction contract, evidence, and conformance suite.", + "layer": "contracts", + "path": "crates/contracts/operations/pointcloud-reconstruction", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "contract.operation", + "version": "0.3.0" + }, + "axiolid-pointcloud-reconstruction-sdf": { + "description": "Reference pointcloud reconstruction: signed-distance field from samples, extracted as a level set.", + "layer": "providers", + "path": "crates/providers/pointcloud/sdf", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "provider.pointcloud", + "version": "0.3.0" + }, + "axiolid-predicates": { + "description": "Certified exact-arithmetic geometric predicates", + "layer": "algorithms", + "path": "crates/algorithms/predicates", + "released": "0.3.1", + "released_date": "2026-09-27", + "role": "algorithm.reference", + "version": "0.3.1" + }, + "axiolid-primitive": { + "description": "Exact parametric primitive solids used as CSG leaves", + "layer": "representations", + "path": "crates/representations/analytic/primitive", + "released": "0.3.1", + "released_date": "2026-09-27", + "role": "representation.atomic", + "version": "0.3.1" + }, + "axiolid-profile": { + "description": "Exact 2D profile representations for sweeps and sectioned solids", + "layer": "representations", + "path": "crates/representations/region/profile", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "representation.region", + "version": "0.3.0" + }, + "axiolid-project": { + "description": "Projection of triangle meshes onto a plane, and prism intersection", + "layer": "algorithms", + "path": "crates/algorithms/planar/project", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "algorithm.planar", + "version": "0.3.0" + }, + "axiolid-ray-mesh": { + "description": "Narrow-phase ray/triangle-mesh nearest-hit intersection", + "layer": "algorithms", + "path": "crates/algorithms/query/intersection/ray-mesh", + "released": null, + "released_date": null, + "role": "algorithm.query", + "version": "0.4.0" + }, + "axiolid-reference": { + "description": "Portable scalar reference implementation and certified predicates", + "layer": "algorithms", + "path": "crates/algorithms/reference", + "released": "0.3.1", + "released_date": "2026-09-27", + "role": "algorithm.reference", + "version": "0.3.1" + }, + "axiolid-refine": { + "description": "Mesh refinement and smoothing with bounded, reported deviation", + "layer": "algorithms", + "path": "crates/algorithms/discrete/refine", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "algorithm.discrete", + "version": "0.3.0" + }, + "axiolid-route": { + "description": "Exact planar shortest path over a visibility graph", + "layer": "algorithms", + "path": "crates/algorithms/planar/route", + "released": "0.3.5", + "released_date": "2026-09-28", + "role": "algorithm.planar", + "version": "0.3.5" + }, + "axiolid-spatial": { + "description": "Acceleration structures: BVH and uniform point grid, and their queries; barycentric and mean-value coordinates.", + "layer": "algorithms", + "path": "crates/algorithms/query/spatial", + "released": "0.3.1", + "released_date": "2026-09-27", + "role": "algorithm.query", + "version": "0.3.1" + }, + "axiolid-surface": { + "description": "Exact, format-neutral surface values: planes, quadrics, tori, and B-spline patches.", + "layer": "representations", + "path": "crates/representations/analytic/surface", + "released": "0.3.1", + "released_date": "2026-09-27", + "role": "representation.atomic", + "version": "0.3.1" + }, + "axiolid-tessellation-contract": { + "description": "Curves, surfaces and B-reps to triangles under an explicit tolerance.", + "layer": "contracts", + "path": "crates/contracts/operations/tessellate", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "contract.operation", + "version": "0.3.0" + }, + "axiolid-topology": { + "description": "Typed-handle B-rep topology independent of curve and surface implementations", + "layer": "representations", + "path": "crates/representations/topology", + "released": "0.3.0", + "released_date": "2026-09-23", + "role": "representation.topology", + "version": "0.3.0" + }, + "axiolid-triangulate": { + "description": "Constrained Delaunay triangulation with bounded quality refinement", + "layer": "algorithms", + "path": "crates/algorithms/planar/triangulate", + "released": "0.3.1", + "released_date": "2026-09-27", + "role": "algorithm.planar", + "version": "0.3.1" + } + }, + "crates": { + "groups": [ + { + "crates": [ + "axiolid-core" + ], + "key": "foundation", + "title": "Foundation" + }, + { + "crates": [ + "axiolid-brep", + "axiolid-curve", + "axiolid-field", + "axiolid-fixtures", + "axiolid-linear", + "axiolid-mesh", + "axiolid-model", + "axiolid-pointcloud", + "axiolid-primitive", + "axiolid-profile", + "axiolid-surface", + "axiolid-topology" + ], + "key": "representations", + "title": "Representations" + }, + { + "crates": [ + "axiolid-contracts", + "axiolid-curve-evaluate-contract", + "axiolid-exact-compile-contract", + "axiolid-guarantees", + "axiolid-mesh-boolean-contract", + "axiolid-mesh-compile-contract", + "axiolid-mesh-contracts", + "axiolid-mesh-section-contract", + "axiolid-pointcloud-reconstruction-contract", + "axiolid-tessellation-contract" + ], + "key": "contracts", + "title": "Contracts" + }, + { + "crates": [ + "axiolid-arrangement", + "axiolid-brep-audit", + "axiolid-brep-boolean", + "axiolid-collide", + "axiolid-construct", + "axiolid-decimate", + "axiolid-decompose", + "axiolid-evaluate", + "axiolid-exact", + "axiolid-field-ops", + "axiolid-heal", + "axiolid-inspect", + "axiolid-levelset", + "axiolid-linear-intersection", + "axiolid-measure", + "axiolid-minkowski", + "axiolid-nurbs", + "axiolid-overlay", + "axiolid-predicates", + "axiolid-project", + "axiolid-ray-mesh", + "axiolid-reference", + "axiolid-refine", + "axiolid-route", + "axiolid-spatial", + "axiolid-triangulate" + ], + "key": "algorithms", + "title": "Algorithms" + }, + { + "crates": [ + "axiolid-mesh-boolean-boolmesh", + "axiolid-pointcloud-reconstruction-sdf" + ], + "key": "providers", + "title": "Providers" + }, + { + "crates": [ + "axiolid-backend-cpu", + "axiolid-backend-gpu", + "axiolid-dispatch", + "axiolid-mesh-compile" + ], + "key": "execution", + "title": "Execution" + }, + { + "crates": [ + "axiolid", + "axiolid-capi" + ], + "key": "facade", + "title": "Facade" + } + ], + "total": 57 + } +} diff --git a/docs/AGENTS.md b/docs/AGENTS.md deleted file mode 100644 index 155ec23b..00000000 --- a/docs/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Documentation - -Use Keep a Changelog in `CHANGELOG.md`. Add architecture decisions in `adr/` using `_template.md` before irreversible boundary or dependency changes. Research records must distinguish measured evidence from proposals. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 764ed7f6..439536c4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -100,6 +100,26 @@ All notable changes to Axiolid are documented in this file. parallel-axis moments, hemispheres, cones, a half torus) and an 18-fault mutation probe. +### Changed + +- Context lives beside the code (ADR 0078). The root `AGENTS.md` is the + only one; the nested `AGENTS.md` files, every `PLAN.md`, `docs/plans/` + and root-level session plans are gone, their content moved into module + docs, ADR amendments, tests and issues (#199-#202). + `cargo xtask context check` keeps it that way and runs in the gate. +- Every crate has its own `README.md`, declared as its crates.io page, + with links to docs.rs, its reference page and the source. +- The crate reference on the documentation site is generated: + `cargo xtask docs` writes one page per crate from its manifest, README + and changelog, the grouped index, the sidebar and the per-crate + changelog page (replacing `scripts/assemble-crate-changelogs.py`); + `--check` runs in the gate and in the Pages workflow, which now also + hosts rustdoc under `/api/rustdoc/`. +- The machine-checked declarations moved from `architecture/` to + `docs/architecture/`. +- The README quick start uses `cargo add axiolid --features standard`; + the plain facade compiles no geometry. + ### Fixed - Exact revolutions were built inside out (#125): their surface frames were diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a8c4cc1e..c144a930 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -50,7 +50,7 @@ next cannot be honestly attempted without it. | [Solid modelling breadth](https://github.com/axiolid/kernel/milestone/11) | General exact boolean, offset/shelling, and fillet. The heaviest geometry in the sequence and the most dependent on the validity foundation beneath it: these operations produce the self-intersections that measurement and validity detects and mesh processing repairs. | | [Mesh-side breadth without exact loss](https://github.com/axiolid/kernel/milestone/12) | Closes the functional gaps against mesh-first kernels: per-vertex attributes that survive a boolean, component decomposition, refinement and smoothing, implicit level-set input, and the standard mesh queries. Scheduled after solid modelling breadth because these are additive breadth, not foundations -- and constrained by it: a mesh capability may not be bought by degrading an exact one. Refinement is the clearest case for doing this here rather than adopting a mesh library, since we keep the analytic surface and can evaluate it instead of interpolating between triangles. | | [v1.0: Stable public API](https://github.com/axiolid/kernel/milestone/5) | An API is only worth stabilising once the capability surface behind it is real. Committing earlier would freeze scaffolding. | -| [Geometry breadth: OCCT/CGAL gaps](https://github.com/axiolid/kernel/milestone/13) | Every capability OCCT and CGAL have that Axiolid lacks or only partly has, one issue per cluster, graded in `architecture/capability-ledger.toml`. Unordered on purpose: it is a map of what is missing, not a sequence. Items are pulled into the gates above when a consumer needs them, and `cargo xtask gaps` shows which are unblocked. General surface intersection (#119) comes first because curved booleans, fillets, offsets and curved measurement all wait on it. | +| [Geometry breadth: OCCT/CGAL gaps](https://github.com/axiolid/kernel/milestone/13) | Every capability OCCT and CGAL have that Axiolid lacks or only partly has, one issue per cluster, graded in `docs/architecture/capability-ledger.toml`. Unordered on purpose: it is a map of what is missing, not a sequence. Items are pulled into the gates above when a consumer needs them, and `cargo xtask gaps` shows which are unblocked. General surface intersection (#119) comes first because curved booleans, fillets, offsets and curved measurement all wait on it. | | [Backlog: accepted, unscheduled](https://github.com/axiolid/kernel/milestone/6) | Not a capability gate and deliberately undated: work that is accepted and wanted but not committed to a release. Pull from here when a gate above is clear, rather than widening a gate in flight. | Minor versions can be inserted between any two of these at any time. The diff --git a/docs/adr/0009-layered-geometry-dag.md b/docs/adr/0009-layered-geometry-dag.md index 4f244abf..85014487 100644 --- a/docs/adr/0009-layered-geometry-dag.md +++ b/docs/adr/0009-layered-geometry-dag.md @@ -96,3 +96,10 @@ small operation traits, and physically separate execution/adaptor crates. - `crates/facade/axiolid-backend-{cpu,gpu}/` - `packages/ifc/ifc-geometry/references/ifc4-add2-tc1-geometry-declarations.tsv` - `packages/ifc/ifc-geometry/tests/declaration_manifest.rs` + +## Amendment 2026-09-28: `crates/AGENTS.md` and `crates/PLAN.md` retired + +[ADR 0078](./0078-context-lives-beside-the-code.md) removed both files. The +layer DAG and exact dependency allowlists are enforced by +`cargo xtask architecture check`. The standing rules are in the root +`AGENTS.md`, and open work is tracked in GitHub issues. diff --git a/docs/adr/0015-adopt-earcut-polygon-triangulation.md b/docs/adr/0015-adopt-earcut-polygon-triangulation.md index 22842358..f071e875 100644 --- a/docs/adr/0015-adopt-earcut-polygon-triangulation.md +++ b/docs/adr/0015-adopt-earcut-polygon-triangulation.md @@ -69,7 +69,8 @@ gates. - Extend the differential oracle to holes if `axiolid-reference` ever grows a working hole path, so the audit is total rather than partial. - Corner radii on rectangle profiles are not yet approximated; they currently - produce sharp corners. Tracked in `axiolid-mesh-compile/PLAN.md`. + produce sharp corners. *(Amended 2026-09-28: done under kernel#111; see + `crates/algorithms/construction/construct/tests/rounded_rectangle.rs`.)* ## Relation to existing code diff --git a/docs/adr/0045-boolean-construction-arithmetic.md b/docs/adr/0045-boolean-construction-arithmetic.md index b45a3d0d..598ba903 100644 --- a/docs/adr/0045-boolean-construction-arithmetic.md +++ b/docs/adr/0045-boolean-construction-arithmetic.md @@ -105,3 +105,28 @@ size: - The gap to CGAL below 1e-12 remains open and is now documented rather than unmeasured. Closing it would require exact constructions, which this ADR declines. + +## Amendment 2026-09-28: coordinate magnitude, and no RTC facility + +A boolean measured at survey coordinates (1e6 to 1e7) looked badly wrong, +which was first read as a case for exact construction. Probes showed the +error was already present in the input's volume, before any boolean ran. It +came from catastrophic cancellation in a divergence-theorem sum taken about +the world origin. `volume_properties` and `surface_properties` in +`axiolid-measure` now sum about the mesh's first vertex, and +`tests/conditioning.rs` there holds thin plates at 1e7 to within 1e-12 +relative. Re-basing per triangle was also measured. It was worse by up to six +orders of magnitude because it leaves one operand of `a . (b x c)` at world +magnitude, so it was not adopted. `second_moments` stays about the origin +because its value depends on that origin by contract. + +Two further arguments for a relative-to-centre (RTC) coordinate facility were +measured and refuted (`axiolid-predicates` and +`axiolid-mesh-boolean-boolmesh`, `tests/survey_scale.rs`). Coincidence that +both sides author the same way survives rounding at every magnitude, so +`orient3d` still certifies `Zero` and a flush union is exact at 1e7. The +filter's escalation rate does not change with magnitude either, so RTC would +not make predicates faster. Axiolid therefore has no RTC or local-origin +facility and no offset ownership model. The input's own grid, about 1.9e-9 m +at 1e7, is a property of the caller's coordinates. Revisit this only if a +real workload fails in a way these probes do not cover. diff --git a/docs/adr/0049-certified-curved-surface-analysis.md b/docs/adr/0049-certified-curved-surface-analysis.md index 479703f1..9bd5cb7c 100644 --- a/docs/adr/0049-certified-curved-surface-analysis.md +++ b/docs/adr/0049-certified-curved-surface-analysis.md @@ -140,7 +140,8 @@ exact fast case, unchanged. - `crates/algorithms/parametric/nurbs/src/certified_surface_arcs.rs` - `crates/algorithms/parametric/nurbs/tests/curved_arcs.rs` -- `docs/plans/certified-boundary-roots.md` +- `docs/plans/certified-boundary-roots.md` (removed by ADR 0078; see the + amendment below) ## Exact elementary curves (addendum) @@ -313,3 +314,18 @@ Consequence for future work: a bare floating-point threshold in a structural predicate should be treated as a bug unless it is comparing dimensionless quantities. Scale invariance is now asserted directly, by deriving the same shape at three scales and requiring one verdict. + +## Amendment 2026-09-28: the boundary-roots plan is closed + +`docs/plans/certified-boundary-roots.md` was removed under +[ADR 0078](./0078-context-lives-beside-the-code.md). It found that a root +lying exactly on a patch edge cannot be certified by the 3x3 Krawczyk operator +at any budget or tolerance, because strict containment in the box is +unsatisfiable there. This was a limit of the proof, not of the budget. The fix +it proposed landed: `PinnedEdge` and `krawczyk_root_on_edge` in +`certified_curve_surface_intersection.rs` pin the edge parameter and certify +the reduced system, and a chord ending on the boundary of both patches splits +both of them (`CertifiedSurfacePairSplit3::DualSplit` in `axiolid-construct`). The affine trace in +`certified_surface_surface_intersection.rs` stays single-span and degree 1. +General B-spline pairs are traced by `spline_pair_intersection` instead +(capability ledger row B10). diff --git a/docs/adr/0078-context-lives-beside-the-code.md b/docs/adr/0078-context-lives-beside-the-code.md new file mode 100644 index 00000000..ce2e29b1 --- /dev/null +++ b/docs/adr/0078-context-lives-beside-the-code.md @@ -0,0 +1,77 @@ +# 0078 — Context lives beside the code; open work lives in issues + +- **Status:** Accepted +- **Date:** 2026-09-28 +- **Deciders:** Friedrich Schrödter +- **Supersedes:** — + +## Context + +The repository kept its contributor context in a tree of progressive +`AGENTS.md` files, one per ownership directory and crate (about ninety), +plus a `PLAN.md` beside many crates, `docs/plans/`, and root-level agent +session plans (`PLAN-*.md`, `*-FINDING.md`). Each was meant to add only +what its directory needed on top of its parent. + +In practice they drifted. Most crate files repeated the same boilerplate +("follow the parent", "public values derive `Debug` and `Clone`", +dependency lists that `cargo xtask architecture check` already enforces). +The statements that were unique were often stale: kernel#25 found six of +seven unchecked `PLAN.md` boxes describing work that was already done, +and `scripts/check-roadmap-freshness.py` grew a special case to stop +`PLAN.md` from recording status. A reader could not tell which of the +three or four files on a path was authoritative, and crates.io showed +the workspace README for every crate. + +openbimrs/ifc made the same move in its PR #179 and has run on it since. + +## Decision + +Every piece of context has exactly one home, chosen by what it is: + +| Content | Home | +| --- | --- | +| Rules every contributor must know before touching the repository | the root `AGENTS.md`, the only file of that name (at most 120 lines) | +| A durable decision and its reasoning | an ADR in `docs/adr/` | +| An invariant, pitfall or reason tied to specific code | the `//!` or `///` doc of that code | +| A rule that can be checked | a test, `cargo xtask architecture check` or `cargo xtask context check` | +| Open work, gaps, next steps | a GitHub issue; a code marker is written `TODO(#N)` | +| What a crate is for and what it deliberately does not do, when nothing above holds it | the crate's `README.md` (its crates.io page, at most 150 lines) | + +No nested `AGENTS.md` or `CLAUDE.md`, no `PLAN.md`, no `docs/plans/` and +no checked-in session plans. The repository root holds only the workspace +manifest, lockfile, toolchain pin, licence, `README.md` and `AGENTS.md`. +The machine-checked declarations that used to sit in a root +`architecture/` directory (closure profiles, capability ledger, reference +packages, semver exceptions) move to `docs/architecture/`, beside the maps +generated from them. A crate README repeats nothing a test, ADR +or module doc already says. It has no checkboxes and makes no claim +that goes stale at the next release, such as "not yet published". + +`cargo xtask context check` enforces this and runs in `scripts/gate.sh`. +`scripts/probe_context_gate.sh` shows each rule can fail. + +## Alternatives considered + +| Option | Why not | +| --- | --- | +| Keep nested `AGENTS.md`, lint for drift | The drift is semantic (a true-looking statement about code that changed), which no lint catches. Module docs sit next to the code and get reviewed with it. | +| Keep `PLAN.md` without status (the kernel#25 rule) | A plan without status still goes stale. It duplicates the issue tracker, which already has status, discussion and cross-links. | +| One `AGENTS.md` per crate, no parents | This still duplicates the README and module docs, and agents that read the nearest file still miss the root rules. | + +## Consequences + +**Positive** + +- One place to look for each kind of fact. A reviewer sees a stale + invariant in the diff that changes the code it describes. +- crates.io shows each crate's own page. +- Open work is visible, triaged and linked on the project board. + +**Negative** + +- Agents no longer get directory-specific context just by opening a + directory. They must read the crate README and the module docs they + touch, and the root `AGENTS.md` says so. +- Branches that edit a nested `AGENTS.md` or `PLAN.md` conflict with the + deletion. Move the edit into the new home when rebasing. diff --git a/docs/adr/README.md b/docs/adr/README.md index 18739a65..15f7cd70 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -62,3 +62,4 @@ Package names and paths in older accepted records describe the tree at the time | [0075](./0075-general-brep-boolean.md) | General B-rep boolean: our own general-fuse pipeline | | [0076](./0076-ruled-quadric-sections.md) | Ruled quadric sections: exact quartic intersection curves | | [0077](./0077-implicit-section-curves.md) | Implicit section curves: certified tracing of analytic sections | +| [0078](./0078-context-lives-beside-the-code.md) | Context lives beside the code; open work lives in issues | diff --git a/docs/architecture/2d-only-consumers.md b/docs/architecture/2d-only-consumers.md index 0065047b..bb16a297 100644 --- a/docs/architecture/2d-only-consumers.md +++ b/docs/architecture/2d-only-consumers.md @@ -29,7 +29,7 @@ excludes: This is a dependency-graph guarantee, not merely a feature convention. CI resolves the isolated fixture as its own workspace, compares the complete -resolved package set with `architecture/closure-profiles.toml`, compiles and +resolved package set with `docs/architecture/closure-profiles.toml`, compiles and runs it, and mutation-tests the closure gate. ## Choose a larger profile only for a larger job diff --git a/docs/architecture/AGENTS.md b/docs/architecture/AGENTS.md deleted file mode 100644 index 51a9a57f..00000000 --- a/docs/architecture/AGENTS.md +++ /dev/null @@ -1,8 +0,0 @@ -# Architecture documentation - -This directory contains generated and hand-authored architecture maps for the physical workspace, package-role DAG, and capability/provider ownership. - -- `current-target-crate-map.md` is the migration truth and conflict register. -- Generated maps must come from `cargo metadata` through `cargo xtask architecture docs`. -- Do not claim runtime capability from package metadata; implementation plus conformance is authoritative. -- Treat `openbim.geometry` as external conformance vocabulary; keep Pkl runtime/schema types and all source/wire/vendor DTOs outside Axiolid contracts. diff --git a/architecture/capability-ledger.toml b/docs/architecture/capability-ledger.toml similarity index 99% rename from architecture/capability-ledger.toml rename to docs/architecture/capability-ledger.toml index b0c43934..f0772152 100644 --- a/architecture/capability-ledger.toml +++ b/docs/architecture/capability-ledger.toml @@ -20,6 +20,16 @@ # - `issue` is a key from [[issue]]; the issue's number is recorded there. # `blocked_by` mirrors GitHub's native issue dependencies; `gaps next` # skips an issue until every blocker's rows are implemented. +# - The gate rejects evidence that no longer resolves, reference paths +# outside the pinned trees, and dangling issue keys. It does not judge +# whether a grade is true; review does. +# - Reopening a `scoped` row is a maintainer decision: argue against its +# `scope_rationale` in an issue first. An [[issue]] with `needs_decision` +# is listed apart by `gaps`; ask the maintainer rather than picking an +# option. +# - Re-pinning OCCT or CGAL means regenerating reference-packages.toml from +# the new tree and re-checking every row's reference paths. The sources are +# needed for nothing else: the rows carry what the comparison learned. # # Grades came from reading public functions and tests at `cb8d203`; see # docs/research/geometry-capability-comparison-occt-cgal.md. diff --git a/architecture/closure-profiles.toml b/docs/architecture/closure-profiles.toml similarity index 100% rename from architecture/closure-profiles.toml rename to docs/architecture/closure-profiles.toml diff --git a/docs/architecture/crate-map.md b/docs/architecture/crate-map.md index 43b65e07..bcffbbc3 100644 --- a/docs/architecture/crate-map.md +++ b/docs/architecture/crate-map.md @@ -1,4 +1,6 @@ - +--- +# Generated by `cargo xtask architecture docs`; do not edit manually. +--- # Axiolid crate map @@ -6,65 +8,65 @@ Architecture metadata describes ownership, not runtime capability support. Capab | Package | Path | Layer | Role | Domain | Visibility | Allowed internal dependencies | | --- | --- | --- | --- | --- | --- | --- | -| `axiolid` | [`crates/facade/axiolid`](https://github.com/axiolid/kernel/tree/main/crates/facade/axiolid) | `facade` | `facade` | `public` | public | `axiolid-backend-cpu`, `axiolid-backend-gpu`, `axiolid-brep`, `axiolid-construct`, `axiolid-contracts`, `axiolid-core`, `axiolid-curve`, `axiolid-dispatch`, `axiolid-evaluate`, `axiolid-field`, `axiolid-field-ops`, `axiolid-heal`, `axiolid-linear`, `axiolid-linear-intersection`, `axiolid-measure`, `axiolid-mesh`, `axiolid-mesh-boolean-boolmesh`, `axiolid-mesh-boolean-contract`, `axiolid-mesh-compile-contract`, `axiolid-mesh-contracts`, `axiolid-mesh-section-contract`, `axiolid-model`, `axiolid-nurbs`, `axiolid-overlay`, `axiolid-pointcloud`, `axiolid-pointcloud-reconstruction-contract`, `axiolid-pointcloud-reconstruction-sdf`, `axiolid-predicates`, `axiolid-primitive`, `axiolid-profile`, `axiolid-project`, `axiolid-ray-mesh`, `axiolid-reference`, `axiolid-route`, `axiolid-spatial`, `axiolid-surface`, `axiolid-tessellation-contract`, `axiolid-topology` | -| `axiolid-arrangement` | [`crates/algorithms/planar/arrangement`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/planar/arrangement) | `algorithms` | `algorithm.planar` | `planar.arrangement` | public | `axiolid-core`, `axiolid-guarantees`, `axiolid-predicates` | -| `axiolid-backend-cpu` | [`crates/execution/cpu`](https://github.com/axiolid/kernel/tree/main/crates/execution/cpu) | `execution` | `execution.context` | `cpu` | public | — | -| `axiolid-backend-gpu` | [`crates/execution/gpu`](https://github.com/axiolid/kernel/tree/main/crates/execution/gpu) | `execution` | `execution.context` | `gpu` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-mesh`, `axiolid-mesh-compile-contract`, `axiolid-model` | +| [`axiolid`](/reference/crates/axiolid) | [`crates/facade/axiolid`](https://github.com/axiolid/kernel/tree/main/crates/facade/axiolid) | `facade` | `facade` | `public` | public | `axiolid-backend-cpu`, `axiolid-backend-gpu`, `axiolid-brep`, `axiolid-construct`, `axiolid-contracts`, `axiolid-core`, `axiolid-curve`, `axiolid-dispatch`, `axiolid-evaluate`, `axiolid-field`, `axiolid-field-ops`, `axiolid-heal`, `axiolid-linear`, `axiolid-linear-intersection`, `axiolid-measure`, `axiolid-mesh`, `axiolid-mesh-boolean-boolmesh`, `axiolid-mesh-boolean-contract`, `axiolid-mesh-compile-contract`, `axiolid-mesh-contracts`, `axiolid-mesh-section-contract`, `axiolid-model`, `axiolid-nurbs`, `axiolid-overlay`, `axiolid-pointcloud`, `axiolid-pointcloud-reconstruction-contract`, `axiolid-pointcloud-reconstruction-sdf`, `axiolid-predicates`, `axiolid-primitive`, `axiolid-profile`, `axiolid-project`, `axiolid-ray-mesh`, `axiolid-reference`, `axiolid-route`, `axiolid-spatial`, `axiolid-surface`, `axiolid-tessellation-contract`, `axiolid-topology` | +| [`axiolid-arrangement`](/reference/crates/axiolid-arrangement) | [`crates/algorithms/planar/arrangement`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/planar/arrangement) | `algorithms` | `algorithm.planar` | `planar.arrangement` | public | `axiolid-core`, `axiolid-guarantees`, `axiolid-predicates` | +| [`axiolid-backend-cpu`](/reference/crates/axiolid-backend-cpu) | [`crates/execution/cpu`](https://github.com/axiolid/kernel/tree/main/crates/execution/cpu) | `execution` | `execution.context` | `cpu` | public | — | +| [`axiolid-backend-gpu`](/reference/crates/axiolid-backend-gpu) | [`crates/execution/gpu`](https://github.com/axiolid/kernel/tree/main/crates/execution/gpu) | `execution` | `execution.context` | `gpu` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-mesh`, `axiolid-mesh-compile-contract`, `axiolid-model` | | `axiolid-benchmark` | [`tools/benchmark`](https://github.com/axiolid/kernel/tree/main/tools/benchmark) | `tools` | `tool.benchmark` | `workspace` | internal | `axiolid`, `axiolid-core`, `axiolid-curve`, `axiolid-guarantees`, `axiolid-measure`, `axiolid-mesh`, `axiolid-predicates`, `axiolid-spatial` | -| `axiolid-brep` | [`crates/representations/brep`](https://github.com/axiolid/kernel/tree/main/crates/representations/brep) | `representations` | `representation.composed` | `brep` | public | `axiolid-core`, `axiolid-curve`, `axiolid-surface`, `axiolid-topology` | -| `axiolid-brep-audit` | [`crates/algorithms/repair/brep-audit`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/repair/brep-audit) | `algorithms` | `algorithm.repair` | `repair.brep` | public | `axiolid-brep`, `axiolid-contracts`, `axiolid-core`, `axiolid-curve`, `axiolid-evaluate`, `axiolid-surface`, `axiolid-topology` | -| `axiolid-brep-boolean` | [`crates/algorithms/construction/brep-boolean`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/construction/brep-boolean) | `algorithms` | `algorithm.construction` | `construction.brep-boolean` | public | `axiolid-brep`, `axiolid-brep-audit`, `axiolid-construct`, `axiolid-core`, `axiolid-curve`, `axiolid-evaluate`, `axiolid-measure`, `axiolid-nurbs`, `axiolid-overlay`, `axiolid-primitive`, `axiolid-profile`, `axiolid-surface`, `axiolid-topology` | -| `axiolid-capi` | [`crates/facade/axiolid-capi`](https://github.com/axiolid/kernel/tree/main/crates/facade/axiolid-capi) | `facade` | `facade.native-c` | `integration` | public | `axiolid` | -| `axiolid-collide` | [`crates/algorithms/query/collide`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/query/collide) | `algorithms` | `algorithm.query` | `query.collide` | public | `axiolid-core` | -| `axiolid-construct` | [`crates/algorithms/construction/construct`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/construction/construct) | `algorithms` | `algorithm.construction` | `construction` | public | `axiolid-brep`, `axiolid-brep-audit`, `axiolid-contracts`, `axiolid-core`, `axiolid-curve`, `axiolid-evaluate`, `axiolid-exact`, `axiolid-guarantees`, `axiolid-heal`, `axiolid-measure`, `axiolid-mesh`, `axiolid-mesh-boolean-contract`, `axiolid-mesh-contracts`, `axiolid-nurbs`, `axiolid-overlay`, `axiolid-predicates`, `axiolid-primitive`, `axiolid-profile`, `axiolid-reference`, `axiolid-surface`, `axiolid-tessellation-contract`, `axiolid-topology` | -| `axiolid-contracts` | [`crates/contracts/common/base`](https://github.com/axiolid/kernel/tree/main/crates/contracts/common/base) | `contracts` | `contract.common` | `common` | public | `axiolid-core`, `axiolid-guarantees` | -| `axiolid-core` | [`crates/foundation/core`](https://github.com/axiolid/kernel/tree/main/crates/foundation/core) | `foundation` | `foundation.values` | `numeric` | public | — | -| `axiolid-curve` | [`crates/representations/analytic/curve`](https://github.com/axiolid/kernel/tree/main/crates/representations/analytic/curve) | `representations` | `representation.atomic` | `analytic.curve` | public | `axiolid-core`, `axiolid-linear` | -| `axiolid-curve-evaluate-contract` | [`crates/contracts/operations/curve-evaluate`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/curve-evaluate) | `contracts` | `contract.operation` | `curve.evaluate` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-curve` | -| `axiolid-decimate` | [`crates/algorithms/discrete/decimate`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/discrete/decimate) | `algorithms` | `algorithm.discrete` | `discrete.decimate` | public | `axiolid-core`, `axiolid-heal`, `axiolid-measure`, `axiolid-mesh` | -| `axiolid-decompose` | [`crates/algorithms/discrete/decompose`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/discrete/decompose) | `algorithms` | `algorithm.discrete` | `discrete.decompose` | public | `axiolid-construct`, `axiolid-contracts`, `axiolid-core`, `axiolid-measure`, `axiolid-mesh`, `axiolid-mesh-boolean-boolmesh`, `axiolid-mesh-boolean-contract`, `axiolid-reference` | -| `axiolid-dispatch` | [`crates/execution/dispatch`](https://github.com/axiolid/kernel/tree/main/crates/execution/dispatch) | `execution` | `execution.dispatch` | `providers` | public | `axiolid-backend-cpu`, `axiolid-contracts`, `axiolid-core`, `axiolid-mesh`, `axiolid-mesh-boolean-contract`, `axiolid-mesh-contracts`, `axiolid-mesh-section-contract`, `axiolid-pointcloud`, `axiolid-pointcloud-reconstruction-contract`, `axiolid-pointcloud-reconstruction-sdf` | -| `axiolid-evaluate` | [`crates/algorithms/parametric/evaluate`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/parametric/evaluate) | `algorithms` | `algorithm.parametric` | `evaluate` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-curve`, `axiolid-curve-evaluate-contract`, `axiolid-surface` | -| `axiolid-exact` | [`crates/algorithms/exact`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/exact) | `algorithms` | `algorithm.reference` | `reference.exact` | public | `axiolid-core`, `axiolid-guarantees` | -| `axiolid-exact-compile-contract` | [`crates/contracts/operations/exact-compile`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/exact-compile) | `contracts` | `contract.operation` | `brep.compile` | public | `axiolid-brep`, `axiolid-contracts`, `axiolid-model` | -| `axiolid-field` | [`crates/representations/sampled/field`](https://github.com/axiolid/kernel/tree/main/crates/representations/sampled/field) | `representations` | `representation.sampled` | `sampled.field` | public | `axiolid-core` | -| `axiolid-field-ops` | [`crates/algorithms/sampled/field`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/sampled/field) | `algorithms` | `algorithm.sampled` | `sampled.field` | public | `axiolid-core`, `axiolid-field` | -| `axiolid-fixtures` | [`crates/representations/fixtures`](https://github.com/axiolid/kernel/tree/main/crates/representations/fixtures) | `representations` | `representation.fixtures` | `testing.fixtures` | public | `axiolid-core`, `axiolid-mesh` | -| `axiolid-guarantees` | [`crates/contracts/guarantees`](https://github.com/axiolid/kernel/tree/main/crates/contracts/guarantees) | `contracts` | `contract.guarantees` | `proof` | public | — | -| `axiolid-heal` | [`crates/algorithms/repair/heal`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/repair/heal) | `algorithms` | `algorithm.repair` | `repair.mesh` | public | `axiolid-core`, `axiolid-guarantees`, `axiolid-measure`, `axiolid-mesh`, `axiolid-predicates`, `axiolid-spatial` | -| `axiolid-inspect` | [`crates/algorithms/query/inspect`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/query/inspect) | `algorithms` | `algorithm.query` | `query.inspect` | public | `axiolid-core`, `axiolid-exact`, `axiolid-guarantees`, `axiolid-heal`, `axiolid-mesh`, `axiolid-predicates`, `axiolid-spatial` | -| `axiolid-levelset` | [`crates/algorithms/sampled/levelset`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/sampled/levelset) | `algorithms` | `algorithm.sampled` | `sampled.levelset` | public | `axiolid-core`, `axiolid-measure`, `axiolid-mesh` | -| `axiolid-linear` | [`crates/representations/analytic/linear`](https://github.com/axiolid/kernel/tree/main/crates/representations/analytic/linear) | `representations` | `representation.atomic` | `analytic.linear` | public | `axiolid-core` | -| `axiolid-linear-intersection` | [`crates/algorithms/query/intersection/linear`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/query/intersection/linear) | `algorithms` | `algorithm.query` | `query.intersection.linear` | public | `axiolid-core`, `axiolid-guarantees`, `axiolid-linear`, `axiolid-predicates` | -| `axiolid-measure` | [`crates/algorithms/query/measure`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/query/measure) | `algorithms` | `algorithm.query` | `query.measure` | public | `axiolid-brep`, `axiolid-core`, `axiolid-curve`, `axiolid-evaluate`, `axiolid-mesh`, `axiolid-surface`, `axiolid-topology` | -| `axiolid-mesh` | [`crates/representations/discrete/mesh`](https://github.com/axiolid/kernel/tree/main/crates/representations/discrete/mesh) | `representations` | `representation.discrete` | `discrete.mesh` | public | `axiolid-core` | -| `axiolid-mesh-boolean-boolmesh` | [`crates/providers/mesh/boolmesh`](https://github.com/axiolid/kernel/tree/main/crates/providers/mesh/boolmesh) | `providers` | `provider.mesh` | `mesh.boolean` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-dispatch`, `axiolid-fixtures`, `axiolid-guarantees`, `axiolid-measure`, `axiolid-mesh`, `axiolid-mesh-boolean-contract`, `axiolid-mesh-contracts`, `axiolid-predicates`, `axiolid-reference` | -| `axiolid-mesh-boolean-contract` | [`crates/contracts/operations/mesh-boolean`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/mesh-boolean) | `contracts` | `contract.operation` | `mesh.boolean` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-mesh`, `axiolid-mesh-contracts` | -| `axiolid-mesh-compile` | [`crates/execution/compile`](https://github.com/axiolid/kernel/tree/main/crates/execution/compile) | `execution` | `execution.orchestration` | `graph.compile` | public | `axiolid-brep`, `axiolid-construct`, `axiolid-contracts`, `axiolid-core`, `axiolid-curve`, `axiolid-dispatch`, `axiolid-exact-compile-contract`, `axiolid-guarantees`, `axiolid-measure`, `axiolid-mesh`, `axiolid-mesh-boolean-boolmesh`, `axiolid-mesh-boolean-contract`, `axiolid-mesh-compile-contract`, `axiolid-mesh-contracts`, `axiolid-mesh-section-contract`, `axiolid-model`, `axiolid-primitive`, `axiolid-profile`, `axiolid-reference`, `axiolid-surface`, `axiolid-topology` | -| `axiolid-mesh-compile-contract` | [`crates/contracts/operations/compile`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/compile) | `contracts` | `contract.operation` | `mesh.compile` | public | `axiolid-contracts`, `axiolid-mesh`, `axiolid-model` | -| `axiolid-mesh-contracts` | [`crates/contracts/common/mesh`](https://github.com/axiolid/kernel/tree/main/crates/contracts/common/mesh) | `contracts` | `contract.common.mesh` | `mesh.admissibility` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-mesh` | -| `axiolid-mesh-section-contract` | [`crates/contracts/operations/mesh-section`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/mesh-section) | `contracts` | `contract.operation` | `mesh.section` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-mesh`, `axiolid-mesh-contracts` | -| `axiolid-minkowski` | [`crates/algorithms/discrete/minkowski`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/discrete/minkowski) | `algorithms` | `algorithm.discrete` | `discrete.minkowski` | public | `axiolid-construct`, `axiolid-contracts`, `axiolid-core`, `axiolid-decompose`, `axiolid-measure`, `axiolid-mesh`, `axiolid-mesh-boolean-boolmesh`, `axiolid-mesh-boolean-contract` | -| `axiolid-model` | [`crates/representations/modeling/graph`](https://github.com/axiolid/kernel/tree/main/crates/representations/modeling/graph) | `representations` | `representation.graph` | `modeling.graph` | public | `axiolid-core`, `axiolid-curve`, `axiolid-mesh`, `axiolid-primitive`, `axiolid-profile`, `axiolid-surface`, `axiolid-topology` | -| `axiolid-nurbs` | [`crates/algorithms/parametric/nurbs`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/parametric/nurbs) | `algorithms` | `algorithm.parametric` | `parametric.nurbs` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-curve`, `axiolid-evaluate`, `axiolid-exact`, `axiolid-guarantees`, `axiolid-oracle`, `axiolid-predicates`, `axiolid-surface` | +| [`axiolid-brep`](/reference/crates/axiolid-brep) | [`crates/representations/brep`](https://github.com/axiolid/kernel/tree/main/crates/representations/brep) | `representations` | `representation.composed` | `brep` | public | `axiolid-core`, `axiolid-curve`, `axiolid-surface`, `axiolid-topology` | +| [`axiolid-brep-audit`](/reference/crates/axiolid-brep-audit) | [`crates/algorithms/repair/brep-audit`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/repair/brep-audit) | `algorithms` | `algorithm.repair` | `repair.brep` | public | `axiolid-brep`, `axiolid-contracts`, `axiolid-core`, `axiolid-curve`, `axiolid-evaluate`, `axiolid-surface`, `axiolid-topology` | +| [`axiolid-brep-boolean`](/reference/crates/axiolid-brep-boolean) | [`crates/algorithms/construction/brep-boolean`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/construction/brep-boolean) | `algorithms` | `algorithm.construction` | `construction.brep-boolean` | public | `axiolid-brep`, `axiolid-brep-audit`, `axiolid-construct`, `axiolid-core`, `axiolid-curve`, `axiolid-evaluate`, `axiolid-measure`, `axiolid-nurbs`, `axiolid-overlay`, `axiolid-primitive`, `axiolid-profile`, `axiolid-surface`, `axiolid-topology` | +| [`axiolid-capi`](/reference/crates/axiolid-capi) | [`crates/facade/axiolid-capi`](https://github.com/axiolid/kernel/tree/main/crates/facade/axiolid-capi) | `facade` | `facade.native-c` | `integration` | public | `axiolid` | +| [`axiolid-collide`](/reference/crates/axiolid-collide) | [`crates/algorithms/query/collide`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/query/collide) | `algorithms` | `algorithm.query` | `query.collide` | public | `axiolid-core` | +| [`axiolid-construct`](/reference/crates/axiolid-construct) | [`crates/algorithms/construction/construct`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/construction/construct) | `algorithms` | `algorithm.construction` | `construction` | public | `axiolid-brep`, `axiolid-brep-audit`, `axiolid-contracts`, `axiolid-core`, `axiolid-curve`, `axiolid-evaluate`, `axiolid-exact`, `axiolid-guarantees`, `axiolid-heal`, `axiolid-measure`, `axiolid-mesh`, `axiolid-mesh-boolean-contract`, `axiolid-mesh-contracts`, `axiolid-nurbs`, `axiolid-overlay`, `axiolid-predicates`, `axiolid-primitive`, `axiolid-profile`, `axiolid-reference`, `axiolid-surface`, `axiolid-tessellation-contract`, `axiolid-topology` | +| [`axiolid-contracts`](/reference/crates/axiolid-contracts) | [`crates/contracts/common/base`](https://github.com/axiolid/kernel/tree/main/crates/contracts/common/base) | `contracts` | `contract.common` | `common` | public | `axiolid-core`, `axiolid-guarantees` | +| [`axiolid-core`](/reference/crates/axiolid-core) | [`crates/foundation/core`](https://github.com/axiolid/kernel/tree/main/crates/foundation/core) | `foundation` | `foundation.values` | `numeric` | public | — | +| [`axiolid-curve`](/reference/crates/axiolid-curve) | [`crates/representations/analytic/curve`](https://github.com/axiolid/kernel/tree/main/crates/representations/analytic/curve) | `representations` | `representation.atomic` | `analytic.curve` | public | `axiolid-core`, `axiolid-linear` | +| [`axiolid-curve-evaluate-contract`](/reference/crates/axiolid-curve-evaluate-contract) | [`crates/contracts/operations/curve-evaluate`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/curve-evaluate) | `contracts` | `contract.operation` | `curve.evaluate` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-curve` | +| [`axiolid-decimate`](/reference/crates/axiolid-decimate) | [`crates/algorithms/discrete/decimate`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/discrete/decimate) | `algorithms` | `algorithm.discrete` | `discrete.decimate` | public | `axiolid-core`, `axiolid-heal`, `axiolid-measure`, `axiolid-mesh` | +| [`axiolid-decompose`](/reference/crates/axiolid-decompose) | [`crates/algorithms/discrete/decompose`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/discrete/decompose) | `algorithms` | `algorithm.discrete` | `discrete.decompose` | public | `axiolid-construct`, `axiolid-contracts`, `axiolid-core`, `axiolid-measure`, `axiolid-mesh`, `axiolid-mesh-boolean-boolmesh`, `axiolid-mesh-boolean-contract`, `axiolid-reference` | +| [`axiolid-dispatch`](/reference/crates/axiolid-dispatch) | [`crates/execution/dispatch`](https://github.com/axiolid/kernel/tree/main/crates/execution/dispatch) | `execution` | `execution.dispatch` | `providers` | public | `axiolid-backend-cpu`, `axiolid-contracts`, `axiolid-core`, `axiolid-mesh`, `axiolid-mesh-boolean-contract`, `axiolid-mesh-contracts`, `axiolid-mesh-section-contract`, `axiolid-pointcloud`, `axiolid-pointcloud-reconstruction-contract`, `axiolid-pointcloud-reconstruction-sdf` | +| [`axiolid-evaluate`](/reference/crates/axiolid-evaluate) | [`crates/algorithms/parametric/evaluate`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/parametric/evaluate) | `algorithms` | `algorithm.parametric` | `evaluate` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-curve`, `axiolid-curve-evaluate-contract`, `axiolid-surface` | +| [`axiolid-exact`](/reference/crates/axiolid-exact) | [`crates/algorithms/exact`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/exact) | `algorithms` | `algorithm.reference` | `reference.exact` | public | `axiolid-core`, `axiolid-guarantees` | +| [`axiolid-exact-compile-contract`](/reference/crates/axiolid-exact-compile-contract) | [`crates/contracts/operations/exact-compile`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/exact-compile) | `contracts` | `contract.operation` | `brep.compile` | public | `axiolid-brep`, `axiolid-contracts`, `axiolid-model` | +| [`axiolid-field`](/reference/crates/axiolid-field) | [`crates/representations/sampled/field`](https://github.com/axiolid/kernel/tree/main/crates/representations/sampled/field) | `representations` | `representation.sampled` | `sampled.field` | public | `axiolid-core` | +| [`axiolid-field-ops`](/reference/crates/axiolid-field-ops) | [`crates/algorithms/sampled/field`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/sampled/field) | `algorithms` | `algorithm.sampled` | `sampled.field` | public | `axiolid-core`, `axiolid-field` | +| [`axiolid-fixtures`](/reference/crates/axiolid-fixtures) | [`crates/representations/fixtures`](https://github.com/axiolid/kernel/tree/main/crates/representations/fixtures) | `representations` | `representation.fixtures` | `testing.fixtures` | public | `axiolid-core`, `axiolid-mesh` | +| [`axiolid-guarantees`](/reference/crates/axiolid-guarantees) | [`crates/contracts/guarantees`](https://github.com/axiolid/kernel/tree/main/crates/contracts/guarantees) | `contracts` | `contract.guarantees` | `proof` | public | — | +| [`axiolid-heal`](/reference/crates/axiolid-heal) | [`crates/algorithms/repair/heal`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/repair/heal) | `algorithms` | `algorithm.repair` | `repair.mesh` | public | `axiolid-core`, `axiolid-guarantees`, `axiolid-measure`, `axiolid-mesh`, `axiolid-predicates`, `axiolid-spatial` | +| [`axiolid-inspect`](/reference/crates/axiolid-inspect) | [`crates/algorithms/query/inspect`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/query/inspect) | `algorithms` | `algorithm.query` | `query.inspect` | public | `axiolid-core`, `axiolid-exact`, `axiolid-guarantees`, `axiolid-heal`, `axiolid-mesh`, `axiolid-predicates`, `axiolid-spatial` | +| [`axiolid-levelset`](/reference/crates/axiolid-levelset) | [`crates/algorithms/sampled/levelset`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/sampled/levelset) | `algorithms` | `algorithm.sampled` | `sampled.levelset` | public | `axiolid-core`, `axiolid-measure`, `axiolid-mesh` | +| [`axiolid-linear`](/reference/crates/axiolid-linear) | [`crates/representations/analytic/linear`](https://github.com/axiolid/kernel/tree/main/crates/representations/analytic/linear) | `representations` | `representation.atomic` | `analytic.linear` | public | `axiolid-core` | +| [`axiolid-linear-intersection`](/reference/crates/axiolid-linear-intersection) | [`crates/algorithms/query/intersection/linear`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/query/intersection/linear) | `algorithms` | `algorithm.query` | `query.intersection.linear` | public | `axiolid-core`, `axiolid-guarantees`, `axiolid-linear`, `axiolid-predicates` | +| [`axiolid-measure`](/reference/crates/axiolid-measure) | [`crates/algorithms/query/measure`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/query/measure) | `algorithms` | `algorithm.query` | `query.measure` | public | `axiolid-brep`, `axiolid-core`, `axiolid-curve`, `axiolid-evaluate`, `axiolid-mesh`, `axiolid-surface`, `axiolid-topology` | +| [`axiolid-mesh`](/reference/crates/axiolid-mesh) | [`crates/representations/discrete/mesh`](https://github.com/axiolid/kernel/tree/main/crates/representations/discrete/mesh) | `representations` | `representation.discrete` | `discrete.mesh` | public | `axiolid-core` | +| [`axiolid-mesh-boolean-boolmesh`](/reference/crates/axiolid-mesh-boolean-boolmesh) | [`crates/providers/mesh/boolmesh`](https://github.com/axiolid/kernel/tree/main/crates/providers/mesh/boolmesh) | `providers` | `provider.mesh` | `mesh.boolean` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-dispatch`, `axiolid-fixtures`, `axiolid-guarantees`, `axiolid-measure`, `axiolid-mesh`, `axiolid-mesh-boolean-contract`, `axiolid-mesh-contracts`, `axiolid-predicates`, `axiolid-reference` | +| [`axiolid-mesh-boolean-contract`](/reference/crates/axiolid-mesh-boolean-contract) | [`crates/contracts/operations/mesh-boolean`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/mesh-boolean) | `contracts` | `contract.operation` | `mesh.boolean` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-mesh`, `axiolid-mesh-contracts` | +| [`axiolid-mesh-compile`](/reference/crates/axiolid-mesh-compile) | [`crates/execution/compile`](https://github.com/axiolid/kernel/tree/main/crates/execution/compile) | `execution` | `execution.orchestration` | `graph.compile` | public | `axiolid-brep`, `axiolid-construct`, `axiolid-contracts`, `axiolid-core`, `axiolid-curve`, `axiolid-dispatch`, `axiolid-exact-compile-contract`, `axiolid-guarantees`, `axiolid-measure`, `axiolid-mesh`, `axiolid-mesh-boolean-boolmesh`, `axiolid-mesh-boolean-contract`, `axiolid-mesh-compile-contract`, `axiolid-mesh-contracts`, `axiolid-mesh-section-contract`, `axiolid-model`, `axiolid-primitive`, `axiolid-profile`, `axiolid-reference`, `axiolid-surface`, `axiolid-topology` | +| [`axiolid-mesh-compile-contract`](/reference/crates/axiolid-mesh-compile-contract) | [`crates/contracts/operations/compile`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/compile) | `contracts` | `contract.operation` | `mesh.compile` | public | `axiolid-contracts`, `axiolid-mesh`, `axiolid-model` | +| [`axiolid-mesh-contracts`](/reference/crates/axiolid-mesh-contracts) | [`crates/contracts/common/mesh`](https://github.com/axiolid/kernel/tree/main/crates/contracts/common/mesh) | `contracts` | `contract.common.mesh` | `mesh.admissibility` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-mesh` | +| [`axiolid-mesh-section-contract`](/reference/crates/axiolid-mesh-section-contract) | [`crates/contracts/operations/mesh-section`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/mesh-section) | `contracts` | `contract.operation` | `mesh.section` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-mesh`, `axiolid-mesh-contracts` | +| [`axiolid-minkowski`](/reference/crates/axiolid-minkowski) | [`crates/algorithms/discrete/minkowski`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/discrete/minkowski) | `algorithms` | `algorithm.discrete` | `discrete.minkowski` | public | `axiolid-construct`, `axiolid-contracts`, `axiolid-core`, `axiolid-decompose`, `axiolid-measure`, `axiolid-mesh`, `axiolid-mesh-boolean-boolmesh`, `axiolid-mesh-boolean-contract` | +| [`axiolid-model`](/reference/crates/axiolid-model) | [`crates/representations/modeling/graph`](https://github.com/axiolid/kernel/tree/main/crates/representations/modeling/graph) | `representations` | `representation.graph` | `modeling.graph` | public | `axiolid-core`, `axiolid-curve`, `axiolid-mesh`, `axiolid-primitive`, `axiolid-profile`, `axiolid-surface`, `axiolid-topology` | +| [`axiolid-nurbs`](/reference/crates/axiolid-nurbs) | [`crates/algorithms/parametric/nurbs`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/parametric/nurbs) | `algorithms` | `algorithm.parametric` | `parametric.nurbs` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-curve`, `axiolid-evaluate`, `axiolid-exact`, `axiolid-guarantees`, `axiolid-oracle`, `axiolid-predicates`, `axiolid-surface` | | `axiolid-oracle` | [`tools/oracle`](https://github.com/axiolid/kernel/tree/main/tools/oracle) | `tools` | `tool.oracle` | `verification.oracle` | internal | `axiolid-contracts`, `axiolid-core`, `axiolid-curve`, `axiolid-evaluate`, `axiolid-surface` | -| `axiolid-overlay` | [`crates/algorithms/planar/overlay`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/planar/overlay) | `algorithms` | `algorithm.planar` | `planar.overlay` | public | `axiolid-core`, `axiolid-exact`, `axiolid-guarantees` | -| `axiolid-pointcloud` | [`crates/representations/discrete/pointcloud`](https://github.com/axiolid/kernel/tree/main/crates/representations/discrete/pointcloud) | `representations` | `representation.discrete` | `discrete.pointcloud` | public | `axiolid-core` | -| `axiolid-pointcloud-reconstruction-contract` | [`crates/contracts/operations/pointcloud-reconstruction`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/pointcloud-reconstruction) | `contracts` | `contract.operation` | `pointcloud.reconstruction` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-mesh`, `axiolid-pointcloud` | -| `axiolid-pointcloud-reconstruction-sdf` | [`crates/providers/pointcloud/sdf`](https://github.com/axiolid/kernel/tree/main/crates/providers/pointcloud/sdf) | `providers` | `provider.pointcloud` | `pointcloud.reconstruction` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-levelset`, `axiolid-mesh`, `axiolid-pointcloud`, `axiolid-pointcloud-reconstruction-contract`, `axiolid-spatial` | -| `axiolid-predicates` | [`crates/algorithms/predicates`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/predicates) | `algorithms` | `algorithm.reference` | `reference.predicates` | public | `axiolid-core`, `axiolid-exact`, `axiolid-guarantees` | -| `axiolid-primitive` | [`crates/representations/analytic/primitive`](https://github.com/axiolid/kernel/tree/main/crates/representations/analytic/primitive) | `representations` | `representation.atomic` | `analytic.primitive` | public | `axiolid-core` | -| `axiolid-profile` | [`crates/representations/region/profile`](https://github.com/axiolid/kernel/tree/main/crates/representations/region/profile) | `representations` | `representation.region` | `region.profile` | public | `axiolid-core`, `axiolid-curve` | -| `axiolid-project` | [`crates/algorithms/planar/project`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/planar/project) | `algorithms` | `algorithm.planar` | `planar.project` | public | `axiolid-core`, `axiolid-mesh`, `axiolid-overlay` | -| `axiolid-ray-mesh` | [`crates/algorithms/query/intersection/ray-mesh`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/query/intersection/ray-mesh) | `algorithms` | `algorithm.query` | `query.intersection.ray-mesh` | public | `axiolid-core`, `axiolid-guarantees`, `axiolid-mesh`, `axiolid-predicates`, `axiolid-spatial` | -| `axiolid-reference` | [`crates/algorithms/reference`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/reference) | `algorithms` | `algorithm.reference` | `reference` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-curve`, `axiolid-dispatch`, `axiolid-evaluate`, `axiolid-guarantees`, `axiolid-measure`, `axiolid-mesh`, `axiolid-mesh-boolean-contract`, `axiolid-mesh-contracts`, `axiolid-mesh-section-contract`, `axiolid-predicates`, `axiolid-primitive`, `axiolid-spatial`, `axiolid-surface` | -| `axiolid-refine` | [`crates/algorithms/discrete/refine`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/discrete/refine) | `algorithms` | `algorithm.discrete` | `discrete.refine` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-evaluate`, `axiolid-heal`, `axiolid-measure`, `axiolid-mesh`, `axiolid-surface` | -| `axiolid-route` | [`crates/algorithms/planar/route`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/planar/route) | `algorithms` | `algorithm.planar` | `planar.route` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-exact`, `axiolid-guarantees`, `axiolid-overlay`, `axiolid-predicates`, `axiolid-triangulate` | -| `axiolid-spatial` | [`crates/algorithms/query/spatial`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/query/spatial) | `algorithms` | `algorithm.query` | `query.spatial` | public | `axiolid-core` | -| `axiolid-surface` | [`crates/representations/analytic/surface`](https://github.com/axiolid/kernel/tree/main/crates/representations/analytic/surface) | `representations` | `representation.atomic` | `analytic.surface` | public | `axiolid-core`, `axiolid-curve` | -| `axiolid-tessellation-contract` | [`crates/contracts/operations/tessellate`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/tessellate) | `contracts` | `contract.operation` | `tessellate` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-mesh`, `axiolid-model` | -| `axiolid-topology` | [`crates/representations/topology`](https://github.com/axiolid/kernel/tree/main/crates/representations/topology) | `representations` | `representation.topology` | `topology` | public | `axiolid-core` | -| `axiolid-triangulate` | [`crates/algorithms/planar/triangulate`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/planar/triangulate) | `algorithms` | `algorithm.planar` | `planar.triangulate` | public | `axiolid-core`, `axiolid-guarantees`, `axiolid-predicates` | +| [`axiolid-overlay`](/reference/crates/axiolid-overlay) | [`crates/algorithms/planar/overlay`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/planar/overlay) | `algorithms` | `algorithm.planar` | `planar.overlay` | public | `axiolid-core`, `axiolid-exact`, `axiolid-guarantees` | +| [`axiolid-pointcloud`](/reference/crates/axiolid-pointcloud) | [`crates/representations/discrete/pointcloud`](https://github.com/axiolid/kernel/tree/main/crates/representations/discrete/pointcloud) | `representations` | `representation.discrete` | `discrete.pointcloud` | public | `axiolid-core` | +| [`axiolid-pointcloud-reconstruction-contract`](/reference/crates/axiolid-pointcloud-reconstruction-contract) | [`crates/contracts/operations/pointcloud-reconstruction`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/pointcloud-reconstruction) | `contracts` | `contract.operation` | `pointcloud.reconstruction` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-mesh`, `axiolid-pointcloud` | +| [`axiolid-pointcloud-reconstruction-sdf`](/reference/crates/axiolid-pointcloud-reconstruction-sdf) | [`crates/providers/pointcloud/sdf`](https://github.com/axiolid/kernel/tree/main/crates/providers/pointcloud/sdf) | `providers` | `provider.pointcloud` | `pointcloud.reconstruction` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-levelset`, `axiolid-mesh`, `axiolid-pointcloud`, `axiolid-pointcloud-reconstruction-contract`, `axiolid-spatial` | +| [`axiolid-predicates`](/reference/crates/axiolid-predicates) | [`crates/algorithms/predicates`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/predicates) | `algorithms` | `algorithm.reference` | `reference.predicates` | public | `axiolid-core`, `axiolid-exact`, `axiolid-guarantees` | +| [`axiolid-primitive`](/reference/crates/axiolid-primitive) | [`crates/representations/analytic/primitive`](https://github.com/axiolid/kernel/tree/main/crates/representations/analytic/primitive) | `representations` | `representation.atomic` | `analytic.primitive` | public | `axiolid-core` | +| [`axiolid-profile`](/reference/crates/axiolid-profile) | [`crates/representations/region/profile`](https://github.com/axiolid/kernel/tree/main/crates/representations/region/profile) | `representations` | `representation.region` | `region.profile` | public | `axiolid-core`, `axiolid-curve` | +| [`axiolid-project`](/reference/crates/axiolid-project) | [`crates/algorithms/planar/project`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/planar/project) | `algorithms` | `algorithm.planar` | `planar.project` | public | `axiolid-core`, `axiolid-mesh`, `axiolid-overlay` | +| [`axiolid-ray-mesh`](/reference/crates/axiolid-ray-mesh) | [`crates/algorithms/query/intersection/ray-mesh`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/query/intersection/ray-mesh) | `algorithms` | `algorithm.query` | `query.intersection.ray-mesh` | public | `axiolid-core`, `axiolid-guarantees`, `axiolid-mesh`, `axiolid-predicates`, `axiolid-spatial` | +| [`axiolid-reference`](/reference/crates/axiolid-reference) | [`crates/algorithms/reference`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/reference) | `algorithms` | `algorithm.reference` | `reference` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-curve`, `axiolid-dispatch`, `axiolid-evaluate`, `axiolid-guarantees`, `axiolid-measure`, `axiolid-mesh`, `axiolid-mesh-boolean-contract`, `axiolid-mesh-contracts`, `axiolid-mesh-section-contract`, `axiolid-predicates`, `axiolid-primitive`, `axiolid-spatial`, `axiolid-surface` | +| [`axiolid-refine`](/reference/crates/axiolid-refine) | [`crates/algorithms/discrete/refine`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/discrete/refine) | `algorithms` | `algorithm.discrete` | `discrete.refine` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-evaluate`, `axiolid-heal`, `axiolid-measure`, `axiolid-mesh`, `axiolid-surface` | +| [`axiolid-route`](/reference/crates/axiolid-route) | [`crates/algorithms/planar/route`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/planar/route) | `algorithms` | `algorithm.planar` | `planar.route` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-exact`, `axiolid-guarantees`, `axiolid-overlay`, `axiolid-predicates`, `axiolid-triangulate` | +| [`axiolid-spatial`](/reference/crates/axiolid-spatial) | [`crates/algorithms/query/spatial`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/query/spatial) | `algorithms` | `algorithm.query` | `query.spatial` | public | `axiolid-core` | +| [`axiolid-surface`](/reference/crates/axiolid-surface) | [`crates/representations/analytic/surface`](https://github.com/axiolid/kernel/tree/main/crates/representations/analytic/surface) | `representations` | `representation.atomic` | `analytic.surface` | public | `axiolid-core`, `axiolid-curve` | +| [`axiolid-tessellation-contract`](/reference/crates/axiolid-tessellation-contract) | [`crates/contracts/operations/tessellate`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/tessellate) | `contracts` | `contract.operation` | `tessellate` | public | `axiolid-contracts`, `axiolid-core`, `axiolid-mesh`, `axiolid-model` | +| [`axiolid-topology`](/reference/crates/axiolid-topology) | [`crates/representations/topology`](https://github.com/axiolid/kernel/tree/main/crates/representations/topology) | `representations` | `representation.topology` | `topology` | public | `axiolid-core` | +| [`axiolid-triangulate`](/reference/crates/axiolid-triangulate) | [`crates/algorithms/planar/triangulate`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/planar/triangulate) | `algorithms` | `algorithm.planar` | `planar.triangulate` | public | `axiolid-core`, `axiolid-guarantees`, `axiolid-predicates` | | `xtask` | [`tools/xtask`](https://github.com/axiolid/kernel/tree/main/tools/xtask) | `tools` | `tool.architecture` | `workspace` | internal | — | Temporary domains are explicit migration debt and must disappear when their responsibility split lands. diff --git a/docs/architecture/current-target-crate-map.md b/docs/architecture/current-target-crate-map.md index ecbf8154..81df2e0b 100644 --- a/docs/architecture/current-target-crate-map.md +++ b/docs/architecture/current-target-crate-map.md @@ -4,27 +4,7 @@ Implemented from base `372b16f64fa9be962df48751a654f8cda3f3b4a0` under ADR 0035. ## Physical ownership -| Package | Path | Role | -| --- | --- | --- | -| `axiolid-core` | `crates/foundation/core` | dependency root | -| `axiolid-curve`, `axiolid-surface`, `axiolid-primitive` | `crates/representations/analytic/*` | analytic values | -| `axiolid-profile` | `crates/representations/region/profile` | bounded region values | -| `axiolid-topology`, `axiolid-brep` | `crates/representations/{topology,brep}` | topology/exact B-rep values | -| `axiolid-mesh` | `crates/representations/discrete/mesh` | discrete mesh values | -| `axiolid-field` | `crates/representations/sampled/field` | sampled-field values/configuration/evidence | -| `axiolid-model` | `crates/representations/modeling/graph` | authored immutable graph | -| `axiolid-guarantees` | `crates/contracts/guarantees` | certification/escalation/precision vocabulary | -| `axiolid-contracts` | `crates/contracts/common/base` | common backend/execution/diagnostic contracts | -| `axiolid-mesh-contracts` | `crates/contracts/common/mesh` | shared mesh admissibility | -| operation contract packages | `crates/contracts/operations/*` | tessellation, mesh Boolean, mesh section, graph-to-mesh compile schemas | -| focused algorithm packages | `crates/algorithms/*` | reference, NURBS, construction, query, planar, sampled, repair | -| `axiolid-mesh-boolean-boolmesh` | `crates/providers/mesh/boolmesh` | concrete optional provider | -| `axiolid-dispatch` | `crates/execution/dispatch` | registration/fallback/device/budget policy | -| `axiolid-mesh-compile` | `crates/execution/compile` | reference graph-to-mesh execution | -| CPU/GPU packages | `crates/execution/{cpu,gpu}` | execution contexts/adapters | -| `axiolid` | `crates/facade/axiolid` | additive public feature facade | - -The generated [crate map](./crate-map.md) and [dependency graph](./dependency-graph.md) are authoritative and freshness-checked by `cargo xtask architecture check`. +The [crate reference](/reference/) lists every package by layer, and the generated [crate map](./crate-map.md) and [dependency graph](./dependency-graph.md) give each package's path, role and allowed dependencies. They are freshness-checked by `cargo xtask docs --check`. ## Public migration table diff --git a/docs/architecture/dependency-graph.md b/docs/architecture/dependency-graph.md index 20608ae7..065ca2e8 100644 --- a/docs/architecture/dependency-graph.md +++ b/docs/architecture/dependency-graph.md @@ -1,4 +1,6 @@ - +--- +# Generated by `cargo xtask architecture docs`; do not edit manually. +--- # Axiolid dependency graph diff --git a/architecture/reference-packages.toml b/docs/architecture/reference-packages.toml similarity index 100% rename from architecture/reference-packages.toml rename to docs/architecture/reference-packages.toml diff --git a/architecture/semver-exceptions.toml b/docs/architecture/semver-exceptions.toml similarity index 100% rename from architecture/semver-exceptions.toml rename to docs/architecture/semver-exceptions.toml diff --git a/docs/benchmarking-plan.md b/docs/benchmarking-plan.md deleted file mode 100644 index 0e5b03e2..00000000 --- a/docs/benchmarking-plan.md +++ /dev/null @@ -1,98 +0,0 @@ -# Benchmarking foundation — plan - -Status: in progress. Written before execution so it survives context compaction. - -## Goal - -Build a trustworthy measurement system BEFORE any CPU/SIMD/Rayon/GPU work. -We have architectural seams (`CpuFeatures`, `ExecutionTarget`, dispatch) but no -numbers, so we cannot identify high-value workloads, quantify regressions, -determine scaling, or validate acceleration decisions. - -Explicitly NOT in this goal: implementing CPU/GPU acceleration. No git submodule. - -## Verified starting state (measured, not assumed) - -- Kernel has 3 pre-existing bench files, inconsistently wired: - - `crates/algorithms/predicates/benches/predicates.rs` — 118 lines, NO - `[[bench]]` declaration (builds only via cargo auto-discovery), no criterion, - hand-rolled `Instant` timing. Its doc comment says "run with - `-p axiolid-reference`" which is the WRONG crate. - - `crates/algorithms/reference/benches/clash.rs` — 90 lines, declared, criterion. - - `crates/providers/mesh/boolmesh/benches/subtract_many.rs` — 162 lines, declared, criterion. -- `criterion 0.5` already in root `[workspace.dependencies]`. -- **No CI workflow runs `cargo test` or `gate.sh`.** Workflows are docs, native, - publish, roadmap, issue-triage only. Correctness CI is a prerequisite gap for - a *performance* gate. -- `gate.sh` runs clippy `--all-targets -D warnings` → benches must be lint-clean. -- valgrind NOT installed (needed for iai-callgrind). Installable; sudo works. -- Architecture gate (`tools/xtask`) requires EVERY workspace member to declare - `[package.metadata.axiolid]`. Layer `tools` must live under `tools/` and must - be `public = false`. This DECIDES the location of the bench crate. -- `axiolid-fixtures` is dev-dependency-only (used by boolmesh dev-deps). -- Sibling repo `../benchmarks` is mature: cross-kernel comparison vs boolmesh, - ifc-lite, Manifold, CGAL, with volume validation, mutation-tested verifier, - determinism probe. NOT a workspace member by design (absolute/relative paths - to an arbitrary kernel checkout). Keep it that way. - -## Design - -### Location: `tools/benchmark/` (kernel repo) - -Layer `tools`, `publish = false`, so: -- architecture gate accepts it (path prefix `tools/` matches layer `tools`), -- it never enters the crates.io publish plan (50-crate release unaffected), -- it may depend on any layer (`"tools" => true` in the layering matrix). - -Rejected `crates/benchmark/`: layer rules force a `crates//` path to a -non-tools layer, which would make the bench crate publishable and put it in the -release cascade. - -### Contents - -``` -tools/benchmark/ - src/lib.rs shared utilities: workload generation, validation - benches/ criterion microbenchmarks + iai-callgrind regression - data/ small deterministic synthetic corpora (in-repo, tiny) -``` - -Key principle carried from `../benchmarks/AGENTS.md` (learned the hard way): -**a kernel that declines to answer is not faster than one that answers.** Every -timed result must be validated against a derived ground truth, and the harness -must fail on mismatch rather than report a fast wrong answer. - -### Split of responsibility - -- **kernel `tools/benchmark/`** — internal, single-kernel: microbenchmarks, - deterministic regression counts, scaling curves, end-to-end scenarios. -- **`../benchmarks` (unchanged role)** — cross-kernel comparison, larger corpora, - base-vs-head of arbitrary kernel checkouts. - -## Workstreams - -1. Scaffold `tools/benchmark` crate + architecture metadata; gate stays green. -2. Shared utilities: deterministic workload generators (seeded, no HashMap - iteration order), validation helpers. -3. Criterion microbenchmarks over real hot paths. -4. iai-callgrind deterministic instruction-count benchmarks (needs valgrind). -5. Scaling tests (complexity/growth, not just single-point timings). -6. Correctness validation wired into every benchmark. -7. CI: correctness workflow first, then a performance gate. -8. Consolidate the 3 orphaned bench files into the new area. - -## Validation strategy - -- `gate.sh` green after every step (clippy `--all-targets` covers benches). -- `cargo bench --no-run` builds all benches. -- iai-callgrind: instruction counts stable across two consecutive runs. -- Mutation-check the validators: perturb expected values, confirm benches fail. - -## Risks - -- iai-callgrind needs valgrind on every machine that runs the gate → make it - opt-in / skipped-when-absent, never a hard gate failure on a dev box. -- Criterion wall-clock in CI is noisy → wall-clock benches inform, iai-callgrind - gates. Do not gate on wall-clock. -- Adding a workspace member changes `cargo metadata` → must re-run publish plan - and verify the 50-crate release list is unchanged (expect exactly 50 still). diff --git a/docs/capabilities.md b/docs/capabilities.md index beea71a3..5a785b08 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -85,17 +85,17 @@ described below. | Mass properties | Implemented for meshes and exact B-reps, planar and curved | `axiolid-measure`: `MeshMeasure` implements `Measure` (area, signed volume, volume centroid, second moments), and `exact_properties` measures an `ExactBRep` WITHOUT tessellating it: straight-edged planar faces by an exact fan, and cylinders, cones, spheres, tori, elliptical cylinders, B-spline faces and arc-bounded planar faces by Green's theorem over their own parameters with quadrature held to 1e-13 relative ([ADR 0073](/adr/0073-curved-face-mass-properties)). Verified against closed forms (Pappus, parallel-axis moments, hemispheres, cones, a half torus) and an exact-vs-mesh differential. A face whose pcurves bound no domain is refused by name; open shells are refused rather than assigned a plausible volume. Exact support is behind the `exact` feature so a mesh-only consumer does not acquire B-rep geometry | | Mesh defect diagnosis | Implemented: located defects, not counts | `axiolid-inspect`; coplanar overlap decided exactly via `orient2d`; repair is separate | | Convex hull (3D) | Implemented for point sets | `axiolid-construct`: `convex_hull` builds a closed, outward-oriented `TriMesh` by incremental insertion. Every visibility decision goes through certified `orient3d`; there is no epsilon in the module. Degenerate input is typed rather than approximated: fewer than four points, all-collinear, and all-coplanar are distinguishable refusals, so a caller can fall back to the 2D hull in `axiolid-reference`. Verified against closed-form volumes and by re-deciding containment for every input point against every face | -| Exact boolean (planar-faced solids) | Implemented for general planar-faced operands | `axiolid-construct`: `boolean_polyhedra_exact` accepts arbitrary planar-faced solids, convex or non-convex, in any orientation, for union, intersection and difference. Faces are split only against ORIGINAL input planes, never derived ones, so vertices stay one intersection from input data. Containment uses exact ray-crossing parity, which a convex-only "inside every plane" test gets wrong for concave regions. Coplanar contact is classified by normal agreement so a shared face is never duplicated. A containment probe that meets a vertex or edge exactly retries a fixed family of ray directions before refusing. Contact configurations are covered by a lattice sweep (disjoint, contained, identical, face/edge/vertex touching, and an epsilon ladder in both gap and overlap). LONG chains of grid-aligned subtraction still refuse partway through, and a thin overlap below 1e-12 produces an open shell; both are measured and recorded in the crate PLAN.md rather than worked around. Verified by the volume identity |A u B| + |A n B| = |A| + |B|, a boolmesh differential, and v0.7 diagnosis. Curved operands (including v0.6 revolution output) are REFUSED by name, not tessellated | +| Exact boolean (planar-faced solids) | Implemented for general planar-faced operands | `axiolid-construct`: `boolean_polyhedra_exact` accepts arbitrary planar-faced solids, convex or non-convex, in any orientation, for union, intersection and difference. Faces are split only against ORIGINAL input planes, never derived ones, so vertices stay one intersection from input data. Containment uses exact ray-crossing parity, which a convex-only "inside every plane" test gets wrong for concave regions. Coplanar contact is classified by normal agreement so a shared face is never duplicated. A containment probe that meets a vertex or edge exactly retries a fixed family of ray directions before refusing. Contact configurations are covered by a lattice sweep (disjoint, contained, identical, face/edge/vertex touching, and an epsilon ladder in both gap and overlap). LONG chains of grid-aligned subtraction still refuse partway through, and a thin overlap below 1e-12 produces an open shell; both are measured, open limits rather than worked around. Verified by the volume identity |A u B| + |A n B| = |A| + |B|, a boolmesh differential, and v0.7 diagnosis. Curved operands (including v0.6 revolution output) are REFUSED by name, not tessellated | | Solid offset and shelling | Implemented for planar-faced solids | `axiolid-construct`: `offset_solid` (miter, outward and inward) and `shell_solid`. Vertices are moved to the meeting point of their incident pushed planes, so concave edges are handled and topology is preserved. An offset that closes the gap between opposing walls is REFUSED, not emitted. NOT sphere offset, NOT variable-distance, NOT Minkowski, and no face removal to open a shell | | Constant-radius fillet | Implemented for one straight edge of a prism | `axiolid-construct`: `fillet_extruded_profile` emits a real `Surface::Cylinder` blend tangent to both adjacent walls, not a many-segment chamfer. Tangency follows from placing the axis on the internal bisector and is verified by measuring axis-to-wall distance. Refuses radii reaching past a neighbouring corner, variable radius, edge loops, and curved edges | -| Mesh decimation | Implemented, deviation-bounded | `axiolid-decimate`: edge-collapse reduction to a triangle budget or a maximum deviation. The deviation is measured and reported per run, not estimated, and accumulates per vertex so repeated collapses cannot drift past the bound. Collapses that would create a non-manifold edge are refused and counted. Output is deterministic. NOT quadric-based: cost is edge length and placement is the midpoint, which is predictable but not optimal for a given budget; see the crate PLAN.md, which also records that the normal-inversion guard lacks a mutation probe | +| Mesh decimation | Implemented, deviation-bounded | `axiolid-decimate`: edge-collapse reduction to a triangle budget or a maximum deviation. The deviation is measured and reported per run, not estimated, and accumulates per vertex so repeated collapses cannot drift past the bound. Collapses that would create a non-manifold edge are refused and counted. Output is deterministic. NOT quadric-based: cost is edge length and placement is the midpoint, which is predictable but not optimal for a given budget | | Connected-component decomposition | Implemented: `decompose` / `compose` | `axiolid-mesh`; connectivity is by shared vertex index, not geometric proximity; coincident-but-distinct vertices stay separate bodies until welded | | Clearance / min gap | Implemented for triangle meshes | `axiolid-reference`; BVH-accelerated; reports clash as 0.0 and out-of-range as none | | Point containment / winding number | Implemented, exact | `axiolid-inspect`; certified `orient3d` parity; signed, so inside-out shells are visible | | Ray casting | Implemented for triangle meshes | `axiolid-inspect`; exact membership; only the hit parameter is f64 | | Genus | Implemented for closed two-manifolds | `axiolid-inspect`; refuses meshes with boundary or non-manifold edges | | Boolean construction arithmetic | f64 constructions; predicates verify only | The production mesh boolean constructs intersection coordinates in `f64`; `axiolid-predicates` is a DEV-dependency of the provider and re-decides orientation in tests rather than driving construction. Measured against Manifold, CGAL (exact constructions) and OCCT on byte-identical operands: error does not accumulate over chained operations (5.62e-16 at n=64, at or better than the exact kernels), and degradation appears only as operands approach coincidence. Published thresholds by relative operand overlap: above 1e-6 invisible, 1e-6 to 1e-12 smooth degradation, below 1e-12 severe. Axiolid never collapses to zero volume on this sweep, where Manifold and OCCT both do. `BooleanEvidence::relative_overlap` reports the measured conditioning so a caller can refuse on its own threshold; Axiolid does not choose one on its behalf; see [ADR 0045](/adr/0045-boolean-construction-arithmetic) | -| Spatial, healing | Focused crates / staged capability | `axiolid-spatial`, `axiolid-heal`; consult each crate’s `PLAN.md`; do not infer broad CAD coverage | +| Spatial, healing | Focused crates / staged capability | `axiolid-spatial`, `axiolid-heal`; consult each crate’s README and API docs; do not infer broad CAD coverage | ## Execution and acceleration diff --git a/docs/contributing/breaking-changes.md b/docs/contributing/breaking-changes.md index d93d324b..f9c4dca8 100644 --- a/docs/contributing/breaking-changes.md +++ b/docs/contributing/breaking-changes.md @@ -77,7 +77,7 @@ prepared.) ### Exceptions -`architecture/semver-exceptions.toml` accepts findings the tool gets +`docs/architecture/semver-exceptions.toml` accepts findings the tool gets wrong, by name: the crate, the lint and the exact item paths, with the reason each still resolves as before, and a test that proves it. The gate prints every accepted finding; anything not listed still fails, and so diff --git a/docs/guide/contributing.md b/docs/guide/contributing.md index 8a6b463c..c51582ba 100644 --- a/docs/guide/contributing.md +++ b/docs/guide/contributing.md @@ -29,7 +29,7 @@ The feature matrix protects minimal builds. The probe mutates the declared layer - Treat scalar paths as correctness oracles; benchmark and differentially test a faster path before claiming a performance win. - Keep operation capability tied to an executable provider implementation. - Record irreversible architecture choices under [`docs/adr/`](../adr/README.md). -- Update a crate `PLAN.md` only with concrete next work, not aspirational coverage claims. +- Put open work in a GitHub issue, not in a checked-in plan. A code marker is written `TODO(#N)`. A crate's `README.md` says what the crate is for and makes no coverage claim that the code and tests do not back ([ADR 0078](../adr/0078-context-lives-beside-the-code.md)). ## Documentation @@ -40,6 +40,14 @@ npm --prefix docs ci npm --prefix docs run docs:dev ``` +### Records + +- `CHANGELOG.md` files follow [Keep a Changelog](https://keepachangelog.com/). +- Write an ADR from `docs/adr/_template.md` before an irreversible boundary or dependency change. +- A research record in `docs/research/` keeps measured evidence apart from proposals. +- `cargo xtask docs` generates every page derived from the crates: the [crate reference](/reference/) (from each crate's manifest, `README.md` and `CHANGELOG.md`), the [per-crate changelog](/reference/changelog), and `docs/architecture/crate-map.md` and `dependency-graph.md`. Do not edit them by hand; change the source and regenerate. `cargo xtask docs --check` runs in the gate and before every deploy. Package metadata does not prove a capability: the implementation and its conformance tests do. +- A published crate's `README.md` is its crates.io page. It ends with its docs.rs, reference-page and source links, and states no version or publication status; `cargo xtask docs --check` enforces both. + ### Diagrams, geometry, and equations - Use fenced `mermaid` for architecture, state, and data-flow diagrams. Include diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 25d8106b..18ef64f8 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -4,16 +4,15 @@ Axiolid is a workspace, not a mandatory all-in-one dependency. Prefer a leaf crate when its public contract is sufficient; use the `axiolid` facade when the feature-gated composition is more convenient. -```toml -[dependencies] +```bash # Core scalar values, transforms, bounds, and tolerance policy. -axiolid-core = { git = "https://github.com/axiolid/kernel.git" } +cargo add axiolid-core -# Or: a small facade with core values, meshes, and the portable CPU shell. -axiolid = { git = "https://github.com/axiolid/kernel.git" } +# Or: the facade with core values, meshes, and the portable CPU shell. +cargo add axiolid --features standard ``` -The repository is currently consumed directly from Git while crates.io publication is not yet established. Pin a `rev` in reproducible applications. +The [crate reference](/reference/) lists every crate by layer, with its latest release and the facade feature that exposes it; [Selecting a package](/reference/selecting-packages) says which to start from. ## Start with core values @@ -29,21 +28,23 @@ assert_eq!(source, world); ## Opt into capabilities deliberately -```toml +```bash # Mesh-oriented construction, triangulation, spatial operations, and the # optional mesh-Boolean provider. -axiolid = { git = "https://github.com/axiolid/kernel.git", features = ["discrete"] } +cargo add axiolid --features discrete # Representation vocabulary plus general NURBS reference algorithms. -axiolid = { git = "https://github.com/axiolid/kernel.git", features = ["parametric"] } +cargo add axiolid --features parametric # Or select only curve/surface values and the general NURBS algorithms. -axiolid = { git = "https://github.com/axiolid/kernel.git", default-features = false, features = ["nurbs"] } +cargo add axiolid --features nurbs ``` +The facade's default feature set is empty, so a build names what it needs. The [`axiolid` reference page](/reference/crates/axiolid#features) lists every feature and what it enables; the bundles are: + | Bundle | Includes | Does not imply | | --- | --- | --- | -| default | core values, mesh facade, portable CPU shell | every mesh algorithm | +| `standard` | core values, mesh facade, portable CPU shell | every mesh algorithm | | `discrete` | mesh-centric representations and operation contracts | a selected executable provider | | `application` | supported portable provider selection plus v0.4 reference workflows | exact Boolean parity | | `parametric` | curve, surface, topology, primitive, and graph vocabulary plus general NURBS reference algorithms | a complete CAD modeling/intersection kernel | @@ -66,8 +67,8 @@ For the architecture-specific feature matrix and mutation probes, see [Contribut Enable `application` when a program wants one coherent portable provider path instead of assembling registries and implementation crates itself: -```toml -axiolid = { git = "https://github.com/axiolid/kernel.git", rev = "", features = ["application"] } +```bash +cargo add axiolid --features application ``` Provider choice is still explicit: diff --git a/docs/plans/certified-boundary-roots.md b/docs/plans/certified-boundary-roots.md deleted file mode 100644 index 276a054d..00000000 --- a/docs/plans/certified-boundary-roots.md +++ /dev/null @@ -1,208 +0,0 @@ -# Certified boundary roots — the gate on curved-surface booleans - -Standing scope note. Measured 2026-09-09 against `b69f3dc`. - -## Why this document exists - -An external reviewer observed that Axiolid has "no curved-surface boolean". -That is accurate. This note records WHERE the blocker actually is, because -the obvious guesses are wrong and cost real time to rule out. - -## What is already built - -The numerics are not the gap. Measured sizes in `axiolid-nurbs` -(10,236 LOC total, 6,799 of it certified): - -| file | LOC | role | -|---|---|---| -| `certified_surface_bezier.rs` | 1244 | outward-rounded surface enclosures | -| `certified_curve_intersection.rs` | 1125 | certified curve/curve | -| `certified_curve_surface_intersection.rs` | 698 | **the blocker lives here** | -| `certified_bezier.rs` | 649 | interval arithmetic, `Interval {lo, hi}` | -| `certified_surface_surface_intersection.rs` | 640 | SSI driver | -| `certified_surface_inversion.rs` | — | point inversion | -| `certified_refinement.rs` | 509 | `RefinementBudget` | - -Krawczyk root proofs, interval arithmetic, explicit work budgets, and -`BudgetExceeded` / `ProjectionStatus::BudgetExhausted` refusals all exist. - -## Where the wall is - -`split_surface_pair_certified` refuses the dual-boundary case — two patches -whose intersection segment ends on a boundary of BOTH — with -`IntersectionUnresolved`. It never reaches topology classification. - -Measured chain: - -``` -split_surface_pair_certified - -> intersect_surface_surface_certified - -> trace_affine_pair - -> collect_boundary_roots (8 boundary curve/surface queries) - -> intersect_curve_surface_certified - -> Unresolved => any_unresolved = true - -> endpoints=0, any_unresolved=true - -> AffineTraceOutcome::Unresolved(vec![]) -``` - -Instrumented output for `xy_plane(-1,1)` against `xz_plane(-1,1)`: - -``` -SSI endpoints=0 any_unresolved=true <- dual-boundary case -SSI endpoints=2 any_unresolved=false <- the 9 supported cases -``` - -## It is a proof limit, not a budget limit - -This is the part worth recording, because it rules out the cheap fix. -Escalating every knob leaves the verdict unchanged: - -``` -tol=1e-7 nodes=100000 depth=64 -> UNRESOLVED IntersectionUnresolved -tol=1e-10 nodes=100000 depth=64 -> UNRESOLVED IntersectionUnresolved -tol=1e-12 nodes=100000 depth=64 -> UNRESOLVED IntersectionUnresolved -tol=1e-14 nodes=100000 depth=64 -> UNRESOLVED IntersectionUnresolved -tol=1e-16 nodes=100000 depth=64 -> UNRESOLVED IntersectionUnresolved -tol=1e-7 nodes=100000 depth=32 -> UNRESOLVED IntersectionUnresolved -tol=1e-7 nodes=1000 depth=64 -> UNRESOLVED IntersectionUnresolved -``` - -(`max_refinement_work` is hard-capped at 100000, so larger budgets are -rejected by options validation rather than tried.) - -Invariant at 1e-16 — below double epsilon. More work cannot help. - -## Root cause - -In `certified_curve_surface_intersection.rs`, a Krawczyk root is accepted -only when `certificate_meets_resolution` holds. A root lying exactly ON a -patch boundary sits at the edge of the parameter box, so the Krawczyk -operator cannot prove strict containment in the box interior — which is -what its contraction argument requires. The solver then contracts, finds -`contracted == current.parameters`, and pushes to `unresolved`: - -```rust -if contracted == current.parameters { - push_result(&mut unresolved, contracted)?; - continue; -} -``` - -Subdivision cannot separate a root from a boundary it lies on, at any -depth. The refusal is correct given the method; the method is the limit. - -## What closing it requires - -Not a tuning change. A boundary-aware root certificate: restrict the -system to the boundary (one parameter pinned at a knot extreme, reducing -curve/surface to a 1-D problem in the remaining parameter) and prove the -root there with its own certificate, then confirm the pinned parameter is -exactly at the extreme rather than near it. The exact-arithmetic substrate -for that already exists — `exact_sum_is_zero` in the SSI file uses a -Shewchuk nonoverlapping expansion to decide the affine cross-term identity -without any tolerance. - -Sequence, each step independently gateable: - -1. **Boundary-restricted certificate** in `certified_curve_surface_intersection.rs`. - Pin one parameter, certify the reduced system, prove the pin exactly. - Verify with a mutation that a near-boundary root is NOT accepted as on-boundary. -2. **Plumb it** through `collect_boundary_roots` so an on-boundary root - stops setting `any_unresolved`. -3. **Dual-chord topology** in `trimmed_intersection_classify.rs`: the - `(true, true, _, _)` arm, which partitions BOTH patches into 4 faces. - This is where `UnsupportedEndpointOwnership` stops being the answer. -4. **Lift the affine restriction**. `is_exact_single_span_affine` requires - `u_degree == 1 && v_degree == 1`, a single span, and no weights. Genuinely - curved booleans need the non-affine path: traces become curves, not the - line segments `add_line_edge` currently assumes. - -Steps 1–3 unlock the dual-boundary PLANAR case. Step 4 is the actual -curved-surface boolean and is the largest of the four by a wide margin. - -## Honest status - -Do not describe the curved-surface boolean as close. Steps 1 and 2 are -contained and well-understood. Step 3 is ordinary topology work. Step 4 is -a project: every assembly path that assumes a straight intersection edge -(`add_line_edge`, `add_pcurve` over `Interval::UNIT`) has to learn curved -trace geometry first. - - -## Step 1 outcome (measured) - -Step 1 is DONE and the proof gap is closed. `krawczyk_root_on_edge` pins -the parameter that sits on a domain edge and certifies the reduced -two-unknown system, where the root is interior. - -Measured before: the dual-boundary case reached -`endpoints=0 any_unresolved=true` -- no boundary root could be certified. - -Measured after: `endpoints=4 any_unresolved=false`. Four roots certified, -zero unresolved. The certification blocker is gone. - -### The next blocker is NOT what this doc predicted - -`trace_affine_pair` refuses `endpoints > 2`: - -```rust -if any_unresolved || endpoints.len() == 1 || endpoints.len() > 2 { - return Ok(AffineTraceOutcome::Unresolved(Vec::new())); -} -``` - -Four endpoints is CORRECT for the dual-boundary case: two planar patches -crossing symmetrically meet four domain edges, because the chord runs -boundary-to-boundary on BOTH patches. The guard assumes a single trace has -exactly two endpoints, which holds only when one patch contains the -chord's interior. - -So the remaining work is trace ASSEMBLY, not certification: pair the four -certified endpoints into the correct chord per patch, then split both -patches instead of one. That also means `CertifiedTrimmedSurfacePair3`'s -`split_surface: SurfacePairMember` / `unsplit_face` shape must generalise -- -in a dual chord there is no unsplit face. That is a breaking type change -and belongs with steps 2-3, not smuggled into step 1. - -### Honest coverage note - -The discarded-row verification inside `krawczyk_root_on_edge` is NOT -covered: every off-patch input reachable today is rejected earlier by -`residual_excludes_zero`, so deleting the check fails no test. It is -retained as defence and marked as unverified in the source. - -## Step 2 outcome (measured) - -Shipped: the dual-boundary chord now splits BOTH patches, four closed -trimmed faces sharing one intersection edge. - -My step 1 prediction was WRONG in two ways, both corrected by measuring: - -1. I predicted the blocker was assembling a 4-endpoint dual chord. - Measurement showed the four endpoints were only TWO distinct points, - each reported twice -- once from scanning each surface's boundaries. - A chord ending on a boundary of BOTH patches is found from both - sides. Before step 1 nothing was certifiable there, so the - duplication had never surfaced. The fix was de-duplication, not - assembly. - -2. I predicted this forces a breaking change to - `CertifiedTrimmedSurfacePair3`. It does not. The enum is - `#[non_exhaustive]`, so a new `DualSplit` variant carrying a new - `CertifiedDualSplitSurfacePair3` was added alongside `Split`. - The single-split type keeps its exact meaning -- `unsplit_face` and - `embedded_curve` stay honest because they are absent from the dual - type, where no unsplit face exists. - -### What the dual type does NOT claim - -`CertifiedDualSplitSurfacePair3` has no `embedded_curve`: with both -patches partitioned there is no containing face to embed a dangling -edge into. Every one of the four loops uses the shared edge, asserted -in test. - -### Remaining - -Still planar-only: `is_exact_single_span_affine` requires degree 1, -single span, no weights. Genuinely curved surface/surface intersection -is unchanged by steps 1 and 2 and remains the large piece of work. diff --git a/docs/plans/crate-naming-convention.md b/docs/plans/crate-naming-convention.md deleted file mode 100644 index e78a890c..00000000 --- a/docs/plans/crate-naming-convention.md +++ /dev/null @@ -1,62 +0,0 @@ -# Crate naming convention, enforced - -Status: done (ADR 0064) - -## The rule - -Derived from `[package.metadata.axiolid]` `role` + `domain`, which the -architecture model already parses. Names are checked, not conventions -remembered. - -1. `role = contract.operation` -> `axiolid--contract` -2. `role = provider.*` -> `axiolid--`, engine non-empty -3. anything else -> must NOT end in `-contract`, and must NOT - equal a contract's name with `-contract` stripped - -`` is the `domain` field with `.` replaced by `-`. - -Rule 3's second half is the one that catches the real defect: the -unsuffixed capability name reads as THE implementation of that -capability. Rule 2 means providers always carry an engine suffix, so -the unsuffixed name is reserved and belongs to nobody. - -## Why derive from metadata rather than a hand-written list - -ADR 0004 chose machine-checkable metadata over naming convention. That -was right, and it is why naming drifted: nothing read the names. The -fix is not to abandon the metadata but to DERIVE the expected name from -it, so the two cannot disagree silently. - -## Current state against the rule - -Compliant, no change: `axiolid-mesh-boolean-contract`, -`axiolid-mesh-section-contract`, `axiolid-curve-evaluate-contract`, -`axiolid-mesh-compile-contract`, `axiolid-mesh-boolean-boolmesh`, -`axiolid-pointcloud-reconstruction-sdf`. - -### Fixed here (metadata only, not breaking) - -- `axiolid-pointcloud-reconstruction-contract` domain - `operation.pointcloud-reconstruction` -> `pointcloud.reconstruction`. - The `operation.` prefix restated the role; stripping it makes the - derived name match the actual name, so the crate becomes compliant - WITHOUT a rename. Its provider already used `pointcloud.reconstruction`, - so this also makes contract and provider agree on their domain. -- `axiolid-tessellation-contract` domain `operation.tessellate` -> - `tessellate`. Does not make it compliant, but reduces the violation to - exactly one thing: noun vs verb in the crate name. - -### Grandfathered (renames are BREAKING; all three are published) - -Confirmed on the sparse index: 0.1.0 and 0.2.0 both live for each. - -| Crate | Should be | Why deferred | -| --- | --- | --- | -| `axiolid-tessellation-contract` | `axiolid-tessellate-contract` | published; 26 refs | -| `axiolid-exact-compile-contract` | `axiolid-brep-compile-contract` | published; domain is `brep.compile` | -| `axiolid-mesh-compile` | `axiolid-graph-compile` | published; 48 refs; impersonates the contract | - -Deferred to 0.3.0, the next breaking release. The exception list is -CLOSED: a new violation fails the gate. An exception that has been fixed -ALSO fails, so the list cannot rot into a permanent amnesty. - diff --git a/docs/plans/curve-evaluate-contract.md b/docs/plans/curve-evaluate-contract.md deleted file mode 100644 index e6e010b9..00000000 --- a/docs/plans/curve-evaluate-contract.md +++ /dev/null @@ -1,111 +0,0 @@ -# Curve evaluation contract (issue #106) - -Status: done (ADR 0063) - -## Goal - -Let a consumer NAME curve evaluation without binding an engine, so an -IFC bridge can request "point and frame at a distance" behind a feature -gate, exactly as it already does for mesh compilation and booleans. - -## What the issue got right - -- Curve evaluation is the only capability with no contract crate. -- `Backend`, `CapabilityId` and the conformance pattern already exist. -- A contract-only dependency is genuinely cheap (20 vs 51 crates). -- `frame_at` is the real request: a tangent alone does not fix roll. - -## What it got wrong: this is NOT only packaging - -The proposed trait takes a DISTANCE. Three findings say that cannot be -a thin re-export of existing functions. - -### 1. The Frenet frame is unsafe for placement - -Verified numerically (`scratch/framecheck.py`). On an alignment whose -plan is straight and whose profile is a crest then a sag: - -``` -crest -> Frenet normal z = -1.0 -sag -> Frenet normal z = +1.0 -straight -> UNDEFINED (zero curvature) -``` - -The Frenet normal FLIPS at an inflection and is undefined on any -straight run. A sign placed with it would be upright on the crest, -upside down in the sag, and unplaceable on the straight between them. -This is precisely the "wrong convention tilts a road sign" failure the -issue warns about -- so the convention must NOT be Frenet. - -### 2. The convention that works is reference-up - -`right = normalise(tangent x up)`, `up' = right x tangent`, with `up` -the global +Z. Same check, same alignment: - -``` -crest/sag/straight -> up_z stays ~0.9988..1.0, right stays [0,-1,0] -``` - -Stable through inflections and across straights. Degenerate only when -the tangent is parallel to the reference (a truly vertical curve), -where `|tangent x up| = 0` and the frame must be REFUSED, not guessed. - -### 3. Distance is not the parameter, and for Elevated3 it is not even - 3D arc length - -No arc-length reparameterisation exists anywhere in the kernel. For -`Elevated3` the native parameter is PLAN distance, and the 3D arc -length differs by `sqrt(1 + grade^2)`: - -``` -grade 2% -> 0.020 m drift per 100 m -grade 5% -> 0.125 m drift per 100 m -grade 10% -> 0.499 m drift per 100 m -``` - -Silently treating one as the other misplaces a drainage structure by -half a metre per 100 m. The contract must SAY which it means. - -## Design - -New crate `crates/contracts/operations/curve-evaluate`, -`axiolid-curve-evaluate-contract`, mirroring the mesh-boolean layout -(`lib.rs` + `contract.rs` + `conformance.rs`). - -```rust -pub trait CurveEvaluator: Backend { - fn distance_convention(&self, curve: &Curve3) -> DistanceConvention; - fn point_at(&self, curve: &Curve3, distance: Scalar) -> GeomResult; - fn tangent_at(&self, curve: &Curve3, distance: Scalar) -> GeomResult; - fn frame_at(&self, curve: &Curve3, distance: Scalar) -> GeomResult; -} -``` - -`DistanceConvention` is the honesty valve: `ArcLength3d`, -`PlanDistance`, or `Unsupported`. A caller that needs true 3D arc -length on a graded alignment can see it will not get it, instead of -discovering the drift on site. - -Exactness tiers, following the house two-tier rule: - -| Family | Distance recoverable | How | -| --- | --- | --- | -| `Line` | exact | `t = d / |direction|` (direction may be non-unit) | -| `Circle` | exact | `angle = d / radius` | -| `Polyline` | exact | walk segments, interpolate the remainder | -| `Intrinsic` | exact, identity | the parameter already IS arc length | -| `Elevated` | exact in PLAN distance | both halves authored against it | -| `Ellipse` | refused | needs elliptic-integral inversion | -| `BSpline` | refused | needs numeric arc-length inversion | - -Refusing by name beats returning a parameter-as-distance lie. - -## Validation - -- Frame stays upright across a crest/sag inflection and on a straight. -- Vertical tangent refuses rather than returning a degenerate frame. -- Circle: distance d lands exactly at angle d/r; full turn = TAU*r. -- Line with a NON-unit direction lands at true distance. -- Ellipse and BSpline refuse, and say why. -- Conformance suite runnable by any future provider. -- Mutation testing on every exactness and refusal claim. diff --git a/docs/plans/pointcloud-capability.md b/docs/plans/pointcloud-capability.md deleted file mode 100644 index 10ea7a22..00000000 --- a/docs/plans/pointcloud-capability.md +++ /dev/null @@ -1,83 +0,0 @@ -# Pointcloud capability (#59 umbrella, #60–#65) - -Status: **COMPLETE** — all of #60–#65 landed 2026-09-05. - -Architecture check: 52 packages. Layering probe: MUTATION MATRIX PASSED. -Workspace: 1,250 tests / 0 failed with `--all-features`. - -## Outcome - -- [x] #60 `axiolid-pointcloud` — representation value, 10 tests -- [x] #61 `PointIndex` KNN/radius in `axiolid-spatial` — 13 tests, brute-force verified -- [x] #62 reconstruction contract — request/evidence/refusal + exported conformance suite -- [x] #63 `axiolid-pointcloud-reconstruction-sdf` reference provider — 10 tests -- [x] #64 dispatch registry — 9 tests incl. the dropped-provider mutation -- [x] #65 facade features + gate + measurement table -- [x] ADR 0044 promoted Proposed -> Accepted with as-built notes - -## Decisions made during implementation - -1. **The provider is not an adopted dependency.** ADR 0044 anticipated - vendoring a reconstruction library. Instead the provider composes - `PointIndex` (#61) with `axiolid-levelset` (#87) — both already in-tree - and audited. No new licensing surface, and the contract is verifiable now - rather than after a vendoring decision. -2. **Refusal is not a fallback trigger.** A provider saying "this data cannot - support that request" is an answer about the data, not a provider failure. - Falling through would search for a provider willing to guess. -3. **Positions-only reconstruction is weaker and says so.** With no normals - there is no way to determine an inside, so the result is a shrink-wrap - offset by the sample spacing, reported through `used_normals`. -4. **`matches_device` was promoted to a shared module** rather than copied - into the new registry, so routing semantics cannot drift per operation. - -## Goal - -Make the kernel *optionally* capable of pointcloud work: represent point -samples, query them, and reconstruct meshes from them — without admitting -any source-format (LAS/LAZ/E57/PCD/COPC) type into `crates/`. - -## Order (dependency-forced) - -1. **#60** `axiolid-pointcloud` — representation only, deps = `axiolid-core`. -2. **#61** KNN + radius queries in `axiolid-spatial` - (`crates/algorithms/query/spatial`), callback-based. -3. **#62** `axiolid-pointcloud-reconstruction-contract` under - `crates/contracts/operations/pointcloud-reconstruction/`. -4. **#63** provider under `crates/providers/pointcloud//`. -5. **#64** dispatch feature in `axiolid-dispatch`. -6. **#65** facade features + architecture gate + docs + size measurement. -7. **#59** close umbrella once 60–65 are closed. Needs an **ADR** recording - the ingestion boundary. - -## Hard constraints - -- `axiolid-pointcloud` is representation-only: no topology, no algorithms, - no source-format types, no deps beyond `axiolid-core`. -- Typed refusal everywhere; never a silent empty result. -- Queries must not allocate on the hot path — callback-based, mirroring the - existing spatial queries. -- Broad-phase candidates must never be reported as exact adjacency. -- Facade features additive only; **no change to default features**. -- Layering: `algorithms` may NOT depend on `providers`. Cross that seam - through a contract, as `decompose::split::Splitter` does. - -## Verification per issue - -- `cargo xtask architecture check` -- `scripts/probe_layering_gate.sh` -- `bash scripts/gate.sh` -- mutation evidence per new capability -- #65 additionally: `default-features = false, features = ["pointcloud"]` - size measurement recorded in `current-target-crate-map.md`. - -## Progress - -- [ ] #60 representation -- [ ] #61 spatial queries -- [ ] #62 contract -- [ ] #63 provider -- [ ] #64 dispatch -- [ ] #65 facade + gate -- [ ] ADR: ingestion boundary -- [ ] #59 umbrella closed diff --git a/docs/plans/v0.10-mesh-milestone.md b/docs/plans/v0.10-mesh-milestone.md deleted file mode 100644 index f622bf8b..00000000 --- a/docs/plans/v0.10-mesh-milestone.md +++ /dev/null @@ -1,140 +0,0 @@ -# v0.10 Mesh Milestone — Execution Plan - -Standing objective. Re-read this after any context compaction. - -## Sequence (agreed with Friedrich, 2026-09-05) - -1. **#84** Per-vertex attribute channel — FIRST. Changes the core mesh value; - doing it later forces rework of #86/#87 which both create new vertices. -2. **#86** Refinement & smoothing, surface-aware. -3. **#87** Implicit modelling: level-set extraction from an SDF. -4. **NEW** Convex decomposition — file as its own issue, prerequisite for #88. -5. **#88** General Minkowski sum/difference — LAST, needs (4). - -Land each fully: tests + mutation evidence + gate + push + changelog + release. -Do not half-land four issues. - -## Ground truth measured at start (commit 814780e, v0.11.0) - -- `TriMesh { positions, indices, normals: Option }` - in `crates/representations/discrete/mesh/src/triangle.rs`. -- `NormalAttribute { values: Vec, indices: Option> }` is the - EXISTING precedent for an independently-indexed per-vertex channel. -- **71 TriMesh construction sites across 27 files** (non-test) — this is #84's - blast radius. -- `boolmesh::convert::from_manifold` calls `TriMesh::new(positions, indices)`, - which means **normals are ALREADY DROPPED through booleans today**. #84 must - state a policy rather than pretend preservation exists. -- `decimate` is the precedent contract to mirror: - `decimate(&TriMesh, DecimateTarget, Tolerance) -> Result<(TriMesh, DecimateReport), DecimateError>` - with `rejected_unsafe`, `rejected_deviation`, `max_deviation`, `is_noop()`. - -## Constraints - -- Pure Rust, vendor-neutral. `layering.rs` bans `ifc` in geometry crates. -- Exactness over convenience: bounded refusal beats silent approximation. -- `main` is SHARED: rebase + re-gate before push. -- Periodicity stays opt-in. -- Author: `Friedrich Schrödter `. -- Env: `RUSTUP_TOOLCHAIN=1.88.0`, `CARGO_TARGET_DIR=/mnt/backup/build-cache/axiolid-target`, - `TMPDIR=$HOME/scratch/tmp`, unset leaked CARGO_*/GIT_* first. - -## Verification per issue - -- `scripts/gate.sh` green. -- Unit tests PLUS mutation evidence: removing the enforcement must fail a test. -- Name the exact type/function an `ifc-geometry` consumer calls. - -## #84 design decision (measured, not assumed) - -PROBE RESULT (`boolmesh` difference, input with a normals channel): -`input normals present=true, output normals present=false` — attributes are -**silently dropped today**, with no error and no report. - -Crucially this is DELIBERATE, not an oversight. `convert.rs::from_manifold` -documents it: boolmesh computes FACE normals, and re-exporting those as VERTEX -normals "would misrepresent hard edges created by the cut." - -So #84 is NOT "make attributes survive every operation" — that would force a -lie at exactly the cut boundary. #84 is **make the fate of each channel -explicit and typed**, so a consumer learns what happened instead of comparing -`is_some()` before and after. - -Design: carry attribute fate in the EXISTING `BooleanEvidence` channel -(`crates/contracts/operations/mesh-boolean/src/evidence.rs`), which already -follows this philosophy for `disjoint_tools`: "not an error here, so it is -reported rather than rejected." No new plumbing invented. - -Policy vocabulary (per channel): Preserved / Interpolated / Dropped(reason). -An operation that creates new vertices must state which applies. - -## #86 BLOCKER FOUND (2026-09-05) — needs a decision - -Surface-aware refinement needs to place an edge midpoint ON the analytic -surface. I assumed `evaluate::surface::invert` would do it. It will not, and -it says so explicitly: - -``` -invert REFUSED: point is 0.0761 from the surface, beyond the 1e-6 tolerance: -inversion names a point ON the surface and does not project -``` - -`invert` is a strict ON-SURFACE inversion by design. A chord midpoint of a -faceted cylinder is by definition OFF the surface (radius 0.9239 vs 1.0), so -inversion is the wrong tool. - -Measured proof the naive approach silently did nothing: new vertex radii -stayed at 0.923880 and `max_deviation` was 2.5e-16. My `_ => linear` fallback -swallowed the refusal — exactly the silent approximation the constraints -forbid. That fallback must go regardless of which option is chosen. - -What exists: -- `axiolid-nurbs` has `project_surface_certified` / `project_curve*`, but - only for B-spline surfaces, and iterative. -- NO closed-form projection exists for the elementary surfaces - (`Plane`, `Cylinder`, `Cone`, `Sphere`, `Torus`). - -Options: -- **A** Add exact closed-form `project` for Plane/Cylinder/Sphere/Cone in - `axiolid-evaluate` (all four have closed forms; Torus needs a quartic and - can be refused). Then surface-aware refinement is exact for exactly the - surfaces IFC tessellation actually produces. -- **B** Reuse `axiolid-nurbs` certified projection. Wrong layer for - `axiolid-refine`, iterative, and does not cover analytic surfaces anyway. -- **C** Drop surface-awareness from #86 and ship linear refinement only. - Rejected unless Friedrich says otherwise: the issue states surface-aware - refinement is "the whole point". - -Recommendation: **A**. Same shape as the curve `invert2/invert3` work in -v0.11.0 — exact where a closed form exists, bounded refusal otherwise. - -## Status — MILESTONE COMPLETE - -- [x] #84 DONE — `349b7ff`. Attribute channels as DISCLOSURE. -- [x] #86 DONE — `f3ff3d7`. Needed `evaluate::surface::project`. -- [x] #87 DONE — `fb61bb1` + `c546e97`. Kuhn tetrahedra + simulation of - simplicity (SOS_DELTA = 1e-3 of an edge). -- [x] #96 DONE — `4a7ba77`. Convex decomposition, switchable Splitter. -- [x] #88 DONE — `93b1280`. Minkowski sum and difference. - -## Carried lessons - -- Cap a cut by MEASURING which edges the clipped shell left used once, - never by predicting the cross-section per triangle. -- Concavity must be measured against FACE PLANES, not the convex hull. -- Minkowski difference is an EROSION, `⋂ᵢ (A − vᵢ)`, valid only for a - convex subject. It is not a hull of pairwise differences. -- Symmetric test fixtures hide sign errors. The erosion sign mutant - survived until a tool placed OFF the origin was used. - -## Provider access from an algorithms crate - -`algorithms` may not depend on `providers`; the gate enforces it. Route -through the CONTRACT both layers share — see `decompose::split::Splitter` -and `minkowski_sum_with`. - -## Release note - -v0.11.0 is the last tag. #84, #86, #87, #96, #88 are all on main and -UNRELEASED. The milestone is complete, so v0.12.0 can be cut on Friedrich's -call. diff --git a/docs/reference/changelog.md b/docs/reference/changelog.md index 81cf8f3f..29698adc 100644 --- a/docs/reference/changelog.md +++ b/docs/reference/changelog.md @@ -1,4 +1,6 @@ - +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- # Per-crate changelog diff --git a/docs/reference/crates.md b/docs/reference/crates.md deleted file mode 100644 index f21613d2..00000000 --- a/docs/reference/crates.md +++ /dev/null @@ -1,36 +0,0 @@ -# Crate map - -The facade is convenient; leaf packages are the enforceable boundaries. Select the smallest package set that satisfies the use case. The generated, metadata-checked inventory is the [architecture crate map](/architecture/crate-map). - -| Layer | Packages | Responsibility | -| --- | --- | --- | -| Foundation | `axiolid-core` | Numeric policy, identity, errors, bounds, tolerance | -| Representations | `axiolid-linear`, `axiolid-curve`, `axiolid-surface`, `axiolid-primitive`, `axiolid-profile`, `axiolid-topology`, `axiolid-brep`, `axiolid-mesh`, `axiolid-field`, `axiolid-model` | Portable geometry values and authored graph | -| Guarantees/common contracts | `axiolid-guarantees`, `axiolid-contracts`, `axiolid-mesh-contracts` | Proof/refusal vocabulary, execution diagnostics, shared mesh admissibility | -| Operation contracts | `axiolid-tessellation-contract`, `axiolid-mesh-boolean-contract`, `axiolid-mesh-section-contract`, `axiolid-mesh-compile-contract` | Typed provider-neutral request/result/evidence seams | -| Algorithms | `axiolid-predicates`, `axiolid-linear-intersection`, `axiolid-evaluate`, `axiolid-reference`, `axiolid-nurbs`, `axiolid-construct`, `axiolid-spatial`, `axiolid-measure`, `axiolid-overlay`, `axiolid-field-ops`, `axiolid-heal` | Format-neutral implementations over values/contracts | -| Providers | `axiolid-mesh-boolean-boolmesh` | Concrete optional operation provider | -| Execution | `axiolid-dispatch`, `axiolid-mesh-compile`, `axiolid-backend-cpu`, `axiolid-backend-gpu` | Registration, fallback/device policy, graph execution, contexts/adapters | -| Facade | `axiolid` | Additive capability features and re-exports | - -## Selecting a package - -- Core scalar/vector/transform/tolerance values: `axiolid-core`. -- Mesh or sampled-field values without algorithms: `axiolid-mesh` or `axiolid-field`. -- Sampling/morphology/navigation over fields: `axiolid-field-ops`. -- Certified exact-arithmetic predicates: `axiolid-predicates` (the focused substrate). -- Broad reference oracles: `axiolid-reference` (a convenience umbrella; do not depend on it from a narrow package). -- Linear values without the curve aggregate: `axiolid-linear`. -- NURBS analysis and exact shape-preserving transformations: `axiolid-nurbs`. -- Neutral authored graph storage: `axiolid-model`. -- Exact analytic B-rep results: `axiolid-brep`; this is not a tessellator. -- Mesh Boolean or plane-section portability: depend on the operation contract, then choose a provider/dispatch policy explicitly. -- Graph-to-mesh execution: `axiolid-mesh-compile-contract` for the seam and `axiolid-mesh-compile` for the reference implementation. - -Representation-only facade use remains: - -```toml -axiolid = { default-features = false, features = ["model"] } -``` - -This must not resolve compiler, field-operation, mesh-Boolean-provider, source-format, or GPU dependencies. diff --git a/docs/reference/crates/axiolid-arrangement.md b/docs/reference/crates/axiolid-arrangement.md new file mode 100644 index 00000000..49cf4bc7 --- /dev/null +++ b/docs/reference/crates/axiolid-arrangement.md @@ -0,0 +1,31 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-arrangement + +Editable planar subdivision with persistent half-edge topology. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-arrangement`](https://crates.io/crates/axiolid-arrangement) | +| Layer | algorithms (`algorithm.planar`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_arrangement/index.html) · [docs.rs](https://docs.rs/axiolid-arrangement) | +| Source | [`crates/algorithms/planar/arrangement/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/planar/arrangement) | + +## Overview + +An editable planar subdivision: a doubly-connected edge list whose vertices, half-edges and faces keep stable handles across edits, so a caller can move a vertex or split a face without rebuilding the plane or losing track of which face is which. Orientation decisions use certified predicates, and the unbounded outer region is a real face. It is deliberately neutral: it exposes faces, boundaries, areas and adjacency, and leaves deciding that a face is a room to the caller. For one-shot polygon booleans with no retained structure, use `axiolid-overlay` instead. + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-guarantees`](./axiolid-guarantees) +- [`axiolid-predicates`](./axiolid-predicates) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/algorithms/planar/arrangement/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/planar/arrangement/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-backend-cpu.md b/docs/reference/crates/axiolid-backend-cpu.md new file mode 100644 index 00000000..eae6c07a --- /dev/null +++ b/docs/reference/crates/axiolid-backend-cpu.md @@ -0,0 +1,42 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-backend-cpu + +Portable and runtime-optimized CPU execution context for Axiolid geometry. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-backend-cpu`](https://crates.io/crates/axiolid-backend-cpu) | +| Facade | [`axiolid`](./axiolid) feature `cpu` | +| Layer | execution (`execution.context`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_backend_cpu/index.html) · [docs.rs](https://docs.rs/axiolid-backend-cpu) | +| Source | [`crates/execution/cpu/`](https://github.com/axiolid/kernel/tree/main/crates/execution/cpu) | + +## Overview + +A CPU execution context for Axiolid providers: runtime instruction-set +detection (`CpuFeatures`), measured cache and core topology for tuning +(`CpuTopology`), and an optional context-owned Rayon pool. The default build +is portable and single-threaded; the `simd` feature lets providers select an +instruction set at run time, and `parallel` adds a bounded local pool instead +of touching Rayon's global one. It bundles no geometry algorithm and is not +the correctness oracle: that is `axiolid-reference` (ADR 0012). Operation +providers compose this context. + +## Features + +Default: none. + +| Feature | Enables | +| --- | --- | +| `parallel` | `rayon` | +| `simd` | — | + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/execution/cpu/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/execution/cpu/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-backend-gpu.md b/docs/reference/crates/axiolid-backend-gpu.md new file mode 100644 index 00000000..09626451 --- /dev/null +++ b/docs/reference/crates/axiolid-backend-gpu.md @@ -0,0 +1,47 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-backend-gpu + +GPU executor adapter contract for batched Axiolid geometry. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-backend-gpu`](https://crates.io/crates/axiolid-backend-gpu) | +| Facade | [`axiolid`](./axiolid) feature `gpu` | +| Layer | execution (`execution.context`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_backend_gpu/index.html) · [docs.rs](https://docs.rs/axiolid-backend-gpu) | +| Source | [`crates/execution/gpu/`](https://github.com/axiolid/kernel/tree/main/crates/execution/gpu) | + +## Overview + +An API-neutral seam for GPU graph compilation. `GpuCompiler` adapts any +`GpuGraphExecutor` to the `MeshCompiler` contract: it validates device, +precision, residency and roots before submitting one batch, and checks the +executor's results afterwards. The crate chooses no GPU API (CUDA, Metal, +Vulkan, WebGPU) and ships no executor, so default builds carry no driver +stack. Concrete executors live in their own crates, including out of tree +(ADR 0011). + +## Design notes + +- Each further GPU operation gets its own narrow executor trait and + adapter, never a method on one catch-all backend. +- A GPU path is evidence only if a CPU differential test checks it. Maturing + the executor is tracked in + [#22](https://github.com/axiolid/kernel/issues/22). + +## Depends on + +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-mesh-compile-contract`](./axiolid-mesh-compile-contract) +- [`axiolid-model`](./axiolid-model) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/execution/gpu/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/execution/gpu/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-brep-audit.md b/docs/reference/crates/axiolid-brep-audit.md new file mode 100644 index 00000000..9764e175 --- /dev/null +++ b/docs/reference/crates/axiolid-brep-audit.md @@ -0,0 +1,43 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-brep-audit + +Geometric consistency auditing for exact boundary representations. + +| | | +| --- | --- | +| Latest release | 0.3.1 (2026-09-27) | +| crates.io | [`axiolid-brep-audit`](https://crates.io/crates/axiolid-brep-audit) | +| Layer | algorithms (`algorithm.repair`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_brep_audit/index.html) · [docs.rs](https://docs.rs/axiolid-brep-audit) | +| Source | [`crates/algorithms/repair/brep-audit/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/repair/brep-audit) | + +## Overview + +Geometric consistency auditing for exact B-reps. It evaluates curves and surfaces and checks that edge vertices lie on their 3D curves and that every pcurve, mapped through its face's surface, lands on the curve of the edge it trims. It complements the exact, tolerance-free topological audit in `axiolid-topology`; because it compares positions, it needs a tolerance and reports agreement to within it (ADR 0052). It diagnoses only and repairs nothing. + +## Depends on + +- [`axiolid-brep`](./axiolid-brep) +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-curve`](./axiolid-curve) +- [`axiolid-evaluate`](./axiolid-evaluate) +- [`axiolid-surface`](./axiolid-surface) +- [`axiolid-topology`](./axiolid-topology) + +## Changes + +Latest release, 0.3.1 (2026-09-27): + +### Changed + +- An implicit pcurve (ADR 0077) is parameterised by its cells, not in + proportion to its edge. It passes when every lifted sample projects onto + the edge within tolerance, inside the edge's span, in the order the use + runs, and starting and ending at the use's ends. Every other pcurve is + still checked against the edge at proportional parameters. + +Full history: [`crates/algorithms/repair/brep-audit/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/repair/brep-audit/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-brep-boolean.md b/docs/reference/crates/axiolid-brep-boolean.md new file mode 100644 index 00000000..1d6f265f --- /dev/null +++ b/docs/reference/crates/axiolid-brep-boolean.md @@ -0,0 +1,127 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-brep-boolean + +General exact B-rep booleans over analytic faces (ADR 0075). + +| | | +| --- | --- | +| Latest release | 0.1.0 (2026-09-27) | +| crates.io | [`axiolid-brep-boolean`](https://crates.io/crates/axiolid-brep-boolean) | +| Layer | algorithms (`algorithm.construction`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_brep_boolean/index.html) · [docs.rs](https://docs.rs/axiolid-brep-boolean) | +| Source | [`crates/algorithms/construction/brep-boolean/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/construction/brep-boolean) | + +## Overview + +Exact union, intersection and difference of two exact B-rep solids whose +faces lie on planes, cylinders, elliptical cylinders, cones, spheres, tori +and B-spline surfaces: the general-fuse pipeline of ADR 0075 (section edges, +face splitting in each face's own parameters, certified classification, +sewing). Operands that touch rather than cross are handled. Nothing is +meshed or fitted: a configuration the pipeline cannot build exactly is +refused with a typed error. It does not tessellate its result and does not +work on meshes; mesh booleans are operation providers selected through the +execution layer. + +## Design notes + +`axiolid-construct` keeps narrower exact booleans that predate this crate: +planar polyhedra (`polyhedron::boolean_polyhedra_exact`) and coaxial +prisms and plane cuts over one arc arrangement (`boolean_exact`). They are +independent exact paths, and this crate's tests check vertical-column +results against them. + +## Depends on + +- [`axiolid-brep`](./axiolid-brep) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-curve`](./axiolid-curve) +- [`axiolid-evaluate`](./axiolid-evaluate) +- [`axiolid-measure`](./axiolid-measure) +- [`axiolid-nurbs`](./axiolid-nurbs) +- [`axiolid-surface`](./axiolid-surface) +- [`axiolid-topology`](./axiolid-topology) + +## Changes + +Latest release, 0.1.0 (2026-09-27): + +### Added + +- Surfaces tangent along a whole curve and crossing there: the line of + contact is a section edge (the tangent-contact rule now applies to + closed forms only), and pieces leaving a vertex with the same direction + and bend are ordered by their chords a small way out. +- A traced pair that only touches adds no section, as the closed forms' + touching does. A pcurve whose trace on its face cannot be decided falls + back to the section's own space curve read on the face, and failed + traces are not repeated. Each surface pair's closed form is computed + once per boolean. +- Two B-spline faces meeting each other: their section is traced in both + faces' parameter boxes and carried on both (`Curve3::PairSection`), its + pcurve on each read from the solve. The last refusal by face type is + gone; sections whose branches cross where the surfaces touch stay + refused by name. +- Faces that wind round their surface without a seam edge (a dome bounded + by its rim alone, a can by its two rims), as files may deliver them. The + boolean first gives each a seam edge along the iso-curve where its loops + wrap, joining the lower loop, the seam, the upper loop or a pole, and the + seam back into one loop. +- Sections through a sphere's pole or a cone's apex are cut there, and the + collapsed pole piece is split where they end. Meridians and latitudes, + cone rulings and circles, and a torus's tube and ring circles get their + straight pcurves, affine in the curve's own parameter. + +- B-spline faces (#167, ADR 0075 stage 3) against planes, quadrics and + tori: + - The section is traced on the spline (ADR 0077). + - On the analytic face its pcurve is the same space curve read in the + face's parameters (`Curve2::Lifted`), so it shares the edge's + parameter. + - An edge next to a B-spline face is cut where it meets the section's + other surface. + - Classification rays meet B-spline faces through the spline trace. + - Two B-spline faces meeting each other are refused by name + (`UnsupportedSection`). + +- Faces on spheres, cones, tori and elliptical cylinders (#167, ADR 0075 + stage 2), meeting in any section #119 builds: + - A section with no line or conic is traced inside one face's parameter + box (ADR 0077). + - Every section on every analytic face gets an exact implicit pcurve, cut + out of the other surface's traced equation between the section's ends. + - A sphere's pole or a cone's apex closes loops as a collapsed piece that + is no edge. + - Seam circles are cut by the cone of normals along them. + - Section branches that cross (a Steinmetz pair) are split where they + meet. + - Frame components that are only rounding residue are cleared before + intersecting. + +- Operands that touch (#167, ADR 0075 stage 2): faces on one surface share + their overlap (each face's edges are imprinted on the other, and a region + on the other solid's boundary is kept once by normal agreement); sections + along an existing edge split only the other face; tangent contact adds no + section; pieces leaving a vertex in one direction are ordered by + curvature; solids meeting along an edge are paired radially around it so + each stays manifold. Cavities go to the smallest solid around them, in + results of several solids too. + +- `section_edges` (#167, ADR 0075 stage 1): the exact intersection curves of + two exact B-reps' faces, each trimmed to where it lies inside both faces. + Crossings with a boundary edge are found against the adjacent face's + surface, or across a seam against the plane through the ruling. +- `boolean(a, b, operator, tolerance)` (#167, ADR 0075 stage 1): the exact + union, intersection and difference of two exact solids whose faces lie on + planes and cylinders and meet in lines, circles and ellipses -- not only + vertical columns. Regions are classified by exact ray parity with + certified face membership and sewn into shells; cavities become voids. + Every result audits clean and measures exactly. +- `split_face` (#167): a plane or cylinder face cut along its section edges + into regions, traced in the face's parameters with exact pcurves (lines, + conics, rulings, circles about the axis, `Sinusoid2` for oblique cuts). + +Full history: [`crates/algorithms/construction/brep-boolean/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/construction/brep-boolean/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-brep.md b/docs/reference/crates/axiolid-brep.md new file mode 100644 index 00000000..8d3251e1 --- /dev/null +++ b/docs/reference/crates/axiolid-brep.md @@ -0,0 +1,45 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-brep + +Exact analytic B-rep result contracts over neutral topology. + +| | | +| --- | --- | +| Latest release | 0.3.1 (2026-09-27) | +| crates.io | [`axiolid-brep`](https://crates.io/crates/axiolid-brep) | +| Facade | [`axiolid`](./axiolid) feature `brep` | +| Layer | representations (`representation.composed`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_brep/index.html) · [docs.rs](https://docs.rs/axiolid-brep) | +| Source | [`crates/representations/brep/`](https://github.com/axiolid/kernel/tree/main/crates/representations/brep) | + +## Overview + +The strict exact B-rep result: an `axiolid-topology` graph bound to owned +catalogs of `Curve3` edge supports, `Curve2` pcurves and `Surface` face +supports, with every edge and pcurve interval stated explicitly and checked +before the value can exist. Faces and edges can carry persistent structural +names that survive rebuilds. It does not evaluate, intersect, tessellate or +traverse geometry. + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-curve`](./axiolid-curve) +- [`axiolid-surface`](./axiolid-surface) +- [`axiolid-topology`](./axiolid-topology) + +## Changes + +Latest release, 0.3.1 (2026-09-27): + +### Added + +- `ExactBRepBuilder::append`: copy another exact B-rep's vertices, edges, + loops, faces and shells, with their curves, surfaces, intervals and + names, and return the new shell handles; optionally with every face used + reversed, which turns an outer shell into a void (#111). + +Full history: [`crates/representations/brep/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/representations/brep/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-capi.md b/docs/reference/crates/axiolid-capi.md new file mode 100644 index 00000000..6c2c49fa --- /dev/null +++ b/docs/reference/crates/axiolid-capi.md @@ -0,0 +1,36 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-capi + +Versioned, memory-safe C ABI for the Axiolid application facade. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-capi`](https://crates.io/crates/axiolid-capi) | +| Layer | facade (`facade.native-c`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_capi/index.html) · [docs.rs](https://docs.rs/axiolid-capi) | +| Source | [`crates/facade/axiolid-capi/`](https://github.com/axiolid/kernel/tree/main/crates/facade/axiolid-capi) | + +## Overview + +A versioned C ABI over the `axiolid::application` facade, for C and C++ +applications. Every symbol carries the `axiolid_v0_4_` prefix; results are +reached through scalar handles owned by a context, data is copied into +caller-sized buffers so no Rust allocation crosses the boundary, and no +function unwinds. Exact and triangle-mesh results are distinguishable, and +an unsupported exact operation is refused rather than tessellated. The C +header is generated from the Rust surface into `include/axiolid.h`. This is +the only Axiolid crate that contains `unsafe` code. See ADR 0040. + +## Depends on + +- [`axiolid`](./axiolid) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/facade/axiolid-capi/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/facade/axiolid-capi/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-collide.md b/docs/reference/crates/axiolid-collide.md new file mode 100644 index 00000000..ea39a3d3 --- /dev/null +++ b/docs/reference/crates/axiolid-collide.md @@ -0,0 +1,29 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-collide + +Convex collision queries: separating axis, overlap, and separation distance. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-collide`](https://crates.io/crates/axiolid-collide) | +| Layer | algorithms (`algorithm.query`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_collide/index.html) · [docs.rs](https://docs.rs/axiolid-collide) | +| Source | [`crates/algorithms/query/collide/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/query/collide) | + +## Overview + +Convex collision queries by the separating axis theorem: whether two convex shapes overlap and, when they do not, how far apart they are and along which axis. It deliberately does not report penetration depth; callers who need EPA-style contact for physics want a physics engine. For clearance between triangle meshes, use `axiolid-inspect`. + +## Depends on + +- [`axiolid-core`](./axiolid-core) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/algorithms/query/collide/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/query/collide/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-construct.md b/docs/reference/crates/axiolid-construct.md new file mode 100644 index 00000000..95370f07 --- /dev/null +++ b/docs/reference/crates/axiolid-construct.md @@ -0,0 +1,80 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-construct + +Solid generation: profiles, lofts, sweeps, revolutions and half-space clipping. + +| | | +| --- | --- | +| Latest release | 0.3.5 (2026-09-28) | +| crates.io | [`axiolid-construct`](https://crates.io/crates/axiolid-construct) | +| Facade | [`axiolid`](./axiolid) feature `generate` | +| Layer | algorithms (`algorithm.construction`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_construct/index.html) · [docs.rs](https://docs.rs/axiolid-construct) | +| Source | [`crates/algorithms/construction/construct/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/construction/construct) | + +## Overview + +Solid generation from exact inputs: extrusion, revolution, sweeps, lofts, +centre-line profiles, half-space clipping proxies, offsets, fillets and +chamfers on supported families, and focused exact booleans (planar +polyhedra, coaxial column solids). Every `Profile` variant extrudes to an +exact B-rep, and full-turn revolution covers any profile that lowers to a +contour; sweeps and lofts produce meshes by default. Geometry the kernel cannot +represent exactly is refused, never tessellated in its place. The crate +takes geometry and returns geometry: it owns no operation graph, cache, +execution context or provider dispatch (ADR 0023); `axiolid-mesh-compile` +does those and calls in here. + +## Design notes + +Tests that check a generated mesh is accepted by a mesh Boolean provider +live in `crates/execution/compile/tests/`, not here. A dev-dependency on a +provider or execution crate would pull this algorithms crate's tests above +its tier; the architecture check only enforces the tier edge for normal +dependencies, so the allowlist in `Cargo.toml` is what keeps it out. + +## Depends on + +- [`axiolid-brep`](./axiolid-brep) +- [`axiolid-brep-audit`](./axiolid-brep-audit) +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-curve`](./axiolid-curve) +- [`axiolid-exact`](./axiolid-exact) +- [`axiolid-guarantees`](./axiolid-guarantees) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-mesh-boolean-contract`](./axiolid-mesh-boolean-contract) +- [`axiolid-mesh-contracts`](./axiolid-mesh-contracts) +- [`axiolid-nurbs`](./axiolid-nurbs) +- [`axiolid-overlay`](./axiolid-overlay) +- [`axiolid-predicates`](./axiolid-predicates) +- [`axiolid-primitive`](./axiolid-primitive) +- [`axiolid-profile`](./axiolid-profile) +- [`axiolid-reference`](./axiolid-reference) +- [`axiolid-surface`](./axiolid-surface) +- [`axiolid-tessellation-contract`](./axiolid-tessellation-contract) +- [`axiolid-topology`](./axiolid-topology) + +## Changes + +Latest release, 0.3.5 (2026-09-28): + +### Fixed + +- Structural sections mesh (#193): `profile_rings` flattens every + `Profile::Section` family -- I, asymmetric I, L, T, U, C, Z, trapezium -- + from the exact contour `section_contour` builds, fillets and toe radii + chorded within the budget, straight edges exact. It refused them with + `Unsupported { ProfileTriangulation }` before. Rectangles with corner + radii mesh with them, through `rectangle_contour`: the mesh path used to + drop the radii and mesh a sharp box. +- Circles are chorded from half a step off the axes, not from angle 0 + (#194): the same chords, turned, so a chord's middle, inside the circle, + sits at every quarter turn. A circular void tangent to a face along an + axis direction, as openings are, leaves a sliver of material under it + instead of a chord point on the face, which pinched the solid. + +Full history: [`crates/algorithms/construction/construct/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/construction/construct/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-contracts.md b/docs/reference/crates/axiolid-contracts.md new file mode 100644 index 00000000..5bdab59e --- /dev/null +++ b/docs/reference/crates/axiolid-contracts.md @@ -0,0 +1,46 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-contracts + +Common backend-neutral execution and diagnostic contracts. + +| | | +| --- | --- | +| Latest release | 0.3.1 (2026-09-25) | +| crates.io | [`axiolid-contracts`](https://crates.io/crates/axiolid-contracts) | +| Facade | [`axiolid`](./axiolid) feature `contracts` | +| Layer | contracts (`contract.common`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_contracts/index.html) · [docs.rs](https://docs.rs/axiolid-contracts) | +| Source | [`crates/contracts/common/base/`](https://github.com/axiolid/kernel/tree/main/crates/contracts/common/base) | + +## Overview + +Common, provider-neutral contracts shared by every operation: backend +identity and descriptors, cancellation, diagnostics and `GeomError`, output +bounds and execution options, operation plans, and the integration profiles +a downstream application checks against. It defines no operation schema of +its own (those are the sibling `axiolid-*-contract` packages) and does no +provider selection or fallback, which belong to `axiolid-dispatch`. + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-guarantees`](./axiolid-guarantees) + +## Changes + +Latest release, 0.3.1 (2026-09-25): + +### Added + +- `ExecutionOptions::with_chord_error` and `ExecutionOptions::chord_error` + (#165): an explicit bound on how far a provider's straight chords may sit + from the curve they replace, separate from the linear tolerance. The + tolerance is a coincidence test; used as a chord budget it leaves a 5 mm + arc a few chords at `Tolerance::MILLIMETRE`, and small profiles mesh + percent-level off. `None` (the default) keeps each provider's previous + behaviour. A non-finite or non-positive budget is refused (`None`). + +Full history: [`crates/contracts/common/base/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/contracts/common/base/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-core.md b/docs/reference/crates/axiolid-core.md new file mode 100644 index 00000000..576e7b7c --- /dev/null +++ b/docs/reference/crates/axiolid-core.md @@ -0,0 +1,31 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-core + +Geometry data types and tolerance policy. No algorithms, no backends. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-core`](https://crates.io/crates/axiolid-core) | +| Layer | foundation (`foundation.values`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_core/index.html) · [docs.rs](https://docs.rs/axiolid-core) | +| Source | [`crates/foundation/core/`](https://github.com/axiolid/kernel/tree/main/crates/foundation/core) | + +## Overview + +The dependency root of Axiolid: points, vectors, frames, transforms, +intervals, bounding boxes, simple 2D and 3D primitives, and the explicit +`Tolerance` policy every tolerance-sensitive operation takes. It holds data +only. There are no algorithms, no serialization, no source-format +identifiers, and no hardware backends here, and it depends on no other +Axiolid package. Coordinates are `f64` in whatever length unit the caller's +model uses. + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/foundation/core/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/foundation/core/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-curve-evaluate-contract.md b/docs/reference/crates/axiolid-curve-evaluate-contract.md new file mode 100644 index 00000000..6f1d888f --- /dev/null +++ b/docs/reference/crates/axiolid-curve-evaluate-contract.md @@ -0,0 +1,37 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-curve-evaluate-contract + +Portable curve evaluation capability contract: point, tangent and oriented frame at a distance. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-curve-evaluate-contract`](https://crates.io/crates/axiolid-curve-evaluate-contract) | +| Layer | contracts (`contract.operation`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_curve_evaluate_contract/index.html) · [docs.rs](https://docs.rs/axiolid-curve-evaluate-contract) | +| Source | [`crates/contracts/operations/curve-evaluate/`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/curve-evaluate) | + +## Overview + +The curve-evaluation capability as a contract: point, tangent and oriented +frame at a place on a `Curve3`, plus a conformance suite every provider must +pass. A caller says whether its number is a distance or a native parameter +(`CurveMeasure`), and a provider says which distance it measures for each +curve (`DistanceConvention`) or that it cannot. Frames are reference-up, not +Frenet. This crate evaluates nothing itself; `axiolid-evaluate` provides the +scalar implementation. See ADR 0063. + +## Depends on + +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-curve`](./axiolid-curve) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/contracts/operations/curve-evaluate/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/contracts/operations/curve-evaluate/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-curve.md b/docs/reference/crates/axiolid-curve.md new file mode 100644 index 00000000..dbc52d08 --- /dev/null +++ b/docs/reference/crates/axiolid-curve.md @@ -0,0 +1,94 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-curve + +Exact, format-neutral curve values: lines, conics, B-splines, natural-equation and elevated curves. + +| | | +| --- | --- | +| Latest release | 0.3.1 (2026-09-27) | +| crates.io | [`axiolid-curve`](https://crates.io/crates/axiolid-curve) | +| Facade | [`axiolid`](./axiolid) feature `curves` | +| Layer | representations (`representation.atomic`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_curve/index.html) · [docs.rs](https://docs.rs/axiolid-curve) | +| Source | [`crates/representations/analytic/curve/`](https://github.com/axiolid/kernel/tree/main/crates/representations/analytic/curve) | + +## Overview + +Exact, format-neutral curve values: lines and polylines (from +`axiolid-linear`), conics, rational and polynomial B-splines, +natural-equation (intrinsic) curves, elevated alignment curves, and the +curves where surfaces meet. Knots, multiplicities, weights and domains are +kept as authored. It declares the `CurveEvaluator` seam but evaluates +nothing; `axiolid-evaluate` does that. Composite, trimmed, offset and +surface-bound curves are relations in `axiolid-model`, which keeps curves +and surfaces free of a dependency cycle. + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-linear`](./axiolid-linear) + +## Changes + +Latest release, 0.3.1 (2026-09-27): + +### Added + +- `Field2::value` and `SeriesField2::value` evaluate the value alone, + without the jet, and agree with `jet` to the last bit. +- Bridge cells in `ImplicitCurve2` (`ImplicitCell::bridge`, a cubic into a + point where two branches cross, bounded by its Bezier control values), + `ImplicitCell::part`, `reversed` and `solved_range`, + `ImplicitCurve2::solve_cell`, and `Field2::scale_at` (the size of the + terms that make up the value at a point, which its rounding scales + with). + +- `Curve3::PairSection` (`PairSection3`, `PairNode`): the section of two + B-spline surfaces, carried by nodes on both and defined between them by + the surfaces themselves (where both meet on the plane across the chord). + It has `solve` (the parameters on both surfaces and the point), `rates`, + `second_rates`, `sub`, `reversed`, `parameter_of` and `side`, and + `pair_section::solve4` for the 4x4 systems behind it (ADR 0077). +- `BSplineSurface` is defined here, and `axiolid_surface` re-exports it + unchanged, so a traced curve can carry a B-spline carrier + (`Carrier::Spline`). It has `BSplineSurface::jet` (point and first and + second partials, rational) and `domain`. +- `Field2` is now an enum of `SeriesField2` (the former struct: powers and + harmonics) and `PatchField2` (piecewise Bernstein polynomials on a grid, + bounded by their coefficients over any box and continued past the grid by + their edge polynomials). `ImplicitCurve2::clipped` cuts a curve to a box. +- `Curve2::Lifted(LiftedCurve2)`: a space curve read in an analytic + surface's parameters, sharing the curve's parameter. It is the pcurve, + on the analytic face, of a section only a B-spline can carry. + +- `ImplicitCurve2::sub`, `rotated`, `reversed`, `shifted`, `closure` and + `turning_points`. `implicit::{bound, bound_simple, partial}` give + interval bounds and partial derivatives of a `Field2` over parameter + boxes. + +- `Curve2::Implicit(ImplicitCurve2)` and + `Curve3::ImplicitSection(ImplicitSection3)` (#119, ADR 0077): a stretch of + a `Field2`'s zero set in monotone cells, where each point is the field's + unique root in its cell's bracket, and the same curve on its analytic + `Carrier` (plane, ruled surface, sphere or torus). + +- `Curve2::QuadraticGraph(QuadraticGraph2)` and + `Curve3::RuledSection(RuledSection3)` (#119, ADR 0076): one root branch of + `a(t) v^2 + b(t) v + c(t) = 0` with degree-2 trigonometric coefficients + (`Trig2`, `Branch`), and the same curve lifted onto a cylinder, + elliptical cylinder or cone (`RuledCarrier`). The exact pcurve and edge + of a quadric's cut across a ruled surface. +- `Curve2::AngleGraph(AngleGraph2)` and `Curve3::TorusSection(TorusSection3)` + (#119, ADR 0076): the solution `u(t)` of `a(t) cos u + b(t) sin u = c(t)`, + and the same curve on a torus (`TorusCarrier`). The exact pcurve and edge + of a plane's or sphere's cut across a torus. + +- `Curve2::Sinusoid(Sinusoid2)`: the graph `v = mean + a cos(t) + b sin(t)`, + the exact pcurve of a plane's cut across a cylinder in its (angle, height) + parameters (ADR 0071). The parameter is the first coordinate. Additive: + `Curve2` is `#[non_exhaustive]`. + +Full history: [`crates/representations/analytic/curve/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/representations/analytic/curve/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-decimate.md b/docs/reference/crates/axiolid-decimate.md new file mode 100644 index 00000000..a84e29b1 --- /dev/null +++ b/docs/reference/crates/axiolid-decimate.md @@ -0,0 +1,39 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-decimate + +Edge-collapse mesh decimation with a bounded, reported deviation. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-decimate`](https://crates.io/crates/axiolid-decimate) | +| Layer | algorithms (`algorithm.discrete`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_decimate/index.html) · [docs.rs](https://docs.rs/axiolid-decimate) | +| Source | [`crates/algorithms/discrete/decimate/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/discrete/decimate) | + +## Overview + +Edge-collapse decimation of triangle meshes with a bounded, reported +deviation. The caller asks for a triangle budget or a maximum deviation; +either way the result never moves a vertex further than the caller's +bound, and `DecimateReport` states the collapses performed, the refusals +by cause and the largest distance any vertex actually moved. Collapses that +would invert a triangle or create a non-manifold edge are refused. Output +is deterministic. It does not remesh isotropically, detect sharp features +or use quadric error metrics: the cost is edge length and the new vertex +is the edge midpoint. For adding triangles instead, see `axiolid-refine`. + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-measure`](./axiolid-measure) +- [`axiolid-mesh`](./axiolid-mesh) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/algorithms/discrete/decimate/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/discrete/decimate/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-decompose.md b/docs/reference/crates/axiolid-decompose.md new file mode 100644 index 00000000..ebf47ce1 --- /dev/null +++ b/docs/reference/crates/axiolid-decompose.md @@ -0,0 +1,41 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-decompose + +Convex decomposition of a solid, exact or approximate and always labelled. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-decompose`](https://crates.io/crates/axiolid-decompose) | +| Layer | algorithms (`algorithm.discrete`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_decompose/index.html) · [docs.rs](https://docs.rs/axiolid-decompose) | +| Source | [`crates/algorithms/discrete/decompose/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/discrete/decompose) | + +## Overview + +Convex decomposition of a closed triangle-mesh solid. `Strategy::Exact` +splits at reflex features until every part is convex and the union +reproduces the input; `Strategy::Approximate` stops once each part is +within a stated concavity bound, giving far fewer parts. The returned +`Decomposition` always says which it is, and the approximate path reports +the concavity it actually reached. It works on meshes only, not on exact +B-reps. + +## Depends on + +- [`axiolid-construct`](./axiolid-construct) +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-measure`](./axiolid-measure) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-mesh-boolean-contract`](./axiolid-mesh-boolean-contract) +- [`axiolid-reference`](./axiolid-reference) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/algorithms/discrete/decompose/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/discrete/decompose/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-dispatch.md b/docs/reference/crates/axiolid-dispatch.md new file mode 100644 index 00000000..b787e30b --- /dev/null +++ b/docs/reference/crates/axiolid-dispatch.md @@ -0,0 +1,55 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-dispatch + +Provider registration, ordering, fallback, and execution policy. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-dispatch`](https://crates.io/crates/axiolid-dispatch) | +| Facade | [`axiolid`](./axiolid) feature `dispatch-mesh-boolean`, `dispatch-mesh-section`, `dispatch-pointcloud-reconstruction` | +| Layer | execution (`execution.dispatch`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_dispatch/index.html) · [docs.rs](https://docs.rs/axiolid-dispatch) | +| Source | [`crates/execution/dispatch/`](https://github.com/axiolid/kernel/tree/main/crates/execution/dispatch) | + +## Overview + +Runtime registries for operation providers: `MeshBooleanRegistry`, +`MeshPlaneSectionRegistry` and `PointcloudReconstructionRegistry`, each +behind its own feature. A registry owns provider ordering, device matching, +fallback to the next provider, and memory-budget admission. It defines no +request or result types: those live in the operation-contract crates, and +providers implement them without depending on this crate. The `parallel` +feature scopes each dispatched call to a caller-owned CPU pool. + +## Features + +Default: none. + +| Feature | Enables | +| --- | --- | +| `mesh-boolean` | [`axiolid-core`](./axiolid-core), [`axiolid-mesh`](./axiolid-mesh), [`axiolid-mesh-contracts`](./axiolid-mesh-contracts), [`axiolid-mesh-boolean-contract`](./axiolid-mesh-boolean-contract) | +| `mesh-section` | [`axiolid-core`](./axiolid-core), [`axiolid-mesh`](./axiolid-mesh), [`axiolid-mesh-contracts`](./axiolid-mesh-contracts), [`axiolid-mesh-section-contract`](./axiolid-mesh-section-contract) | +| `parallel` | [`axiolid-backend-cpu`](./axiolid-backend-cpu), `axiolid-backend-cpu/parallel` | +| `pointcloud-reconstruction` | [`axiolid-pointcloud`](./axiolid-pointcloud), [`axiolid-pointcloud-reconstruction-contract`](./axiolid-pointcloud-reconstruction-contract) | + +## Depends on + +- [`axiolid-backend-cpu`](./axiolid-backend-cpu) +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-mesh-boolean-contract`](./axiolid-mesh-boolean-contract) +- [`axiolid-mesh-contracts`](./axiolid-mesh-contracts) +- [`axiolid-mesh-section-contract`](./axiolid-mesh-section-contract) +- [`axiolid-pointcloud`](./axiolid-pointcloud) +- [`axiolid-pointcloud-reconstruction-contract`](./axiolid-pointcloud-reconstruction-contract) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/execution/dispatch/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/execution/dispatch/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-evaluate.md b/docs/reference/crates/axiolid-evaluate.md new file mode 100644 index 00000000..30023671 --- /dev/null +++ b/docs/reference/crates/axiolid-evaluate.md @@ -0,0 +1,69 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-evaluate + +Analytic and spline curve/surface evaluation, jets, and inversion. + +| | | +| --- | --- | +| Latest release | 0.3.1 (2026-09-27) | +| crates.io | [`axiolid-evaluate`](https://crates.io/crates/axiolid-evaluate) | +| Facade | [`axiolid`](./axiolid) feature `evaluate` | +| Layer | algorithms (`algorithm.parametric`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_evaluate/index.html) · [docs.rs](https://docs.rs/axiolid-evaluate) | +| Source | [`crates/algorithms/parametric/evaluate/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/parametric/evaluate) | + +## Overview + +The scalar evaluation oracle for parametric geometry (ADR 0012, ADR 0036): +native-domain evaluation of analytic and B-spline curves and surfaces, +derivatives, jets, adaptive flattening, elementary surface inversion, +arc-length evaluation of intrinsic (natural-equation) and elevated curves, +and `ReferenceCurveEvaluator`, the reference implementation of the +curve-evaluation contract (ADR 0063). It has no mesh, spatial, measure or +provider dependency, so a parametric consumer gets evaluation without the +`axiolid-reference` umbrella. It favours obvious correctness over speed: no +intrinsics, threading or feature gates. + +## Depends on + +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-curve`](./axiolid-curve) +- [`axiolid-curve-evaluate-contract`](./axiolid-curve-evaluate-contract) +- [`axiolid-surface`](./axiolid-surface) + +## Changes + +Latest release, 0.3.1 (2026-09-27): + +### Added + +- Evaluation, derivatives and inversion of `Curve3::PairSection`. A + `Curve2::Lifted` reading a pair section on one of its own B-spline + surfaces takes that surface's parameters straight from the solve. +- `surface::locate`, `curve::locate2` and `curve::locate3`: parameters of + a point, iterating where no closed form exists (B-spline surfaces and + curves: seeded Newton, verified by the round trip). `invert`, `invert2` + and `invert3` keep their closed-form-only contract. +- Evaluation, derivatives and inversion of `Curve2::Lifted`. An + `ImplicitSection` on a B-spline carrier is inverted through the + surface's `locate`. + +- Evaluation, derivatives and inversion of `Curve2::Implicit` and + `Curve3::ImplicitSection` (ADR 0077). `invert2` and `invert3` now also + cover `QuadraticGraph`, `AngleGraph`, `RuledSection` and `TorusSection`, + reading the angle off the point and trying whole turns. + +- Evaluation, first and second derivatives of `Curve2::QuadraticGraph`, + `Curve3::RuledSection`, `Curve2::AngleGraph` and `Curve3::TorusSection` + (#119, ADR 0076); a parameter outside the graph's + spans is refused, not extrapolated. + +- `Curve2::Sinusoid` evaluation: point, first and second derivative, a + one-turn domain, and exact inversion (the parameter is the point's first + coordinate, then its height is checked) (ADR 0071). + +Full history: [`crates/algorithms/parametric/evaluate/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/parametric/evaluate/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-exact-compile-contract.md b/docs/reference/crates/axiolid-exact-compile-contract.md new file mode 100644 index 00000000..d74e72fa --- /dev/null +++ b/docs/reference/crates/axiolid-exact-compile-contract.md @@ -0,0 +1,35 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-exact-compile-contract + +Portable graph-to-exact-B-rep compilation contract. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-exact-compile-contract`](https://crates.io/crates/axiolid-exact-compile-contract) | +| Layer | contracts (`contract.operation`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_exact_compile_contract/index.html) · [docs.rs](https://docs.rs/axiolid-exact-compile-contract) | +| Source | [`crates/contracts/operations/exact-compile/`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/exact-compile) | + +## Overview + +The contract for compiling an `axiolid-model` geometry graph into an exact +B-rep. An implementation either preserves analytic supports and trims or +refuses; there is deliberately no variant that returns a mesh, so a caller +that asked for exactness is never handed an approximation. It is a contract +only; `axiolid-mesh-compile` provides an implementation. + +## Depends on + +- [`axiolid-brep`](./axiolid-brep) +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-model`](./axiolid-model) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/contracts/operations/exact-compile/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/contracts/operations/exact-compile/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-exact.md b/docs/reference/crates/axiolid-exact.md new file mode 100644 index 00000000..188c14a1 --- /dev/null +++ b/docs/reference/crates/axiolid-exact.md @@ -0,0 +1,53 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-exact + +Filtered exact arithmetic: interval filter, dyadic big integers, a + b*sqrt(c). + +| | | +| --- | --- | +| Latest release | 0.1.1 (2026-09-28) | +| crates.io | [`axiolid-exact`](https://crates.io/crates/axiolid-exact) | +| Layer | algorithms (`algorithm.reference`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_exact/index.html) · [docs.rs](https://docs.rs/axiolid-exact) | +| Source | [`crates/algorithms/exact/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/exact) | + +## Overview + +Filtered exact arithmetic for *constructions* over `f64` input (ADR 0068): +signs and orderings of values `f64` cannot hold, such as where two segments +cross, where a line meets a circle, or roots of the form +`(a + b*sqrt(c)) / d`. Each sign question is one expression evaluated first +in outward-rounded interval arithmetic and, only if that cannot decide, in +exact dyadic big-integer arithmetic (`num-bigint`). It has no division and +evaluates no square roots; approximate values are available for output +only. + +## Design notes + +Choosing between this crate and `axiolid-predicates`: if the question is the +sign of a polynomial in the *input* coordinates (orientation, in-circle), +use `axiolid-predicates`, which needs no big integers. If the question is +about a *constructed* point (a crossing, a line/circle hit), use this crate. + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-guarantees`](./axiolid-guarantees) + +## Changes + +Latest release, 0.1.1 (2026-09-28): + +### Added + +- `FixedInterval`: a real number between two big-integer bounds at a + chosen number of fractional bits, every operation rounded outward, and + `FixedInterval::sin_cos`, certified `sin` and `cos` of a dyadic angle + (Taylor series with the Lagrange remainder, after halving the angle). + For signs of values no finite arithmetic holds exactly, asked again at + a higher precision until they show (#181). + +Full history: [`crates/algorithms/exact/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/exact/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-field-ops.md b/docs/reference/crates/axiolid-field-ops.md new file mode 100644 index 00000000..a12909c1 --- /dev/null +++ b/docs/reference/crates/axiolid-field-ops.md @@ -0,0 +1,45 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-field-ops + +Sampling, morphology, clearance, and navigation over Axiolid layered fields. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-field-ops`](https://crates.io/crates/axiolid-field-ops) | +| Facade | [`axiolid`](./axiolid) feature `field-ops` | +| Layer | algorithms (`algorithm.sampled`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_field_ops/index.html) · [docs.rs](https://docs.rs/axiolid-field-ops) | +| Source | [`crates/algorithms/sampled/field/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/sampled/field) | + +## Overview + +Deterministic algorithms over `axiolid-field` layered fields: scalar CPU triangle coverage, planar masks with metric dilation, erosion and connected components, clearance along the layering axis, and, behind the opt-in `navigation` feature, geometry-only route finding under an explicit agent envelope. It reports coverage, spans, components and route existence, never an application verdict such as accessibility or compliance. + +## Design notes + +There is no GPU coverage provider. The CPU sampler is per-cell independent work over a flat +triangle slice, so a batch provider can be added once a benchmark on an agreed workload shows it +pays; without that evidence it would be complexity with no measured benefit. + +## Features + +Default: none. + +| Feature | Enables | +| --- | --- | +| `navigation` | — | + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-field`](./axiolid-field) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/algorithms/sampled/field/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/sampled/field/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-field.md b/docs/reference/crates/axiolid-field.md new file mode 100644 index 00000000..ecec279a --- /dev/null +++ b/docs/reference/crates/axiolid-field.md @@ -0,0 +1,35 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-field + +Frame-neutral layered spatial-field values and validated sampling configuration. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-field`](https://crates.io/crates/axiolid-field) | +| Facade | [`axiolid`](./axiolid) feature `field` | +| Layer | representations (`representation.sampled`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_field/index.html) · [docs.rs](https://docs.rs/axiolid-field) | +| Source | [`crates/representations/sampled/field/`](https://github.com/axiolid/kernel/tree/main/crates/representations/sampled/field) | + +## Overview + +Frame-neutral, deterministic layered spatial-field values: row-major cells +in an explicit frame, with surface crossings and positive-length occupancy +spans kept in separate channels so a zero-thickness facet never reads as +filled space. It owns the values, their validation and caller-supplied +configuration and budgets. Sampling, morphology, clearance and navigation +live in `axiolid-field-ops`. + +## Depends on + +- [`axiolid-core`](./axiolid-core) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/representations/sampled/field/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/representations/sampled/field/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-fixtures.md b/docs/reference/crates/axiolid-fixtures.md new file mode 100644 index 00000000..d6c52dff --- /dev/null +++ b/docs/reference/crates/axiolid-fixtures.md @@ -0,0 +1,37 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-fixtures + +Shared adversarial and degenerate geometry fixtures with provenance. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-fixtures`](https://crates.io/crates/axiolid-fixtures) | +| Layer | representations (`representation.fixtures`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_fixtures/index.html) · [docs.rs](https://docs.rs/axiolid-fixtures) | +| Source | [`crates/representations/fixtures/`](https://github.com/axiolid/kernel/tree/main/crates/representations/fixtures) | + +## Overview + +A shared corpus of adversarial and degenerate mesh fixtures (sliver and +zero-area triangles, open shells, extreme scale disparity, coplanar contact +between operands, and the like), each with a `Provenance` saying where the +case comes from, its licence, and what an implementation must do with it. +Fixtures are built in code rather than stored as files, so the exact bit +patterns that make them degenerate cannot be rounded away by an exporter. +Differential tests, for example in `axiolid-mesh-boolean-boolmesh`, iterate +`corpus()`. Every fixture is original work under the repository licence. + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-mesh`](./axiolid-mesh) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/representations/fixtures/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/representations/fixtures/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-guarantees.md b/docs/reference/crates/axiolid-guarantees.md new file mode 100644 index 00000000..54131d98 --- /dev/null +++ b/docs/reference/crates/axiolid-guarantees.md @@ -0,0 +1,28 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-guarantees + +Certified-value and escalation vocabulary for geometry contracts. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-guarantees`](https://crates.io/crates/axiolid-guarantees) | +| Layer | contracts (`contract.guarantees`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_guarantees/index.html) · [docs.rs](https://docs.rs/axiolid-guarantees) | +| Source | [`crates/contracts/guarantees/`](https://github.com/axiolid/kernel/tree/main/crates/contracts/guarantees) | + +## Overview + +Provider-neutral vocabulary for what a geometric answer is worth: certified +values, signs that may be indeterminate, reported precision, and the +escalation ladder a provider climbs when a fast answer is not certain. It +depends on no representation or provider, so any contract can use it. + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/contracts/guarantees/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/contracts/guarantees/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-heal.md b/docs/reference/crates/axiolid-heal.md new file mode 100644 index 00000000..6127dfed --- /dev/null +++ b/docs/reference/crates/axiolid-heal.md @@ -0,0 +1,44 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-heal + +Explicit diagnosis and opt-in repair contracts for dirty geometry. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-heal`](https://crates.io/crates/axiolid-heal) | +| Facade | [`axiolid`](./axiolid) feature `heal` | +| Layer | algorithms (`algorithm.repair`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_heal/index.html) · [docs.rs](https://docs.rs/axiolid-heal) | +| Source | [`crates/algorithms/repair/heal/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/repair/heal) | + +## Overview + +Explicit diagnosis and opt-in repair of triangle meshes. `diagnose` reports non-manifold edges, inconsistent winding, boundary edges, duplicate vertices, degenerate triangles and exact self-intersections without touching the mesh. Repairs (weld vertices, drop degenerate elements, unify orientation, orient outward) run only when a caller names them in a `RepairPlan`, and the `RepairReport` records what was applied, what was skipped and what happened to each attribute channel. There is no repair-everything mode, and no other operation heals implicitly. + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-guarantees`](./axiolid-guarantees) +- [`axiolid-measure`](./axiolid-measure) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-predicates`](./axiolid-predicates) +- [`axiolid-spatial`](./axiolid-spatial) + +## Changes + +Latest release, 0.3.0 (2026-09-23): + +### Added + +- Repairs carry corner-indexed channels: weld leaves them untouched (they index values, not positions, so a seam is lossless), dropping and flipping triangles move their entries (#112). + +### Fixed + +- Repairs keep attribute channels and normals in step with the geometry they rewrite (#114). Weld compacts per-vertex channels and normals; a seam drops the channel by name, a hard edge switches normals to corner-indexed. Dropping or flipping triangles moves corner-indexed normals with them. +- `RepairReport::attribute_fates` names every input channel's fate. + +Full history: [`crates/algorithms/repair/heal/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/repair/heal/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-inspect.md b/docs/reference/crates/axiolid-inspect.md new file mode 100644 index 00000000..119f12b7 --- /dev/null +++ b/docs/reference/crates/axiolid-inspect.md @@ -0,0 +1,47 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-inspect + +Mesh queries: clearance, containment, ray casting, and genus. + +| | | +| --- | --- | +| Latest release | 0.3.3 (2026-09-27) | +| crates.io | [`axiolid-inspect`](https://crates.io/crates/axiolid-inspect) | +| Layer | algorithms (`algorithm.query`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_inspect/index.html) · [docs.rs](https://docs.rs/axiolid-inspect) | +| Source | [`crates/algorithms/query/inspect/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/query/inspect) | + +## Overview + +Queries over triangle meshes: clearance between meshes, point containment and winding number, ray casting, line of sight, genus and per-component topology, plane detection, and intersection or difference volumes with a certified error bound. Containment reuses the exact ray-parity test of the mesh boolean, so the two cannot disagree. Every query reports a measurement or a typed refusal and leaves the verdict ("too close", "hidden") to the caller. + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-exact`](./axiolid-exact) +- [`axiolid-guarantees`](./axiolid-guarantees) +- [`axiolid-heal`](./axiolid-heal) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-predicates`](./axiolid-predicates) +- [`axiolid-spatial`](./axiolid-spatial) + +## Changes + +Latest release, 0.3.3 (2026-09-27): + +### Added + +- `topology` (#144): every connected component of a two-manifold triangle + mesh, classified exactly from its connectivity -- counts, Euler + characteristic, boundary loops, orientability and consistent winding, and + the surface (`SurfaceKind::Orientable { genus }` or + `NonOrientable { crosscaps }`). A closed orientable component also gets a + basis of its first homology: `2g` simple closed edge loops, by the + tree-cotree construction. Meshes with an edge on three or more triangles, + or a vertex whose triangles form several fans, are refused + (`TopologyError::NonManifold`). `genus` is unchanged. + +Full history: [`crates/algorithms/query/inspect/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/query/inspect/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-levelset.md b/docs/reference/crates/axiolid-levelset.md new file mode 100644 index 00000000..f218b025 --- /dev/null +++ b/docs/reference/crates/axiolid-levelset.md @@ -0,0 +1,30 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-levelset + +Level-set extraction: a closed manifold mesh from a sampled scalar field. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-levelset`](https://crates.io/crates/axiolid-levelset) | +| Layer | algorithms (`algorithm.sampled`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_levelset/index.html) · [docs.rs](https://docs.rs/axiolid-levelset) | +| Source | [`crates/algorithms/sampled/levelset/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/sampled/levelset) | + +## Overview + +Level-set extraction: a closed two-manifold triangle mesh from a scalar field sampled on a grid. Cells are split into Kuhn tetrahedra rather than marched as cubes, so shared faces always split the same way and the result is watertight by construction; exact grid tangency is resolved by simulation of simplicity. The mesh interpolates the field linearly along cell edges, so it is an approximation whose error shrinks with the grid, not a certified surface. + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-mesh`](./axiolid-mesh) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/algorithms/sampled/levelset/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/sampled/levelset/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-linear-intersection.md b/docs/reference/crates/axiolid-linear-intersection.md new file mode 100644 index 00000000..67857ef8 --- /dev/null +++ b/docs/reference/crates/axiolid-linear-intersection.md @@ -0,0 +1,33 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-linear-intersection + +Portable, deterministic intersections for linear geometry. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-linear-intersection`](https://crates.io/crates/axiolid-linear-intersection) | +| Facade | [`axiolid`](./axiolid) feature `linear-intersection` | +| Layer | algorithms (`algorithm.query`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_linear_intersection/index.html) · [docs.rs](https://docs.rs/axiolid-linear-intersection) | +| Source | [`crates/algorithms/query/intersection/linear/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/query/intersection/linear) | + +## Overview + +Certified 2D intersections for lines and segments, with a minimal dependency closure so a line-query application does not pull in curves, surfaces, meshes or B-rep (ADR 0036). Results are classifications, not optional points: crossing, endpoint contact, parallel-disjoint, coincident, collinear-disjoint and overlap are distinct variants. Topology comes from certified predicates; the tolerance only governs acceptance of the computed coordinate. Invalid input is a typed refusal naming the operand. + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-guarantees`](./axiolid-guarantees) +- [`axiolid-linear`](./axiolid-linear) +- [`axiolid-predicates`](./axiolid-predicates) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/algorithms/query/intersection/linear/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/query/intersection/linear/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-linear.md b/docs/reference/crates/axiolid-linear.md new file mode 100644 index 00000000..2c32db56 --- /dev/null +++ b/docs/reference/crates/axiolid-linear.md @@ -0,0 +1,35 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-linear + +Format-neutral line, ray, segment, and polyline representations. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-linear`](https://crates.io/crates/axiolid-linear) | +| Facade | [`axiolid`](./axiolid) feature `linear` | +| Layer | representations (`representation.atomic`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_linear/index.html) · [docs.rs](https://docs.rs/axiolid-linear) | +| Source | [`crates/representations/analytic/linear/`](https://github.com/axiolid/kernel/tree/main/crates/representations/analytic/linear) | + +## Overview + +Format-neutral linear values: lines, rays, segments and polylines in 2D and +3D. It holds data only (no evaluation, tolerance policy or algorithms) and +depends only on `axiolid-core`, so an application that needs lines alone +does not compile curves, surfaces, meshes or topology. `axiolid-curve` +re-exports these types unchanged; use this crate when lines are all you +need. + +## Depends on + +- [`axiolid-core`](./axiolid-core) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/representations/analytic/linear/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/representations/analytic/linear/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-measure.md b/docs/reference/crates/axiolid-measure.md new file mode 100644 index 00000000..b162cb03 --- /dev/null +++ b/docs/reference/crates/axiolid-measure.md @@ -0,0 +1,53 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-measure + +Metric properties: area, volume, centroid, moments of inertia. + +| | | +| --- | --- | +| Latest release | 0.3.2 (2026-09-27) | +| crates.io | [`axiolid-measure`](https://crates.io/crates/axiolid-measure) | +| Facade | [`axiolid`](./axiolid) feature `measure` | +| Layer | algorithms (`algorithm.query`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_measure/index.html) · [docs.rs](https://docs.rs/axiolid-measure) | +| Source | [`crates/algorithms/query/measure/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/query/measure) | + +## Overview + +Metric properties of geometry: surface area, signed volume, centroids and second moments of triangle meshes, closest points and distances between segments, triangles and meshes, Frechet distance between polylines, and winding numbers. Undefined quantities are refused: an open or non-manifold mesh gets an error, not a plausible volume. The optional `exact` feature adds mass properties and certified boundary distance for exact B-reps without imposing them on mesh-only consumers. + +## Features + +Default: none. + +| Feature | Enables | +| --- | --- | +| `exact` | [`axiolid-brep`](./axiolid-brep), [`axiolid-curve`](./axiolid-curve), [`axiolid-evaluate`](./axiolid-evaluate), [`axiolid-surface`](./axiolid-surface), [`axiolid-topology`](./axiolid-topology) | + +## Depends on + +- [`axiolid-brep`](./axiolid-brep) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-curve`](./axiolid-curve) +- [`axiolid-evaluate`](./axiolid-evaluate) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-surface`](./axiolid-surface) +- [`axiolid-topology`](./axiolid-topology) + +## Changes + +Latest release, 0.3.2 (2026-09-27): + +### Added + +- Fréchet distance between polylines (#147): `frechet_distance` (the + continuous distance: the least critical value of the free space that the + Alt-Godau decision accepts), `discrete_frechet_distance` (Eiter-Mannila, + `O(nm)` time, `O(m)` memory) and the decision `frechet_at_most`, each + with a `_2d` form. Empty polylines, non-finite points and an invalid + leash are `FrechetError`s. Floating point, not certified. + +Full history: [`crates/algorithms/query/measure/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/query/measure/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-mesh-boolean-boolmesh.md b/docs/reference/crates/axiolid-mesh-boolean-boolmesh.md new file mode 100644 index 00000000..f5e3a8ed --- /dev/null +++ b/docs/reference/crates/axiolid-mesh-boolean-boolmesh.md @@ -0,0 +1,68 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-mesh-boolean-boolmesh + +boolmesh-backed MeshBoolean provider. + +| | | +| --- | --- | +| Latest release | 0.3.1 (2026-09-27) | +| crates.io | [`axiolid-mesh-boolean-boolmesh`](https://crates.io/crates/axiolid-mesh-boolean-boolmesh) | +| Facade | [`axiolid`](./axiolid) feature `portable-provider` | +| Layer | providers (`provider.mesh`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_mesh_boolean_boolmesh/index.html) · [docs.rs](https://docs.rs/axiolid-mesh-boolean-boolmesh) | +| Source | [`crates/providers/mesh/boolmesh/`](https://github.com/axiolid/kernel/tree/main/crates/providers/mesh/boolmesh) | + +## Overview + +A `MeshBoolean` provider for closed, outward-oriented triangle meshes: union, +intersection, difference, and symmetric difference composed from those. The +algorithm is absorbed from the `boolmesh` crate into a private module (ADR +0014, ADR 0047); this crate adds the conversion, an orientation gate on every +input, result checks, and batch overrides (`subtract_many` fuses disjoint +cutters, `union_many` reduces as a balanced tree). It also offers +`subtract_boxes_analytic`, an opt-in closed-form path for axis-aligned box +cutters in an axis-aligned box. Register it with `axiolid-dispatch` or call it +directly. + +## Design notes + +- Results carry attribute channels but no normals; derive normals from the + topology you want. +- The general path is `Determinism::Topological`. Use the analytic box path + when you need byte-identical output across processes. +- The `parallel` feature threads inside one solve and is off by default; + `parallel-batch` runs independent `union_many` pairs concurrently with + output identical to the sequential path. + +## Features + +Default: none. + +| Feature | Enables | +| --- | --- | +| `parallel` | `rayon` | +| `parallel-batch` | `rayon` | + +## Depends on + +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-mesh-boolean-contract`](./axiolid-mesh-boolean-contract) +- [`axiolid-mesh-contracts`](./axiolid-mesh-contracts) + +## Changes + +Latest release, 0.3.1 (2026-09-27): + +### Fixed + +- The scratch probe discards one warmup boolean before measuring, and + measures peaks above the bytes already live, so the first operation is + no longer charged for process startup (#110). A `scratch_bound` test + fails if any measured peak exceeds the declared 4 KiB per triangle. + +Full history: [`crates/providers/mesh/boolmesh/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/providers/mesh/boolmesh/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-mesh-boolean-contract.md b/docs/reference/crates/axiolid-mesh-boolean-contract.md new file mode 100644 index 00000000..d1f7476b --- /dev/null +++ b/docs/reference/crates/axiolid-mesh-boolean-contract.md @@ -0,0 +1,45 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-mesh-boolean-contract + +Portable mesh boolean request, result, evidence, and conformance contract. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-mesh-boolean-contract`](https://crates.io/crates/axiolid-mesh-boolean-contract) | +| Facade | [`axiolid`](./axiolid) feature `mesh-boolean` | +| Layer | contracts (`contract.operation`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_mesh_boolean_contract/index.html) · [docs.rs](https://docs.rs/axiolid-mesh-boolean-contract) | +| Source | [`crates/contracts/operations/mesh-boolean/`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/mesh-boolean) | + +## Overview + +The portable mesh-boolean contract: the `MeshBoolean` provider trait, the +evidence a provider must report with its result, and a conformance suite. +Operand admissibility comes from `axiolid-mesh-contracts`. It performs no +booleans itself; providers such as `axiolid-mesh-boolean-boolmesh` +implement it, and `axiolid-dispatch` chooses between them. + +## Depends on + +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-mesh-contracts`](./axiolid-mesh-contracts) + +## Changes + +Latest release, 0.3.0 (2026-09-23): + +### Added + +- `merge_fates`: compose per-channel fates across sequential steps (#116). + +### Fixed + +- Composed evidence reported only the last step's attribute fates: `BooleanEvidence::absorb` (the `subtract_many`/`union_many` defaults) and `symmetric_difference_via_composition` now compose them, so a channel a middle step derived or dropped is reported that way. + +Full history: [`crates/contracts/operations/mesh-boolean/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/contracts/operations/mesh-boolean/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-mesh-compile-contract.md b/docs/reference/crates/axiolid-mesh-compile-contract.md new file mode 100644 index 00000000..95c12709 --- /dev/null +++ b/docs/reference/crates/axiolid-mesh-compile-contract.md @@ -0,0 +1,46 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-mesh-compile-contract + +Portable graph-to-mesh compilation contract. + +| | | +| --- | --- | +| Latest release | 0.3.1 (2026-09-24) | +| crates.io | [`axiolid-mesh-compile-contract`](https://crates.io/crates/axiolid-mesh-compile-contract) | +| Facade | [`axiolid`](./axiolid) feature `graph-compile` | +| Layer | contracts (`contract.operation`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_mesh_compile_contract/index.html) · [docs.rs](https://docs.rs/axiolid-mesh-compile-contract) | +| Source | [`crates/contracts/operations/compile/`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/compile) | + +## Overview + +The contract for compiling an `axiolid-model` geometry graph into a +triangle mesh. The outcome says whether the mesh is closed, and the +contract never claims to preserve an exact B-rep. It is a contract only: +`axiolid-mesh-compile` implements it, and `axiolid-exact-compile-contract` +is the exact counterpart. + +## Depends on + +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-model`](./axiolid-model) + +## Changes + +Latest release, 0.3.1 (2026-09-24): + +### Added + +- `MeshClosure` and `CompileOutcome::closure` (#161): whether a compiled mesh + bounds a solid (`Solid`), is a surface model with area but no volume + (`Surface`), or was not reported (`Unknown`, the default for `untracked` + and `tracked`, so existing compilers build unchanged). + `CompileOutcome::solid_mesh` returns the mesh only for `Solid`, so volume + readers refuse a surface model instead of measuring a closed shell the + source never declared a solid. `with_closure` sets it. + +Full history: [`crates/contracts/operations/compile/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/contracts/operations/compile/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-mesh-compile.md b/docs/reference/crates/axiolid-mesh-compile.md new file mode 100644 index 00000000..77a02876 --- /dev/null +++ b/docs/reference/crates/axiolid-mesh-compile.md @@ -0,0 +1,75 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-mesh-compile + +Scalar reference MeshCompiler: profiles, extrusion, transforms, boolean dispatch. + +| | | +| --- | --- | +| Latest release | 0.3.5 (2026-09-28) | +| crates.io | [`axiolid-mesh-compile`](https://crates.io/crates/axiolid-mesh-compile) | +| Layer | execution (`execution.orchestration`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_mesh_compile/index.html) · [docs.rs](https://docs.rs/axiolid-mesh-compile) | +| Source | [`crates/execution/compile/`](https://github.com/axiolid/kernel/tree/main/crates/execution/compile) | + +## Overview + +The scalar reference compilers for an `axiolid-model` geometry graph. +`ReferenceMeshCompiler` walks the graph and produces one `TriMesh` per root: +it resolves instances and collections, composes transforms, tessellates +profiles, sweeps, B-reps and authored meshes, and hands booleans to whichever +`MeshBoolean` provider it is given. `ReferenceExactCompiler` compiles the +families it supports to exact B-reps and refuses the rest by name. This crate +owns graph traversal and dispatch; the construction algorithms themselves +(profile flattening, extrusion, revolution, sweeps) belong to +`axiolid-construct`. + +## Design notes + +- The caller always supplies the tolerance through `ExecutionOptions`; + there is no built-in default. Without an explicit chord budget, curves are + flattened to the linear tolerance. +- Read volume through `CompileOutcome::solid_mesh`: a surface model can + compile to a closed mesh without bounding a solid. + +## Depends on + +- [`axiolid-brep`](./axiolid-brep) +- [`axiolid-construct`](./axiolid-construct) +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-curve`](./axiolid-curve) +- [`axiolid-exact-compile-contract`](./axiolid-exact-compile-contract) +- [`axiolid-guarantees`](./axiolid-guarantees) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-mesh-boolean-contract`](./axiolid-mesh-boolean-contract) +- [`axiolid-mesh-compile-contract`](./axiolid-mesh-compile-contract) +- [`axiolid-mesh-contracts`](./axiolid-mesh-contracts) +- [`axiolid-model`](./axiolid-model) +- [`axiolid-primitive`](./axiolid-primitive) +- [`axiolid-profile`](./axiolid-profile) +- [`axiolid-reference`](./axiolid-reference) +- [`axiolid-surface`](./axiolid-surface) +- [`axiolid-topology`](./axiolid-topology) + +## Changes + +Latest release, 0.3.5 (2026-09-28): + +### Fixed + +- A boolean whose result touches itself is refused, not returned (#194): + where operands meet tangentially -- a void tangent to its host's face -- + the solid has no material between two faces, and the mesh boolean keeps + two copies of the vertices there, closed by index but pinched by + position. Consumers welding by position saw an edge with four faces. The + result is now checked, positions welded, for an edge with more than two + faces or a vertex with separate fans, and refused with + `GeomError::Degenerate` naming the edge or point of contact. Circular + voids tangent along an axis direction no longer pinch at all (construct). +- Structural sections and rounded rectangles extrude to meshes (#193, via + construct). + +Full history: [`crates/execution/compile/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/execution/compile/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-mesh-contracts.md b/docs/reference/crates/axiolid-mesh-contracts.md new file mode 100644 index 00000000..6e4db753 --- /dev/null +++ b/docs/reference/crates/axiolid-mesh-contracts.md @@ -0,0 +1,35 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-mesh-contracts + +Shared mesh admissibility contracts. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-mesh-contracts`](https://crates.io/crates/axiolid-mesh-contracts) | +| Facade | [`axiolid`](./axiolid) feature `mesh-contracts` | +| Layer | contracts (`contract.common.mesh`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_mesh_contracts/index.html) · [docs.rs](https://docs.rs/axiolid-mesh-contracts) | +| Source | [`crates/contracts/common/mesh/`](https://github.com/axiolid/kernel/tree/main/crates/contracts/common/mesh) | + +## Overview + +Shared admissibility rules for mesh-valued operations: what a triangle mesh +must satisfy to be accepted as a solid operand, with a typed rejection when +it does not. Axiolid owns this definition so that every provider accepts +exactly the same inputs; it does not select or run a provider. + +## Depends on + +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-mesh`](./axiolid-mesh) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/contracts/common/mesh/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/contracts/common/mesh/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-mesh-section-contract.md b/docs/reference/crates/axiolid-mesh-section-contract.md new file mode 100644 index 00000000..53f5c85c --- /dev/null +++ b/docs/reference/crates/axiolid-mesh-section-contract.md @@ -0,0 +1,36 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-mesh-section-contract + +Portable mesh plane-section request, result, and evidence contract. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-mesh-section-contract`](https://crates.io/crates/axiolid-mesh-section-contract) | +| Facade | [`axiolid`](./axiolid) feature `mesh-section` | +| Layer | contracts (`contract.operation`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_mesh_section_contract/index.html) · [docs.rs](https://docs.rs/axiolid-mesh-section-contract) | +| Source | [`crates/contracts/operations/mesh-section/`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/mesh-section) | + +## Overview + +The portable contract for cutting a triangle mesh with a plane: limits, +section contours, evidence, and a conformance suite. It computes nothing +itself; providers implement `MeshPlaneSection`, and `axiolid-dispatch` +selects one. See ADR 0033. + +## Depends on + +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-mesh-contracts`](./axiolid-mesh-contracts) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/contracts/operations/mesh-section/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/contracts/operations/mesh-section/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-mesh.md b/docs/reference/crates/axiolid-mesh.md new file mode 100644 index 00000000..373c8519 --- /dev/null +++ b/docs/reference/crates/axiolid-mesh.md @@ -0,0 +1,55 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-mesh + +Triangle meshes: the discrete representation every backend consumes. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-mesh`](https://crates.io/crates/axiolid-mesh) | +| Facade | [`axiolid`](./axiolid) feature `mesh` | +| Layer | representations (`representation.discrete`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_mesh/index.html) · [docs.rs](https://docs.rs/axiolid-mesh) | +| Source | [`crates/representations/discrete/mesh/`](https://github.com/axiolid/kernel/tree/main/crates/representations/discrete/mesh) | + +## Overview + +Triangle and polygon mesh values: `TriMesh` as the compact exchange type, +`PolygonMesh` keeping n-gons and holes until explicit triangulation, u32 +indices, named per-vertex and per-corner attribute channels, derived edge +adjacency, connected components, and a deterministic structural audit that +reports defects instead of rejecting dirty input. `MeshView` and +`TriangleMeshView` let a foreign mesh be read without copying it. Mesh +operations such as booleans, sections and repair live in other crates. + +## Design notes + +Rendering appearance (materials, shaders, colours as styling) is not part of a +mesh value. Data that must follow the geometry, such as a material id per +vertex, travels as an attribute channel. + +## Depends on + +- [`axiolid-core`](./axiolid-core) + +## Changes + +Latest release, 0.3.0 (2026-09-23): + +### Added + +- `AttributeFate::then`: the fate of a channel through two sequential steps (dropped wins and keeps the first reason; any interpolation interpolates). +- Corner-indexed attribute channels (#112): `AttributeChannel::corner_indices`, one entry per triangle corner, mirroring `NormalAttribute::indices`. Source formats store texture coordinates this way; positions stay shared, so UV seams no longer force a choice between splitting vertices (breaking closure) and smearing values. +- `AttributeChannel::corner_indexed`, `is_corner_indexed`, `value_count`, `at_corner` (reads either addressing, `None` for an unmapped corner), and `AttributeChannel::UNMAPPED` for triangles that carry no value. +- `validate_structure` checks corner channels: whole tuples, one entry per corner, entries in range, and each triangle fully mapped or fully unmapped. New `MeshValidationError` variants name the channel. +- `DropReason::ConflictingValues`: merged vertices carried different values, so a per-vertex channel could not keep both (#114). +- `DropReason::IncompatibleChannels`: inputs being combined define one channel name with a different width or blend (#115). + +### Changed + +- **Breaking** (minor slot pre-1.0, ADR 0067): `AttributeChannel` gains the public field `corner_indices`, so struct-literal construction must add `corner_indices: None`; `AttributeChannel::new` is unaffected. `DropReason` is now `#[non_exhaustive]`, so an exhaustive `match` on it needs a wildcard arm. No caller in this workspace or in openbim does either. + +Full history: [`crates/representations/discrete/mesh/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/representations/discrete/mesh/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-minkowski.md b/docs/reference/crates/axiolid-minkowski.md new file mode 100644 index 00000000..34f5bfd3 --- /dev/null +++ b/docs/reference/crates/axiolid-minkowski.md @@ -0,0 +1,40 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-minkowski + +Minkowski sum and difference of planar-faced solids. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-minkowski`](https://crates.io/crates/axiolid-minkowski) | +| Layer | algorithms (`algorithm.discrete`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_minkowski/index.html) · [docs.rs](https://docs.rs/axiolid-minkowski) | +| Source | [`crates/algorithms/discrete/minkowski/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/discrete/minkowski) | + +## Overview + +Minkowski sum and difference of closed planar-faced (triangle-mesh) solids. +The sum of two convex solids is computed exactly as the hull of pairwise +vertex sums; non-convex operands are decomposed into convex parts and the +pairwise sums unioned through a caller-supplied mesh Boolean provider, +under a budget. The difference is computed as an erosion, not as a hull of +pairwise differences, and refuses a non-convex subject rather than return a +result that is too large. Curved operands are refused. + +## Depends on + +- [`axiolid-construct`](./axiolid-construct) +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-decompose`](./axiolid-decompose) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-mesh-boolean-contract`](./axiolid-mesh-boolean-contract) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/algorithms/discrete/minkowski/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/discrete/minkowski/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-model.md b/docs/reference/crates/axiolid-model.md new file mode 100644 index 00000000..2bda3240 --- /dev/null +++ b/docs/reference/crates/axiolid-model.md @@ -0,0 +1,50 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-model + +Format-neutral geometry item tree. The currency between a format reader and a kernel. + +| | | +| --- | --- | +| Latest release | 0.3.2 (2026-09-27) | +| crates.io | [`axiolid-model`](https://crates.io/crates/axiolid-model) | +| Facade | [`axiolid`](./axiolid) feature `model` | +| Layer | representations (`representation.graph`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_model/index.html) · [docs.rs](https://docs.rs/axiolid-model) | +| Source | [`crates/representations/modeling/graph/`](https://github.com/axiolid/kernel/tree/main/crates/representations/modeling/graph) | + +## Overview + +The format-neutral geometry graph that source adapters lower into and +kernels consume: an immutable, append-only DAG of typed nodes preserving +exact curves, surfaces, profiles, primitives, topology, curve and surface +relations, CSG instructions and instancing, alongside source meshes. Handles +are branded per graph and references must point to earlier nodes of the +right family, so cycles, dangling references and cross-graph handles cannot +be built. It evaluates, tessellates and compiles nothing, and keeps source +identifiers outside the graph. + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-curve`](./axiolid-curve) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-primitive`](./axiolid-primitive) +- [`axiolid-profile`](./axiolid-profile) +- [`axiolid-surface`](./axiolid-surface) +- [`axiolid-topology`](./axiolid-topology) + +## Changes + +Latest release, 0.3.2 (2026-09-27): + +### Fixed + +- A `SolidOperation::BoundedHalfSpace` boundary must be a 2D curve (#162). + The graph accepted a 3D curve, which the compiler refuses, so such a + graph validated and then could never compile; it is now refused when the + graph is built. + +Full history: [`crates/representations/modeling/graph/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/representations/modeling/graph/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-nurbs.md b/docs/reference/crates/axiolid-nurbs.md new file mode 100644 index 00000000..b7d59e9a --- /dev/null +++ b/docs/reference/crates/axiolid-nurbs.md @@ -0,0 +1,57 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-nurbs + +General polynomial and rational B-spline analysis and transformation algorithms. + +| | | +| --- | --- | +| Latest release | 0.3.3 (2026-09-28) | +| crates.io | [`axiolid-nurbs`](https://crates.io/crates/axiolid-nurbs) | +| Facade | [`axiolid`](./axiolid) feature `nurbs` | +| Layer | algorithms (`algorithm.parametric`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_nurbs/index.html) · [docs.rs](https://docs.rs/axiolid-nurbs) | +| Source | [`crates/algorithms/parametric/nurbs/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/parametric/nurbs) | + +## Overview + +Format-neutral algorithms over polynomial and rational B-spline curves and +surfaces: differential geometry, exact shape-preserving transforms (knot +insertion, reversal, splitting, Bezier decomposition, degree elevation), +tolerance-bounded knot removal and degree reduction, interpolation and +lofting, certified projection, inversion and intersection queries, exact +analytic curve and surface intersection, and verified periodic seams. +Lossy operations measure their deviation and refuse above the caller's +tolerance. It owns no importer, tessellator or file-format vocabulary, and +it uses `axiolid-evaluate` for evaluation rather than reimplementing it. + +## Depends on + +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-curve`](./axiolid-curve) +- [`axiolid-evaluate`](./axiolid-evaluate) +- [`axiolid-exact`](./axiolid-exact) +- [`axiolid-guarantees`](./axiolid-guarantees) +- [`axiolid-predicates`](./axiolid-predicates) +- [`axiolid-surface`](./axiolid-surface) + +## Changes + +Latest release, 0.3.3 (2026-09-28): + +### Changed + +- Traced sections decide signs of analytic series fields with certified + arithmetic where `f64` cannot (#181): exact zeros by a Laurent identity + in the harmonics' common angle, point signs by fixed-point intervals at + rising precision, and box signs by the Bernstein coefficients of a + certified Taylor form. The tier answers only where the field is flat + (along a line of contact or at a singular point), so ordinary sections + trace exactly as before. Lines of contact that rounding hides over more + than a twentieth of the window, refused as `Undecided` until now, are + traced on analytic fields as on B-spline ones (ADR 0077). + +Full history: [`crates/algorithms/parametric/nurbs/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/parametric/nurbs/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-overlay.md b/docs/reference/crates/axiolid-overlay.md new file mode 100644 index 00000000..c07d8329 --- /dev/null +++ b/docs/reference/crates/axiolid-overlay.md @@ -0,0 +1,61 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-overlay + +Deterministic validated planar overlay contract. + +| | | +| --- | --- | +| Latest release | 0.3.5 (2026-09-28) | +| crates.io | [`axiolid-overlay`](https://crates.io/crates/axiolid-overlay) | +| Facade | [`axiolid`](./axiolid) feature `overlay` | +| Layer | algorithms (`algorithm.planar`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_overlay/index.html) · [docs.rs](https://docs.rs/axiolid-overlay) | +| Source | [`crates/algorithms/planar/overlay/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/planar/overlay) | + +## Overview + +Validated, deterministic planar booleans (intersection, union, difference, xor) and offsets over regions with holes, plus the planar operations built on them: arc-aware booleans and arrangements, polyline strokes, Minkowski morphology bounds, minimum enclosing circles and rectangles, and visibility. Inputs are validated and refused with a typed error rather than repaired. It answers a query and keeps no structure; editable subdivisions with persistent identity live in `axiolid-arrangement`. + +## Design notes + +- Straight-edged booleans (`Region`, `overlay`, `union_soup`) and arc-aware ones (`arc_overlay`, + `ArcArrangement`) share one exact core in `src/exact_arc.rs` (ADR 0070, #173): every + topological decision is an exact sign, and output is rounded once, so an input vertex comes + back bit-identical. `i_overlay` remains only for offsets. The core's maintenance rules and + verification commands are in that module's docs. + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-exact`](./axiolid-exact) +- [`axiolid-guarantees`](./axiolid-guarantees) + +## Changes + +Latest release, 0.3.5 (2026-09-28): + +### Changed + +- Straight-edge booleans are exact (#173). `overlay`, `union_soup` and + `Region` no longer go through `i_overlay`'s integer grid, which snapped + every output coordinate, including untouched input vertices, to a step + of about 1.5e-8 of the operands' extent. They now run on the exact + subdivision the arc path uses: every ring of both operands cut at once, + each piece classified by exact signs, kept by the operand's winding + number under the fill rule. An input vertex the operation does not move + comes back bit-identical (so `[0,4]x[0,0.2]` clipped by a box around it + has area `0.8`, not `0.800000011920929`), and a crossing of two segments + is the double nearest to the exact crossing. Fill rules and ring + orientation keep their meaning (tested against the old backend). A vertex + where the boundary runs straight on is dropped only when exactly + straight. +- Arc and arrangement output: rational vertices (every segment crossing) + are now correctly rounded instead of rounded to about 50 bits. +- The exact subdivision indexes edges and rings in box trees, so building + it over thousands of rings (a projected mesh's triangles) is no longer + quadratic in the ring count. + +Full history: [`crates/algorithms/planar/overlay/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/planar/overlay/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-pointcloud-reconstruction-contract.md b/docs/reference/crates/axiolid-pointcloud-reconstruction-contract.md new file mode 100644 index 00000000..b78299cf --- /dev/null +++ b/docs/reference/crates/axiolid-pointcloud-reconstruction-contract.md @@ -0,0 +1,38 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-pointcloud-reconstruction-contract + +Portable pointcloud-to-surface reconstruction contract, evidence, and conformance suite. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-pointcloud-reconstruction-contract`](https://crates.io/crates/axiolid-pointcloud-reconstruction-contract) | +| Facade | [`axiolid`](./axiolid) feature `pointcloud-reconstruction` | +| Layer | contracts (`contract.operation`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_pointcloud_reconstruction_contract/index.html) · [docs.rs](https://docs.rs/axiolid-pointcloud-reconstruction-contract) | +| Source | [`crates/contracts/operations/pointcloud-reconstruction/`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/pointcloud-reconstruction) | + +## Overview + +The portable contract for reconstructing a surface from an +`axiolid-pointcloud`: request, result, evidence, typed refusal, and a +conformance suite. A reconstruction is an estimate, so the contract makes a +provider report interpolated surface and resolved sample spacing, and +refuse rather than return an empty or fabricated mesh. Providers such as +`axiolid-pointcloud-reconstruction-sdf` implement it. See ADR 0044. + +## Depends on + +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-pointcloud`](./axiolid-pointcloud) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/contracts/operations/pointcloud-reconstruction/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/contracts/operations/pointcloud-reconstruction/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-pointcloud-reconstruction-sdf.md b/docs/reference/crates/axiolid-pointcloud-reconstruction-sdf.md new file mode 100644 index 00000000..4c8ac2b5 --- /dev/null +++ b/docs/reference/crates/axiolid-pointcloud-reconstruction-sdf.md @@ -0,0 +1,43 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-pointcloud-reconstruction-sdf + +Reference pointcloud reconstruction: signed-distance field from samples, extracted as a level set. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-pointcloud-reconstruction-sdf`](https://crates.io/crates/axiolid-pointcloud-reconstruction-sdf) | +| Facade | [`axiolid`](./axiolid) feature `pointcloud-provider` | +| Layer | providers (`provider.pointcloud`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_pointcloud_reconstruction_sdf/index.html) · [docs.rs](https://docs.rs/axiolid-pointcloud-reconstruction-sdf) | +| Source | [`crates/providers/pointcloud/sdf/`](https://github.com/axiolid/kernel/tree/main/crates/providers/pointcloud/sdf) | + +## Overview + +The reference `PointcloudReconstruction` provider. `SdfReconstruction` builds +a signed-distance field from the samples (nearest neighbours through +`axiolid-spatial`) and extracts its zero level set with `axiolid-levelset`. +With normals the surface passes through the samples; without them it wraps +around them, and the evidence says which. It is not a hole filler: where the +capture has no data the surface is extrapolated, and those triangles are +counted. It has no external dependency, so a better reconstruction can +replace it behind the same contract. + +## Depends on + +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-levelset`](./axiolid-levelset) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-pointcloud`](./axiolid-pointcloud) +- [`axiolid-pointcloud-reconstruction-contract`](./axiolid-pointcloud-reconstruction-contract) +- [`axiolid-spatial`](./axiolid-spatial) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/providers/pointcloud/sdf/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/providers/pointcloud/sdf/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-pointcloud.md b/docs/reference/crates/axiolid-pointcloud.md new file mode 100644 index 00000000..ed2ebeb6 --- /dev/null +++ b/docs/reference/crates/axiolid-pointcloud.md @@ -0,0 +1,36 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-pointcloud + +Portable point-sampled geometry values with optional per-point channels. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-pointcloud`](https://crates.io/crates/axiolid-pointcloud) | +| Facade | [`axiolid`](./axiolid) feature `pointcloud` | +| Layer | representations (`representation.discrete`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_pointcloud/index.html) · [docs.rs](https://docs.rs/axiolid-pointcloud) | +| Source | [`crates/representations/discrete/pointcloud/`](https://github.com/axiolid/kernel/tree/main/crates/representations/discrete/pointcloud) | + +## Overview + +Point-sampled geometry: an ordered set of 3D points with optional normal, +colour and intensity channels, each validated to have exactly one entry per +point. A pointcloud is a sample of a surface, with no topology or +adjacency. It is not a file format (LAS, E57 and similar are parsed outside +the kernel) and not an algorithm: queries live in `axiolid-spatial` +and reconstruction behind `axiolid-pointcloud-reconstruction-contract`. See +ADR 0044. + +## Depends on + +- [`axiolid-core`](./axiolid-core) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/representations/discrete/pointcloud/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/representations/discrete/pointcloud/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-predicates.md b/docs/reference/crates/axiolid-predicates.md new file mode 100644 index 00000000..e8bc0626 --- /dev/null +++ b/docs/reference/crates/axiolid-predicates.md @@ -0,0 +1,48 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-predicates + +Certified exact-arithmetic geometric predicates. + +| | | +| --- | --- | +| Latest release | 0.3.1 (2026-09-27) | +| crates.io | [`axiolid-predicates`](https://crates.io/crates/axiolid-predicates) | +| Facade | [`axiolid`](./axiolid) feature `predicates` | +| Layer | algorithms (`algorithm.reference`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_predicates/index.html) · [docs.rs](https://docs.rs/axiolid-predicates) | +| Source | [`crates/algorithms/predicates/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/predicates) | + +## Overview + +Certified geometric predicates: `orient2d`, `orient3d`, `incircle` and +`insphere`, built on error-free transformations and expansion arithmetic, +with static filters for callers that can bound their coordinates. Every +public predicate is a filtered cascade that escalates to exact arithmetic +instead of comparing against an epsilon, and returns a `Certified` sign. +The crate is deliberately narrow: no curve, surface, mesh, B-rep, provider +or big-integer dependency. `axiolid-reference` re-exports it unchanged +(ADR 0036); for signs of constructed values, see `axiolid-exact`. + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-guarantees`](./axiolid-guarantees) + +## Changes + +Latest release, 0.3.1 (2026-09-27): + +### Fixed + +- `incircle` and `insphere`: the exact fallback rounded the coordinate + differences to `f64` before its exact expansion arithmetic, so on nearly + cocircular (cospherical) points whose differences do not fit an `f64` -- + exactly where the filter hands over -- it could return the wrong sign. + Delaunay flips driven by it cycled for ever (#190). The differences are + now exact two-term expansions and every product after them is an + expansion product; checked against an exact dyadic determinant. + +Full history: [`crates/algorithms/predicates/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/predicates/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-primitive.md b/docs/reference/crates/axiolid-primitive.md new file mode 100644 index 00000000..1f9192ef --- /dev/null +++ b/docs/reference/crates/axiolid-primitive.md @@ -0,0 +1,44 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-primitive + +Exact parametric primitive solids used as CSG leaves. + +| | | +| --- | --- | +| Latest release | 0.3.1 (2026-09-27) | +| crates.io | [`axiolid-primitive`](https://crates.io/crates/axiolid-primitive) | +| Facade | [`axiolid`](./axiolid) feature `primitives` | +| Layer | representations (`representation.atomic`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_primitive/index.html) · [docs.rs](https://docs.rs/axiolid-primitive) | +| Source | [`crates/representations/analytic/primitive/`](https://github.com/axiolid/kernel/tree/main/crates/representations/analytic/primitive) | + +## Overview + +Exact parametric primitive solids and half-spaces, used as CSG leaves. The +values stay exact until someone explicitly tessellates them: constructors do +no tessellation or boolean work, and the finite margin used when a +half-space has to be clipped for meshing is an explicit parameter. It has no +mesh or kernel dependency. + +## Depends on + +- [`axiolid-core`](./axiolid-core) + +## Changes + +Latest release, 0.3.1 (2026-09-27): + +### Added + +- `Primitive::Torus` (#142): a ring torus about local +z, by major and + minor radius. Horn and spindle tori are not solids and are refused by + the tessellator. +- `Primitive::Wedge` (#142): OCCT's `MakeWedge` general form with the + height along local +z -- a base rectangle at z = 0 and a top rectangle, + narrowed or shifted, at z = height. The top may collapse to a ridge or + an apex. + +Full history: [`crates/representations/analytic/primitive/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/representations/analytic/primitive/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-profile.md b/docs/reference/crates/axiolid-profile.md new file mode 100644 index 00000000..2558d795 --- /dev/null +++ b/docs/reference/crates/axiolid-profile.md @@ -0,0 +1,36 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-profile + +Exact 2D profile representations for sweeps and sectioned solids. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-profile`](https://crates.io/crates/axiolid-profile) | +| Facade | [`axiolid`](./axiolid) feature `profiles` | +| Layer | representations (`representation.region`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_profile/index.html) · [docs.rs](https://docs.rs/axiolid-profile) | +| Source | [`crates/representations/region/profile/`](https://github.com/axiolid/kernel/tree/main/crates/representations/region/profile) | + +## Overview + +Exact 2D profiles for sweeps and sectioned solids: parameterised +rectangles, circles, ellipses and structural sections, closed contours of +bounded exact curve segments with holes, centre-line profiles, and +transformed or composite profiles. It stores profile intent only. Boolean +cleanup, offsetting and triangulation are algorithms in higher tiers, so a +consumer can read profiles without them. + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-curve`](./axiolid-curve) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/representations/region/profile/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/representations/region/profile/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-project.md b/docs/reference/crates/axiolid-project.md new file mode 100644 index 00000000..d2d78f90 --- /dev/null +++ b/docs/reference/crates/axiolid-project.md @@ -0,0 +1,32 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-project + +Projection of triangle meshes onto a plane, and prism intersection. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-project`](https://crates.io/crates/axiolid-project) | +| Facade | [`axiolid`](./axiolid) feature `project` | +| Layer | algorithms (`algorithm.planar`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_project/index.html) · [docs.rs](https://docs.rs/axiolid-project) | +| Source | [`crates/algorithms/planar/project/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/planar/project) | + +## Overview + +Projection of triangle meshes onto a plane and intersection with prisms: the bridge between the kernel's 3D meshes and its planar booleans. `project_mesh` folds a mesh onto a plane, unions the result and keeps holes, and reports how many edge-on triangles it dropped instead of hiding them. It computes geometry, not a footprint: choosing the mesh, the reference plane and what to include is left to the consumer (ADR 0066). + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-overlay`](./axiolid-overlay) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/algorithms/planar/project/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/planar/project/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-ray-mesh.md b/docs/reference/crates/axiolid-ray-mesh.md new file mode 100644 index 00000000..7ad22799 --- /dev/null +++ b/docs/reference/crates/axiolid-ray-mesh.md @@ -0,0 +1,32 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-ray-mesh + +Narrow-phase ray/triangle-mesh nearest-hit intersection. + +| | | +| --- | --- | +| Latest release | not released (`main` is 0.4.0) | +| Facade | [`axiolid`](./axiolid) feature `ray-mesh` | +| Layer | algorithms (`algorithm.query`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_ray_mesh/index.html) | +| Source | [`crates/algorithms/query/intersection/ray-mesh/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/query/intersection/ray-mesh) | + +## Overview + +Narrow-phase ray/triangle-mesh intersection: the nearest hit with its parameter, barycentric coordinates and a certified front/back/coplanar side. It composes with a broad phase such as `axiolid-spatial` by taking candidate triangle indices, but does not depend on one. Degenerate triangles are refused rather than silently missed. It owns the intersection only, not what a ray means to the caller. + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-guarantees`](./axiolid-guarantees) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-predicates`](./axiolid-predicates) + +## Changes + +No release yet. + +Full history: [`crates/algorithms/query/intersection/ray-mesh/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/query/intersection/ray-mesh/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-reference.md b/docs/reference/crates/axiolid-reference.md new file mode 100644 index 00000000..028d89c5 --- /dev/null +++ b/docs/reference/crates/axiolid-reference.md @@ -0,0 +1,61 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-reference + +Portable scalar reference implementation and certified predicates. + +| | | +| --- | --- | +| Latest release | 0.3.1 (2026-09-27) | +| crates.io | [`axiolid-reference`](https://crates.io/crates/axiolid-reference) | +| Facade | [`axiolid`](./axiolid) feature `portable-provider` | +| Layer | algorithms (`algorithm.reference`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_reference/index.html) · [docs.rs](https://docs.rs/axiolid-reference) | +| Source | [`crates/algorithms/reference/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/reference) | + +## Overview + +The portable scalar reference implementation that every optimized backend +is differentially tested against (ADR 0012): a solid boolean oracle, an +exact-sign mesh plane-section oracle, triangle/triangle and +segment/triangle relations, clash detection, convex hulls, polygon +triangulation and tessellation. It favours readability over speed: no +intrinsics, threading, feature gates or `unsafe`. It is also a convenience +umbrella that re-exports `axiolid-predicates` and `axiolid-evaluate` +unchanged (ADR 0036); a consumer that needs only certified signs or curve +evaluation should depend on those directly. + +## Depends on + +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-curve`](./axiolid-curve) +- [`axiolid-evaluate`](./axiolid-evaluate) +- [`axiolid-guarantees`](./axiolid-guarantees) +- [`axiolid-measure`](./axiolid-measure) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-mesh-boolean-contract`](./axiolid-mesh-boolean-contract) +- [`axiolid-mesh-contracts`](./axiolid-mesh-contracts) +- [`axiolid-mesh-section-contract`](./axiolid-mesh-section-contract) +- [`axiolid-predicates`](./axiolid-predicates) +- [`axiolid-primitive`](./axiolid-primitive) +- [`axiolid-spatial`](./axiolid-spatial) +- [`axiolid-surface`](./axiolid-surface) + +## Changes + +Latest release, 0.3.1 (2026-09-27): + +### Added + +- `tessellate_primitive` meshes `Primitive::Torus` and `Primitive::Wedge` + (#142). The torus is a grid of planar trapezoids sized by the chord + budget, round the axis for the outer equator and round the tube for the + tube; horn and spindle tori, and non-positive or non-finite radii, are + refused by name. The wedge's faces are planar and shared corners of a + collapsed top are merged, so a ridge or apex wedge is still a closed, + outward-wound solid; a reversed or non-finite top range is refused. + +Full history: [`crates/algorithms/reference/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/reference/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-refine.md b/docs/reference/crates/axiolid-refine.md new file mode 100644 index 00000000..1eada007 --- /dev/null +++ b/docs/reference/crates/axiolid-refine.md @@ -0,0 +1,43 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-refine + +Mesh refinement and smoothing with bounded, reported deviation. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-refine`](https://crates.io/crates/axiolid-refine) | +| Layer | algorithms (`algorithm.discrete`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_refine/index.html) · [docs.rs](https://docs.rs/axiolid-refine) | +| Source | [`crates/algorithms/discrete/refine/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/discrete/refine) | + +## Overview + +Mesh refinement and Laplacian smoothing with bounded, reported deviation. +Refinement splits triangles; when the source surface of a tessellated +B-rep is supplied, each new vertex is placed on that surface instead of at +the edge midpoint, so refinement converges on the real geometry rather than +subdividing the facets. Smoothing keeps boundary vertices bit-identical by +default. It does not reduce triangle counts (see `axiolid-decimate`) and +does not implement limit-surface subdivision schemes. + +## Depends on + +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-evaluate`](./axiolid-evaluate) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-surface`](./axiolid-surface) + +## Changes + +Latest release, 0.3.0 (2026-09-23): + +### Fixed + +- A refinement that creates no vertex returns the input's channels and normals. It previously reported them `Preserved` and returned a mesh without them. + +Full history: [`crates/algorithms/discrete/refine/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/discrete/refine/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-route.md b/docs/reference/crates/axiolid-route.md new file mode 100644 index 00000000..f257493e --- /dev/null +++ b/docs/reference/crates/axiolid-route.md @@ -0,0 +1,77 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-route + +Exact planar shortest path over a visibility graph. + +| | | +| --- | --- | +| Latest release | 0.3.5 (2026-09-28) | +| crates.io | [`axiolid-route`](https://crates.io/crates/axiolid-route) | +| Facade | [`axiolid`](./axiolid) feature `route` | +| Layer | algorithms (`algorithm.planar`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_route/index.html) · [docs.rs](https://docs.rs/axiolid-route) | +| Source | [`crates/algorithms/planar/route/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/planar/route) | + +## Overview + +Exact planar shortest paths over a visibility graph, plus distance maps, farthest points and forced walks built on the same graph. Which edges exist is decided with certified `orient2d`, so the combinatorics are exact; path lengths are sums of square roots in `f64` and carry ordinary rounding. Oversized input is refused with a proven lower bound rather than truncated, and the budget is a caller parameter. It reports routes and typed unreachable reasons, never whether a route is acceptable. For grid-sampled routing over layered fields, see `axiolid-field-ops`. + +## Depends on + +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-guarantees`](./axiolid-guarantees) +- [`axiolid-overlay`](./axiolid-overlay) +- [`axiolid-predicates`](./axiolid-predicates) +- [`axiolid-triangulate`](./axiolid-triangulate) + +## Changes + +Latest release, 0.3.5 (2026-09-28): + +### Added + +- Seeded weighted maps (#198): `weighted_distance_map_seeded` and + `weighted_distance_map_seeded_within` take `(point, weight)` targets, + each starting at its own non-negative cost, as `distance_map_weighted` + does for plain maps (#197); both bounds start there, and + `WeightedReach::cost` counts the weight. With every weight zero the map + and its answers are `weighted_distance_map`'s exactly. +- `weighted_forced_walk` and `weighted_forced_walk_within` (#198): the + cheapest walk that enters a polygon over two weighted maps with the same + costs, bracketed with a witness as `forced_walk` is. The cell bound falls + at twice the steepest factor meeting the cell, and the pair bound runs + over both maps' vertices and cost-edge intervals with their lower + bounds, at the cell's own factor when no cost edge meets it. + `WeightedForcedWalk::shortest` brackets the cheapest walk overall; the + result is never narrower than the maps' brackets at the witness, and the + search stops at the tolerance plus those. + +### Changed + +- A cost region touching the region's boundary up to rounding is taken as + touching it (#198), no longer refused with `MapError::CostCrossing`: a + cost vertex within 2^-24 of the region's extent (and a few ulps) of a + region edge, on its free side, is moved just beyond it, and a cost edge + may cross a region edge that near one of either edge's ends. An edge so + left on or beyond a wall is wall-borne, as one exactly along it. So a + footprint clipped to the free region, its corners rounded by the + overlay, builds a map, and leaves no sliver along a wall costing 1. + +### Fixed + +- Weighted maps on cost edges at an angle (#198). The points cutting such + an edge are interpolated and lie off its line by rounding, which made + three decisions go wrong: an interval's sides were taken of its rounded + ends, so could both read the factor of one side; an edge could appear to + cross every hop to its own intervals and block them -- both could put + the lower bound above the distance; and every hop along the edge was + charged the greatest factor above. Sides are now taken of the edge's + exact ends, an edge never blocks hops from its own line, and a hop along + an edge is costed above as the walk along the edge itself, which lies + that close to it. + +Full history: [`crates/algorithms/planar/route/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/planar/route/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-spatial.md b/docs/reference/crates/axiolid-spatial.md new file mode 100644 index 00000000..3178fd61 --- /dev/null +++ b/docs/reference/crates/axiolid-spatial.md @@ -0,0 +1,40 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-spatial + +Acceleration structures: BVH and uniform point grid, and their queries; barycentric and mean-value coordinates. + +| | | +| --- | --- | +| Latest release | 0.3.1 (2026-09-27) | +| crates.io | [`axiolid-spatial`](https://crates.io/crates/axiolid-spatial) | +| Facade | [`axiolid`](./axiolid) feature `spatial` | +| Layer | algorithms (`algorithm.query`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_spatial/index.html) · [docs.rs](https://docs.rs/axiolid-spatial) | +| Source | [`crates/algorithms/query/spatial/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/query/spatial) | + +## Overview + +Deterministic, callback-based spatial acceleration: a median-split BVH over bounded objects and a uniform grid for point KNN and radius search, both behind the `SpatialIndex` query contract. They return candidates only, never exact intersections. It also provides barycentric and mean-value coordinates for interpolating values given at triangle, tetrahedron and polygon corners. + +## Depends on + +- [`axiolid-core`](./axiolid-core) + +## Changes + +Latest release, 0.3.1 (2026-09-27): + +### Added + +- Barycentric coordinates (#143): `triangle_barycentric2`, + `triangle_barycentric3` (for the point's projection onto the triangle's + plane) and `tetrahedron_barycentric`, exact at corners; and + `mean_value_coordinates2` for simple polygons, convex or not, which + interpolate the boundary linearly and reproduce points inside. Shapes + thinner than the linear tolerance, non-simple polygons and points where + mean-value weights cancel are refused with `BarycentricError`. + +Full history: [`crates/algorithms/query/spatial/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/query/spatial/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-surface.md b/docs/reference/crates/axiolid-surface.md new file mode 100644 index 00000000..7ec2cf1a --- /dev/null +++ b/docs/reference/crates/axiolid-surface.md @@ -0,0 +1,42 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-surface + +Exact, format-neutral surface values: planes, quadrics, tori, and B-spline patches. + +| | | +| --- | --- | +| Latest release | 0.3.1 (2026-09-27) | +| crates.io | [`axiolid-surface`](https://crates.io/crates/axiolid-surface) | +| Facade | [`axiolid`](./axiolid) feature `surfaces` | +| Layer | representations (`representation.atomic`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_surface/index.html) · [docs.rs](https://docs.rs/axiolid-surface) | +| Source | [`crates/representations/analytic/surface/`](https://github.com/axiolid/kernel/tree/main/crates/representations/analytic/surface) | + +## Overview + +Exact, format-neutral surface values: planes, circular and elliptical +cylinders, cones, spheres, tori, and rational or polynomial B-spline +surfaces with their knot grids and weights kept as authored. It declares the +`SurfaceEvaluator` seam but evaluates nothing. Bounded, swept, offset and +curve-on-surface relationships are nodes in `axiolid-model`, not types +here. + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-curve`](./axiolid-curve) + +## Changes + +Latest release, 0.3.1 (2026-09-27): + +### Changed + +- `BSplineSurface` is defined in `axiolid-curve` (so a curve traced on a + B-spline surface can carry its carrier, ADR 0077) and re-exported here + unchanged: same fields, same derives. Requires `axiolid-curve` 0.3.1. + +Full history: [`crates/representations/analytic/surface/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/representations/analytic/surface/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-tessellation-contract.md b/docs/reference/crates/axiolid-tessellation-contract.md new file mode 100644 index 00000000..cb03c889 --- /dev/null +++ b/docs/reference/crates/axiolid-tessellation-contract.md @@ -0,0 +1,39 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-tessellation-contract + +Curves, surfaces and B-reps to triangles under an explicit tolerance. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-tessellation-contract`](https://crates.io/crates/axiolid-tessellation-contract) | +| Facade | [`axiolid`](./axiolid) feature `tessellation` | +| Layer | contracts (`contract.operation`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_tessellation_contract/index.html) · [docs.rs](https://docs.rs/axiolid-tessellation-contract) | +| Source | [`crates/contracts/operations/tessellate/`](https://github.com/axiolid/kernel/tree/main/crates/contracts/operations/tessellate) | + +## Overview + +The contract for turning exact geometry from an `axiolid-model` graph into +triangles under an explicit tolerance: `TessellationOptions` (which has no +default chord error), the `Tessellator` trait, and a `TessellatedMesh` that +carries the tolerance it was built to. Adjacent faces must share one +discretisation of each topological edge, because tessellating faces +independently is not watertight. It is a contract only; providers implement +it. + +## Depends on + +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-model`](./axiolid-model) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/contracts/operations/tessellate/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/contracts/operations/tessellate/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-topology.md b/docs/reference/crates/axiolid-topology.md new file mode 100644 index 00000000..6ebd631a --- /dev/null +++ b/docs/reference/crates/axiolid-topology.md @@ -0,0 +1,36 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-topology + +Typed-handle B-rep topology independent of curve and surface implementations. + +| | | +| --- | --- | +| Latest release | 0.3.0 (2026-09-23) | +| crates.io | [`axiolid-topology`](https://crates.io/crates/axiolid-topology) | +| Facade | [`axiolid`](./axiolid) feature `topology` | +| Layer | representations (`representation.topology`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_topology/index.html) · [docs.rs](https://docs.rs/axiolid-topology) | +| Source | [`crates/representations/topology/`](https://github.com/axiolid/kernel/tree/main/crates/representations/topology) | + +## Overview + +Exact B-rep topology with typed handles: vertices, edges, edge uses, loops, +faces, shells and solids, each with its own handle type and explicit +orientation, plus a structural audit. Geometry is linked through a +caller-chosen handle type (`BRep`), so the graph does not depend on any +curve or surface model and serves exact kernels, mesh converters and import +adapters alike. `axiolid-brep` binds it to Axiolid's own curves and +surfaces. + +## Depends on + +- [`axiolid-core`](./axiolid-core) + +## Changes + +Released in the workspace-wide 0.3.0 release (2026-09-23), before crates versioned independently; its notes are in the [workspace changelog](/CHANGELOG). + +Full history: [`crates/representations/topology/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/representations/topology/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid-triangulate.md b/docs/reference/crates/axiolid-triangulate.md new file mode 100644 index 00000000..08a9f74d --- /dev/null +++ b/docs/reference/crates/axiolid-triangulate.md @@ -0,0 +1,50 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid-triangulate + +Constrained Delaunay triangulation with bounded quality refinement. + +| | | +| --- | --- | +| Latest release | 0.3.1 (2026-09-27) | +| crates.io | [`axiolid-triangulate`](https://crates.io/crates/axiolid-triangulate) | +| Layer | algorithms (`algorithm.planar`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid_triangulate/index.html) · [docs.rs](https://docs.rs/axiolid-triangulate) | +| Source | [`crates/algorithms/planar/triangulate/`](https://github.com/axiolid/kernel/tree/main/crates/algorithms/planar/triangulate) | + +## Overview + +Constrained Delaunay triangulation with bounded quality refinement. Every constraint edge survives as a union of output edges, the result is Delaunay away from the constraints (decided by the certified `incircle` predicate), and optional Ruppert refinement drives interior angles toward a caller-chosen minimum. Refinement carries an explicit Steiner budget and reports when it was capped, so an unmet angle bound is never returned silently. + +## Depends on + +- [`axiolid-core`](./axiolid-core) +- [`axiolid-guarantees`](./axiolid-guarantees) +- [`axiolid-predicates`](./axiolid-predicates) + +## Changes + +Latest release, 0.3.1 (2026-09-27): + +### Fixed + +- `triangulate`: after recovering constraint edges, unconstrained edges are + flipped back to locally Delaunay (Lawson's flips). Recovery used to leave + long triangles whose circumcircles held points no constraint hid, so the + result was not constrained Delaunay as documented -- with finely sampled + walls, triangles spanned whole rooms (#139). +- `triangulate` no longer loops for ever on outlines like a square turned + 45 degrees (#190): legalisation after inserting a point checked the new + diagonal instead of the two edges across from the point, so the real + edges were never checked and thin quadrilaterals flipped back and forth. + It now checks those edges, flips only strictly convex quadrilaterals, + splits the edge (and both triangles beside it) when a point lands on + one, and carries a flip bound. +- Constraint recovery no longer gives up at the first crossing edge it + cannot flip (#190): it follows Anglada's queue, retrying edges that + cannot flip yet and requeuing new diagonals that still cross, and + reports `CrossingConstraints` only when a full pass flips nothing. + +Full history: [`crates/algorithms/planar/triangulate/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/algorithms/planar/triangulate/CHANGELOG.md) diff --git a/docs/reference/crates/axiolid.md b/docs/reference/crates/axiolid.md new file mode 100644 index 00000000..3b8a2b4c --- /dev/null +++ b/docs/reference/crates/axiolid.md @@ -0,0 +1,130 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# axiolid + +Feature-gated facade for Axiolid's format-neutral geometry stack. + +| | | +| --- | --- | +| Latest release | not released (`main` is 0.4.0) | +| Layer | facade (`facade`) | +| API documentation | [rustdoc](/api/rustdoc/axiolid/index.html) | +| Source | [`crates/facade/axiolid/`](https://github.com/axiolid/kernel/tree/main/crates/facade/axiolid) | + +## Overview + +The feature-gated entry point to Axiolid's format-neutral geometry stack. +It re-exports the representation, algorithm, contract and provider packages +behind Cargo features and adds a small application layer over them; it owns +no geometry semantics of its own. `default = []`, so a consumer names the +capabilities it needs (for example `mesh`, `brep`, `nurbs`, +`tessellation`) or a bundle (`standard`, `discrete`, `parametric`, +`advanced`, `full`) and compiles nothing else. Exact geometry stays exact: +nothing here converts to a mesh unless the caller asked for a mesh and +supplied a tolerance. Every package behind a feature can also be used +directly. + +## Features + +Default: none. + +| Feature | Enables | +| --- | --- | +| `advanced` | `discrete`, `parametric`, `heal` | +| `application` | `discrete`, `dispatch-mesh-boolean`, `dispatch-mesh-section`, `portable-provider` | +| `brep` | `topology`, [`axiolid-brep`](./axiolid-brep) | +| `contracts` | [`axiolid-contracts`](./axiolid-contracts) | +| `cpu` | [`axiolid-backend-cpu`](./axiolid-backend-cpu) | +| `curves` | `linear`, [`axiolid-curve`](./axiolid-curve) | +| `discrete` | `integration`, `mesh`, `profiles`, `primitives`, `tessellation`, `spatial`, `ray-mesh`, `measure`, `mesh-boolean`, `mesh-section`, `generate`, `cpu` | +| `dispatch-mesh-boolean` | `mesh-boolean`, [`axiolid-dispatch`](./axiolid-dispatch), `axiolid-dispatch/mesh-boolean` | +| `dispatch-mesh-section` | `mesh-section`, [`axiolid-dispatch`](./axiolid-dispatch), `axiolid-dispatch/mesh-section` | +| `dispatch-pointcloud-reconstruction` | `pointcloud-reconstruction`, [`axiolid-dispatch`](./axiolid-dispatch), `axiolid-dispatch/pointcloud-reconstruction` | +| `evaluate` | `curves`, `surfaces`, [`axiolid-evaluate`](./axiolid-evaluate) | +| `field` | [`axiolid-field`](./axiolid-field) | +| `field-navigation` | `field-ops`, `axiolid-field-ops/navigation` | +| `field-ops` | `field`, [`axiolid-field-ops`](./axiolid-field-ops) | +| `full` | `advanced`, `parallel`, `simd`, `gpu` | +| `generate` | `mesh`, `profiles`, `curves`, `primitives`, `mesh-boolean`, `brep`, [`axiolid-construct`](./axiolid-construct) | +| `gpu` | `graph-compile`, [`axiolid-backend-gpu`](./axiolid-backend-gpu) | +| `graph-compile` | `contracts`, `model`, `mesh`, [`axiolid-mesh-compile-contract`](./axiolid-mesh-compile-contract) | +| `heal` | `mesh`, [`axiolid-heal`](./axiolid-heal) | +| `integration` | `contracts` | +| `linear` | [`axiolid-linear`](./axiolid-linear) | +| `linear-intersection` | `linear`, `predicates`, [`axiolid-linear-intersection`](./axiolid-linear-intersection) | +| `measure` | `mesh`, [`axiolid-measure`](./axiolid-measure) | +| `mesh` | [`axiolid-mesh`](./axiolid-mesh) | +| `mesh-boolean` | `mesh-contracts`, [`axiolid-mesh-boolean-contract`](./axiolid-mesh-boolean-contract) | +| `mesh-contracts` | `contracts`, `mesh`, [`axiolid-mesh-contracts`](./axiolid-mesh-contracts) | +| `mesh-section` | `mesh-contracts`, [`axiolid-mesh-section-contract`](./axiolid-mesh-section-contract) | +| `model` | `mesh`, `profiles`, `curves`, `surfaces`, `topology`, `primitives`, [`axiolid-model`](./axiolid-model) | +| `nurbs` | `curves`, `surfaces`, `evaluate`, [`axiolid-nurbs`](./axiolid-nurbs) | +| `overlay` | [`axiolid-overlay`](./axiolid-overlay) | +| `parallel` | `cpu`, `axiolid-backend-cpu/parallel`, `axiolid-dispatch?/parallel` | +| `parametric` | `integration`, `model`, `curves`, `surfaces`, `topology`, `nurbs` | +| `pointcloud` | [`axiolid-pointcloud`](./axiolid-pointcloud) | +| `pointcloud-provider` | `pointcloud-reconstruction`, [`axiolid-pointcloud-reconstruction-sdf`](./axiolid-pointcloud-reconstruction-sdf) | +| `pointcloud-queries` | `pointcloud`, `spatial` | +| `pointcloud-reconstruction` | `pointcloud`, `contracts`, `mesh`, [`axiolid-pointcloud-reconstruction-contract`](./axiolid-pointcloud-reconstruction-contract) | +| `portable-provider` | [`axiolid-mesh-boolean-boolmesh`](./axiolid-mesh-boolean-boolmesh), [`axiolid-reference`](./axiolid-reference) | +| `predicates` | [`axiolid-predicates`](./axiolid-predicates) | +| `primitives` | [`axiolid-primitive`](./axiolid-primitive) | +| `profiles` | [`axiolid-profile`](./axiolid-profile) | +| `project` | `mesh`, `overlay`, [`axiolid-project`](./axiolid-project) | +| `ray-mesh` | `mesh`, `predicates`, [`axiolid-ray-mesh`](./axiolid-ray-mesh) | +| `route` | `overlay`, `predicates`, [`axiolid-route`](./axiolid-route) | +| `simd` | `cpu`, `axiolid-backend-cpu/simd` | +| `spatial` | `mesh`, [`axiolid-spatial`](./axiolid-spatial), `ahash` | +| `standard` | `mesh`, `cpu`, `integration` | +| `surfaces` | `curves`, [`axiolid-surface`](./axiolid-surface) | +| `tessellation` | `mesh`, `curves`, `surfaces`, `topology`, [`axiolid-tessellation-contract`](./axiolid-tessellation-contract) | +| `topology` | `curves`, `surfaces`, [`axiolid-topology`](./axiolid-topology) | + +## Depends on + +- [`axiolid-backend-cpu`](./axiolid-backend-cpu) +- [`axiolid-backend-gpu`](./axiolid-backend-gpu) +- [`axiolid-brep`](./axiolid-brep) +- [`axiolid-construct`](./axiolid-construct) +- [`axiolid-contracts`](./axiolid-contracts) +- [`axiolid-core`](./axiolid-core) +- [`axiolid-curve`](./axiolid-curve) +- [`axiolid-dispatch`](./axiolid-dispatch) +- [`axiolid-evaluate`](./axiolid-evaluate) +- [`axiolid-field`](./axiolid-field) +- [`axiolid-field-ops`](./axiolid-field-ops) +- [`axiolid-heal`](./axiolid-heal) +- [`axiolid-linear`](./axiolid-linear) +- [`axiolid-linear-intersection`](./axiolid-linear-intersection) +- [`axiolid-measure`](./axiolid-measure) +- [`axiolid-mesh`](./axiolid-mesh) +- [`axiolid-mesh-boolean-boolmesh`](./axiolid-mesh-boolean-boolmesh) +- [`axiolid-mesh-boolean-contract`](./axiolid-mesh-boolean-contract) +- [`axiolid-mesh-compile-contract`](./axiolid-mesh-compile-contract) +- [`axiolid-mesh-contracts`](./axiolid-mesh-contracts) +- [`axiolid-mesh-section-contract`](./axiolid-mesh-section-contract) +- [`axiolid-model`](./axiolid-model) +- [`axiolid-nurbs`](./axiolid-nurbs) +- [`axiolid-overlay`](./axiolid-overlay) +- [`axiolid-pointcloud`](./axiolid-pointcloud) +- [`axiolid-pointcloud-reconstruction-contract`](./axiolid-pointcloud-reconstruction-contract) +- [`axiolid-pointcloud-reconstruction-sdf`](./axiolid-pointcloud-reconstruction-sdf) +- [`axiolid-predicates`](./axiolid-predicates) +- [`axiolid-primitive`](./axiolid-primitive) +- [`axiolid-profile`](./axiolid-profile) +- [`axiolid-project`](./axiolid-project) +- [`axiolid-ray-mesh`](./axiolid-ray-mesh) +- [`axiolid-reference`](./axiolid-reference) +- [`axiolid-route`](./axiolid-route) +- [`axiolid-spatial`](./axiolid-spatial) +- [`axiolid-surface`](./axiolid-surface) +- [`axiolid-tessellation-contract`](./axiolid-tessellation-contract) +- [`axiolid-topology`](./axiolid-topology) + +## Changes + +No release yet. + +Full history: [`crates/facade/axiolid/CHANGELOG.md`](https://github.com/axiolid/kernel/blob/main/crates/facade/axiolid/CHANGELOG.md) diff --git a/docs/reference/index.md b/docs/reference/index.md new file mode 100644 index 00000000..7c6e2949 --- /dev/null +++ b/docs/reference/index.md @@ -0,0 +1,101 @@ +--- +# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate. +--- + +# Crate reference + +Axiolid publishes 57 crates, each versioned and released on its own. The [`axiolid`](./crates/axiolid) facade re-exports them behind features; every crate can also be used directly. [Selecting a package](./selecting-packages) says which to start from. + +Every page here is generated from the crate itself: its manifest, its crate documentation and its changelog. Sections follow the layers of the [crate map](/architecture/crate-map). + +## Foundation + +| Crate | Role | Latest release | Description | +| --- | --- | --- | --- | +| [`axiolid-core`](./crates/axiolid-core) | `foundation.values` | 0.3.0 | Geometry data types and tolerance policy. No algorithms, no backends. | + +## Representations + +| Crate | Role | Latest release | Description | +| --- | --- | --- | --- | +| [`axiolid-brep`](./crates/axiolid-brep) | `representation.composed` | 0.3.1 | Exact analytic B-rep result contracts over neutral topology | +| [`axiolid-curve`](./crates/axiolid-curve) | `representation.atomic` | 0.3.1 | Exact, format-neutral curve values: lines, conics, B-splines, natural-equation and elevated curves. | +| [`axiolid-field`](./crates/axiolid-field) | `representation.sampled` | 0.3.0 | Frame-neutral layered spatial-field values and validated sampling configuration. | +| [`axiolid-fixtures`](./crates/axiolid-fixtures) | `representation.fixtures` | 0.3.0 | Shared adversarial and degenerate geometry fixtures with provenance | +| [`axiolid-linear`](./crates/axiolid-linear) | `representation.atomic` | 0.3.0 | Format-neutral line, ray, segment, and polyline representations | +| [`axiolid-mesh`](./crates/axiolid-mesh) | `representation.discrete` | 0.3.0 | Triangle meshes: the discrete representation every backend consumes. | +| [`axiolid-model`](./crates/axiolid-model) | `representation.graph` | 0.3.2 | Format-neutral geometry item tree. The currency between a format reader and a kernel. | +| [`axiolid-pointcloud`](./crates/axiolid-pointcloud) | `representation.discrete` | 0.3.0 | Portable point-sampled geometry values with optional per-point channels. | +| [`axiolid-primitive`](./crates/axiolid-primitive) | `representation.atomic` | 0.3.1 | Exact parametric primitive solids used as CSG leaves | +| [`axiolid-profile`](./crates/axiolid-profile) | `representation.region` | 0.3.0 | Exact 2D profile representations for sweeps and sectioned solids | +| [`axiolid-surface`](./crates/axiolid-surface) | `representation.atomic` | 0.3.1 | Exact, format-neutral surface values: planes, quadrics, tori, and B-spline patches. | +| [`axiolid-topology`](./crates/axiolid-topology) | `representation.topology` | 0.3.0 | Typed-handle B-rep topology independent of curve and surface implementations | + +## Contracts + +| Crate | Role | Latest release | Description | +| --- | --- | --- | --- | +| [`axiolid-contracts`](./crates/axiolid-contracts) | `contract.common` | 0.3.1 | Common backend-neutral execution and diagnostic contracts | +| [`axiolid-curve-evaluate-contract`](./crates/axiolid-curve-evaluate-contract) | `contract.operation` | 0.3.0 | Portable curve evaluation capability contract: point, tangent and oriented frame at a distance | +| [`axiolid-exact-compile-contract`](./crates/axiolid-exact-compile-contract) | `contract.operation` | 0.3.0 | Portable graph-to-exact-B-rep compilation contract | +| [`axiolid-guarantees`](./crates/axiolid-guarantees) | `contract.guarantees` | 0.3.0 | Certified-value and escalation vocabulary for geometry contracts | +| [`axiolid-mesh-boolean-contract`](./crates/axiolid-mesh-boolean-contract) | `contract.operation` | 0.3.0 | Portable mesh boolean request, result, evidence, and conformance contract | +| [`axiolid-mesh-compile-contract`](./crates/axiolid-mesh-compile-contract) | `contract.operation` | 0.3.1 | Portable graph-to-mesh compilation contract | +| [`axiolid-mesh-contracts`](./crates/axiolid-mesh-contracts) | `contract.common.mesh` | 0.3.0 | Shared mesh admissibility contracts | +| [`axiolid-mesh-section-contract`](./crates/axiolid-mesh-section-contract) | `contract.operation` | 0.3.0 | Portable mesh plane-section request, result, and evidence contract | +| [`axiolid-pointcloud-reconstruction-contract`](./crates/axiolid-pointcloud-reconstruction-contract) | `contract.operation` | 0.3.0 | Portable pointcloud-to-surface reconstruction contract, evidence, and conformance suite. | +| [`axiolid-tessellation-contract`](./crates/axiolid-tessellation-contract) | `contract.operation` | 0.3.0 | Curves, surfaces and B-reps to triangles under an explicit tolerance. | + +## Algorithms + +| Crate | Role | Latest release | Description | +| --- | --- | --- | --- | +| [`axiolid-arrangement`](./crates/axiolid-arrangement) | `algorithm.planar` | 0.3.0 | Editable planar subdivision with persistent half-edge topology | +| [`axiolid-brep-audit`](./crates/axiolid-brep-audit) | `algorithm.repair` | 0.3.1 | Geometric consistency auditing for exact boundary representations | +| [`axiolid-brep-boolean`](./crates/axiolid-brep-boolean) | `algorithm.construction` | 0.1.0 | General exact B-rep booleans over analytic faces (ADR 0075) | +| [`axiolid-collide`](./crates/axiolid-collide) | `algorithm.query` | 0.3.0 | Convex collision queries: separating axis, overlap, and separation distance | +| [`axiolid-construct`](./crates/axiolid-construct) | `algorithm.construction` | 0.3.5 | Solid generation: profiles, lofts, sweeps, revolutions and half-space clipping | +| [`axiolid-decimate`](./crates/axiolid-decimate) | `algorithm.discrete` | 0.3.0 | Edge-collapse mesh decimation with a bounded, reported deviation | +| [`axiolid-decompose`](./crates/axiolid-decompose) | `algorithm.discrete` | 0.3.0 | Convex decomposition of a solid, exact or approximate and always labelled | +| [`axiolid-evaluate`](./crates/axiolid-evaluate) | `algorithm.parametric` | 0.3.1 | Analytic and spline curve/surface evaluation, jets, and inversion | +| [`axiolid-exact`](./crates/axiolid-exact) | `algorithm.reference` | 0.1.1 | Filtered exact arithmetic: interval filter, dyadic big integers, a + b*sqrt(c) | +| [`axiolid-field-ops`](./crates/axiolid-field-ops) | `algorithm.sampled` | 0.3.0 | Sampling, morphology, clearance, and navigation over Axiolid layered fields. | +| [`axiolid-heal`](./crates/axiolid-heal) | `algorithm.repair` | 0.3.0 | Explicit diagnosis and opt-in repair contracts for dirty geometry | +| [`axiolid-inspect`](./crates/axiolid-inspect) | `algorithm.query` | 0.3.3 | Mesh queries: clearance, containment, ray casting, and genus | +| [`axiolid-levelset`](./crates/axiolid-levelset) | `algorithm.sampled` | 0.3.0 | Level-set extraction: a closed manifold mesh from a sampled scalar field | +| [`axiolid-linear-intersection`](./crates/axiolid-linear-intersection) | `algorithm.query` | 0.3.0 | Portable, deterministic intersections for linear geometry | +| [`axiolid-measure`](./crates/axiolid-measure) | `algorithm.query` | 0.3.2 | Metric properties: area, volume, centroid, moments of inertia. | +| [`axiolid-minkowski`](./crates/axiolid-minkowski) | `algorithm.discrete` | 0.3.0 | Minkowski sum and difference of planar-faced solids | +| [`axiolid-nurbs`](./crates/axiolid-nurbs) | `algorithm.parametric` | 0.3.3 | General polynomial and rational B-spline analysis and transformation algorithms | +| [`axiolid-overlay`](./crates/axiolid-overlay) | `algorithm.planar` | 0.3.5 | Deterministic validated planar overlay contract | +| [`axiolid-predicates`](./crates/axiolid-predicates) | `algorithm.reference` | 0.3.1 | Certified exact-arithmetic geometric predicates | +| [`axiolid-project`](./crates/axiolid-project) | `algorithm.planar` | 0.3.0 | Projection of triangle meshes onto a plane, and prism intersection | +| [`axiolid-ray-mesh`](./crates/axiolid-ray-mesh) | `algorithm.query` | not released | Narrow-phase ray/triangle-mesh nearest-hit intersection | +| [`axiolid-reference`](./crates/axiolid-reference) | `algorithm.reference` | 0.3.1 | Portable scalar reference implementation and certified predicates | +| [`axiolid-refine`](./crates/axiolid-refine) | `algorithm.discrete` | 0.3.0 | Mesh refinement and smoothing with bounded, reported deviation | +| [`axiolid-route`](./crates/axiolid-route) | `algorithm.planar` | 0.3.5 | Exact planar shortest path over a visibility graph | +| [`axiolid-spatial`](./crates/axiolid-spatial) | `algorithm.query` | 0.3.1 | Acceleration structures: BVH and uniform point grid, and their queries; barycentric and mean-value coordinates. | +| [`axiolid-triangulate`](./crates/axiolid-triangulate) | `algorithm.planar` | 0.3.1 | Constrained Delaunay triangulation with bounded quality refinement | + +## Providers + +| Crate | Role | Latest release | Description | +| --- | --- | --- | --- | +| [`axiolid-mesh-boolean-boolmesh`](./crates/axiolid-mesh-boolean-boolmesh) | `provider.mesh` | 0.3.1 | boolmesh-backed MeshBoolean provider | +| [`axiolid-pointcloud-reconstruction-sdf`](./crates/axiolid-pointcloud-reconstruction-sdf) | `provider.pointcloud` | 0.3.0 | Reference pointcloud reconstruction: signed-distance field from samples, extracted as a level set. | + +## Execution + +| Crate | Role | Latest release | Description | +| --- | --- | --- | --- | +| [`axiolid-backend-cpu`](./crates/axiolid-backend-cpu) | `execution.context` | 0.3.0 | Portable and runtime-optimized CPU execution context for Axiolid geometry | +| [`axiolid-backend-gpu`](./crates/axiolid-backend-gpu) | `execution.context` | 0.3.0 | GPU executor adapter contract for batched Axiolid geometry | +| [`axiolid-dispatch`](./crates/axiolid-dispatch) | `execution.dispatch` | 0.3.0 | Provider registration, ordering, fallback, and execution policy | +| [`axiolid-mesh-compile`](./crates/axiolid-mesh-compile) | `execution.orchestration` | 0.3.5 | Scalar reference MeshCompiler: profiles, extrusion, transforms, boolean dispatch. | + +## Facade + +| Crate | Role | Latest release | Description | +| --- | --- | --- | --- | +| [`axiolid`](./crates/axiolid) | `facade` | not released | Feature-gated facade for Axiolid's format-neutral geometry stack | +| [`axiolid-capi`](./crates/axiolid-capi) | `facade.native-c` | 0.3.0 | Versioned, memory-safe C ABI for the Axiolid application facade. | diff --git a/docs/reference/selecting-packages.md b/docs/reference/selecting-packages.md new file mode 100644 index 00000000..82cbd92a --- /dev/null +++ b/docs/reference/selecting-packages.md @@ -0,0 +1,23 @@ +# Selecting a package + +The facade is convenient; leaf packages are the enforceable boundaries. Pick the smallest set that covers the use case. The [crate reference](/reference/) lists every package by layer. + +- Core scalar, vector, transform and tolerance values: [`axiolid-core`](/reference/crates/axiolid-core). +- Mesh or sampled-field values without algorithms: [`axiolid-mesh`](/reference/crates/axiolid-mesh) or [`axiolid-field`](/reference/crates/axiolid-field). +- Sampling, morphology and navigation over fields: [`axiolid-field-ops`](/reference/crates/axiolid-field-ops). +- Certified exact-arithmetic predicates: [`axiolid-predicates`](/reference/crates/axiolid-predicates), the focused substrate. +- Broad reference oracles: [`axiolid-reference`](/reference/crates/axiolid-reference). It is a convenience umbrella; a narrow package does not depend on it. +- Linear values without the curve aggregate: [`axiolid-linear`](/reference/crates/axiolid-linear). +- NURBS analysis and exact shape-preserving transformations: [`axiolid-nurbs`](/reference/crates/axiolid-nurbs). +- Neutral authored graph storage: [`axiolid-model`](/reference/crates/axiolid-model). +- Exact analytic B-rep results: [`axiolid-brep`](/reference/crates/axiolid-brep). It does not tessellate. +- Mesh Boolean or plane-section portability: depend on the operation contract, then choose a provider or dispatch policy explicitly. +- Graph-to-mesh execution: [`axiolid-mesh-compile-contract`](/reference/crates/axiolid-mesh-compile-contract) for the seam, [`axiolid-mesh-compile`](/reference/crates/axiolid-mesh-compile) for the reference implementation. + +Representation-only facade use: + +```bash +cargo add axiolid --no-default-features --features model +``` + +This must not resolve compiler, field-operation, mesh-Boolean-provider, source-format or GPU dependencies. The [closure profiles](/architecture/closure-profiles) are the gate-checked examples of narrow dependency sets. diff --git a/docs/scripts/check-docs-ui.mjs b/docs/scripts/check-docs-ui.mjs index 93bf5ae3..823bda51 100644 --- a/docs/scripts/check-docs-ui.mjs +++ b/docs/scripts/check-docs-ui.mjs @@ -53,7 +53,7 @@ function sourceCheck(root) { requireIncludes(css, ".glossary-term::after {\n display: none;", "touch tooltip overflow guard"); const forbidden = new RegExp(["M", "CS", "|Axi", "oval"].join(""), "i"); - const scanned = [...markdownFiles(docs), join(root, "crates/PLAN.md")]; + const scanned = markdownFiles(docs); for (const path of scanned) { const match = readFileSync(path, "utf8").match(forbidden); if (match) fail(`application-specific boundary name remains in ${path}: ${match[0]}`); diff --git a/native/AGENTS.md b/native/AGENTS.md deleted file mode 100644 index e741b1a2..00000000 --- a/native/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Native integration - -CMake source-build and installed-package support for `axiolid-capi`. `Axiolid::axiolid` is the stable consumer target. Keep platform filenames centralized and test both source and archive modes with `tests/native/cmake-consumer`. diff --git a/native/README.md b/native/README.md new file mode 100644 index 00000000..64880214 --- /dev/null +++ b/native/README.md @@ -0,0 +1,46 @@ +# Native integration + +CMake support for `axiolid-capi`, the C ABI. A C or C++ project links the one +stable target `Axiolid::axiolid`, in any of three ways: + +- a source build through `add_subdirectory(/native)` (this + `CMakeLists.txt`, which drives Cargo), +- an installed or extracted release archive through + `find_package(Axiolid 0.4 CONFIG)` (`cmake/AxiolidConfig.cmake`), +- `cmake/AxiolidFetch.cmake`, which fetches a source tree pinned to an + immutable 40-hex `GIT_COMMIT` and refuses a branch or tag. + +`AXIOLID_LINKAGE` selects `SHARED` (default) or `STATIC`. The consumer guide is +[downstream integration](../docs/guide/downstream-integration.md). The ABI is +specified in [C ABI v0.4](../docs/architecture/c-abi-v0.4.md), and archives +in [native distribution](../docs/architecture/native-distribution.md). + +## Design notes + +- Platform library file names (`.so`, `.dylib`, `.dll` and import library) are + set in one block of `CMakeLists.txt`. Keep them there. +- Nothing here may set host-specific codegen or the consumer's global + C/C++ flags. `scripts/check-native-packaging.sh` rejects both, and it + scans this directory, so name the flags only in that script. +- Platform-specific CI setup belongs in `.github/workflows/native.yml`. The + CMake files and test fixtures stay platform-neutral. + +## Tests + +`tests/native/cmake-consumer/` is a black-box C and C++ consumer. It uses only +`find_package` or `add_subdirectory`, the `Axiolid::axiolid` target and the +generated public header, and it must run both a successful operation and a +typed-refusal path. `scripts/test-native-cmake.py` copies it outside the +workspace and builds it from the source tree, from an installed package and +from an extracted archive. With `--mutations`, it also shows that a missing +required symbol, header or package config breaks the build. + +```bash +scripts/check-native-packaging.sh # everything below, as the gate runs it +python3 scripts/test-native-cmake.py --build-type Release --linkage SHARED --mutations +python3 -m unittest tests/native/test_native_packaging.py +``` + +`tests/native/test_native_packaging.py` covers the archive layouts, fail-closed +archive paths, reproducible writers and binary checks. `tests/native/reject-mutable-ref.cmake` shows that +`axiolid_fetch` refuses a mutable ref. diff --git a/scripts/assemble-crate-changelogs.py b/scripts/assemble-crate-changelogs.py deleted file mode 100644 index 3c7c66c7..00000000 --- a/scripts/assemble-crate-changelogs.py +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env python3 -"""Assemble docs/reference/changelog.md from every crate's own CHANGELOG.md. - -Versions now diverge per crate (ADR 0067), so no single file lists every -release in order. This script does not replace docs/CHANGELOG.md, which stays -the hand-written narrative for workspace-wide events (breaking bumps, major -releases); it builds the crate-indexed companion the docs site needs once a -patch can ship for one crate without a corresponding workspace entry. - -Output is grouped by crate, newest release first per crate, matching the -Keep-a-Changelog heading shape each crate file already uses. Committed to -source and checked for drift, the same pattern as -`cargo xtask architecture docs` / `architecture check`. - -Run: python3 scripts/assemble-crate-changelogs.py [--check] ---check (default) fails if the generated file would change. --write applies it. -""" - -from __future__ import annotations - -import argparse -import json -import re -import subprocess -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parent.parent -OUTPUT = ROOT / "docs" / "reference" / "changelog.md" -MARKER = "" - -HEADING_RE = re.compile(r"(?m)^## \[([^\]]+)\] - (\d{4}-\d{2}-\d{2})\s*$") - - -def metadata() -> dict: - result = subprocess.run( - ["cargo", "metadata", "--format-version", "1", "--no-deps", "--locked"], - cwd=ROOT, - check=True, - text=True, - stdout=subprocess.PIPE, - ) - return json.loads(result.stdout) - - -def publishable_crates() -> list[tuple[str, Path]]: - """(name, changelog_path) for every publishable workspace crate, sorted.""" - data = metadata() - members = set(data["workspace_members"]) - crates = [] - for package in data["packages"]: - if package["id"] not in members or package.get("publish") == []: - continue - manifest = Path(package["manifest_path"]) - crates.append((package["name"], manifest.parent / "CHANGELOG.md")) - return sorted(crates, key=lambda pair: pair[0]) - - -def released_sections(text: str) -> list[tuple[str, str, str]]: - """(version, date, body) for every dated heading, skipping Unreleased. - - Unreleased entries are excluded on purpose: this page is what a consumer - reads to decide whether upgrading a specific crate version is worth it, - and unreleased work is not yet a version they can depend on. - """ - matches = list(HEADING_RE.finditer(text)) - sections = [] - for index, match in enumerate(matches): - version, date = match.group(1), match.group(2) - body_start = match.end() - body_end = matches[index + 1].start() if index + 1 < len(matches) else len(text) - sections.append((version, date, text[body_start:body_end].strip("\n"))) - return sections - - -def render(crates: list[tuple[str, Path]]) -> str: - lines = [ - MARKER, - "", - "# Per-crate changelog", - "", - "Every publishable crate versions and publishes independently" - " ([ADR 0067](/adr/0067-crates-version-independently)); this page" - " collects each crate's own `CHANGELOG.md`, newest release first per" - " crate. Workspace-wide narrative — breaking bumps and coordinated" - " releases — stays in the [top-level changelog](/CHANGELOG).", - "", - ] - any_release = False - for name, changelog in crates: - if not changelog.exists(): - continue - text = changelog.read_text(encoding="utf-8") - sections = released_sections(text) - if not sections: - continue - any_release = True - lines.append(f"## {name}") - lines.append("") - for version, date, body in sections: - lines.append(f"### {version} - {date}") - lines.append("") - if body: - lines.append(body) - lines.append("") - lines.append("") - if not any_release: - lines.append( - "No crate has a dated release yet under independent versioning; " - "every crate's history to date lives in the top-level changelog." - ) - lines.append("") - return "\n".join(lines).rstrip("\n") + "\n" - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - mode = parser.add_mutually_exclusive_group() - mode.add_argument("--check", action="store_true", default=True, help="fail on drift (default)") - mode.add_argument("--write", action="store_true", help="write the generated file") - args = parser.parse_args() - - crates = publishable_crates() - generated = render(crates) - - if args.write: - OUTPUT.parent.mkdir(parents=True, exist_ok=True) - OUTPUT.write_text(generated, encoding="utf-8") - print(f"ASSEMBLE_CHANGELOG=WRITE crates={len(crates)} -> {OUTPUT}") - return 0 - - if not OUTPUT.exists(): - print(f"ASSEMBLE_CHANGELOG=DRIFT {OUTPUT} does not exist; run with --write") - return 1 - current = OUTPUT.read_text(encoding="utf-8") - if current != generated: - print(f"ASSEMBLE_CHANGELOG=DRIFT {OUTPUT} is stale; run with --write") - return 1 - print(f"ASSEMBLE_CHANGELOG=OK crates={len(crates)}") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/check-roadmap-freshness.py b/scripts/check-roadmap-freshness.py index b509ff57..a1333b80 100755 --- a/scripts/check-roadmap-freshness.py +++ b/scripts/check-roadmap-freshness.py @@ -24,11 +24,6 @@ from pathlib import Path ROADMAP = Path(__file__).resolve().parent.parent / "docs" / "ROADMAP.md" -CRATES = Path(__file__).resolve().parent.parent / "crates" - -# Crate `PLAN.md` files are design notes. The moment one records per-item -# status it drifts from the code, and a contributor reading a stale -# unchecked box reimplements something that already exists (kernel#25). # Headings that assert a state this page cannot keep current. STALE_HEADINGS = re.compile( @@ -85,33 +80,6 @@ def milestone_description_problems() -> list[str]: return bad -def plan_status_problems() -> list[str]: - """Crate `PLAN.md` files must not record per-item status. - - A checkbox or a progress heading in a design note is a status claim - nobody updates when the code moves. Six of the seven unchecked boxes - that motivated kernel#25 described work that was already implemented. - """ - problems: list[str] = [] - for plan in sorted(CRATES.rglob("PLAN.md")): - text = plan.read_text(encoding="utf-8") - rel = plan.relative_to(CRATES.parent) - for match in CHECKBOX.finditer(text): - line = text[: match.start()].count("\n") + 1 - problems.append( - f"{rel}:{line}: task checkbox — per-item status belongs on " - f"the project board, not in a design note" - ) - for match in STALE_HEADINGS.finditer(text): - line = text[: match.start()].count("\n") + 1 - heading = match.group(0).strip() - problems.append( - f"{rel}:{line}: heading {heading!r} asserts progress state " - f"this file cannot keep current" - ) - return problems - - def main() -> int: if not ROADMAP.exists(): print(f"roadmap: {ROADMAP} not found") @@ -135,7 +103,6 @@ def main() -> int: f"page cannot keep current" ) - problems.extend(plan_status_problems()) for pointer in REQUIRED_POINTERS: if pointer not in text: diff --git a/scripts/check-semver.py b/scripts/check-semver.py index 346c329b..c1788e11 100644 --- a/scripts/check-semver.py +++ b/scripts/check-semver.py @@ -121,7 +121,7 @@ def run_semver_checks(crates, baseline): return process.returncode, process.stdout -EXCEPTIONS = ROOT / "architecture" / "semver-exceptions.toml" +EXCEPTIONS = ROOT / "docs" / "architecture" / "semver-exceptions.toml" def load_exceptions() -> list[dict]: diff --git a/scripts/gate.sh b/scripts/gate.sh index 220de005..de84b645 100755 --- a/scripts/gate.sh +++ b/scripts/gate.sh @@ -17,6 +17,10 @@ step "naming mutation probe" scripts/probe_naming_gate.sh step "isolated build mutation probe" scripts/probe_isolated_build_gate.sh step "closure check" cargo xtask architecture closure check step "closure mutation probe" scripts/probe_closure_gate.sh +step "context (ADR 0078)" cargo xtask context check +step "context mutation probe" scripts/probe_context_gate.sh +step "generated docs" cargo xtask docs --check +step "docs mutation probe" scripts/probe_docs_gate.sh step "roadmap freshness" python3 scripts/check-roadmap-freshness.py step "capability evidence" python3 scripts/check-capabilities.py step "capability evidence mutation probe" scripts/probe_capabilities_gate.sh @@ -41,7 +45,6 @@ step "release script tests" python3 -m unittest scripts.test_release_scripts step "crate release script tests" python3 -m unittest scripts.test_crate_release step "release publish plan" python3 scripts/publish-workspace.py step "release package preflight" python3 scripts/verify-packages.py -step "per-crate changelog assembly" python3 scripts/assemble-crate-changelogs.py --check step "semver policy" python3 scripts/check-semver.py step "semver mutation probe" scripts/probe_semver_gate.sh # Derived from `cargo metadata`, never hand-maintained: a new publishable diff --git a/scripts/probe_context_gate.sh b/scripts/probe_context_gate.sh new file mode 100755 index 00000000..d94964c8 --- /dev/null +++ b/scripts/probe_context_gate.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Mutation probe: can `cargo xtask context check` actually fail? +# Each mutation breaks one rule of ADR 0078; the check must reject every one, +# for the right reason, and accept the untouched tree. +set -uo pipefail +cd "$(dirname "$0")/.." || exit 1 +cargo build --quiet -p xtask || exit 1 +fail=0 +created=() +BAK="${TMPDIR:-/tmp}/contextprobe.$$" +mkdir -p "$BAK" +cleanup() { + for f in "${created[@]}"; do rm -f "$f"; rmdir -p "$(dirname "$f")" 2>/dev/null; done + for f in "$BAK"/*.path; do [ -e "$f" ] && cp "${f%.path}" "$(cat "$f")"; done + rm -rf "$BAK" +} +trap cleanup EXIT +check() { + printf " %-58s" "$1" + local out got + out=$(cargo xtask context check 2>&1) + got=$? + if [ "$2" = "$got" ] && { [ -z "${3:-}" ] || grep -qF -- "$3" <<<"$out"; }; then + echo ok + else + echo "MISS (want=$2${3:+ \"$3\"} got=$got)"; fail=1 + fi + cleanup + created=() + mkdir -p "$BAK" +} +save() { local key; key=$(echo "$1" | tr '/' '_'); cp "$1" "$BAK/$key"; echo "$1" >"$BAK/$key.path"; } +new() { mkdir -p "$(dirname "$1")"; printf '%s\n' "$2" >"$1"; created+=("$1"); } + +CRATE=crates/foundation/core +echo "=== baseline ===" +check "untouched tree passes" 0 +echo "=== mutations ===" +new "$CRATE/AGENTS.md" "# nested" +check "nested AGENTS.md" 1 "only the root AGENTS.md exists" +new "$CRATE/PLAN.md" "# plan" +check "crate PLAN.md" 1 "plans are not checked in" +new "PLAN-something.md" "# plan" +check "root session plan" 1 "plans are not checked in" +new "FINDING.md" "# finding" +check "stray root-level note" 1 "the root holds only" +new "docs/plans/x.md" "# plan" +check "docs/plans page" 1 "plans are not checked in" +save "$CRATE/README.md"; rm "$CRATE/README.md" +check "crate without README" 1 "every crate has a README.md" +save "$CRATE/README.md"; printf -- '- [ ] later\n' >>"$CRATE/README.md" +check "README checkbox" 1 "task checkbox" +save "$CRATE/README.md"; seq 200 >>"$CRATE/README.md" +check "oversized README" 1 "limit 150" +save "AGENTS.md"; seq 200 >>AGENTS.md +check "oversized root AGENTS.md" 1 "limit 120" +save "$CRATE/Cargo.toml"; sed -i 's/^readme = "README.md"/readme.workspace = true/' "$CRATE/Cargo.toml" +check "publishable crate without its readme" 1 'declare `readme = "README.md"`' +save "$CRATE/src/lib.rs"; printf '// TODO: later\n' >>"$CRATE/src/lib.rs" +check "TODO without an issue" 1 "without an issue" +save "$CRATE/src/lib.rs"; printf '// TODO(#1): later\n' >>"$CRATE/src/lib.rs" +check "TODO(#N) is accepted" 0 +save "$CRATE/src/lib.rs"; printf '//! See `../gone/README.md`.\n' >>"$CRATE/src/lib.rs" +check "dangling README pointer" 1 "does not resolve" +save "$CRATE/README.md"; printf 'See `crates/x/AGENTS.md`.\n' >>"$CRATE/README.md" +check "pointer to a nested AGENTS.md" 1 "points at a nested AGENTS.md" +[ "$fail" = 0 ] && echo "CONTEXT_PROBE=PASS" || echo "CONTEXT_PROBE=FAIL" +exit "$fail" diff --git a/scripts/probe_docs_gate.sh b/scripts/probe_docs_gate.sh new file mode 100755 index 00000000..a4f17f05 --- /dev/null +++ b/scripts/probe_docs_gate.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# Mutation probe: can `cargo xtask docs --check` actually fail? +# Each mutation makes a generated page stale or breaks a README rule; the +# check must reject every one, for the right reason, and accept the +# untouched tree. +set -uo pipefail +cd "$(dirname "$0")/.." || exit 1 +cargo build --quiet -p xtask || exit 1 +fail=0 +created=() +BAK="${TMPDIR:-/tmp}/docsprobe.$$" +mkdir -p "$BAK" +cleanup() { + for f in "${created[@]}"; do rm -f "$f"; done + for f in "$BAK"/*.path; do [ -e "$f" ] && cp "${f%.path}" "$(cat "$f")"; done + rm -rf "$BAK" +} +trap cleanup EXIT +check() { + printf " %-58s" "$1" + local out got + out=$(cargo xtask docs --check 2>&1) + got=$? + if [ "$2" = "$got" ] && { [ -z "${3:-}" ] || grep -qF -- "$3" <<<"$out"; }; then + echo ok + else + echo "MISS (want=$2${3:+ \"$3\"} got=$got)"; fail=1 + fi + cleanup + created=() + mkdir -p "$BAK" +} +save() { local key; key=$(echo "$1" | tr '/' '_'); cp "$1" "$BAK/$key"; echo "$1" >"$BAK/$key.path"; } +new() { mkdir -p "$(dirname "$1")"; printf '%s\n' "$2" >"$1"; created+=("$1"); } + +CRATE=crates/foundation/core +PAGE=docs/reference/crates/axiolid-core.md +REF='- Reference page: [axiolid.github.io/kernel](https://axiolid.github.io/kernel/reference/crates/axiolid-core)' +echo "=== baseline ===" +check "untouched tree passes" 0 +echo "=== generated pages ===" +save "$PAGE"; printf 'hand edit\n' >>"$PAGE" +check "hand-edited crate page" 1 "$PAGE is stale" +save docs/reference/index.md; rm docs/reference/index.md +check "missing index" 1 "docs/reference/index.md is stale" +new docs/reference/crates/axiolid-gone.md "# axiolid-gone" +check "page for a crate that no longer exists" 1 "axiolid-gone.md is no longer generated" +save docs/.vitepress/data/facts.json; printf '{}\n' >docs/.vitepress/data/facts.json +check "stale sidebar facts" 1 "facts.json is stale" +save docs/architecture/crate-map.md; printf 'x\n' >>docs/architecture/crate-map.md +check "stale architecture crate map" 1 "crate-map.md is stale" +save "$CRATE/README.md"; printf '\n## Design notes\n\nA new note.\n' >>"$CRATE/README.md" +check "README change not regenerated" 1 "$PAGE is stale" +save "$CRATE/CHANGELOG.md"; printf '\n## [9.9.9] - 2099-01-01\n\n- probe\n' >>"$CRATE/CHANGELOG.md" +check "release not regenerated (page)" 1 "$PAGE is stale" +save "$CRATE/CHANGELOG.md"; printf '\n## [9.9.9] - 2099-01-01\n\n- probe\n' >>"$CRATE/CHANGELOG.md" +check "release not regenerated (changelog page)" 1 "docs/reference/changelog.md is stale" +save "$CRATE/CHANGELOG.md"; sed -i 's/^## \[Unreleased\]$/&\n\n### Added\n\n- pending/' "$CRATE/CHANGELOG.md" +check "an [Unreleased] entry needs no regeneration" 0 +echo "=== READMEs ===" +save "$CRATE/README.md"; grep -vF -- "$REF" "$CRATE/README.md" >"$BAK/r" && cp "$BAK/r" "$CRATE/README.md" +check "README without its reference link" 1 "missing the line \`$REF\`" +save "$CRATE/README.md"; sed -i 's#reference/crates/axiolid-core)#reference/crates/axiolid-mesh)#' "$CRATE/README.md" +check "README linking another crate's page" 1 "missing the line \`$REF\`" +save "$CRATE/README.md"; sed -i 's#docs.rs/axiolid-core)#docs.rs/axiolid)#' "$CRATE/README.md" +check "README without its docs.rs link" 1 "docs.rs/axiolid-core" +save "$CRATE/README.md"; printf 'This crate is not yet published.\n' >>"$CRATE/README.md" +check "README publication claim" 1 "publication claim" +save "$CRATE/README.md"; printf 'Stable since 0.3.1.\n' >>"$CRATE/README.md" +check "README version claim" 1 "version claim" +[ "$fail" = 0 ] && echo "DOCS_PROBE=PASS" || echo "DOCS_PROBE=FAIL" +exit "$fail" diff --git a/scripts/probe_gaps_gate.sh b/scripts/probe_gaps_gate.sh index 1eae0d09..fc916c84 100755 --- a/scripts/probe_gaps_gate.sh +++ b/scripts/probe_gaps_gate.sh @@ -4,7 +4,7 @@ # reject every one, and accept the untouched file. set -uo pipefail cd "$(dirname "$0")/.." || exit 1 -LEDGER=architecture/capability-ledger.toml +LEDGER=docs/architecture/capability-ledger.toml BAK="${TMPDIR:-/tmp}/gapsprobe.bak" cp "$LEDGER" "$BAK" restore() { cp "$BAK" "$LEDGER"; } diff --git a/scripts/probe_semver_gate.sh b/scripts/probe_semver_gate.sh index 839f0d0c..ca9e76b5 100755 --- a/scripts/probe_semver_gate.sh +++ b/scripts/probe_semver_gate.sh @@ -48,7 +48,7 @@ rc=$? check "gate rejects a removed public method" 1 $rc echo "=== mutation: an exception covers only its own items ===" -# axiolid-surface has an accepted finding (architecture/semver-exceptions.toml). +# axiolid-surface has an accepted finding (docs/architecture/semver-exceptions.toml). # Renaming a public field of another struct in the same crate is still a # break, so the gate MUST fail: the exception excuses items, not crates. SURF=crates/representations/analytic/surface/src/elementary.rs diff --git a/scripts/test_crate_release.py b/scripts/test_crate_release.py index 7d558477..87d34158 100644 --- a/scripts/test_crate_release.py +++ b/scripts/test_crate_release.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -"""Regression tests for prepare-crate-release.py and assemble-crate-changelogs.py. +"""Regression tests for prepare-crate-release.py. Network-free and side-effect-free: no subprocess, no cargo metadata call, only -the pure functions each script exposes, mirroring test_release_scripts.py's -pattern for prepare-release.py. +the pure functions the script exposes, mirroring test_release_scripts.py's +pattern for prepare-release.py. The per-crate changelog page it feeds is +assembled and tested by `cargo xtask docs` (tools/xtask/src/docs/changelog.rs). """ from __future__ import annotations @@ -26,7 +27,6 @@ def load_script(name: str, filename: str): prepare_crate = load_script("prepare_crate_release", "prepare-crate-release.py") -assemble = load_script("assemble_crate_changelogs", "assemble-crate-changelogs.py") class PrepareCrateReleaseTests(unittest.TestCase): @@ -112,54 +112,5 @@ def test_changelog_unreleased_body_splits_on_the_next_heading(self) -> None: self.assertIn("## [0.3.0]", suffix) -class AssembleCrateChangelogsTests(unittest.TestCase): - def test_released_sections_skips_unreleased_and_orders_by_appearance(self) -> None: - text = ( - "# Changelog\n\n" - "## [Unreleased]\n\n- pending, must not appear\n\n" - "## [0.3.1] - 2026-09-20\n\n### Added\n\n- b\n\n" - "## [0.3.0] - 2026-09-01\n\n### Added\n\n- a\n" - ) - sections = assemble.released_sections(text) - self.assertEqual([s[0] for s in sections], ["0.3.1", "0.3.0"]) - self.assertEqual([s[1] for s in sections], ["2026-09-20", "2026-09-01"]) - self.assertIn("- b", sections[0][2]) - self.assertNotIn("pending", "".join(s[2] for s in sections)) - - def test_released_sections_is_empty_when_only_unreleased_exists(self) -> None: - text = "# Changelog\n\n## [Unreleased]\n\n- pending\n" - self.assertEqual(assemble.released_sections(text), []) - - def test_render_skips_crates_with_no_dated_release(self) -> None: - with tempfile.TemporaryDirectory() as temporary: - unreleased_only = Path(temporary) / "unreleased.md" - unreleased_only.write_text("# Changelog\n\n## [Unreleased]\n\n- pending\n", encoding="utf-8") - released = Path(temporary) / "released.md" - released.write_text( - "# Changelog\n\n## [Unreleased]\n\n## [0.3.1] - 2026-09-20\n\n- shipped\n", - encoding="utf-8", - ) - output = assemble.render( - [("axiolid-unreleased", unreleased_only), ("axiolid-released", released)] - ) - self.assertNotIn("axiolid-unreleased", output) - self.assertIn("axiolid-released", output) - self.assertIn("0.3.1", output) - self.assertIn("shipped", output) - - def test_render_reports_when_nothing_has_shipped_yet(self) -> None: - with tempfile.TemporaryDirectory() as temporary: - unreleased_only = Path(temporary) / "unreleased.md" - unreleased_only.write_text("# Changelog\n\n## [Unreleased]\n\n- pending\n", encoding="utf-8") - output = assemble.render([("axiolid-probe", unreleased_only)]) - self.assertIn("No crate has a dated release yet", output) - - def test_render_skips_a_crate_with_no_changelog_file(self) -> None: - missing = Path(tempfile.gettempdir()) / "axiolid-does-not-exist-CHANGELOG.md" - self.assertFalse(missing.exists()) - output = assemble.render([("axiolid-missing", missing)]) - self.assertIn("No crate has a dated release yet", output) - - if __name__ == "__main__": unittest.main() diff --git a/tests/consumers/AGENTS.md b/tests/consumers/AGENTS.md deleted file mode 100644 index 5c76c65e..00000000 --- a/tests/consumers/AGENTS.md +++ /dev/null @@ -1,65 +0,0 @@ -# Closure fixtures - -Each directory is an isolated downstream application used to verify one profile -in `architecture/closure-profiles.toml` (ADR 0036). - -## Rules - -- Every fixture has an EMPTY `[workspace]` table. It must be its own workspace - root, otherwise workspace feature unification masks what a real consumer - resolves and the measurement becomes meaningless. -- Depend on leaf packages by relative path with `default-features = false`. -- `Cargo.lock` is gitignored per fixture; the checker resolves with `--offline`. -- Exercise real behaviour, not just symbol names. A fixture that only mentions a - type can pass while the package is unusable. -- Adding a dependency here changes a declared compatibility promise. Update the - profile deliberately and regenerate the closure docs. - -## Verify - -```bash -cargo xtask architecture closure check -cargo xtask architecture closure explain -bash scripts/probe_closure_gate.sh -``` - -## Build output is never tracked - -Each consumer is a real cargo project, so running one creates a local -`target/`. Those artifacts must never enter git. - -The root `.gitignore` had `/target`, which is anchored and matches only -the repo-root directory. Nested `tests/consumers/*/target/` was therefore -NOT ignored, and 1427 artifact files (70 MB) were committed by accident. - -The rule is now `**/target/`, which matches at any depth. Verify with: - -```bash -git check-ignore -v tests/consumers/2d-curves/target -``` - -A `.gitignore` rule does NOT apply to a file already tracked, so fixing -the pattern alone changes nothing: the path must be untracked first with -`git rm -r --cached

` (index only -- leaves files on disk). - - -## Measuring what a profile costs - -scripts/closure-bench.py measures every profile here: resolved -package count, cold-build median, and target/ size. It is the -repeatable form of the ADR 0036 spot check (kernel#12). - -```bash -python3 scripts/closure-bench.py --reps 3 -``` - -It measures the NOISE FLOOR first, by rebuilding one profile -repeatedly, and prints it. A gap smaller than that floor is not a -result -- on this box the two smallest profiles are -indistinguishable from each other, and the harness says so rather -than ranking them. - -Each profile gets its own CARGO_TARGET_DIR under /mnt/backup, wiped -before every run, so builds are genuinely cold and no consumer -target/ is ever created inside the repo. - diff --git a/tests/consumers/README.md b/tests/consumers/README.md new file mode 100644 index 00000000..979a8c9f --- /dev/null +++ b/tests/consumers/README.md @@ -0,0 +1,69 @@ +# Consumer fixtures + +Each directory is a small downstream application that depends on Axiolid the +way a real consumer would. Each one backs one profile in +[`docs/architecture/closure-profiles.toml`](../docs/architecture/closure-profiles.toml) +([ADR 0036](../../docs/adr/0036-use-case-specific-compilation-closures.md)). The +profile names the internal packages that must resolve and the ones that must +not. A fixture proves that a focused application can build without pulling in +the rest of the kernel. + +| Fixture | What it proves | +| --- | --- | +| `core-only` | `axiolid-core` is usable alone: points, vectors, frames, intervals, tolerances | +| `linear-data` | Alignments, centrelines and polylines can be stored and measured with no query algorithm | +| `linear-intersection-minimal` | Line queries need only core, linear, linear intersection and predicates | +| `2d-curves` | 2D plan geometry and transforms, with unit conversion left to the application, pull in no solids or CSG | +| `parametric-curves` | Curve and surface evaluation works without the reference umbrella | +| `spatial-rule-checker` | Proximity rules over points need a spatial index and core values, with no discrete geometry | +| `mesh-rule-checker` | Mesh rule checks need only mesh values, spatial acceleration and measurement | +| `cad-exact` | Analytic curves and surfaces, topology, exact B-rep and NURBS resolve together | +| `rust-facade-application` | The `axiolid` facade with portable providers runs the reference workflows | +| `c-abi-profile` | The internal Rust closure behind the C ABI. It calls only the version symbol. The C compile, link and run probe is `crates/facade/axiolid-capi/tests/c/smoke.c` | +| `full` | Every facade feature at once: the upper bound the narrow profiles are measured against | + +## Rules for a fixture + +- Its `Cargo.toml` has an empty `[workspace]` table, so it is its own + workspace root. Inside the kernel workspace, feature unification would hide + what a real consumer resolves. +- It depends on leaf packages by relative path with `default-features = false`. + The facade and C ABI fixtures depend on `axiolid` or `axiolid-capi` with the + features their profile names. +- It exercises real behaviour, not only type names. A fixture that only names + a type can pass while the package is unusable. +- Adding a dependency changes a declared compatibility promise. Change the + profile on purpose, then regenerate the closure docs. +- A fixture's `.gitignore` ignores its `Cargo.lock`. The checker resolves with + `--offline` and writes the lock on first resolution. Build output is ignored + by the root `**/target/` rule and never committed. + +## Checks + +```bash +cargo xtask architecture closure check +cargo xtask architecture closure explain +cargo xtask architecture closure docs # after a profile change +bash scripts/probe_closure_gate.sh # each closure rule can fail +python3 scripts/closure-bench.py --reps 3 # package count, cold build, target size +``` + +`closure-bench.py` measures the noise floor first by rebuilding one profile +several times, and it does not rank profiles whose gap is smaller than that +floor. Each profile builds cold in its own `CARGO_TARGET_DIR` under `--bench-root`, +outside the repository. + +## Downstream gate + +`scripts/test-downstream-consumers.py` copies six of these fixtures +(`linear-intersection-minimal`, `mesh-rule-checker`, `2d-curves`, +`parametric-curves`, `cad-exact`, `rust-facade-application`) into separate +temporary workspaces. It replaces each path dependency with an exact version +pinned to one immutable Git commit, then checks the source identities Cargo +resolved. `tests/downstream/test_downstream_consumers.py` unit-tests that +manifest rewriting and policy: + +```bash +python3 -m unittest tests/downstream/test_downstream_consumers.py +python3 scripts/test-downstream-consumers.py +``` diff --git a/tests/consumers/c-abi-profile/AGENTS.md b/tests/consumers/c-abi-profile/AGENTS.md deleted file mode 100644 index 730a0534..00000000 --- a/tests/consumers/c-abi-profile/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# C ABI closure probe - -This standalone fixture freezes the internal Rust closure of the native C ABI. The executable only exercises the version symbol; the actual C compiler/link/runtime probe lives in `crates/facade/axiolid-capi/tests/c`. diff --git a/tests/downstream/AGENTS.md b/tests/downstream/AGENTS.md deleted file mode 100644 index fb8ab228..00000000 --- a/tests/downstream/AGENTS.md +++ /dev/null @@ -1,12 +0,0 @@ -# Downstream gate tests - -Unit tests here validate the black-box consumer policy and manifest renderer used by `scripts/test-downstream-consumers.py`. - -The executable Rust fixtures remain under `tests/consumers/`; the gate copies their sources into independent temporary workspaces, replaces development-only path dependencies with exact-version dependencies pinned to an immutable Git artifact, and inspects Cargo's resolved source identities. - -## Verify - -```bash -python3 -m unittest tests/downstream/test_downstream_consumers.py -python3 scripts/test-downstream-consumers.py -``` diff --git a/tests/native/AGENTS.md b/tests/native/AGENTS.md deleted file mode 100644 index 8fdae36f..00000000 --- a/tests/native/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Native consumer tests - -`cmake-consumer/` exercises only the public C API through the stable `Axiolid::axiolid` target. `test-native-cmake.py` copies it outside the workspace before configuring source, installed, or extracted-archive modes; the consumer must execute both semantic success and typed-refusal paths. Its mutation mode removes a required symbol, header, and package config. `test_native_packaging.py` owns archive/path/binary-identity mutation tests. Keep fixtures platform-neutral; platform-specific setup belongs in `.github/workflows/native.yml`. diff --git a/tests/native/cmake-consumer/AGENTS.md b/tests/native/cmake-consumer/AGENTS.md deleted file mode 100644 index 556c0218..00000000 --- a/tests/native/cmake-consumer/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Native CMake consumer - -Black-box C consumer configured in source-tree or installed-package mode. It must use only `find_package`/`Axiolid::axiolid` semantics and the generated public header. diff --git a/tools/AGENTS.md b/tools/AGENTS.md deleted file mode 100644 index 9614481d..00000000 --- a/tools/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Workspace tools - -Developer-only automation. `xtask/` validates architecture metadata and generates architecture documentation. diff --git a/tools/benchmark/AGENTS.md b/tools/benchmark/AGENTS.md deleted file mode 100644 index af5dae99..00000000 --- a/tools/benchmark/AGENTS.md +++ /dev/null @@ -1,172 +0,0 @@ -# tools/benchmark - -The kernel's internal measurement system. Single-kernel benchmarks only — -cross-kernel comparison against OCCT, CGAL, Manifold, ifc-lite, and Truck lives -in the sibling `benchmarks` repository, which can point at an arbitrary kernel -checkout to compare base against head. - -## Why it is here and not in `crates/` - -Layer `tools`, `publish = false`. The architecture gate requires a `tools` layer -package to live under `tools/`, and keeping it unpublished keeps it out of the -50-crate release cascade entirely — measurement scaffolding is not a public -capability. - -## Layout - -``` -src/workload.rs deterministic inputs with derived ground truth -src/validate.rs the correctness checks every benchmark must pass -benches/micro.rs criterion wall-clock, for finding where time goes -benches/regression.rs iai-callgrind instruction counts, for CI gating -examples/filter_probe.rs which orient3d inputs actually escalate -``` - -## The two benchmark kinds answer different questions - -**`micro.rs` (criterion, wall-clock).** Reports elapsed time and throughput. -Informative for finding hot paths, useless as a gate: frequency scaling and -noisy neighbours move elapsed time by more than most real regressions, so any -threshold loose enough to avoid false alarms catches nothing. - -**`regression.rs` (iai-callgrind, instruction counts).** Counts instructions -under valgrind emulation. Independent of machine load and reproducible -run-to-run — measured bit-identical across consecutive runs here — which is what -makes automated comparison trustworthy. This is the CI gate. - -Neither replaces the other. Instruction count ignores cache behaviour and memory -latency, so an optimisation that improves locality without removing instructions -will not show up in `regression.rs` at all. - -## Running - -```bash -cargo bench -p axiolid-benchmark --bench micro # wall-clock -scripts/bench-regression.sh # instruction counts -``` - -`bench-regression.sh` skips cleanly when valgrind or `iai-callgrind-runner` is -absent, so a developer without them can still run the gate. - -```bash -sudo apt-get install valgrind -cargo install iai-callgrind-runner --version 0.16.1 # must match Cargo.toml -``` - -## The rule every benchmark obeys - -**A result that is not validated is not a measurement.** Three ways to look fast -while being wrong: declining the work, failing silently, and being optimised -away. Every workload therefore carries a ground truth *derived from its -construction* — never read back from the code under test — and the harness fails -rather than reporting a fast wrong answer. - -This is carried from the sibling repo, where an unvalidated column once reported -a kernel returning zero volume in 0.2 ms as the fastest result in the table. - -## Pitfalls found while building this - -- **`[profile.bench]` inherits `strip = true` from `[profile.release]`.** A - stripped binary silently defeats callgrind's function toggle: the run - succeeds, reports `Collected: 0`, and prints zero instructions for every - benchmark. That looks like a working harness. `strip = false` is now explicit - in the root manifest with a comment saying why. -- **`#[library_benchmark]` rejects `///` doc comments** on the benchmarked - function. Use `//`. -- **The `iai-callgrind-runner` binary version must equal the `iai-callgrind` - library version.** The runner has no plain `--version` output — it reports a - diagnostic instead — so do not try to parse one. -- **Guessing at a degenerate input does not produce a degenerate input.** Two - attempts to construct a near-degenerate `orient3d` case (`z = 1e-30`, then - `1e-17`) both measured identically to the well-separated case, because - shrinking the offset shrinks the predicate's error bound with it. Only exact - zero and sub-normal magnitudes escalate. `examples/filter_probe.rs` asks the - filter directly instead of assuming; use it before adding a predicate case. - -## Measured baseline (Xeon w7-3565X, 20 cores) - -Recorded so a later reader can tell drift from noise. Instruction counts, not -time — reproducible on any machine. - -| benchmark | instructions | -|---|---| -| `volume_box` | 8,714 | -| `volume_sphere` (1,024 samples) | 2,624,625 | -| `orient3d_filtered` | 2,002 | -| `orient3d_exact` | 7,379 | -| `point_index_build` (1,024 points) | 335,610 | - -The `orient3d` pair is the interesting one: certification costs ~3.7x the -filtered path, and that gap is what any future optimisation of the predicates -has to move. - -## Scenarios and scaling - -`benches/scenario.rs` drives `axiolid::application::Application` -- the public -facade, not internal crates -- so a scenario measures what a consumer actually -pays: dispatch, provider selection, validation, and the geometry itself. The -wall-subtraction rows sweep opening count; the section rows sweep mesh density. -Both validate their output (signed volume against a derived ground truth, -contour count against the expected cut) so a fast wrong answer cannot look -like a win. - -`tests/scaling.rs` asserts complexity rather than recording it. A benchmark -says an operation took 40us; these say the cost grows linearly with triangle -count and that a bounded radius query does not grow with cloud size. They run -in the normal test suite, so a change of complexity class fails the gate -instead of appearing as a slow drift on a chart nobody reads. - -Both are mutation-checked: perturbing the query radius so it scans the whole -cloud, or changing a workload seed so runs stop being reproducible, must fail. - -## Rejected: radix sort in the mesh audit - -Recorded so it is not attempted twice. `callgrind_annotate` showed 68.8% of -`volume_sphere` inside the audit's `sort_unstable_by_key`. Replacing it with -an LSD radix sort over the `(low, high)` key: - -| metric | comparison sort | radix sort | -|---|---|---| -| instructions | 2,512,504 | 2,037,744 (**-18.9%**) | -| D1 misses | 11,607 | 50,648 (**+336%**) | -| wall clock, 399k tris | 41.2 ms | 70.3 ms (**2.2x slower**) | - -Fewer instructions, more than twice the time. The 256-bucket scatter defeats -the cache; the comparison sort's access pattern does not. **The audit is -memory-bound, not compute-bound** -- which is also why SIMD would not help it. - -The lesson generalises: instruction count is a deterministic *regression* -signal, not a speed metric. Always confirm a win in wall clock and cache -counters before claiming one. - - -## Where the boolean path's cost actually is - -Profiled `boolean_subtract` (8.25M instructions, the most expensive -benchmark here) with `callgrind_annotate`: - -| region | share | -| --- | --- | -| `boolmesh` + libc + libm | 66.6% | -| allocator (`malloc`/`free`/`realloc`) | 11.3% | -| `shadows01` (single hottest fn, inside `boolmesh`) | 16.0% | -| transcendental (`acos` etc.) | 1.0% | -| **Axiolid's own crates** | **0.0% (884 instructions)** | - -Two consequences for acceleration work: - -1. **SIMD or rayon inside Axiolid cannot speed this path up.** There is - almost no Axiolid code executing. Optimisation here means changing the - provider, contributing upstream, or adding a second provider -- not - vectorising kernel code. -2. **The grouping path exposes no parallelism on this workload.** - `examples/grouping_probe.rs` measures it: 4/16/64/256 openings all - collapse to ONE group, because the tools are mutually disjoint and get - fused into a single cut. Groups run sequentially by necessity (each cuts - the previous result), so parallel-across-groups would gain nothing here. - Re-run the probe before assuming otherwise on a different workload. - -The 11.3% allocator share is the one Axiolid-side lever visible: it comes -from per-operation `TriMesh` clones and temporaries. Reducing it needs a -measured before/after like any other change, and both wall-clock and -instruction counts, given the audit result above. diff --git a/tools/benchmark/Cargo.toml b/tools/benchmark/Cargo.toml index 46574b6b..19aff3c5 100644 --- a/tools/benchmark/Cargo.toml +++ b/tools/benchmark/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true # Never published. This is measurement scaffolding for the kernel's own # development, not a public capability, and keeping it unpublished keeps it out diff --git a/tools/benchmark/README.md b/tools/benchmark/README.md new file mode 100644 index 00000000..3256cade --- /dev/null +++ b/tools/benchmark/README.md @@ -0,0 +1,67 @@ +# axiolid-benchmark + +The kernel's own measurement system: deterministic workloads with a ground truth +derived from how they were built, validation that every benchmark must pass +before its number counts, wall-clock and instruction-count benchmarks, and +tests that assert how cost grows. It measures this kernel only. Comparison +against OCCT, CGAL, Manifold, ifc-lite and Truck lives in the sibling +`benchmarks` repository, which can point at any kernel checkout and compare a +base against a head. The crate is in the `tools` layer and is never published, +so it stays out of the release. + +## Layout + +| Path | What it answers | +| --- | --- | +| `src/workload.rs` | Seeded inputs with derived ground truth, byte-identical on every machine | +| `src/validate.rs` | The correctness checks a result must pass before it is reported | +| `src/dataset.rs` | Small inputs that once broke something, built in code with the reason beside them | +| `benches/micro.rs` | Where wall-clock time goes (criterion) | +| `benches/regression.rs` | Instruction counts under callgrind (iai-callgrind), the CI regression signal | +| `benches/scenario.rs` | End-to-end cost through the public `axiolid::application` facade | +| `benches/audit.rs` | How the mesh audit scales with triangle count | +| `tests/scaling.rs` | Growth rate asserted from operation counts, run by `cargo test` | +| `examples/filter_probe.rs` | Which `orient3d` inputs actually escalate past the floating-point filter | +| `examples/grouping_probe.rs` | How much independent work the grouped wall subtraction exposes | + +## Running + +```bash +cargo bench -p axiolid-benchmark --bench micro # wall clock +cargo bench -p axiolid-benchmark --bench scenario # end to end +scripts/bench-regression.sh # instruction counts +cargo test -p axiolid-benchmark # scaling assertions +``` + +`scripts/bench-regression.sh` needs valgrind and an `iai-callgrind-runner` +whose version equals the `iai-callgrind` version in `Cargo.toml`. It skips +cleanly when either is missing: + +```bash +sudo apt-get install valgrind +cargo install iai-callgrind-runner --version 0.16.1 +``` + +On every pull request, `.github/workflows/performance.yml` runs the regression +benchmarks on the base commit and on the head, so the base is measured on the +same runner rather than read from a recorded baseline. + +## Method + +- **An unvalidated result is not a measurement.** A kernel that declines the + work, fails silently, or is optimised away looks fast. Every case checks its + output against the ground truth from `src/workload.rs` and fails instead of + reporting a fast wrong answer. +- **Instruction count shows regressions, not speed.** It ignores cache + behaviour and memory latency, so a change can remove instructions and still + run slower. Before claiming a speedup, confirm it in wall clock and in cache + counters (`valgrind --tool=cachegrind` or `callgrind_annotate`). +- **Wall clock is not a gate.** Frequency scaling and noisy neighbours move it + by more than most real regressions. +- **Ask the predicate, don't guess the input.** A near-degenerate `orient3d` + case built by shrinking an offset does not escalate, because the filter's + error bound shrinks with it. Run `examples/filter_probe.rs` before adding a + predicate case that claims to exercise the exact path. +- **Re-run a probe before assuming parallelism.** On the wall workload, + `examples/grouping_probe.rs` shows 4 to 256 disjoint openings fuse into one + group, so parallelism across groups gains nothing there. diff --git a/tools/benchmark/benches/regression.rs b/tools/benchmark/benches/regression.rs index 8c3d416e..2c48962c 100644 --- a/tools/benchmark/benches/regression.rs +++ b/tools/benchmark/benches/regression.rs @@ -22,6 +22,9 @@ //! `cargo bench --bench regression` needs `valgrind` on PATH. It is therefore //! not part of the default gate; see `scripts/bench-regression.sh`, which skips //! with a clear message rather than failing when valgrind is absent. +//! +//! `#[library_benchmark]` rejects `///` doc comments on the function it +//! wraps, so each benchmark below is described with a plain `//` comment. use axiolid::application::Application; use axiolid::contracts::ExecutionOptions; diff --git a/tools/oracle/AGENTS.md b/tools/oracle/AGENTS.md deleted file mode 100644 index 5898a1e7..00000000 --- a/tools/oracle/AGENTS.md +++ /dev/null @@ -1,34 +0,0 @@ -# axiolid-oracle - -Independent mapped-3D verification oracle for intersection and inversion -results (ADR 0037). Workspace-internal: it is a dev-dependency of the crates it -checks and is never published through the facade. - -## What lives here - -- `src/grid.rs` — deterministic closed-span sampling with an explicit density - budget. Both endpoints are always visited, so a result claimed at a box - corner is never missed by rounding. -- `src/contact.rs` — mapped-3D deviation between two operands over a claimed - parameter box (curve/curve in 2D and 3D, curve/surface, surface/surface). -- `src/distance.rs` — sound refutation of a claimed global minimum distance. - -## The one rule that matters - -This crate must not depend on `axiolid-nurbs`. Its whole value is that it -shares no subdivision, interval, or root-isolation machinery with the -implementations it checks. Adding that dependency silently turns the oracle -into a second opinion from the same code. - -Evaluation comes from `axiolid-evaluate` only. - -## Falsifier, not prover - -- A small `contact_witness` deviation is a witness that near-coincident - geometry exists in the claimed box. A large one only means sampling did not - find any. -- A `closer_point_refutation` hit is a sound disproof of the claimed minimum. - No hit is not a proof of global minimality. - -Tests assert on the reported deviation, so a failure says how far off the -result was, not merely that it disagreed. diff --git a/tools/oracle/Cargo.toml b/tools/oracle/Cargo.toml index 5631524a..56bec471 100644 --- a/tools/oracle/Cargo.toml +++ b/tools/oracle/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -readme.workspace = true +readme = "README.md" publish = false [dependencies] diff --git a/tools/oracle/README.md b/tools/oracle/README.md new file mode 100644 index 00000000..0ad38b58 --- /dev/null +++ b/tools/oracle/README.md @@ -0,0 +1,11 @@ +# axiolid-oracle + +An independent check for intersection, inversion and distance results +([ADR 0037](../../docs/adr/0037-mapped-3d-verification-oracle.md)). It maps a +claimed parameter-space result back into model space through `axiolid-evaluate` +and measures the 3D deviation there: contact between two curves or surfaces over +a claimed parameter box, and a search for a point closer than a claimed global +minimum distance. It is a falsifier, not a prover. A hit disproves a claim, and +no hit proves nothing. It shares no subdivision, interval or root-isolation code +with `axiolid-nurbs`, which it checks, and its dependency allowlist keeps it +that way. It is a dev-dependency of the crates it checks and is never published. diff --git a/tools/xtask/AGENTS.md b/tools/xtask/AGENTS.md deleted file mode 100644 index 2d8542e6..00000000 --- a/tools/xtask/AGENTS.md +++ /dev/null @@ -1,5 +0,0 @@ -# Architecture xtask - -Run `cargo xtask architecture check|list|graph|docs`. Metadata and generated docs must remain deterministic and mutation-verified. - -`cargo xtask gaps [next|list|show|check]` reads `architecture/capability-ledger.toml` (`src/gaps/`). `check` is in `scripts/gate.sh`; `scripts/probe_gaps_gate.sh` proves each rule can fail. A new rule in `verify.rs` gets a new mutation in the probe. diff --git a/tools/xtask/README.md b/tools/xtask/README.md new file mode 100644 index 00000000..5985e080 --- /dev/null +++ b/tools/xtask/README.md @@ -0,0 +1,27 @@ +# xtask + +Developer automation for this workspace, run as `cargo xtask ` (the +alias lives in `.cargo/config.toml`). It checks the architecture metadata every +package declares, generates the architecture docs and the per-crate reference +pages of the docs site, keeps the C header in step +with the C ABI, reads the capability ledger, and guards where repository context +lives. It is never published. + +| Command | What it does | +| --- | --- | +| `architecture check` | Validates `[package.metadata.axiolid]`: layers, roles, placement, exact internal dependency allowlists, naming, unsafe policy, generated-doc freshness | +| `architecture list \| graph \| docs` | Prints the model, or regenerates `docs/architecture/` from `cargo metadata` | +| `architecture closure check \| docs \| explain ` | Resolves each fixture in `tests/consumers/` against `docs/architecture/closure-profiles.toml` | +| `gaps [next \| list \| show \| check]` | Reads `docs/architecture/capability-ledger.toml`; `check` keeps its evidence paths, reference paths and issue keys resolvable | +| `ffi header \| check` | Regenerates or checks `crates/facade/axiolid-capi/include/axiolid.h` | +| `docs [--check]` | Regenerates, or checks, every page derived from the crates: `docs/reference/` (one page per published crate, the index, the per-crate changelog), the sidebar facts in `docs/.vitepress/data/facts.json` and the architecture maps; checks each published README links its docs.rs and reference pages and makes no publication or version claim | +| `context check` | Enforces ADR 0078: one root `AGENTS.md`, no plan files, a `README.md` per crate, `TODO(#N)` markers only | + +Every check runs in `scripts/gate.sh`. Output must be deterministic: generated +docs are compared byte for byte. + +Each gate has a mutation probe in `scripts/` (`probe_layering_gate.sh`, +`probe_closure_gate.sh`, `probe_naming_gate.sh`, `probe_gaps_gate.sh`, +`probe_context_gate.sh`, `probe_docs_gate.sh`) that breaks the input and requires the check to fail. +A new rule gets a new mutation in its probe; a rule that cannot fail is not +a check. diff --git a/tools/xtask/src/architecture.rs b/tools/xtask/src/architecture.rs index 3de89fd6..e4b5a11c 100644 --- a/tools/xtask/src/architecture.rs +++ b/tools/xtask/src/architecture.rs @@ -1,8 +1,8 @@ mod checks; mod closure; -mod model; +pub(crate) mod model; mod naming; -mod render; +pub(crate) mod render; mod source_checks; use model::{Architecture, Result}; diff --git a/tools/xtask/src/architecture/closure.rs b/tools/xtask/src/architecture/closure.rs index 3e1d2ea8..768152a5 100644 --- a/tools/xtask/src/architecture/closure.rs +++ b/tools/xtask/src/architecture/closure.rs @@ -16,7 +16,7 @@ use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; -const PROFILES: &str = "architecture/closure-profiles.toml"; +const PROFILES: &str = "docs/architecture/closure-profiles.toml"; #[derive(Debug, Clone)] pub struct ClosureProfile { diff --git a/tools/xtask/src/architecture/model.rs b/tools/xtask/src/architecture/model.rs index d261a612..a3f7bc1c 100644 --- a/tools/xtask/src/architecture/model.rs +++ b/tools/xtask/src/architecture/model.rs @@ -13,6 +13,8 @@ pub struct PackageArchitecture { pub role: String, pub domain: String, pub public: bool, + /// `publish` is not `false`: the package ships to crates.io. + pub publish: bool, pub format_neutral: bool, pub declared_dependency_packages: BTreeSet, pub allowed_internal_dependencies: BTreeSet, @@ -32,10 +34,11 @@ impl Architecture { .no_deps() .exec() .map_err(|error| format!("cargo metadata failed: {error}"))?; - Self::from_metadata(metadata) + Self::from_metadata(&metadata) } - fn from_metadata(metadata: Metadata) -> Result { + /// Shared with `cargo xtask docs`, which reads the same metadata once. + pub fn from_metadata(metadata: &Metadata) -> Result { let root = PathBuf::from(metadata.workspace_root.as_str()); let workspace: BTreeMap = metadata .workspace_packages() @@ -96,6 +99,7 @@ impl Architecture { role: string(table, "role", &name)?, domain: string(table, "domain", &name)?, public: boolean(table, "public", &name)?, + publish: package.publish.as_ref().is_none_or(|r| !r.is_empty()), format_neutral: boolean(table, "format-neutral", &name)?, declared_dependency_packages, allowed_internal_dependencies, diff --git a/tools/xtask/src/architecture/render.rs b/tools/xtask/src/architecture/render.rs index 70c9a237..d7bff364 100644 --- a/tools/xtask/src/architecture/render.rs +++ b/tools/xtask/src/architecture/render.rs @@ -2,8 +2,8 @@ use super::model::{Architecture, Result}; use std::fmt::Write as _; use std::fs; -const CRATE_MAP: &str = "docs/architecture/crate-map.md"; -const DEPENDENCY_GRAPH: &str = "docs/architecture/dependency-graph.md"; +pub const CRATE_MAP: &str = "docs/architecture/crate-map.md"; +pub const DEPENDENCY_GRAPH: &str = "docs/architecture/dependency-graph.md"; pub fn list(architecture: &Architecture) -> String { let mut output = String::new(); @@ -49,7 +49,7 @@ pub fn graph(architecture: &Architecture) -> String { pub fn crate_map_document(architecture: &Architecture) -> String { let mut output = String::from( - "\n\n# Axiolid crate map\n\nArchitecture metadata describes ownership, not runtime capability support. Capability claims require a trait implementation plus conformance evidence.\n\n| Package | Path | Layer | Role | Domain | Visibility | Allowed internal dependencies |\n| --- | --- | --- | --- | --- | --- | --- |\n", + "---\n# Generated by `cargo xtask architecture docs`; do not edit manually.\n---\n\n# Axiolid crate map\n\nArchitecture metadata describes ownership, not runtime capability support. Capability claims require a trait implementation plus conformance evidence.\n\n| Package | Path | Layer | Role | Domain | Visibility | Allowed internal dependencies |\n| --- | --- | --- | --- | --- | --- | --- |\n", ); for package in architecture.packages.values() { let dependencies = if package.allowed_internal_dependencies.is_empty() { @@ -63,10 +63,16 @@ pub fn crate_map_document(architecture: &Architecture) -> String { .join(", ") }; let visibility = if package.public { "public" } else { "internal" }; + // A published crate has a generated reference page (`cargo xtask docs`). + let name = if package.publish { + format!("[`{0}`](/reference/crates/{0})", package.name) + } else { + format!("`{}`", package.name) + }; writeln!( output, - "| `{}` | [`{}`](https://github.com/axiolid/kernel/tree/main/{}) | `{}` | `{}` | `{}` | {} | {} |", - package.name, + "| {} | [`{}`](https://github.com/axiolid/kernel/tree/main/{}) | `{}` | `{}` | `{}` | {} | {} |", + name, package.path, package.path, package.layer, @@ -83,7 +89,7 @@ pub fn crate_map_document(architecture: &Architecture) -> String { pub fn graph_document(architecture: &Architecture) -> String { format!( - "\n\n# Axiolid dependency graph\n\nEdges point from a package to an internal package it depends on.\n\n```mermaid\n{}```\n", + "---\n# Generated by `cargo xtask architecture docs`; do not edit manually.\n---\n\n# Axiolid dependency graph\n\nEdges point from a package to an internal package it depends on.\n\n```mermaid\n{}```\n", graph(architecture) ) } diff --git a/tools/xtask/src/context.rs b/tools/xtask/src/context.rs new file mode 100644 index 00000000..bd88cc71 --- /dev/null +++ b/tools/xtask/src/context.rs @@ -0,0 +1,336 @@ +//! Guard where the repository keeps its context (ADR 0078). +//! +//! The root `AGENTS.md` is the one file every contributor reads first: short, +//! stable, and the only one of its name. Each crate carries a `README.md` (its +//! crates.io page) with its purpose and the design notes no test, ADR or +//! module doc already holds; the reasoning behind a module lives in its `//!` +//! docs. Open work is not context: it lives in GitHub issues and `TODO(#N)` +//! markers, never in a checked-in plan. +//! +//! `check` keeps that shape: a nested `AGENTS.md`, a plan file or a stray +//! root-level note cannot regrow, a crate cannot ship without a README, a +//! README cannot grow into a manual or a checklist, a code marker cannot +//! float free of an issue, and a pointer to a README cannot dangle after a +//! file moves. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use cargo_metadata::MetadataCommand; + +type Result = std::result::Result; + +/// The root `AGENTS.md` is read in full before any change, so it stays short. +const ROOT_AGENTS_MAX_LINES: usize = 120; + +/// A crate README is a crates.io page and a place for a few design notes, not +/// a manual: the API belongs in rustdoc, the site in `docs/`. +const README_MAX_LINES: usize = 150; + +/// The workspace crate count the README scan must at least reach, so a +/// layout change that makes the filter match nothing fails instead of passing. +const MIN_CRATES: usize = 50; + +/// Files exempt from the marker and pointer scans. Dated records may name +/// files that no longer exist, and rewriting them would falsify history. +fn is_exempt(path: &str) -> bool { + path.starts_with("docs/adr/") + || path.ends_with("CHANGELOG.md") + || path == "docs/reference/changelog.md" + || path.starts_with("docs/research/") + // This checker names the files it forbids. + || path == "tools/xtask/src/context.rs" +} + +pub fn check() -> Result<()> { + let metadata = MetadataCommand::new() + .no_deps() + .exec() + .map_err(|error| format!("cargo metadata failed: {error}"))?; + let root = PathBuf::from(metadata.workspace_root.as_str()); + let files = repository_files(&root)?; + let mut problems = Vec::new(); + + root_agents(&root, &mut problems); + retired_context_files(&files, &mut problems); + root_files(&files, &mut problems); + + let mut crates = 0; + for package in &metadata.packages { + let manifest = PathBuf::from(package.manifest_path.as_str()); + let dir = manifest.parent().expect("a manifest has a directory"); + let rel = relative(&root, dir); + if !(rel.starts_with("crates/") || rel.starts_with("tools/")) { + continue; + } + crates += 1; + crate_readme(&root, dir, &mut problems); + let publishable = package.publish.as_ref().is_none_or(|r| !r.is_empty()); + // cargo reports the path as written, relative to the manifest. + let declared = package.readme.as_ref().map(|p| p.as_str()); + if publishable && declared != Some("README.md") { + problems.push(format!( + "{rel}/Cargo.toml: declare `readme = \"README.md\"` so crates.io shows the crate's own page" + )); + } + } + if crates < MIN_CRATES { + problems.push(format!( + "found {crates} crates under crates/ and tools/, expected at least {MIN_CRATES}: the scan no longer matches the layout" + )); + } + + for file in &files { + if file.ends_with(".rs") && !is_exempt(file) { + todo_markers(&root, file, &mut problems); + } + if (file.ends_with(".rs") || file.ends_with(".md")) && !is_exempt(file) { + pointers(&root, file, &mut problems); + } + } + + if problems.is_empty() { + println!( + "context: ok ({crates} crate READMEs, {} files scanned)", + files.len() + ); + Ok(()) + } else { + Err(format!( + "{} problem(s):\n {}", + problems.len(), + problems.join("\n ") + )) + } +} + +/// Tracked files plus untracked ones not ignored, so a new plan file is caught +/// before it is committed. +fn repository_files(root: &Path) -> Result> { + let output = Command::new("git") + .args(["ls-files", "--cached", "--others", "--exclude-standard"]) + .current_dir(root) + .output() + .map_err(|error| format!("git ls-files failed: {error}"))?; + if !output.status.success() { + return Err("git ls-files failed".into()); + } + let mut files: Vec = String::from_utf8_lossy(&output.stdout) + .lines() + .filter(|path| !path.starts_with("docs/node_modules/") && root.join(path).is_file()) + .map(str::to_owned) + .collect(); + files.sort(); + files.dedup(); + Ok(files) +} + +fn relative(root: &Path, path: &Path) -> String { + path.strip_prefix(root) + .unwrap_or(path) + .to_string_lossy() + .replace('\\', "/") +} + +fn has_checkbox(text: &str) -> Option { + text.lines().position(|line| { + let line = line.trim_start(); + ["- [ ]", "- [x]", "- [X]", "* [ ]", "* [x]", "* [X]"] + .iter() + .any(|box_| line.starts_with(box_)) + }) +} + +fn root_agents(root: &Path, problems: &mut Vec) { + let Ok(text) = std::fs::read_to_string(root.join("AGENTS.md")) else { + problems.push("AGENTS.md: the root AGENTS.md is missing".into()); + return; + }; + let lines = text.lines().count(); + if lines > ROOT_AGENTS_MAX_LINES { + problems.push(format!( + "AGENTS.md: {lines} lines, limit {ROOT_AGENTS_MAX_LINES}; move detail into a README, module doc or ADR" + )); + } + if let Some(line) = has_checkbox(&text) { + problems.push(format!( + "AGENTS.md:{}: task checkbox; open work lives in issues", + line + 1 + )); + } +} + +/// Nested agent context and checked-in plans are retired: their content lives +/// in module docs, READMEs, ADRs, tests and issues. +fn retired_context_files(files: &[String], problems: &mut Vec) { + for file in files { + let name = file.rsplit('/').next().unwrap_or(file).to_ascii_lowercase(); + let nested_agents = (name == "agents.md" || name == "claude.md") && file != "AGENTS.md"; + let plan = name.ends_with(".md") + && (name == "plan.md" + || name.starts_with("plan-") + || name.starts_with("plan_") + || name.ends_with("-plan.md")); + if nested_agents { + problems.push(format!( + "{file}: only the root AGENTS.md exists; put crate context in its README.md or module docs" + )); + } else if plan || file.starts_with("docs/plans/") { + problems.push(format!( + "{file}: plans are not checked in; open work lives in GitHub issues (ADR 0078)" + )); + } + } +} + +/// The repository root holds the workspace manifest and its front pages. +/// Session plans, findings and notes do not belong there: their content goes +/// to issues, ADRs, tests or module docs. +const ROOT_FILES: &[&str] = &[ + "AGENTS.md", + "Cargo.lock", + "Cargo.toml", + "LICENSE", + "README.md", + "rust-toolchain.toml", +]; + +fn root_files(files: &[String], problems: &mut Vec) { + for file in files { + if !file.contains('/') && !file.starts_with('.') && !ROOT_FILES.contains(&file.as_str()) { + problems.push(format!( + "{file}: the root holds only {}; move it into an issue, ADR, test or module doc", + ROOT_FILES.join(", ") + )); + } + } +} + +fn crate_readme(root: &Path, dir: &Path, problems: &mut Vec) { + let readme = dir.join("README.md"); + let rel = relative(root, &readme); + let Ok(text) = std::fs::read_to_string(&readme) else { + problems.push(format!("{rel}: every crate has a README.md")); + return; + }; + let lines = text.lines().count(); + if lines > README_MAX_LINES { + problems.push(format!( + "{rel}: {lines} lines, limit {README_MAX_LINES}; the API belongs in rustdoc, the guide in docs/" + )); + } + if let Some(line) = has_checkbox(&text) { + problems.push(format!( + "{rel}:{}: task checkbox; open work lives in issues", + line + 1 + )); + } +} + +/// A work marker in code names its issue: `TODO(#123)`. Anything else is a +/// plan nobody tracks. +fn todo_markers(root: &Path, file: &str, problems: &mut Vec) { + let Ok(text) = std::fs::read_to_string(root.join(file)) else { + return; + }; + for (index, line) in text.lines().enumerate() { + for marker in ["TODO", "FIXME", "XXX"] { + let mut rest = line; + while let Some(at) = rest.find(marker) { + let before = rest[..at].chars().next_back(); + let after = &rest[at + marker.len()..]; + let is_word = before.is_none_or(|c| !c.is_alphanumeric() && c != '_') + && after + .chars() + .next() + .is_none_or(|c| !c.is_alphanumeric() && c != '_'); + if is_word && !(marker == "TODO" && names_issue(after)) { + problems.push(format!( + "{file}:{}: `{marker}` without an issue; write `TODO(#N)`", + index + 1 + )); + } + rest = after; + } + } + } +} + +fn names_issue(after: &str) -> bool { + let Some(inner) = after.strip_prefix("(#") else { + return false; + }; + let digits = inner.chars().take_while(char::is_ascii_digit).count(); + digits > 0 && inner[digits..].starts_with(')') +} + +/// Path-like tokens ending in a context file name, e.g. `../README.md` or +/// `crates/foo/AGENTS.md`. +fn path_tokens<'a>(text: &'a str, name: &str) -> Vec<(usize, &'a str)> { + let mut tokens = Vec::new(); + for (index, line) in text.lines().enumerate() { + let mut from = 0; + while let Some(at) = line[from..].find(name) { + let end = from + at + name.len(); + let start = line[..from + at] + .rfind(|c: char| !(c.is_alphanumeric() || "._-/".contains(c))) + .map_or(0, |i| i + 1); + tokens.push((index + 1, &line[start..end])); + from = end; + } + } + tokens +} + +fn pointers(root: &Path, file: &str, problems: &mut Vec) { + let Ok(text) = std::fs::read_to_string(root.join(file)) else { + return; + }; + for (line, token) in path_tokens(&text, "PLAN.md") { + problems.push(format!( + "{file}:{line}: `{token}` points at a retired plan; link the issue instead" + )); + } + for (line, token) in path_tokens(&text, "AGENTS.md") { + if token.contains('/') { + problems.push(format!( + "{file}:{line}: `{token}` points at a nested AGENTS.md; only the root one exists" + )); + } + } + let dir = Path::new(file).parent().unwrap_or(Path::new("")); + for (line, token) in path_tokens(&text, "README.md") { + if !token.contains('/') || token.starts_with("http") || token.contains("://") { + continue; + } + let resolves = root.join(dir).join(token).is_file() || root.join(token).is_file(); + if !resolves { + problems.push(format!("{file}:{line}: `{token}` does not resolve")); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn issue_markers_need_a_number() { + assert!(names_issue("(#12) fix")); + assert!(!names_issue("(#) fix")); + assert!(!names_issue(": fix")); + assert!(!names_issue("(12)")); + } + + #[test] + fn tokens_stop_at_punctuation() { + let tokens = path_tokens("see `../foo/README.md`, and [x](a/README.md)", "README.md"); + assert_eq!(tokens, vec![(1, "../foo/README.md"), (1, "a/README.md")]); + } + + #[test] + fn checkboxes_are_found() { + assert_eq!(has_checkbox("a\n - [ ] b"), Some(1)); + assert_eq!(has_checkbox("- [link](x)"), None); + } +} diff --git a/tools/xtask/src/docs.rs b/tools/xtask/src/docs.rs new file mode 100644 index 00000000..47b59a1a --- /dev/null +++ b/tools/xtask/src/docs.rs @@ -0,0 +1,171 @@ +//! `cargo xtask docs`: regenerate, or check, every page the docs site +//! derives from the crates. +//! +//! Each generator returns the content a file should have. This module +//! compares and writes, so `--check` and regeneration can never disagree +//! about what "current" means. Nothing a generated page says is typed twice: +//! it comes from a crate's manifest and `[package.metadata.axiolid]`, its +//! `README.md`, its `CHANGELOG.md`, and the facade features that enable it. +//! +//! ## Internal split +//! +//! - `workspace.rs`: the publishable crates, read once from `cargo metadata`. +//! - `changelog.rs`: release sections of the per-crate changelogs, and the +//! assembled `docs/reference/changelog.md`. +//! - `reference.rs`: one page per crate under `docs/reference/crates/`, and +//! the index that groups them by layer. +//! - `facts.rs`: `docs/.vitepress/data/facts.json`, which builds the sidebar. +//! - `readme.rs`: a crate README's prose for its page, and the lint that +//! keeps each README a crates.io page linking back here. +//! +//! The architecture crate map and dependency graph are regenerated here too, +//! from `architecture::render`, so one command refreshes every derived page. + +mod changelog; +mod facts; +mod readme; +mod reference; +mod workspace; + +use std::path::{Path, PathBuf}; + +use crate::architecture::render; +use workspace::Workspace; + +type Result = std::result::Result; + +/// Directories whose every file is generated. A file there that no generator +/// produced belongs to a crate that is gone or renamed, and is removed. +const MANAGED: &[&str] = &["docs/reference/crates"]; + +/// The published site, which every crate README links back to. +pub(crate) const SITE: &str = "https://axiolid.github.io/kernel"; +const REPO: &str = "https://github.com/axiolid/kernel"; + +/// The banner every wholly generated page starts with. It is a YAML comment +/// in front matter, not an HTML comment: the site renders Markdown with +/// `html: false`, which would print an HTML comment as text. +pub(crate) const BANNER: &str = "---\n# Generated by `cargo xtask docs`. Do not edit: change the source it names and regenerate.\n---"; + +/// One generated file: where it lives and what it should contain. +struct Output { + rel: String, + /// `None` when the file does not exist yet. + current: Option, + updated: String, +} + +impl Output { + fn new(workspace: &Workspace, rel: impl Into, updated: String) -> Self { + let rel = rel.into(); + let current = std::fs::read_to_string(workspace.root.join(&rel)).ok(); + Self { + rel, + current, + updated, + } + } + + fn stale(&self) -> bool { + self.current.as_deref() != Some(self.updated.as_str()) + } +} + +pub fn run(check: bool) -> Result<()> { + let workspace = Workspace::load()?; + let mut outputs = vec![ + changelog::page(&workspace)?, + facts::generate(&workspace)?, + Output::new( + &workspace, + render::CRATE_MAP, + render::crate_map_document(&workspace.architecture), + ), + Output::new( + &workspace, + render::DEPENDENCY_GRAPH, + render::graph_document(&workspace.architecture), + ), + ]; + outputs.extend(reference::generate(&workspace)?); + let lint = readme::problems(&workspace); + + let orphans = orphans(&workspace.root, &outputs); + let stale: Vec<&Output> = outputs.iter().filter(|o| o.stale()).collect(); + if check { + let mut problems: Vec = stale + .iter() + .map(|o| format!("{} is stale; run `cargo xtask docs`", o.rel)) + .collect(); + problems.extend(orphans.iter().map(|p| { + format!( + "{} is no longer generated; run `cargo xtask docs`", + relative(&workspace.root, p) + ) + })); + problems.extend(lint); + if problems.is_empty() { + println!("docs in sync ({} generated files)", outputs.len()); + return Ok(()); + } + return Err(format!( + "{} problem(s):\n {}", + problems.len(), + problems.join("\n ") + )); + } + + for output in &stale { + let path = workspace.root.join(&output.rel); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .map_err(|error| format!("create {}: {error}", parent.display()))?; + } + std::fs::write(&path, &output.updated) + .map_err(|error| format!("write {}: {error}", output.rel))?; + println!("updated {}", output.rel); + } + for orphan in &orphans { + std::fs::remove_file(orphan) + .map_err(|error| format!("remove {}: {error}", orphan.display()))?; + println!("removed {}", relative(&workspace.root, orphan)); + } + if stale.is_empty() && orphans.is_empty() { + println!("docs already in sync ({} generated files)", outputs.len()); + } + // Generation cannot fix a README; report it after writing what it can. + if lint.is_empty() { + Ok(()) + } else { + Err(format!( + "{} README problem(s):\n {}", + lint.len(), + lint.join("\n ") + )) + } +} + +/// Files in a managed directory that no generator produced. +fn orphans(root: &Path, outputs: &[Output]) -> Vec { + let mut out = Vec::new(); + for dir in MANAGED { + let Ok(entries) = std::fs::read_dir(root.join(dir)) else { + continue; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() && !outputs.iter().any(|o| root.join(&o.rel) == path) { + out.push(path); + } + } + } + out.sort(); + out +} + +fn relative(root: &Path, path: &Path) -> String { + path.strip_prefix(root) + .unwrap_or(path) + .to_string_lossy() + .replace('\\', "/") +} diff --git a/tools/xtask/src/docs/changelog.rs b/tools/xtask/src/docs/changelog.rs new file mode 100644 index 00000000..191818dd --- /dev/null +++ b/tools/xtask/src/docs/changelog.rs @@ -0,0 +1,316 @@ +//! Release sections of the per-crate changelogs (ADR 0067), and +//! `docs/reference/changelog.md`, which collects them. +//! +//! A release is a dated `## [x.y.z] - YYYY-MM-DD` section. The release commit +//! dates it, so what a page calls "released" is a property of the commit, not +//! of when the docs are built: reading git tags instead would make every +//! release turn `main`'s docs stale, since the tag is pushed after the +//! release commit merges. `[Unreleased]` is never a release, so adding to it +//! never makes a generated page stale. +//! +//! Crates published only as part of a workspace-wide release before ADR 0067 +//! have no dated section of their own. For them the release is the dated +//! section of `docs/CHANGELOG.md` whose version the manifest still carries. + +use std::cmp::Ordering; + +use super::workspace::{Crate, Workspace}; +use super::{Output, Result, BANNER, REPO}; + +const PAGE: &str = "docs/reference/changelog.md"; +const WORKSPACE_CHANGELOG: &str = "docs/CHANGELOG.md"; + +/// One dated release section. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct Section { + pub(super) version: String, + pub(super) date: String, + pub(super) body: String, +} + +/// What a crate has released, and where the notes for it live. +pub(super) struct Release { + pub(super) version: String, + pub(super) date: String, + /// The notes of this release, links made absolute for the site. `None` + /// when the release was workspace-wide and the crate has no own section. + pub(super) notes: Option, +} + +fn read(workspace: &Workspace, rel: &str) -> Result { + std::fs::read_to_string(workspace.root.join(rel)).map_err(|error| format!("{rel}: {error}")) +} + +fn crate_changelog(workspace: &Workspace, krate: &Crate) -> Result { + read(workspace, &format!("{}/CHANGELOG.md", krate.dir)) +} + +/// The newest release of a crate, if it has one. +pub(super) fn latest(workspace: &Workspace, krate: &Crate) -> Result> { + let own = sections(&crate_changelog(workspace, krate)?); + if let Some(newest) = own + .into_iter() + .max_by(|a, b| compare_versions(&a.version, &b.version)) + { + let notes = (!newest.body.is_empty()).then(|| absolutise(&newest.body, &krate.dir)); + return Ok(Some(Release { + version: newest.version, + date: newest.date, + notes, + })); + } + let shared = sections(&read(workspace, WORKSPACE_CHANGELOG)?); + Ok(shared + .into_iter() + .find(|section| section.version == krate.version) + .map(|section| Release { + version: section.version, + date: section.date, + notes: None, + })) +} + +/// `docs/reference/changelog.md`: every publishable crate's releases, +/// grouped by crate, newest first within each. +pub(super) fn page(workspace: &Workspace) -> Result { + let mut crates = Vec::new(); + for krate in &workspace.crates { + crates.push((krate.name.as_str(), crate_changelog(workspace, krate)?)); + } + Ok(Output::new(workspace, PAGE, render(&crates))) +} + +fn render(crates: &[(&str, String)]) -> String { + let mut lines = vec![ + BANNER.to_owned(), + String::new(), + "# Per-crate changelog".to_owned(), + String::new(), + "Every publishable crate versions and publishes independently \ + ([ADR 0067](/adr/0067-crates-version-independently)); this page \ + collects each crate's own `CHANGELOG.md`, newest release first per \ + crate. Workspace-wide narrative — breaking bumps and coordinated \ + releases — stays in the [top-level changelog](/CHANGELOG)." + .to_owned(), + String::new(), + ]; + let mut any_release = false; + for (name, text) in crates { + let sections = sections(text); + if sections.is_empty() { + continue; + } + any_release = true; + lines.push(format!("## {name}")); + lines.push(String::new()); + for section in sections { + lines.push(format!("### {} - {}", section.version, section.date)); + lines.push(String::new()); + if !section.body.is_empty() { + lines.push(section.body); + lines.push(String::new()); + } + } + lines.push(String::new()); + } + if !any_release { + lines.push( + "No crate has a dated release yet under independent versioning; \ + every crate's history to date lives in the top-level changelog." + .to_owned(), + ); + lines.push(String::new()); + } + format!("{}\n", lines.join("\n").trim_end_matches('\n')) +} + +/// Every dated section in order of appearance. A section's body runs to the +/// next dated heading; `[Unreleased]` is not one, so its notes, which come +/// first, belong to no section. +pub(super) fn sections(text: &str) -> Vec
{ + // (heading start, body start, version, date) + let mut headings = Vec::new(); + let mut offset = 0; + for line in text.split_inclusive('\n') { + if let Some((version, date)) = dated_heading(line) { + headings.push((offset, offset + line.len(), version, date)); + } + offset += line.len(); + } + let mut out = Vec::new(); + for (index, (_, body_start, version, date)) in headings.iter().enumerate() { + let end = headings.get(index + 1).map_or(text.len(), |next| next.0); + out.push(Section { + version: version.clone(), + date: date.clone(), + body: text[*body_start..end].trim_matches('\n').to_owned(), + }); + } + out +} + +/// `## [0.3.1] - 2026-09-27` → (version, date). Only a dated heading counts. +fn dated_heading(line: &str) -> Option<(String, String)> { + let rest = line.strip_prefix("## [")?; + let close = rest.find(']')?; + let version = &rest[..close]; + if version.is_empty() { + return None; + } + let date = rest[close + 1..].strip_prefix(" - ")?.trim_end(); + let shape = date.len() == 10 + && date.bytes().enumerate().all(|(i, b)| match i { + 4 | 7 => b == b'-', + _ => b.is_ascii_digit(), + }); + shape.then(|| (version.to_owned(), date.to_owned())) +} + +/// Versions compared numerically, piece by piece. +pub(super) fn compare_versions(a: &str, b: &str) -> Ordering { + let key = |version: &str| -> Vec { + version + .split(['.', '-', '+']) + .map(|piece| piece.parse().unwrap_or(0)) + .collect() + }; + key(a).cmp(&key(b)) +} + +/// Rewrite repository-relative `[text](target)` links to GitHub URLs. +/// +/// A changelog is read in the repository, where `../x.md` resolves from the +/// crate directory, and quoted on the site, where it does not and VitePress +/// fails the build on the dead link. Site links (`/adr/…`), anchors and URLs +/// stay as they are. +pub(super) fn absolutise(body: &str, dir: &str) -> String { + let mut out = String::with_capacity(body.len()); + let mut rest = body; + while let Some(open) = rest.find('[') { + out.push_str(&rest[..open]); + match link_at(&rest[open..]) { + Some((text, target, length)) => { + let keep = target.contains("://") + || target.starts_with('#') + || target.starts_with('/') + || target.starts_with("mailto:"); + if keep { + out.push_str(&rest[open..open + length]); + } else { + out.push_str(&format!("[{text}]({REPO}/blob/main/{})", join(dir, target))); + } + rest = &rest[open + length..]; + } + None => { + out.push('['); + rest = &rest[open + 1..]; + } + } + } + out.push_str(rest); + out +} + +/// `dir/target` with `.` and `..` resolved. +fn join(dir: &str, target: &str) -> String { + let mut parts: Vec<&str> = dir.split('/').filter(|p| !p.is_empty()).collect(); + for piece in target.split('/') { + match piece { + "" | "." => {} + ".." => { + parts.pop(); + } + _ => parts.push(piece), + } + } + parts.join("/") +} + +/// `[text](target)` at the start of `s`: (text, target, matched length). +fn link_at(s: &str) -> Option<(&str, &str, usize)> { + let inner = s.strip_prefix('[')?; + let close = inner.find(']')?; + if close == 0 { + return None; + } + let after = inner[close + 1..].strip_prefix('(')?; + let end = after.find(')')?; + if end == 0 { + return None; + } + Some((&inner[..close], &after[..end], 1 + close + 2 + end + 1)) +} + +#[cfg(test)] +mod tests { + use super::*; + + const TWO_RELEASES: &str = "# Changelog\n\n\ + ## [Unreleased]\n\n- pending, must not appear\n\n\ + ## [0.3.1] - 2026-09-20\n\n### Added\n\n- b\n\n\ + ## [0.3.0] - 2026-09-01\n\n### Added\n\n- a\n"; + + #[test] + fn sections_skip_unreleased_and_keep_order() { + let found = sections(TWO_RELEASES); + let versions: Vec<&str> = found.iter().map(|s| s.version.as_str()).collect(); + let dates: Vec<&str> = found.iter().map(|s| s.date.as_str()).collect(); + assert_eq!(versions, ["0.3.1", "0.3.0"]); + assert_eq!(dates, ["2026-09-20", "2026-09-01"]); + assert_eq!(found[0].body, "### Added\n\n- b"); + assert!(found.iter().all(|s| !s.body.contains("pending"))); + } + + #[test] + fn only_unreleased_means_no_sections() { + assert!(sections("# Changelog\n\n## [Unreleased]\n\n- pending\n").is_empty()); + } + + #[test] + fn headings_need_a_date() { + assert_eq!( + dated_heading("## [0.2.1] - 2026-09-23\n"), + Some(("0.2.1".into(), "2026-09-23".into())) + ); + assert_eq!(dated_heading("## [Unreleased]"), None); + assert_eq!(dated_heading("## [0.1.0] - soon"), None); + assert_eq!(dated_heading("### [0.1.0] - 2026-09-23"), None); + } + + #[test] + fn page_skips_crates_without_a_release() { + let unreleased = "# Changelog\n\n## [Unreleased]\n\n- pending\n".to_owned(); + let released = + "# Changelog\n\n## [Unreleased]\n\n## [0.3.1] - 2026-09-20\n\n- shipped\n".to_owned(); + let page = render(&[ + ("axiolid-unreleased", unreleased.clone()), + ("axiolid-released", released), + ]); + assert!(!page.contains("axiolid-unreleased")); + assert!(page.contains("## axiolid-released\n\n### 0.3.1 - 2026-09-20\n\n- shipped\n")); + assert!(page.starts_with(BANNER)); + + let empty = render(&[("axiolid-probe", unreleased)]); + assert!(empty.contains("No crate has a dated release yet")); + } + + #[test] + fn versions_compare_numerically() { + assert_eq!(compare_versions("0.10.0", "0.9.0"), Ordering::Greater); + assert_eq!(compare_versions("0.2.0", "0.2.0"), Ordering::Equal); + } + + #[test] + fn relative_links_resolve_against_the_crate() { + assert_eq!( + absolutise( + "see [adr](../../docs/x.md), [site](/adr/1), [u](https://a.b) and [y](#z)", + "crates/a/b" + ), + format!( + "see [adr]({REPO}/blob/main/crates/docs/x.md), [site](/adr/1), [u](https://a.b) and [y](#z)" + ) + ); + assert_eq!(absolutise("[a] (b) [] (c)", "x"), "[a] (b) [] (c)"); + } +} diff --git a/tools/xtask/src/docs/facts.rs b/tools/xtask/src/docs/facts.rs new file mode 100644 index 00000000..18f5ceba --- /dev/null +++ b/tools/xtask/src/docs/facts.rs @@ -0,0 +1,50 @@ +//! `docs/.vitepress/data/facts.json`: the crate facts the site config reads. +//! +//! The Reference sidebar is built from `crates.groups`, so a new crate or +//! layer reaches the sidebar with no edit to `config.ts`. + +use super::changelog; +use super::workspace::{Workspace, LAYERS}; +use super::{Output, Result}; + +const TARGET: &str = "docs/.vitepress/data/facts.json"; + +pub(super) fn generate(workspace: &Workspace) -> Result { + let mut per_crate = serde_json::Map::new(); + for krate in &workspace.crates { + let release = changelog::latest(workspace, krate)?; + per_crate.insert( + krate.name.clone(), + serde_json::json!({ + "description": krate.description, + "layer": krate.layer, + "role": krate.role, + "version": krate.version, + "released": release.as_ref().map(|r| r.version.clone()), + "released_date": release.as_ref().map(|r| r.date.clone()), + "path": krate.dir, + }), + ); + } + let groups: Vec = LAYERS + .iter() + .map(|(key, title)| { + let members: Vec<&str> = workspace + .layer(key) + .into_iter() + .map(|c| c.name.as_str()) + .collect(); + serde_json::json!({ "key": key, "title": title, "crates": members }) + }) + .collect(); + let facts = serde_json::json!({ + "crates": { + "total": workspace.crates.len(), + "groups": groups, + }, + "crate": per_crate, + }); + let mut json = serde_json::to_string_pretty(&facts).map_err(|error| error.to_string())?; + json.push('\n'); + Ok(Output::new(workspace, TARGET, json)) +} diff --git a/tools/xtask/src/docs/readme.rs b/tools/xtask/src/docs/readme.rs new file mode 100644 index 00000000..be3e1e48 --- /dev/null +++ b/tools/xtask/src/docs/readme.rs @@ -0,0 +1,269 @@ +//! A crate's README: the prose its reference page shows, and the lint that +//! keeps it a crates.io page. +//! +//! The README is the one hand-written summary of a crate (ADR 0078), so the +//! page takes its Overview and its `##` sections (the design notes) from +//! there rather than from the `//!` docs, which would say the same twice. +//! +//! Each publishable crate's README is its crates.io page, so it links to the +//! three places the crate is documented and says nothing that turns false at +//! the next release. +//! +//! - The links: `docs.rs/`, the generated reference page +//! `/reference/crates/`, and the repository. Each names the +//! README's own crate, so a README copied from a sibling fails. +//! - No publication claim ("not yet published", "unpublished", …): crates.io +//! copies the README at publish time, where such a line is false on +//! arrival. +//! - No version claim: a three-part version (`0.3.1`), a `v`-prefixed one +//! (`v0.4`) or a `version = "…"` pin. crates.io shows the version; a +//! README that repeats it drifts at the next bump. + +use super::changelog::absolutise; +use super::workspace::{Crate, Workspace}; +use super::{Result, REPO, SITE}; + +/// The README's path relative to the workspace root. +fn readme_path(krate: &Crate) -> String { + format!("{}/{}", krate.dir, "README.md") +} + +/// The parts of a README a reference page shows. +pub(super) struct Prose { + /// The introduction: everything between the title and the install + /// snippet or link list. + pub(super) overview: String, + /// Every `##` section, verbatim. + pub(super) notes: String, +} + +pub(super) fn prose(workspace: &Workspace, krate: &Crate) -> Result { + let rel = readme_path(krate); + let text = std::fs::read_to_string(workspace.root.join(&rel)) + .map_err(|error| format!("{rel}: {error}"))?; + let (overview, notes) = split(&text); + let publish = |part: &str| escape(&absolutise(part.trim(), &krate.dir)); + Ok(Prose { + overview: publish(&overview), + notes: publish(¬es), + }) +} + +/// (introduction, `##` sections) of a README. +fn split(text: &str) -> (String, String) { + let mut overview = Vec::new(); + let mut notes = Vec::new(); + let mut in_intro = true; + let mut in_notes = false; + for line in text.lines().skip_while(|l| !l.starts_with("# ")).skip(1) { + if line.starts_with("## ") { + in_intro = false; + in_notes = true; + } else if in_intro && (line.starts_with("```") || line.starts_with("- ")) { + in_intro = false; + } + if in_intro { + overview.push(line); + } else if in_notes { + notes.push(line); + } + } + (overview.join("\n"), notes.join("\n")) +} + +/// Escape `{{` outside code: VitePress compiles each page as a Vue template, +/// where it would open an interpolation. +fn escape(text: &str) -> String { + let mut out = Vec::new(); + let mut fenced = false; + for line in text.lines() { + if line.trim_start().starts_with("```") { + fenced = !fenced; + } + if fenced || !line.contains("{{") { + out.push(line.to_owned()); + continue; + } + let mut escaped = String::with_capacity(line.len()); + let mut in_code = false; + let mut chars = line.chars().peekable(); + while let Some(c) = chars.next() { + match c { + '`' => { + in_code = !in_code; + escaped.push(c); + } + '{' if !in_code && chars.peek() == Some(&'{') => escaped.push_str("{"), + _ => escaped.push(c), + } + } + out.push(escaped); + } + out.join("\n") +} + +/// `README:line: why` for every rule a publishable README breaks. +pub(super) fn problems(workspace: &Workspace) -> Vec { + let mut out = Vec::new(); + for krate in &workspace.crates { + let rel = readme_path(krate); + match std::fs::read_to_string(workspace.root.join(&rel)) { + Ok(text) => out.extend(check(&krate.name, &rel, &text)), + Err(error) => out.push(format!("{rel}: {error}")), + } + } + out +} + +/// The README lines every publishable crate carries, in this order. +pub(super) fn links(name: &str) -> [String; 3] { + [ + format!("- API documentation: [docs.rs/{name}](https://docs.rs/{name})"), + format!( + "- Reference page: [{}]({SITE}/reference/crates/{name})", + SITE.trim_start_matches("https://") + ), + format!( + "- Source and issues: [{}]({REPO})", + REPO.trim_start_matches("https://") + ), + ] +} + +fn check(name: &str, rel: &str, text: &str) -> Vec { + let mut out = Vec::new(); + for line in links(name) { + if !text.lines().any(|l| l.trim_end() == line) { + out.push(format!("{rel}: missing the line `{line}`")); + } + } + for (index, line) in text.lines().enumerate() { + let at = index + 1; + if claims_unpublished(line) { + out.push(format!( + "{rel}:{at}: publication claim; crates.io copies the README when it publishes, so drop it" + )); + } + if claims_version(line) { + out.push(format!( + "{rel}:{at}: version claim; crates.io shows the version, so the README does not repeat it" + )); + } + } + out +} + +/// Phrases that assert a package is not (yet) on a registry. +const UNPUBLISHED: &[&str] = &[ + "not published", + "not yet published", + "unpublished", + "not been published", + "not released yet", + "not yet released", + "not on crates.io", + "not yet on crates.io", +]; + +fn claims_unpublished(line: &str) -> bool { + let lower = line.to_ascii_lowercase(); + UNPUBLISHED.iter().any(|phrase| lower.contains(phrase)) +} + +/// A three-part version, a `v`-prefixed version, or a `version = "` pin. +fn claims_version(line: &str) -> bool { + if line.contains("version = \"") { + return true; + } + let bytes = line.as_bytes(); + let mut i = 0; + while i < bytes.len() { + let starts_token = + i == 0 || !(bytes[i - 1].is_ascii_alphanumeric() || bytes[i - 1] == b'.'); + if !(starts_token && bytes[i].is_ascii_alphanumeric()) { + i += 1; + continue; + } + let prefixed = bytes[i] == b'v'; + let start = if prefixed { i + 1 } else { i }; + let mut parts = 0; + let mut j = start; + loop { + let digits = bytes[j..].iter().take_while(|b| b.is_ascii_digit()).count(); + if digits == 0 { + break; + } + parts += 1; + j += digits; + if j + 1 < bytes.len() && bytes[j] == b'.' && bytes[j + 1].is_ascii_digit() { + j += 1; + } else { + break; + } + } + if parts >= 3 || (prefixed && parts >= 2) { + return true; + } + i = j.max(i + 1); + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + + fn clean(name: &str) -> String { + format!("# {name}\n\nWhat it is.\n\n{}\n", links(name).join("\n")) + } + + #[test] + fn a_readme_with_its_own_links_passes() { + assert!(check("axiolid-x", "R", &clean("axiolid-x")).is_empty()); + } + + #[test] + fn links_must_name_the_readmes_own_crate() { + let problems = check("axiolid-x", "R", &clean("axiolid-y")); + assert_eq!(problems.len(), 2, "{problems:?}"); + assert!(problems.iter().any(|p| p.contains("docs.rs/axiolid-x"))); + assert!(problems + .iter() + .any(|p| p.contains("reference/crates/axiolid-x"))); + } + + #[test] + fn publication_and_version_claims_are_rejected() { + let text = format!( + "{}\nIt is not yet published.\nSee v0.4 and 0.3.1.\naxiolid = {{ version = \"0.3\" }}\n", + clean("axiolid-x") + ); + let problems = check("axiolid-x", "R", &text); + let lines: Vec<&str> = problems + .iter() + .filter_map(|p| p.split(':').nth(1)) + .collect(); + assert_eq!(lines, ["9", "10", "11"], "{problems:?}"); + } + + #[test] + fn prose_is_the_intro_and_the_sections() { + let text = "# axiolid-x\n\nWhat it is,\nand is not.\n\n```bash\ncargo add axiolid-x\n```\n\n- API documentation: x\n\n## Design notes\n\nWhy.\n\n```rust\nlet a = {{ b }};\n```\n"; + let (overview, notes) = split(text); + assert_eq!(overview.trim(), "What it is,\nand is not."); + assert_eq!( + notes, + "## Design notes\n\nWhy.\n\n```rust\nlet a = {{ b }};\n```" + ); + assert_eq!(escape(¬es), notes); + assert_eq!(escape("a {{ b }} `{{ c }}`"), "a {{ b }} `{{ c }}`"); + } + + #[test] + fn plain_numbers_are_not_versions() { + assert!(!claims_version("ADR 0078, a 0.5 mm gap, 1e-9, kernel#9")); + assert!(!claims_version("orient2d and Point3 values")); + assert!(claims_version("since 1.2.3")); + assert!(claims_version("(v0.4 ABI)")); + } +} diff --git a/tools/xtask/src/docs/reference.rs b/tools/xtask/src/docs/reference.rs new file mode 100644 index 00000000..3cc3e762 --- /dev/null +++ b/tools/xtask/src/docs/reference.rs @@ -0,0 +1,259 @@ +//! One generated reference page per publishable crate, and the index that +//! groups them by layer. + +use std::collections::BTreeMap; + +use super::changelog::{self, Release}; +use super::readme; +use super::workspace::{Crate, Workspace, FACADE, LAYERS}; +use super::{Output, Result, BANNER, REPO}; + +pub(super) fn generate(workspace: &Workspace) -> Result> { + let facade = workspace + .get(FACADE) + .ok_or_else(|| format!("the facade `{FACADE}` is not a published crate"))?; + let mut releases = BTreeMap::new(); + for krate in &workspace.crates { + releases.insert(krate.name.as_str(), changelog::latest(workspace, krate)?); + } + let mut outputs = vec![Output::new( + workspace, + "docs/reference/index.md", + index(workspace, &releases), + )]; + for krate in &workspace.crates { + let page = page( + workspace, + krate, + facade, + releases[krate.name.as_str()].as_ref(), + )?; + outputs.push(Output::new( + workspace, + format!("docs/reference/crates/{}.md", krate.name), + page, + )); + } + Ok(outputs) +} + +fn released(release: Option<&Release>) -> String { + release.map_or_else(|| "not released".to_owned(), |r| r.version.clone()) +} + +fn index(workspace: &Workspace, releases: &BTreeMap<&str, Option>) -> String { + let mut out = vec![ + BANNER.to_owned(), + String::new(), + "# Crate reference".to_owned(), + String::new(), + format!( + "Axiolid publishes {} crates, each versioned and released on its own. The \ + [`{FACADE}`](./crates/{FACADE}) facade re-exports them behind features; every \ + crate can also be used directly. [Selecting a package](./selecting-packages) \ + says which to start from.", + workspace.crates.len() + ), + String::new(), + "Every page here is generated from the crate itself: its manifest, its crate \ + documentation and its changelog. Sections follow the layers of the \ + [crate map](/architecture/crate-map)." + .to_owned(), + ]; + for (layer, title) in LAYERS { + let members = workspace.layer(layer); + if members.is_empty() { + continue; + } + out.extend([ + String::new(), + format!("## {title}"), + String::new(), + "| Crate | Role | Latest release | Description |".to_owned(), + "| --- | --- | --- | --- |".to_owned(), + ]); + for krate in members { + out.push(format!( + "| [`{name}`](./crates/{name}) | `{}` | {} | {} |", + krate.role, + released(releases[krate.name.as_str()].as_ref()), + krate.description, + name = krate.name + )); + } + } + out.push(String::new()); + out.join("\n") +} + +fn page( + workspace: &Workspace, + krate: &Crate, + facade: &Crate, + release: Option<&Release>, +) -> Result { + let mut out = vec![ + BANNER.to_owned(), + String::new(), + format!("# {}", krate.name), + String::new(), + format!("{}.", krate.description.trim_end_matches('.')), + String::new(), + "| | |".to_owned(), + "| --- | --- |".to_owned(), + ]; + match release { + Some(release) => { + out.push(format!( + "| Latest release | {} ({}) |", + release.version, release.date + )); + if release.version != krate.version { + out.push(format!("| On `main` | {} (unreleased) |", krate.version)); + } + out.push(format!( + "| crates.io | [`{0}`](https://crates.io/crates/{0}) |", + krate.name + )); + } + None => out.push(format!( + "| Latest release | not released (`main` is {}) |", + krate.version + )), + } + let exposed = facade_features(facade, &krate.name); + if !exposed.is_empty() { + out.push(format!( + "| Facade | [`{FACADE}`](./{FACADE}) feature {} |", + exposed.join(", ") + )); + } + out.push(format!("| Layer | {} (`{}`) |", krate.layer, krate.role)); + let mut api = Vec::new(); + if let Some(lib) = &krate.lib { + // Built into the site by the docs workflow; VitePress adds the base. + api.push(format!("[rustdoc](/api/rustdoc/{lib}/index.html)")); + } + if release.is_some() { + api.push(format!("[docs.rs](https://docs.rs/{})", krate.name)); + } + if !api.is_empty() { + out.push(format!("| API documentation | {} |", api.join(" · "))); + } + out.push(format!( + "| Source | [`{dir}/`]({REPO}/tree/main/{dir}) |", + dir = krate.dir + )); + + let prose = readme::prose(workspace, krate)?; + if !prose.overview.is_empty() { + out.extend([ + String::new(), + "## Overview".to_owned(), + String::new(), + prose.overview, + ]); + } + if !prose.notes.is_empty() { + out.extend([String::new(), prose.notes]); + } + features(workspace, krate, &mut out); + if !krate.internal_deps.is_empty() { + out.extend([String::new(), "## Depends on".to_owned(), String::new()]); + for dep in &krate.internal_deps { + out.push(format!("- {}", crate_link(workspace, dep))); + } + } + out.extend([String::new(), "## Changes".to_owned(), String::new()]); + match release { + Some(Release { + version, + date, + notes: Some(notes), + }) => { + out.push(format!("Latest release, {version} ({date}):")); + out.push(String::new()); + out.push(notes.clone()); + } + Some(Release { version, date, .. }) => out.push(format!( + "Released in the workspace-wide {version} release ({date}), before crates \ + versioned independently; its notes are in the [workspace changelog](/CHANGELOG)." + )), + None => out.push("No release yet.".to_owned()), + } + out.push(String::new()); + out.push(format!( + "Full history: [`{dir}/CHANGELOG.md`]({REPO}/blob/main/{dir}/CHANGELOG.md)", + dir = krate.dir + )); + out.push(String::new()); + Ok(out.join("\n")) +} + +/// The facade features that enable `name` directly, as inline code. +fn facade_features(facade: &Crate, name: &str) -> Vec { + let dep = format!("dep:{name}"); + facade + .features + .iter() + .filter(|(_, values)| values.contains(&dep)) + .map(|(feature, _)| format!("`{feature}`")) + .collect() +} + +/// The crate's own features, with what each turns on. +fn features(workspace: &Workspace, krate: &Crate, out: &mut Vec) { + let defaults = krate.features.get("default").cloned().unwrap_or_default(); + let listed: Vec<(&String, &Vec)> = krate + .features + .iter() + .filter(|(feature, _)| feature.as_str() != "default") + .collect(); + if listed.is_empty() { + return; + } + out.extend([ + String::new(), + "## Features".to_owned(), + String::new(), + format!( + "Default: {}.", + if defaults.is_empty() { + "none".to_owned() + } else { + defaults + .iter() + .map(|f| format!("`{f}`")) + .collect::>() + .join(", ") + } + ), + String::new(), + "| Feature | Enables |".to_owned(), + "| --- | --- |".to_owned(), + ]); + for (feature, values) in listed { + let enables: Vec = values + .iter() + .map(|value| match value.strip_prefix("dep:") { + Some(dep) => crate_link(workspace, dep), + None => format!("`{value}`"), + }) + .collect(); + let enables = if enables.is_empty() { + "—".to_owned() + } else { + enables.join(", ") + }; + out.push(format!("| `{feature}` | {enables} |")); + } +} + +/// A workspace crate links to its page; anything else is plain code. +fn crate_link(workspace: &Workspace, name: &str) -> String { + if workspace.get(name).is_some() { + format!("[`{name}`](./{name})") + } else { + format!("`{name}`") + } +} diff --git a/tools/xtask/src/docs/workspace.rs b/tools/xtask/src/docs/workspace.rs new file mode 100644 index 00000000..2acf1326 --- /dev/null +++ b/tools/xtask/src/docs/workspace.rs @@ -0,0 +1,124 @@ +//! The publishable crates, as the docs generators see them. +//! +//! Layer, role and internal dependencies come from the architecture model, +//! so the docs and `cargo xtask architecture check` read the same +//! `[package.metadata.axiolid]` through the same code. + +use std::collections::{BTreeMap, BTreeSet}; +use std::path::PathBuf; + +use cargo_metadata::MetadataCommand; + +use super::Result; +use crate::architecture::model::Architecture; + +/// The layers a publishable crate may declare, in reading order, with the +/// heading each gets on the index. A crate in any other layer fails the run, +/// so a new layer cannot silently drop out of the reference. +pub(super) const LAYERS: &[(&str, &str)] = &[ + ("foundation", "Foundation"), + ("representations", "Representations"), + ("contracts", "Contracts"), + ("algorithms", "Algorithms"), + ("providers", "Providers"), + ("execution", "Execution"), + ("facade", "Facade"), +]; + +/// The facade package, whose features expose the other crates. +pub(super) const FACADE: &str = "axiolid"; + +pub(super) struct Crate { + pub(super) name: String, + pub(super) description: String, + /// The version on `main`, which may be ahead of the latest release. + pub(super) version: String, + /// The crate directory relative to the workspace root. + pub(super) dir: String, + pub(super) layer: String, + pub(super) role: String, + /// The library's Rust name, which names its rustdoc directory. + pub(super) lib: Option, + pub(super) features: BTreeMap>, + /// Workspace crates this one depends on outside dev-dependencies. + pub(super) internal_deps: BTreeSet, +} + +pub(super) struct Workspace { + pub(super) root: PathBuf, + /// Publishable crates, sorted by name. + pub(super) crates: Vec, + pub(super) architecture: Architecture, +} + +impl Workspace { + pub(super) fn load() -> Result { + let metadata = MetadataCommand::new() + .no_deps() + .exec() + .map_err(|error| format!("cargo metadata failed: {error}"))?; + let architecture = Architecture::from_metadata(&metadata)?; + let mut crates = Vec::new(); + for package in metadata.workspace_packages() { + let name = package.name.to_string(); + let Some(model) = architecture.packages.get(&name) else { + continue; + }; + if !model.publish { + continue; + } + if !LAYERS.iter().any(|(layer, _)| *layer == model.layer) { + return Err(format!( + "{name}: layer `{}` has no section in the crate reference; add it to docs::workspace::LAYERS", + model.layer + )); + } + let description = package + .description + .as_deref() + .map(str::trim) + .filter(|text| !text.is_empty()) + .ok_or_else(|| format!("{name}: a published crate needs a `description`"))? + .to_owned(); + let lib = package + .targets + .iter() + .find(|target| { + target.is_lib() + || target.is_rlib() + || target.is_cdylib() + || target.is_staticlib() + }) + .map(|target| target.name.replace('-', "_")); + crates.push(Crate { + name, + description, + version: package.version.to_string(), + dir: model.path.clone(), + layer: model.layer.clone(), + role: model.role.clone(), + lib, + features: package.features.clone(), + internal_deps: model.production_internal_dependencies.clone(), + }); + } + crates.sort_by(|a, b| a.name.cmp(&b.name)); + if !crates.iter().any(|c| c.name == FACADE) { + return Err(format!("the facade `{FACADE}` is not a published crate")); + } + Ok(Self { + root: architecture.root.clone(), + crates, + architecture, + }) + } + + pub(super) fn get(&self, name: &str) -> Option<&Crate> { + self.crates.iter().find(|c| c.name == name) + } + + /// Publishable crates in one layer, sorted by name. + pub(super) fn layer(&self, layer: &str) -> Vec<&Crate> { + self.crates.iter().filter(|c| c.layer == layer).collect() + } +} diff --git a/tools/xtask/src/gaps.rs b/tools/xtask/src/gaps.rs index a8058fdf..c374a9cf 100644 --- a/tools/xtask/src/gaps.rs +++ b/tools/xtask/src/gaps.rs @@ -1,6 +1,6 @@ //! Capability ledger: what Axiolid can do, measured against OCCT and CGAL. //! -//! `architecture/capability-ledger.toml` is the durable record of a one-off +//! `docs/architecture/capability-ledger.toml` is the durable record of a one-off //! like-for-like audit. It exists so the next contributor does not rerun //! that audit: `cargo xtask gaps` answers "what is missing, where does the //! reference implementation live, which issue tracks it" in one command. diff --git a/tools/xtask/src/gaps/model.rs b/tools/xtask/src/gaps/model.rs index bd5a9f1d..283c0f1f 100644 --- a/tools/xtask/src/gaps/model.rs +++ b/tools/xtask/src/gaps/model.rs @@ -4,8 +4,8 @@ use std::path::{Path, PathBuf}; pub type Result = std::result::Result; -pub const LEDGER: &str = "architecture/capability-ledger.toml"; -pub const PACKAGES: &str = "architecture/reference-packages.toml"; +pub const LEDGER: &str = "docs/architecture/capability-ledger.toml"; +pub const PACKAGES: &str = "docs/architecture/reference-packages.toml"; /// Every package path that exists in the pinned OCCT and CGAL trees. /// diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs index bcc25926..c449bf09 100644 --- a/tools/xtask/src/main.rs +++ b/tools/xtask/src/main.rs @@ -1,4 +1,6 @@ mod architecture; +mod context; +mod docs; mod ffi; mod gaps; @@ -7,6 +9,8 @@ use std::env; fn usage() -> ! { eprintln!("usage: cargo xtask architecture "); eprintln!(" cargo xtask architecture closure >"); + eprintln!(" cargo xtask context check"); + eprintln!(" cargo xtask docs [--check]"); eprintln!(" cargo xtask ffi "); eprintln!( " cargo xtask gaps [next|list [--open] [--area ]|show |check]" @@ -32,6 +36,22 @@ fn main() { finish("gaps", result); return; } + if command.as_deref() == Some("context") { + match args.next().as_deref() { + Some("check") => finish("context", context::check()), + _ => usage(), + } + return; + } + if command.as_deref() == Some("docs") { + let check = match args.next().as_deref() { + None => false, + Some("--check") => true, + _ => usage(), + }; + finish("docs", docs::run(check)); + return; + } if command.as_deref() == Some("ffi") { let result = match args.next().as_deref() { Some("header") => ffi::header(),