axioval-cli 0.3.0

Command-line runner for normalized Axioval packages
# `axioval-cli`

Strict command-line entry point for normalized Axioval packages and validation runs.

CLI output and exit codes are public automation contracts. Parse packages fail closed, write diagnostics to stderr, and use non-zero status for invalid input or execution failure.

- `--ruleset` repeats for `check` and `validate`, compiled through `compile_rulesets`; qualified rule ids (`package-id/rule-id`) are a report contract.
- `check` exit status: 0 complete and clean, 3 findings, 4 not evaluated without findings, 1 failure, 2 usage (clap). Status 4 exists so an incomplete check can never exit 0; never fold it into 0.
- Build every output before writing any, so a failing run leaves no partial report or archive.
- The CLI is the host: it may enable several facade features and compose an adapter with a sink. Libraries never do.
- Never read the clock for BCF output when `--bcf-date` or `SOURCE_DATE_EPOCH` is given.
- `src/digest.rs` owns the saved result format (`CheckOutput`) and its two views. The summary must stay bounded by distinct groups, never by entry count; the listing must page. Every "next" hint must run unchanged in a POSIX shell: quote through `shell_quote`, which quotes `#` because an unquoted `#` starts a comment.
- Report tables are the `tables` section: one summary group per table (rows counted, columns as the message, a grouped table's group columns first), one listing entry per row (a grouped row prefixed by its group values in brackets). They never affect the status. `report --csv` (`digest::table_csv`) prints exactly one table, selected by `--rule` and `--table`; its header (`scope`, group columns, `<id>_lower`/`<id>_upper` per numeric column) is a public contract, and an unknown value is empty, never zero.
- The result's `objects` field is additive (`serde(default)`), so results saved before it existed still load. It labels resource objects from `Report::resources` (`Report::object`), never from the project alone.
- `src/geometry.rs` is the IFC→Axiolid bridge. It belongs here and nowhere else: the adapters must not depend on each other. Every object must end in exactly one state: exact mesh, tessellated mesh, no body, or unmeasured. Never declare a physical product bodiless because meshing failed; that makes it vanish from contact and free-space checks. The planarity check may only grow by structures proven planar; anything unrecognised stays tessellated. A section profile is polygonal only with every fillet and edge radius absent or zero (its straight and sloped edges are exact); any radius is chorded.
- Space-boundary coverage registers every `IfcSpace` and every `IfcRelSpaceBoundary` naming one. A boundary's `ConnectionGeometry` is read through `ifc-spatial`'s `SpaceBoundary::connection_geometry` and lowered through `ifc-geometry`'s `lower_connection_surface` (surfaces, face surfaces, face-based surface models) in the frame `product_representation_frame` gives the space's `Body` representation (openbimrs/ifc#164), the same frame lowering places the body in; never rebuild that frame or place the mesh afterwards. A curve-bounded plane takes the frame on its basis plane only (openbimrs/ifc#163). A space without a body representation falls back to its placement; its coverage is refused anyway. Anything not lowered (no geometry, not a surface, refused by the lowering or compiler) is an unmeasured boundary, never skipped. `axiolid-mesh-compile` is a direct dependency only to require 0.3.5 (curve-bounded planes, axiolid/kernel#192; section profiles meshed from their exact contour, axiolid/kernel#193; voids tangent to a host face, axiolid/kernel#194).
- Register a geometry service only when every role it needs can be read from IFC; otherwise leave it unregistered so its rules report `missing-service`.
- `with_levels` declares every `IfcBuildingStorey` a `spans-level` level: its placement's world height up to the next storey's of the same spatial parent. A tilted or unreadable placement, or a height shared with a sibling, is an unmeasured level, never a guessed band.
- `--model PATH[:DISCIPLINE]` repeats: each file is imported as its own session and `EvidenceSession::federate` combines them before geometry is attached. The bridge meshes every source into one set, looks each object's model up by its source, builds spatial trees per source, and binds every service to all snapshots. Never key anything on a bare local id: two files share `#10`. Each model's file name is stated as its source's `fileName` metadata. `--discipline-map FIELD:PATTERN=DISCIPLINE` is applied after federation; the result's additive `sources` field lists each source's discipline and origin (`declared`, `mapped` with rule and value) or why the map left it without one.
- `src/compare.rs` is `axioval compare`: two single-model sessions matched by `GlobalId` through `axioval_rules::compare_sessions` (`--property-set` and `--all-property-sets` list sets through enumeration). The same comparison as a rule is the `model-comparison` capability over `check --model a.ifc:base --model b.ifc:revised`; `tests/check.rs` runs it. Its result is a `CheckOutput` with the additive `comparison` field (owned by `digest.rs`), so `report`, `--summary` and `--bcf` stay shared through `emit`; exit status as for `check`. Two files with one name become `name@base` and `name@revised`; never let the two revisions share a source id. `tests/compare.rs` runs it end to end.
- Walkability takes every role from its request; metric routing takes surfaces (`IfcSpace`), portals (`IfcDoor`, opening voids) and connectors (stairs, ramps, transport elements) from IFC classes. Never declare a door clear width from `OverallWidth`: it includes the lining, and an overstated clear width turns an undecided route into a false pass.
- BCF cameras come from `geometry::bounds`: the proximity service's enclosing extent of each object the report names, only with `--geometry`. An unmeasured object is left out, never estimated; without `--geometry` pass `None` so the archive stays byte-identical. `--bcf-version 3.0` fails (status 1, nothing written) when a viewpoint has no camera.
- BCF colouring is on with `--geometry` or when `--bcf-subject-color`/`--bcf-related-color` is given, off with `--bcf-no-color`; without any of them and without `--geometry` pass `colors: None` so the archive stays byte-identical.
- `--bcf-isolate` sets `Options::isolate` and `--bcf-section-box` sets `Options::section_box`; neither is on by default.
- `tests/check.rs` runs the real binary; keep a case for every exit status. Update `docs/src/cli.md` with any change to arguments, output or status.
- `check --locate` builds the IFC `LocationPolicy` (`IfcBuildingStorey`, `IfcSpace`, containment and aggregation, `Name`); `none` must leave the result byte-identical. `report --location` keeps outcomes whose location is unresolved.
- `check` identifies every finding with `Report::identify_findings` over
  `bcf::IFC_GLOBAL_ID_SCHEME`, the scheme the BCF sink keys topics by, so a
  decision and a topic GUID are one key. `--decisions` applies a decisions
  file (loaded before the run, so a bad file fails before any work);
  decisions never change the exit status. `decide` writes the decisions
  file with each finding's basis and writes nothing when any `--finding` is
  not in the result. `report` prints ids and decisions, counts them in the
  summary, lists stale ones as the `stale-decisions` section and filters by
  `--decision`.
- `check --rule-status` records `Report::rules`; the summary lists them bounded by `--top` (rules that did not pass first) and never changes the exit status.