Repository navigation
feat: cut directed oblique and faceted-wall member holes (G14.2 slices 1-2) - #814
Conversation
…ce 1) Generalize the member-through-hole kernel op from the fixed +Z axis to any section-plane direction (0, y, z): the cutter is a directed prism frame (e1 = +X, e2 = d x e1), crossed flat walls classify by their height interval over the footprint and pair in interval order, and the removed volume is certified from the exact Big-rational divergence sum of the constructed opening polyhedron instead of the perpendicular-only prism formula, so an oblique pair certifies the same integral A * t / |n . d|. `new` stays the exact +Z special case with byte-identical G14.1 results. Tubes, sloped single-plane walls and a second hole after an oblique cut keep their refusals until the strip registry of the later slices; the delivered design note records what landed and what deviates from the plan.
…25/27 green) The strip registry (exact section-line planes via area_sign), soft-kink chains with the ring walk over the chains' own kink adjacency, the union-aware placement with exact Big clip areas, the shared kink-point registry and the generalized splice (kept outer paths + opening arcs assembled into pieces) are in place. 23 legacy tests stay green bit for bit; the tube-hole volume and byte-exact-repeat tests fail on one residual assembly defect in the cutting splice of a band whose opening touches both of its boundary edges (the assembly printed keep/arc endpoints disagree with the ring walk).
The chain path spelled a band side as `.p1` whenever the next event sat on
the next footprint edge, so a side covering a whole edge lost the bare
`s{j}` spelling the flat path and the tests keep; a run now carries the
suffix only when it starts past a mid-edge kink crossing. Also clears the
splice's clippy debt for the gate's `-D warnings`: the union-find and
assembly indexing goes through `get`, the never-looping single-piece
iteration and the single-edge `throws` walk are unwrapped, and the dead
re-triangulation branch after the splice block is gone (the splice handles
both the notched and the closed opening).
The tube test scaled the R=10 circle's polygon area down to r=4, but the CurveSpec segment count depends on the radius (48 sides at r=4, 72 at R=10), so the scaled base overstated the cavity area by 0.16% and the assert missed the certified removed volume by 23.1 mm^3. The expected value is now the flat prism over the cavity's own approximation polygon minus the inclined-member term `2*tan(pi/48)*double integral |b|`: the band chords of the 48-band section tilt by pi/48 and shorten every off-center column, which the certified divergence sum reflects exactly (7507.1704368, agreement to 12 digits).
axisrow
left a comment
There was a problem hiding this comment.
Review of PR #814 (G14.2 slices 1-2, head e351b9e)
Verdict: changes requested — one inline finding on the band face ids; the rest of the change looks right.
Scope and verification
- Diffed
c003a6af(upstream master, the true merge base) againste351b9e2: 4 files, about 2.6k lines — exactly the 7 G14.2 commits, no stray or unrelated edits. - CI run 41398846708 is green on this exact head. I checked the run logs shard by shard: the g14 suite runs across the ubuntu shards (23 PASS lines) and the macOS shards (26 PASS lines);
a_small_hole_cuts_a_tube_through_four_chains,oblique_web_hole_removes_area_times_thickness_over_cosine,the_unit_direction_reproduces_new_bit_for_bitanda_non_convex_hole_in_a_tube_is_refused_or_cut_without_crashingall ran and passed, andci-okpasses with the single ignored test registered in.config/ci-ignored.txtunder the exact name. - Never-crash: no new unwrap/expect/panic/indexing in non-test code; the registry, splice and band assembly consistently refuse through typed failures or
internal()on every unchecked access; degenerate directions, ranges and non-convex footprints are covered by tests. - The deliberately unfinished scope (merge-walk band, tilted-strip shadow scale) is honestly stated in the design note and fenced with the registered ignored test — good.
Notes (non-blocking, no change requested)
- Union-aware coverage check in
place_chains: statically,diff_encis enclosed at exponente-1(so it carries an extra factor2^(-e)against a plain area) whileboundscales by2^e; the pairing looks unit-consistent only for e=0, and real fractional coordinates give e<0. The recorded green run of the exact head contradicts my reading, so I am not asking for a change — but a second look at the exponent pairing (probably2e-1, plus a magnitude-based bound instead of2^e) would be cheap insurance for geometries beyond the tested ones. - In the flat-path
Placement::Contactrefusal, the code assumestriangulate_capmust fail and falls back toResultNotCertifiedif it does not; safe, but a more precise defect would help callers diagnose. KinkPointslookup and the event merge rely on exact f64 equality of registered coordinates — fine because both sides reuse the same canonical interpolation; worth remembering if the merge-walk slice changes the event sources.
Finding
See the inline comment on the .p1 run suffix: two kink crossings of one footprint edge emit the same band face id twice, and seal does not dedupe face ids.
Two kink crossings on one footprint edge emitted two band sides with the same `.p1` suffix — two distinct faces sharing one face id with different normals, which the seal does not deduplicate. Each edge now counts its runs and the suffix is the design's `.p<run>`: the bare first run at the edge's own vertex, then `.p1`, `.p2`, ... The reachable paths are unchanged byte for byte (every successful cut today carries at most one mid-edge crossing, so the first run stays bare); a footprint whose edge crosses two kink lines is wider than a band and the splice refuses it with `BodyWallUnsupported` before any id is emitted, so the numbering lands ahead of the merge-walk slice that will first serve that case.
axisrow
left a comment
There was a problem hiding this comment.
Re-review of PR #814 at head 47dfd12 (run 2fb4bc72)
Verdict: approved. No inline findings this round.
The fix since the last review (e351b9e)
The single new commit 47dfd122 replaces the hardcoded .p1 band id suffix with a per-edge run counter (runs: BTreeMap<usize, u32>), emitting the bare s{j} for the first run and .p1, .p2, ... for subsequent runs of the same footprint edge. This resolves the duplicate face id finding; the single-run G14.1 spelling stays byte-identical and the whole g14 suite plus all test shards, quality and wasm checks pass on this head.
One red check on this head, not caused by this PR
dependency licences and advisories (and ci-ok, which gates on it) fail because cargo-deny now sees RUSTSEC advisory GHSA-q7v9-x93c-xqv3 against zip 8.6.0, pulled in transitively through openusd 0.8.0. The version is identical on the base commit c003a6af and this PR never touched Cargo.lock; the previous green run simply predates the advisory. Fixing it (cargo update -p zip or a deny override) belongs in a separate chore change, not a blocker for this review.
Carried-over non-blocking notes
- The
diff_encvsboundexponent pairing in theplace_chainscoverage check still deserves a second look for fractional-coordinate geometries (see the previous review body); the green runs contradict my static reading, so it stays a question, not a request. - The
Placement::Contactfallback toResultNotCertifiedwhentriangulate_capunexpectedly succeeds is safe but could carry a more precise defect. - The deliberately unfinished scope (merge-walk band, tilted-strip shadow scale) remains honestly documented with the registered ignored test.
… zip 8 RUSTSEC-2026-0429 is the fourth extract()-only advisory against `zip` 8.6.0, pulled in only by `openusd` 0.8.0 whose requirement (`^8.2.0`) cannot select the patched 9.x line. Same reason and same revisit point as the three ignored siblings: openusd never extracts, BeamCraft never extracts archives.
axisrow
left a comment
There was a problem hiding this comment.
Re-review of PR #814 at head 23b6cb7 (run cf3e88da)
Verdict: approved. No inline findings.
What changed since 47dfd12
One chore commit, 23b6cb7a: a single deny.toml ignore entry for RUSTSEC-2026-0429 (the zip symlink-extract advisory cargo-deny started failing on), worded exactly like the three existing zip siblings. This resolves the red dependency licences and advisories and ci-ok checks noted in the previous review; keeping it in this PR is pragmatic since the advisory was blocking this branch CI. The advisory revisit point travels with the existing three entries, so nothing new to track.
Verification
- Full check run 41434710468 on this head is green: every test shard, quality, wasm32, cargo-deny and ci-ok pass (watched to completion); only the by-design Windows skip entries remain skipping.
- The code diff is unchanged from the approved head 47dfd12: the per-edge run counter for band face ids stands, single-run G14.1 spelling stays byte-identical.
- The carried-over non-blocking notes from the earlier rounds still apply (the
diff_encvsboundexponent pairing question inplace_chainsfor fractional-coordinate geometries; thePlacement::Contactfallback defect precision; the deferred merge-walk scope stays honestly documented with the registered ignored test).
…ibim-com#814-Apibim-com#817) (#419) * refactor: dispatch CLI commands from the registry and list access and parameters (slices 2-3 of #753) (#760) * refactor: dispatch CLI commands from the registry (second slice of #753) The binary chose an adapter with a chain of `c == "..."` comparisons in main.rs plus a `handles()` function per adapter, a second hand-kept inventory that could drift from the registry. Each registry entry now names its adapter (`Route`), and main.rs matches on `registry::route(word)`. A word that is not registered cannot reach an adapter and takes the store-level path, which refuses it as before, so a command that is dispatched but unregistered is no longer possible. The per-adapter `handles()` functions are removed. Behaviour, exit codes and output of existing commands are unchanged. * test: wait for the dropped rebase request before the retry The coordinator answers each connection on its own thread, so in lost_answers_and_a_killed_server_replay_exactly_once the retry could overtake the request whose client was dropped and do the merge itself, answering workspace_rebased instead of workspace_rebase_replayed. It failed once on the ubuntu CI runner. Wait until the draft head moves, which happens when the dropped request has merged, before sending the retry. * refactor: list access and parameters of every CLI command (third slice of #753) `beamcraft commands` now carries, for every command, whether it reads or writes (Access) and its options: value shape, whether it is required and, for alternatives, a one_of group. The five common request identity options are listed on the commands that take them (Ids::Common) instead of being repeated in 125 entries. The data was taken from each adapter's allow-list and required checks, not from the help prose; where they differ (for example update-element --corners and --axis-point, create-grid, create-plate --material) the code wins. A rule that links optional options (--width with --length) stays in the family help. Tests: the listing carries access and parameters; an unlisted option is refused as unknown (so the probe is valid); every listed option is accepted by its command, which fails if a listed name is wrong or removed. --------- * docs: generate the CLI command reference from the registry (slice 7 of #753, first part) (#761) COMMANDS.md renders every command from the registry as one usage line (required options bare, optional ones in brackets, one-of groups in parentheses) with its family, tier and access. A test fails when the committed file differs from the generated text, and BEAMCRAFT_BLESS=1 rewrites it, so the reference cannot drift from `beamcraft commands`. The README is not shortened yet: it carries rules and examples that the registry does not hold. Also lists three options the adapters accept but the registry missed: update-element --profile and --profile-digest (added by #757) and create-project --environment (added by #755). The "listed option is accepted" test cannot see an omission, so every undeclared sibling option was probed once by hand; no other command accepts an unlisted option except the shared drawing allow-list and a few options that are accepted and ignored or refused later. * docs: interactive-assist features need no CLI command (#762) An agent builds a model by giving coordinates and ids directly, so cursor picking and snapping and the start screen (settings, environments, project-name suggestion, recent projects) do not need a CLI command. Narrow the headless-first rule in AGENTS.md section 3 to features an agent or script needs, and record the decision. The CLI commands themselves are removed in a separate change; the engine APIs stay for the window. * refactor: remove the interactive-assist CLI commands and the tier (slices 4-5 of #753) (#764) An agent builds a model by giving coordinates and ids directly, so the CLI no longer needs cursor picking, snapping or the start screen. Remove pick, snap, show-settings, set-settings, list-environment, suggest-project-name, list-recent and add-recent (owner decision of 2026-10-31, see AGENTS.md section 3). The engine and host functions behind them stay. list-feature, lookup-feature, element-edges and work-plane stay: preview-drawing takes a feature id. With no ui-assist command left, the tier is empty information: drop `Tier`, the `tier` field of `commands` and `commands --tier`. The window still runs pick and the settings commands as a child process, so its start screen, theme persistence and click selection are broken until #763 moves it to the engine; the six UI tests that need them are ignored with that issue's number. The CLI tests of the removed commands are deleted (construction_snaps.rs, the pick and snap tests, the settings tests) or cut down to the commands that remain. The agent skill and the design notes no longer present them. * docs: point the CLI README at the generated command reference (slice 7 of #753) (#765) The README repeated a usage block for most commands. They had drifted (create-plate and update-element lacked many options the commands take) and the generated COMMANDS.md now states them from the registry, with a test that keeps it current. Replace each usage block with a link to the command's entry there; the rules, examples and acceptance notes of each section stay. The one worked example (define-profile then create-member with concrete values) stays too. * Deliver the fork's wave-one work: T03 transforms, A10 bent plates, A11 lofted plates, A04.2 member holes, V14 element filters, V15 view cut planes, T02 construction objects and D04 drawing-view placement, with the registry entries and design notes * feat: deliver the fork's wave-two work (C06, A14.3, A14.4, D04 slice 2, D05, V12, V17, annotations) One cumulative delivery of the fork work upstream does not have yet, cut from the current master (includes #768 wave-one). Per feature: - C06 compensation: the coordinator `compensate` operation and the local `compensate-workspace` semantics behind one authenticated wire, with replay and bounded DEPENDENCY_CONFLICT reports (`coordinator.rs`, `coordinator_cmd.rs`, `workspace/compensation.rs`, `tests/compensation.rs`). - A14.3/A14.4 bolt groups: rows of identical fasteners pinned to a library definition between 2..1024 parts, atomic re-pin updates and generated hole records on every connected plate (`bolt_cmd.rs`, `tests/bolt_groups.rs`, engine and kernel bolt/member-hole support). - D04 slice 2: cut section views and magnified detail views over pinned sources, with the drawing archive, derived, projection, print, pdf and svg plumbing and the sections/views acceptance suites. - D05 sheets: sheet composition of placed drawing views (`drawing/sheet.rs`, `model/sheet.rs`, UI `sheet.rs`, `tests/drawing/sheet.rs`). - V12 named views: named-view session commands and their host/engine state (`views_cmd.rs`, `host/views.rs`, `engine/views.rs`, `tests/views.rs`). - V17 colour filter classes: colour sets per filter class in the window (`colour_sets.rs`, `theme.rs`, `scene.rs`). - Annotations: the annotation element family through the model, engine, layer-engine and CLI, with the annotation panel in the window. - T03 fix: the composed rotation quaternion of `rotate_placement` is normalized (`engine/transform.rs`). - Kernel: member-hole operations with their acceptance suite (`ops/member_hole.rs`, `tests/g14_member_hole.rs`). The interactive-assist tier stays removed (upstream decision): the `pick` and `snap` CLI commands are not reintroduced, the registry entries are withheld and `COMMANDS.md` is regenerated from the shipped registry; snap-scoped test cases stay fork-side. The A14.2 `create-bolt` prototype is superseded by the fork's bolt-group line and is removed here with its suite and its alpha-scenario step. * docs: plan the session crate and move the apps up one layer (F08) The window must stop starting the CLI for settings, recent projects, project names and environments (#763). Those are platform state, not storage and not steel, so they get their own crate instead of growing `host` or making the window depend on an app. The crate is named `session` because a `beamcraft-` prefix would tie a platform name to steel. The table gains a planned row, the apps move to layer 6 so they stay above it, and the architecture note records the direction: one command layer under both apps, with the CLI shrinking to argument parsing and printing. * fix: the window reads settings, projects and environments in-process (#763) The start screen and the theme setting still started `beamcraft` for commands that were removed (show-settings, set-settings, list-environment, list-recent, add-recent, suggest-project-name), so they did not work on master. They are platform state, not storage and not steel, so they move into a new `session` crate that both apps call in their own process; `host` stays disk-only. - `session`: settings (moved from host), project folders and names, environments and their admission, the recent list, and the foreign-layer opener they share. Settings writes of one process share a lock, because the temporary file is named after the process. - The CLI uses the same functions; create-project and the environment seeding stay commands. - The window calls `session` on its worker threads and starts a process only for create-project. pick_args and parse_pick are gone; the six tests that were ignored for this issue run again, comparing with the expected elements instead of a removed command. * fix: write the hole clearance range check as a range (clippy manual_range_contains) The check rejected a clearance below 0 or above the dimension cap with two comparisons. Newer clippy flags that as manual_range_contains, which fails `cargo clippy -- -D warnings` for the whole workspace. The value is already known to be finite at this point, so the range test accepts and refuses the same inputs. * Merge pull request #775 from Apibim-com/docs/platform-steel-audit docs: audit of platform and steel code, and the order to separate them * feat: boundaries check knows the discipline kind (#776) A discipline (steel today, concrete or MEP later) may be depended on only by an app or a tool. That keeps the platform free of any one trade and keeps two disciplines apart. The kind is added to the table parser and to the dependency rules with a unit test; no crate uses it yet, so the table is unchanged. * refactor: the CLI errors are session::Refusal, not PlateError (#777) PlateError lived in apps/beamcraft-cli/src/plate.rs, a steel module, yet about sixty CLI files that have nothing to do with plates imported it. It had the same three fields as session::Refusal, so the platform error moves to session (with the conversions from CommandError, ContentRejection and RevisionError) and every use is renamed. No behaviour changes: the codes, messages and Display text are the same. This removes the first coupling named in docs/design/platform-and-steel.md. * refactor: the platform pipeline moves from plate.rs to session (#778) The layer names, the Base a revision is authored on, load_base, the D1 project claim, open_request and the revision summary were in apps/beamcraft-cli/src/plate.rs next to the plate decoders, and every other CLI module imported them from there. Nothing in them knows about an element kind, so they move to session::pipeline unchanged, and the foreign-layer opener reuses the same request builder instead of its own copy. plate.rs keeps the plate decoders and create_plate. No behaviour changes. * refactor: steel commands, sections and fasteners move to model::steel model/src/command.rs was one 6,478-line file: about three quarters of it were the plate, member, hole, bend, fabrication, weld and bolt commands, with the platform commands (update, copy, array, assembly, level, mark) between them. The steel commands move to model::steel::command, and section.rs and fastener.rs, which only steel uses, move to model::steel::section and model::steel::fastener. command.rs keeps CommandError, Placement, label_is_valid and the platform commands. Every import in the workspace is rewritten; no behaviour changes. * docs: point the command module doc at CreatePlate's new path The module doc linked [`CreatePlate::content_fingerprint`], which no longer resolves now that CreatePlate lives in model::steel::command. cargo doc with -D warnings (the CI Docs step) failed on it. * refactor: the steel bodies of the kernel move to kernel::steel (#780) * chore: stop the macOS test shards queueing for runners (#781) * refactor: the steel files of the engine move to engine::steel (#783) * refactor: the element edit, copy and array commands and the instance types are steel (#787) * chore: let pull requests save rust-cache again (#786) * refactor: compose the head once when reading references (#788) * refactor: the engine files that edit, copy and measure plates and members are steel (#791) * refactor: profile definitions, the plate cross-check and the sweep are steel (#796) * refactor: the structural steel code of model, kernel and engine is the steel crate (#799) * chore: gate CI on one job that proves every test ran (#782) * chore: stop the macOS test shards queueing for runners The account runs at most five macOS jobs at once. Every pull request asked for seven during the test phase: six test shards plus the macOS cold-release measurement. One or two shards therefore waited 30-80 s for a runner on every pull request run of 2026-10-31, on the critical path. - Run the macOS tests in five shards. They use nextest's `count:` partitioning: replaying the measured test durations, it gives a longest shard of about 60 s, against 64 s for six `hash:` shards. - Run the macOS cold-release measurement (#153) only on pushes to master and on manual runs. It is build-time evidence, not a test gate; the Linux leg still builds the release profile on every pull request. - Save rust-cache only from master. A pull request's cache serves only that pull request, competes with master's caches for the 10 GB limit, and its save step delays the shards waiting on the archive job. Linux and Windows keep eight `hash:` shards: replaying their durations, `count:` was no better. Every test still runs on every platform it ran on before. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * chore: gate CI on one job that proves every test ran Branch protection has no required checks yet, and a red pull request can be merged. Nothing proves that the shards together ran every test either: a partition mistake, a dropped matrix leg or a filter inherited by the CI profile would lose tests while every job stayed green. Later speed-ups change the sharding, so this check has to come first. - The shards run a nextest `ci` profile: the `default` profile plus a JUnit report, with retries pinned to 0. Each shard uploads its report. - Each archive job lists its platform's tests with the binaries it just built (`cargo nextest list --profile ci --message-format json`). - A new `ci-ok` job, the one check to require, runs with `always()` and fails unless every other job succeeded (a pull request may skip only the Windows jobs) and, per platform, the reports together contain every listed `(binary, test)` exactly once, nothing was filtered out, and the ignored tests are exactly those named in `.config/ci-ignored.txt` (empty today). It also fails if a job of the workflow is missing from its `needs`, so no new job can bypass it. tools/ci-gate.py holds the checks, using only the Python 3 standard library so any runner can run it; tools/test-ci-gate.py covers each failure mode, and its formats match what cargo-nextest 0.9.149 writes. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX --------- Co-authored-by: Claude <[email protected]> * ci: set project Status to In progress when 'in progress' label is added (#800) This workflow sets the project status to 'In progress' when the corresponding label is added to an issue or pull request. * docs: the roadmap keeps the gates and the scope, not the tasks (#801) ROADMAP.md listed the next work and the status of every gate package. Both duplicated the issues and drifted from them (#738: C07/C08, the #331 acceptance tail, M0.2 and A04.2 had no trace in the file). The roadmap now holds the stage, the gate verdicts and the MVP scope with its deferrals. What to work on, its order and its state are read from the issues, the milestones and the GitHub Project. CLAUDE.md and AGENTS.md say so, and ask an agent that starts work on an issue to add the label "in progress", which the "Label to Project Status" workflow turns into the Project Status. The owner decision is recorded in decisions.md. * docs: the design of the element interface (#795, slice 1) (#802) * docs: the design of the element interface (#795, slice 1) The platform still branches on the kind of element in two different places: the typed operations (copy, edit, mirror, array, evaluate), which exist as symmetric plate/member pairs, and the persistence in layer-engine, whose Shape and Change enums carry typed steel fields. The note derives one small trait for the first from the real signatures and a neutral prim description with registered validators for the second, keeps the F10 registry as it is, and composes the schema descriptor from ordered segments so its bytes stay equal. It also fixes the order of the code slices and what closes each. * docs: the element interface note records what the writers show (#795) Reading the layer-engine writers shows that the neutral attribute list would mean rewriting every writer, so the steel half moves verbatim into steel and steel depends on layer-engine. The read side needs a registry, installed once. The typed-operations trait is not on the way to the discipline kind. * docs: the hook inventory of the layer-engine move (#795) One row per role of the roughly 110 match sites on the steel variants of Shape and Change, with where each is today and the hook it becomes, so the move can go family by family with the suite green after each. * chore: skip the Rust builds for pull requests that change only prose (#792) * chore: stop the macOS test shards queueing for runners The account runs at most five macOS jobs at once. Every pull request asked for seven during the test phase: six test shards plus the macOS cold-release measurement. One or two shards therefore waited 30-80 s for a runner on every pull request run of 2026-10-31, on the critical path. - Run the macOS tests in five shards. They use nextest's `count:` partitioning: replaying the measured test durations, it gives a longest shard of about 60 s, against 64 s for six `hash:` shards. - Run the macOS cold-release measurement (#153) only on pushes to master and on manual runs. It is build-time evidence, not a test gate; the Linux leg still builds the release profile on every pull request. - Save rust-cache only from master. A pull request's cache serves only that pull request, competes with master's caches for the 10 GB limit, and its save step delays the shards waiting on the archive job. Linux and Windows keep eight `hash:` shards: replaying their durations, `count:` was no better. Every test still runs on every platform it ran on before. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * chore: gate CI on one job that proves every test ran Branch protection has no required checks yet, and a red pull request can be merged. Nothing proves that the shards together ran every test either: a partition mistake, a dropped matrix leg or a filter inherited by the CI profile would lose tests while every job stayed green. Later speed-ups change the sharding, so this check has to come first. - The shards run a nextest `ci` profile: the `default` profile plus a JUnit report, with retries pinned to 0. Each shard uploads its report. - Each archive job lists its platform's tests with the binaries it just built (`cargo nextest list --profile ci --message-format json`). - A new `ci-ok` job, the one check to require, runs with `always()` and fails unless every other job succeeded (a pull request may skip only the Windows jobs) and, per platform, the reports together contain every listed `(binary, test)` exactly once, nothing was filtered out, and the ignored tests are exactly those named in `.config/ci-ignored.txt` (empty today). It also fails if a job of the workflow is missing from its `needs`, so no new job can bypass it. tools/ci-gate.py holds the checks, using only the Python 3 standard library so any runner can run it; tools/test-ci-gate.py covers each failure mode, and its formats match what cargo-nextest 0.9.149 writes. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * chore: skip the Rust builds for pull requests that change only prose One commit in six touches only Markdown (10 of 64 since 2026-10-17). Each still ran the whole matrix: two quality jobs, two archive builds, thirteen test shards and the cold release, five of those jobs on the account's five macOS runners. None of them can change outcome when no program reads the changed files. - A `changes` job lists the pull request's files through the API. tools/ci-changes.py prints `prose=true` only when every file is Markdown that no program reads. A file counts as read when a string literal in a `.rs` or `.toml` file names it or a directory above it, resolved as the code resolves it: from the crate for `/`, `../` and `{}/`, else from the repository root. In this repository that means the F08 table, the fixture documents, `COMMANDS.md`, and every crate directory named by a path dependency. A listing error, an empty or truncated list, or any non-Markdown file means the full CI. - On `prose=true` the Rust jobs are skipped and `docs-checks` runs the three checks whose inputs are documents on Linux, in case a document became an input without the classifier knowing. - `ci-ok` allows those skips only on pull requests, only with `prose=true`, and then requires `docs-checks`. Pushes to master always run everything. tools/test-ci-changes.py covers the classifier on a synthetic repository and on this one. tools/test-ci-gate.py adds the five prose cases. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * chore: let the document checks reuse the Linux test-archive cache The prose-only probe (#793) showed document checks taking 112 s: the job runs only on prose pull requests, never on master, so no cache of its own is ever saved where pull requests can restore it. It now restores test-archive-ubuntu's cache under that job's key, with the same profile and environment, and saves nothing. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX --------- Co-authored-by: Claude <[email protected]> * refactor: build the window's integration suites into one test binary (#784) * chore: stop the macOS test shards queueing for runners The account runs at most five macOS jobs at once. Every pull request asked for seven during the test phase: six test shards plus the macOS cold-release measurement. One or two shards therefore waited 30-80 s for a runner on every pull request run of 2026-10-31, on the critical path. - Run the macOS tests in five shards. They use nextest's `count:` partitioning: replaying the measured test durations, it gives a longest shard of about 60 s, against 64 s for six `hash:` shards. - Run the macOS cold-release measurement (#153) only on pushes to master and on manual runs. It is build-time evidence, not a test gate; the Linux leg still builds the release profile on every pull request. - Save rust-cache only from master. A pull request's cache serves only that pull request, competes with master's caches for the 10 GB limit, and its save step delays the shards waiting on the archive job. Linux and Windows keep eight `hash:` shards: replaying their durations, `count:` was no better. Every test still runs on every platform it ran on before. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * chore: gate CI on one job that proves every test ran Branch protection has no required checks yet, and a red pull request can be merged. Nothing proves that the shards together ran every test either: a partition mistake, a dropped matrix leg or a filter inherited by the CI profile would lose tests while every job stayed green. Later speed-ups change the sharding, so this check has to come first. - The shards run a nextest `ci` profile: the `default` profile plus a JUnit report, with retries pinned to 0. Each shard uploads its report. - Each archive job lists its platform's tests with the binaries it just built (`cargo nextest list --profile ci --message-format json`). - A new `ci-ok` job, the one check to require, runs with `always()` and fails unless every other job succeeded (a pull request may skip only the Windows jobs) and, per platform, the reports together contain every listed `(binary, test)` exactly once, nothing was filtered out, and the ignored tests are exactly those named in `.config/ci-ignored.txt` (empty today). It also fails if a job of the workflow is missing from its `needs`, so no new job can bypass it. tools/ci-gate.py holds the checks, using only the Python 3 standard library so any runner can run it; tools/test-ci-gate.py covers each failure mode, and its formats match what cargo-nextest 0.9.149 writes. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * refactor: build the window's integration suites into one test binary Each of the window's eight integration suites was its own test binary, and each linked the whole GUI stack (eframe, egui, wgpu, winit): the costliest links in the workspace, repeated on every platform in every CI run, with eight copies in the nextest archive that every test shard downloads and unpacks. The suites now build into one `ui` binary (`tests/ui.rs`, one module per suite file), the way the CLI's suites already do. `cancel` stays its own target: it relies on being alone in its process. A guard test fails when a suite file is not listed. The shared `support` module gets its own clippy allow, which it used to inherit from the suite that declared it, and its one test now runs once instead of once in each of the four binaries that declared it. No test changes otherwise: each `<suite> <test>` is now `ui <suite>::<test>`. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX --------- Co-authored-by: Claude <[email protected]> * refactor: the write side of layer-engine goes through discipline hooks (#795) (#803) * refactor: the connection family of layer-engine is a discipline hook (#795) Shape::Weld, Shape::Bolt and Shape::BoltGroup are replaced by Shape::Discipline, whose hook answers the kind token, the category, the attributes written, the engineering bounds and the finiteness check. The arms of edit.rs and validate.rs moved unchanged into authored/connection.rs, so the bytes written and the refusals returned are the same. The CLI and the tests build the shapes with Shape::weld, Shape::bolt and Shape::bolt_group. * refactor: the bolt group edit of layer-engine is a discipline change hook (#795) Change::BoltGroup is replaced by Change::Discipline, whose hook answers the target, the kinds it may edit, the groups it reads and writes, its bounds, its attribute writes and its finiteness check. The arms moved unchanged into authored/connection.rs; the CLI builds the edit with Change::bolt_group. * refactor: the member family of layer-engine is a discipline hook (#795) Shape::Member and the Axis, Roll and Profile edits are replaced by the discipline variants. MemberShape and the three edits implement the hooks; the arms of edit.rs and validate.rs moved unchanged into authored/member.rs. The hooks gained three defaults the member needs: the owned cut records, the part class and whether an edit removes the derived result. * refactor: the plate shapes of layer-engine are discipline hooks (#795) Shape::Plate and Shape::LoftedPlate are replaced by the discipline variant. PlateShape and LoftedPlateShape implement the hook; the arms of edit.rs and validate.rs moved unchanged into authored/plate.rs. The hook gained the budget charge of an element's arrays. The tests that mutated a plate in place rebuild it with Shape::plate. * refactor: the plate, hole and cut edits of layer-engine are discipline hooks (#795) Change::Contour, Thickness, ThicknessSide, Corners, Bends, Cuts, HoleRecords and MemberHoles are replaced by the discipline variant. Eight edit types implement the change hook; the arms of edit.rs and validate.rs moved unchanged into authored/plate_change.rs. The hole records keep their own derived-field declaration through declares_mesh. * fix: export ReplaceBoltGroup so its documentation link resolves (#795) The public constructor Change::bolt_group linked to a type in a private module, which rustdoc refuses under -D warnings. * refactor: build the kernel's and steel's integration suites into one test binary each (#785) * chore: stop the macOS test shards queueing for runners The account runs at most five macOS jobs at once. Every pull request asked for seven during the test phase: six test shards plus the macOS cold-release measurement. One or two shards therefore waited 30-80 s for a runner on every pull request run of 2026-10-31, on the critical path. - Run the macOS tests in five shards. They use nextest's `count:` partitioning: replaying the measured test durations, it gives a longest shard of about 60 s, against 64 s for six `hash:` shards. - Run the macOS cold-release measurement (#153) only on pushes to master and on manual runs. It is build-time evidence, not a test gate; the Linux leg still builds the release profile on every pull request. - Save rust-cache only from master. A pull request's cache serves only that pull request, competes with master's caches for the 10 GB limit, and its save step delays the shards waiting on the archive job. Linux and Windows keep eight `hash:` shards: replaying their durations, `count:` was no better. Every test still runs on every platform it ran on before. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * chore: gate CI on one job that proves every test ran Branch protection has no required checks yet, and a red pull request can be merged. Nothing proves that the shards together ran every test either: a partition mistake, a dropped matrix leg or a filter inherited by the CI profile would lose tests while every job stayed green. Later speed-ups change the sharding, so this check has to come first. - The shards run a nextest `ci` profile: the `default` profile plus a JUnit report, with retries pinned to 0. Each shard uploads its report. - Each archive job lists its platform's tests with the binaries it just built (`cargo nextest list --profile ci --message-format json`). - A new `ci-ok` job, the one check to require, runs with `always()` and fails unless every other job succeeded (a pull request may skip only the Windows jobs) and, per platform, the reports together contain every listed `(binary, test)` exactly once, nothing was filtered out, and the ignored tests are exactly those named in `.config/ci-ignored.txt` (empty today). It also fails if a job of the workflow is missing from its `needs`, so no new job can bypass it. tools/ci-gate.py holds the checks, using only the Python 3 standard library so any runner can run it; tools/test-ci-gate.py covers each failure mode, and its formats match what cargo-nextest 0.9.149 writes. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * refactor: build the window's integration suites into one test binary Each of the window's eight integration suites was its own test binary, and each linked the whole GUI stack (eframe, egui, wgpu, winit): the costliest links in the workspace, repeated on every platform in every CI run, with eight copies in the nextest archive that every test shard downloads and unpacks. The suites now build into one `ui` binary (`tests/ui.rs`, one module per suite file), the way the CLI's suites already do. `cancel` stays its own target: it relies on being alone in its process. A guard test fails when a suite file is not listed. The shared `support` module gets its own clippy allow, which it used to inherit from the suite that declared it, and its one test now runs once instead of once in each of the four binaries that declared it. No test changes otherwise: each `<suite> <test>` is now `ui <suite>::<test>`. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * refactor: build the kernel's integration suites into one test binary The kernel's nineteen integration suites were nineteen test binaries, each linking the kernel and compiling its own copy of the shared `common*` helpers. They now build into one `kernel` binary (`tests/kernel.rs`, one module per suite), like the CLI's suites; a guard test fails when a suite file is not listed. The helpers become modules of that binary, declared once; a suite imports them with `use crate::common;`. The G06 suites named `common_mesh` as `common` through `#[path]`; they now write `use crate::common_mesh as common;`. The `fast` nextest profile and the documented `cargo test --test <suite>` commands name the new binary. No test changes otherwise: each `<suite> <test>` is now `kernel <suite>::<test>`. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * Build the steel crate's integration suites into one test binary #799 moved eight of the kernel's suites, with copies of the shared common* helpers, into the new steel crate. The merge from master carried this branch's `use crate::common;` edits into the moved files, which only compile as modules of one binary. Steel now gets the same layout as the kernel: autotests off, one `steel` test target whose root declares the helpers and all eleven suites, and the guard test that fails when a suite file is not listed. The kernel's root drops the suites that left it. The evidence commands of A07, G07 and P03 named kernel test targets that #799 moved; they now name the suites in the steel binary. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX --------- Co-authored-by: Claude <[email protected]> * refactor: layer-engine reads parts and connections through a discipline registry (#795) (#805) * refactor: the P11 instance of layer-engine is a discipline hook (#795) Shape::Instance is replaced by the discipline variant; InstanceShape implements the hook with the arms moved unchanged from edit.rs and validate.rs. The write side of Shape now has no steel variant left. * refactor: layer-engine reads the parts and connections through a discipline registry (#795) Reading a stored layer asks a registered Discipline instead of matching plate, beam and column in the platform: which kinds are parts, the validation of a part and of a connection, and the write group of a property. The steel arms of the snapshot validator and the rebase group table moved unchanged into steel_part, steel_connection and steel_property_group; the registry holds the built-in steel discipline until the apps install it from the steel crate. * docs: say which discipline validates a connection prim (#795) * refactor: build engine, host and layer-engine suites into one binary each (#789) * docs: describe the platform and discipline split after #795 in the architecture (#806) The write side of layer-engine goes through discipline hooks (#803) and the read side through a discipline registry (#805), but the architecture still described layer-engine as writing steel directly. Record the two seams, what is done, what is left before steel becomes kind discipline, and the decisions of the element-interface note. * docs: add the wave-two design notes (#807) Design notes for the wave-two slices that #770 delivered: A14.3 bolt group placement, A14.4 generated holes, C06 compensation, D04 slice 2 section and detail views, D05 drawing sheets, V12 named views and V17 colour filters, plus the T03 and G13 note updates. Statements are verified against the merged master, so the module paths follow the current steel-crate and discipline-hook layout instead of the pre-merge fork layout. Co-authored-by: axisrow <[email protected]> * docs: extend the D01 drawing contract for the D04/D05 surfaces and the D07 slices 2-3 (#808) * docs: extend the drawing contract for the wave-two surfaces Update D01 v0.1 for the surfaces #770 delivered: the D04/D05 drawing surfaces (sections, details, sheets, whole-sheet export) and the D07 slice 2-3 drafting additions (text, hatch, symbol, free_dimension, layers and line types, MAX_LAYERS, MAX_NUMBER_TEXT_BYTES). Contract changes carry their own docs-only PR under AGENTS.md section 7, apart from the design notes. * docs: state the detail region bounds and the identity set exactly Split the detail admission bullet so the region centre reads as the signed paper cap validate_detail enforces (only the radius refuses non-positive), and name the full pairwise-distinct paper-object set - views, sections, details, drawn objects and layers - in both identity sentences, as admission checks it. --------- Co-authored-by: axisrow <[email protected]> * refactor: steel becomes a discipline above the platform (#795) (#809) The platform no longer knows steel. The steel half of layer-engine (the hooks behind Shape::Discipline and Change::Discipline, the steel readers, writers and definitions, the read-back discipline, the authoring facade and its tests) moves into steel::layer, and the Cargo edge flips: steel depends on beamcraft-layer-engine, no platform crate depends on steel, and steel is kind `discipline` in the F08 table. The registry starts empty; both apps call steel::layer::install() at start (the window reads layers through session). The remaining branching on the kind goes too. steel::engine::part::PartRecord chooses between the per-kind pairs (copy, P11 copy, array, patch with re-evaluation, quantity record), so the CLI copy, array and update-element adapters drop CopiedElement, UpdatedElement and Target. The engine's queries ask ElementKind::is_part and is_linear_member instead of matching Plate, Beam or Column. Stored bytes, refusal codes and command output are unchanged. Left on purpose, with reasons in docs/design/element-interface.md section 8: no platform trait and no ElementPatch split (the UpdateElement/v2 fingerprint), one SCHEMA_DESCRIPTOR, and the /World/Steel prim paths and F06 tokens, which are format, not behaviour. The layer-engine move and the flip are axisrow's series from their fork (axisrow/BeamCraft #394, #398-#402, #404, #407, #411, #412), ported onto master. * test: add the G11 accuracy fixtures and refuse subnormal masses (#490) (#810) * refactor: move the application layer out of the CLI (#208) (#811) * refactor: move the application layer out of the CLI (#208) #208 asks for an application API that the apps share instead of one app owning it, and for boundary checks that a layout change cannot bypass. The owner set the direction: the CLI ends up a thin layer over the commands. Move the application layer to crates/commands: - the 40 library modules of beamcraft-cli move unchanged, as git renames, into beamcraft-commands (crates/commands): plates, members, bolts, welds, definitions, grids, numbering, drawings, exports, workspaces, history, publication, rebase, the coordinator and the command registry; - the CLI keeps the binary: argument parsing, help, dispatch and JSON rendering; - F08 gains the kind `service` (layer 7). Only an app or a tool may depend on it, and it may depend on a discipline. The apps move to layer 8. Make the boundary checks fail closed on layout: - beamcraft-boundaries scanned only <pkg>/src, so moving a domain crate's sources to code/ behind `[lib] path` hid std::fs from the scan; - it now refuses target paths outside the standard layout, build scripts in domain crates, and #[path] in domain sources outside tests; - regression tests include that #208 reproduction on disk. Correct the docs: - F08 no longer claims that no package has a third-party dependency, and section 4 rows that said "planned" or "geom holds Point3 only" now match the code; - the engine crate doc no longer says that only one command exists; - development.md gives each worktree its own target directory; - design notes and contracts name beamcraft_commands and the moved paths. Behaviour, output bytes and refusals are unchanged: the full workspace suite passes with the same tests plus the new checker cases. Claude-Session: https://claude.ai/code/session_017VPNWPiS3iNL1ie8gcEgfU * chore: allow MPL for beamcraft-commands in cargo deny and optimize it in release (#208) The new crate is a BeamCraft workspace package like the others: cargo deny allows MPL-2.0 only for listed workspace packages, and the release profile lists every engineering crate for opt-level 3, which the moved code had as part of beamcraft-cli. Claude-Session: https://claude.ai/code/session_017VPNWPiS3iNL1ie8gcEgfU * refactor: merge the starter environment into blank (#812) Blank is the base environment that other environments are built from, so it must be usable on its own. Until now it was empty and a separate starter environment carried the nineteen generic profiles, which made the start window offer two environments where one is a dead end: a model on blank could place no beam or column. The generic profile library moves into environments/blank and the starter environment goes. The start window now prefers blank when no environment is saved; a saved "starter" is no longer usable, so it falls back to blank. Projects created from starter keep working because each project carries a copy of its environment. The CLI test that placed members from starter now runs on blank, the test that blank seeds no profiles goes, and the shipped-environments test checks that blank ships. The design note becomes blank-environment.md and the two ATTRIBUTION rows become one; the values stay invented and follow no standard or catalogue. * feat: model views with their own plane, properties, browser and windows (V12) (#813) * Make object properties and creation use modelling actions Remove exposed command previews and commit ceremonies, add named level assignment, and create members by two picked points and plates by contours. Keep engine admission, stale-reading guards and command publication paths, with real CLI and native-controller regression coverage. * all work with views * feat: views carry a view plane and their own display properties A named view was an orbit pose with an optional plane recipe and work area, so it could not hold a section through three points, a depth range around the plane, its own render mode or hidden kinds. The view book now stores ModelView records: an identity, a name, the view plane (the input frame), projection, visible height, depth above and below the plane, render mode, grid visibility, hidden kinds, work area, camera rotation, colour and opacity, plus browser folders and a favourite mark. Temporary views live in the book but are never written. The constructors come from the model-views branch: coordinate planes, two points (vertical or across the input plane), three points, grid axes and grid batches by family, levels and level batches, part and assembly frames by side, and picked surfaces with an optional edge. pick_visible honours the view's slab, work area and hidden kinds, and clipping() turns the slab and work area into the V15 clip record, so the renderer needs no second clipping path. The session JSON is rewritten in place (version 1): the old pose-based shape is not read any more. * feat: build and change model views headlessly The view commands follow the new view book. save-view builds a view from exactly one plane: an orbit, a coordinate plane with an offset, an explicit frame, two points (vertical or across an input plane), three points, or, reading the store, a grid axis, a level, a part or an assembly by side, or the first face a ray hits. Grid families, levels and the four part sides make batches that add every view or none and suffix repeated names. Every display property of a view has an option, and --copy-from reuses another view's. update-view changes one view in place, its plane included; remove-view and apply-view select a view by identity or name, and apply-view now returns the camera frame and the clip planes a window restores. Folders, quick access and windows stay in the window: they need no agent. * feat: model views in the window, from the browser to the view windows The window works with the new view book. The project browser lists the views by folder with search, quick access and favourites, Ctrl and Shift selection, a context menu (open, open in a window, properties, favourite, duplicate, cut, copy, close, delete with confirmation) and drag to a folder; {3D} is the free view. The View tab creates views (coordinate planes, two or three picked points, the work plane, grid axes and batches, level plans, a picked face with or without an edge, part and assembly views, the current camera), opens their properties, toggles 3D and plane (Ctrl+P), turns the camera to six directions, and opens and tiles windows. A view is applied, not copied into the window state: activating it builds a plane-aligned camera, takes its render mode, grid guides and plane as the input frame, hides its kinds from the scene, tints the model with its colour and opacity, and adds its depth slab and work area to the cut. Picking and snapping see only what the view shows. Each view keeps its session camera, Ctrl+Tab cycles the open views, and the free 3D view comes back as it was left. Up to eight windows show views with their own camera, cut, tint, hidden kinds, render mode and grids, and can be docked. Level plans and assembly views read the store, so the window builds them through save-view; everything else is built in-process by the engine. * feat: view previews, palette commands and the model-views design note The browser can show a preview of each view, drawn off screen through the view's own camera, cut, tint and hidden kinds; a preview is kept until the snapshot or the view changes, one is drawn per frame and at most 64 are kept. The command palette lists every way to create a view and the view commands. A GPU test proves the view tint reaches the pixels (a colour replaces the model colour, zero opacity draws nothing) and that a hidden kind leaves the scene without moving its origin. The V12 design note describes the view model, the session format, the commands and the window, and what stays out of this slice. --------- Co-authored-by: Yury Isachenkov <[email protected]> * feat: cut directed oblique and faceted-wall member holes (G14.2 slices 1-2) (#814) * feat: work with models from the in-model menu (#815) * feat: give the menu hierarchical navigation and drop its dead entries Diagnose & repair, Extend and Exit BeamCraft offered nothing real, and the catalog and About sub-pages had no way back to their section. Sub-pages now name a parent, show "‹ Back" and keep their parent row highlighted, and the menu owns Esc while it is open, so Esc walks up one level at a time instead of closing the whole menu from a sub-page. * feat: open the model folder from the menu The row was disabled, so reaching a model's files meant finding the folder by hand. It now opens the store in Explorer, Finder or the xdg-open handler with std::process::Command (no new crate, the links feature stays off). A missing folder or a missing launcher shows as a note under the menu heading instead of failing silently, and the child is reaped on a thread so Unix keeps no zombie. * refactor: move the new-model form out of the start page The in-model menu needs the same create form as the start page. Keeping two copies would let the name suggestion, the location check and the refusal messages drift apart, so the form, its worker and its note now live in library::NewModelForm and the start page hands its New model tab to it. The start page behaves as before. * feat: let a model window switch to another model in place The menu's New and Open should replace the open model without a detour through the library. Shell now honours an App's open_request: it opens the new model, keeps the current one with the reason on its menu when that fails, and retires the old App for one pass before freeing its textures, so no output still to be painted refers to them. The library return goes through the same slot, which stops the viewport texture entry from leaking on every round trip. * feat: create and open models from inside the menu New and Open used to drop the open model and send the user back to the library. They are now menu pages: New shows the shared new-model form and Open lists the recent models with Browse…, and both switch the window in place through open_request. While a create or open runs, the menu and the commands that would leave or change the window wait for it, so its result is never lost. The library's Recent list is re-read on return, so models opened from the menu appear there. * feat: show the project's facts on a read-only menu page Project properties only opened the Details window. It now opens a menu page with what the store's head already says about the project and what this session runs with, read on a worker so a large model never freezes the window. The page says plainly that project information fields are not stored in models yet. The design note records how the menu works and why it adds no CLI command. * Fix empty profile picker and add environment part presets (#816) * Fix empty profile picker in the window and add part presets to the blank environment The window refreshed definitions through export-view --with-definitions, which named no store, so the project's environment profiles were never listed and no profile could be picked. The call now passes the store. An environment may declare bim:environment:presets, a layer of beam, column and plate presets (name, class, material, profile or thickness). blank ships nine. definitions lists them as environment_presets, and choosing the beam, column or plate tool fills the form from the default preset; a Preset drop-down switches to another and every field stays editable. * Show only the name in the profile and material pickers The environment suffix on profiles and the density on materials added noise; the digest suffix was already hidden. Presets now match a material by its name followed by the digest suffix. * Snap markers for beam and column placement; one-click columns The member tools already snapped but showed no marker and armed nothing before the first click. The pointer now shows the snap marker and caption as for plates, from the first click on. A column is placed with one click on its base point. Its top is the form's height or a level of the model (definitions now lists levels), straight up in world Z. * feat: view windows inside the work area, view properties in the right panel (#817) * chore: drop the upstream CI reform python tools the merge carried in The fork keeps no tracked python (the #178/#182 rule). The four files belong to the upstream CI restructure (#792/#782/#781) that this sync deliberately does not adopt; no fork workflow or tool references them. * docs: stop describing the upstream CI reform the fork does not run The completeness-gate and prose-only paragraphs named tools/ci-gate.py and the ci-ok/docs-checks jobs this sync does not adopt; the removed python tools are gone from the tree. * review: unpark the wide-band hole test and restore the construction snap test Both were review findings on the sync PR: - Upstream parks the wider-than-band hole test (#492) because its tree lacks the merge-walk; the merge tree carries the fork's slice 3 (#418), which walks the band across the chain's kinks, so the test passes unparked. Drop the ignore attribute and the ci-ignored.txt entry with it. - The construction tests' snap test (construction_* snap kinds and --construction-snaps) is fork-only and the machine merge had taken the upstream file; restore the fork's version, which upstream's copy equals minus exactly that test. --------- Co-authored-by: Yuri Isachenkov <[email protected]> Co-authored-by: axisrow <[email protected]> Co-authored-by: Yura Isachenkov <[email protected]> Co-authored-by: Claude <[email protected]> Co-authored-by: Yury Isachenkov <[email protected]>
…dows and the default view (Apibim-com#758-Apibim-com#827) (#453) * refactor: dispatch CLI commands from the registry and list access and parameters (slices 2-3 of #753) (#760) * refactor: dispatch CLI commands from the registry (second slice of #753) The binary chose an adapter with a chain of `c == "..."` comparisons in main.rs plus a `handles()` function per adapter, a second hand-kept inventory that could drift from the registry. Each registry entry now names its adapter (`Route`), and main.rs matches on `registry::route(word)`. A word that is not registered cannot reach an adapter and takes the store-level path, which refuses it as before, so a command that is dispatched but unregistered is no longer possible. The per-adapter `handles()` functions are removed. Behaviour, exit codes and output of existing commands are unchanged. * test: wait for the dropped rebase request before the retry The coordinator answers each connection on its own thread, so in lost_answers_and_a_killed_server_replay_exactly_once the retry could overtake the request whose client was dropped and do the merge itself, answering workspace_rebased instead of workspace_rebase_replayed. It failed once on the ubuntu CI runner. Wait until the draft head moves, which happens when the dropped request has merged, before sending the retry. * refactor: list access and parameters of every CLI command (third slice of #753) `beamcraft commands` now carries, for every command, whether it reads or writes (Access) and its options: value shape, whether it is required and, for alternatives, a one_of group. The five common request identity options are listed on the commands that take them (Ids::Common) instead of being repeated in 125 entries. The data was taken from each adapter's allow-list and required checks, not from the help prose; where they differ (for example update-element --corners and --axis-point, create-grid, create-plate --material) the code wins. A rule that links optional options (--width with --length) stays in the family help. Tests: the listing carries access and parameters; an unlisted option is refused as unknown (so the probe is valid); every listed option is accepted by its command, which fails if a listed name is wrong or removed. --------- * docs: generate the CLI command reference from the registry (slice 7 of #753, first part) (#761) COMMANDS.md renders every command from the registry as one usage line (required options bare, optional ones in brackets, one-of groups in parentheses) with its family, tier and access. A test fails when the committed file differs from the generated text, and BEAMCRAFT_BLESS=1 rewrites it, so the reference cannot drift from `beamcraft commands`. The README is not shortened yet: it carries rules and examples that the registry does not hold. Also lists three options the adapters accept but the registry missed: update-element --profile and --profile-digest (added by #757) and create-project --environment (added by #755). The "listed option is accepted" test cannot see an omission, so every undeclared sibling option was probed once by hand; no other command accepts an unlisted option except the shared drawing allow-list and a few options that are accepted and ignored or refused later. * docs: interactive-assist features need no CLI command (#762) An agent builds a model by giving coordinates and ids directly, so cursor picking and snapping and the start screen (settings, environments, project-name suggestion, recent projects) do not need a CLI command. Narrow the headless-first rule in AGENTS.md section 3 to features an agent or script needs, and record the decision. The CLI commands themselves are removed in a separate change; the engine APIs stay for the window. * refactor: remove the interactive-assist CLI commands and the tier (slices 4-5 of #753) (#764) An agent builds a model by giving coordinates and ids directly, so the CLI no longer needs cursor picking, snapping or the start screen. Remove pick, snap, show-settings, set-settings, list-environment, suggest-project-name, list-recent and add-recent (owner decision of 2026-10-31, see AGENTS.md section 3). The engine and host functions behind them stay. list-feature, lookup-feature, element-edges and work-plane stay: preview-drawing takes a feature id. With no ui-assist command left, the tier is empty information: drop `Tier`, the `tier` field of `commands` and `commands --tier`. The window still runs pick and the settings commands as a child process, so its start screen, theme persistence and click selection are broken until #763 moves it to the engine; the six UI tests that need them are ignored with that issue's number. The CLI tests of the removed commands are deleted (construction_snaps.rs, the pick and snap tests, the settings tests) or cut down to the commands that remain. The agent skill and the design notes no longer present them. * docs: point the CLI README at the generated command reference (slice 7 of #753) (#765) The README repeated a usage block for most commands. They had drifted (create-plate and update-element lacked many options the commands take) and the generated COMMANDS.md now states them from the registry, with a test that keeps it current. Replace each usage block with a link to the command's entry there; the rules, examples and acceptance notes of each section stay. The one worked example (define-profile then create-member with concrete values) stays too. * Deliver the fork's wave-one work: T03 transforms, A10 bent plates, A11 lofted plates, A04.2 member holes, V14 element filters, V15 view cut planes, T02 construction objects and D04 drawing-view placement, with the registry entries and design notes * feat: deliver the fork's wave-two work (C06, A14.3, A14.4, D04 slice 2, D05, V12, V17, annotations) One cumulative delivery of the fork work upstream does not have yet, cut from the current master (includes #768 wave-one). Per feature: - C06 compensation: the coordinator `compensate` operation and the local `compensate-workspace` semantics behind one authenticated wire, with replay and bounded DEPENDENCY_CONFLICT reports (`coordinator.rs`, `coordinator_cmd.rs`, `workspace/compensation.rs`, `tests/compensation.rs`). - A14.3/A14.4 bolt groups: rows of identical fasteners pinned to a library definition between 2..1024 parts, atomic re-pin updates and generated hole records on every connected plate (`bolt_cmd.rs`, `tests/bolt_groups.rs`, engine and kernel bolt/member-hole support). - D04 slice 2: cut section views and magnified detail views over pinned sources, with the drawing archive, derived, projection, print, pdf and svg plumbing and the sections/views acceptance suites. - D05 sheets: sheet composition of placed drawing views (`drawing/sheet.rs`, `model/sheet.rs`, UI `sheet.rs`, `tests/drawing/sheet.rs`). - V12 named views: named-view session commands and their host/engine state (`views_cmd.rs`, `host/views.rs`, `engine/views.rs`, `tests/views.rs`). - V17 colour filter classes: colour sets per filter class in the window (`colour_sets.rs`, `theme.rs`, `scene.rs`). - Annotations: the annotation element family through the model, engine, layer-engine and CLI, with the annotation panel in the window. - T03 fix: the composed rotation quaternion of `rotate_placement` is normalized (`engine/transform.rs`). - Kernel: member-hole operations with their acceptance suite (`ops/member_hole.rs`, `tests/g14_member_hole.rs`). The interactive-assist tier stays removed (upstream decision): the `pick` and `snap` CLI commands are not reintroduced, the registry entries are withheld and `COMMANDS.md` is regenerated from the shipped registry; snap-scoped test cases stay fork-side. The A14.2 `create-bolt` prototype is superseded by the fork's bolt-group line and is removed here with its suite and its alpha-scenario step. * docs: plan the session crate and move the apps up one layer (F08) The window must stop starting the CLI for settings, recent projects, project names and environments (#763). Those are platform state, not storage and not steel, so they get their own crate instead of growing `host` or making the window depend on an app. The crate is named `session` because a `beamcraft-` prefix would tie a platform name to steel. The table gains a planned row, the apps move to layer 6 so they stay above it, and the architecture note records the direction: one command layer under both apps, with the CLI shrinking to argument parsing and printing. * fix: the window reads settings, projects and environments in-process (#763) The start screen and the theme setting still started `beamcraft` for commands that were removed (show-settings, set-settings, list-environment, list-recent, add-recent, suggest-project-name), so they did not work on master. They are platform state, not storage and not steel, so they move into a new `session` crate that both apps call in their own process; `host` stays disk-only. - `session`: settings (moved from host), project folders and names, environments and their admission, the recent list, and the foreign-layer opener they share. Settings writes of one process share a lock, because the temporary file is named after the process. - The CLI uses the same functions; create-project and the environment seeding stay commands. - The window calls `session` on its worker threads and starts a process only for create-project. pick_args and parse_pick are gone; the six tests that were ignored for this issue run again, comparing with the expected elements instead of a removed command. * fix: write the hole clearance range check as a range (clippy manual_range_contains) The check rejected a clearance below 0 or above the dimension cap with two comparisons. Newer clippy flags that as manual_range_contains, which fails `cargo clippy -- -D warnings` for the whole workspace. The value is already known to be finite at this point, so the range test accepts and refuses the same inputs. * Merge pull request #775 from Apibim-com/docs/platform-steel-audit docs: audit of platform and steel code, and the order to separate them * feat: boundaries check knows the discipline kind (#776) A discipline (steel today, concrete or MEP later) may be depended on only by an app or a tool. That keeps the platform free of any one trade and keeps two disciplines apart. The kind is added to the table parser and to the dependency rules with a unit test; no crate uses it yet, so the table is unchanged. * refactor: the CLI errors are session::Refusal, not PlateError (#777) PlateError lived in apps/beamcraft-cli/src/plate.rs, a steel module, yet about sixty CLI files that have nothing to do with plates imported it. It had the same three fields as session::Refusal, so the platform error moves to session (with the conversions from CommandError, ContentRejection and RevisionError) and every use is renamed. No behaviour changes: the codes, messages and Display text are the same. This removes the first coupling named in docs/design/platform-and-steel.md. * refactor: the platform pipeline moves from plate.rs to session (#778) The layer names, the Base a revision is authored on, load_base, the D1 project claim, open_request and the revision summary were in apps/beamcraft-cli/src/plate.rs next to the plate decoders, and every other CLI module imported them from there. Nothing in them knows about an element kind, so they move to session::pipeline unchanged, and the foreign-layer opener reuses the same request builder instead of its own copy. plate.rs keeps the plate decoders and create_plate. No behaviour changes. * refactor: steel commands, sections and fasteners move to model::steel model/src/command.rs was one 6,478-line file: about three quarters of it were the plate, member, hole, bend, fabrication, weld and bolt commands, with the platform commands (update, copy, array, assembly, level, mark) between them. The steel commands move to model::steel::command, and section.rs and fastener.rs, which only steel uses, move to model::steel::section and model::steel::fastener. command.rs keeps CommandError, Placement, label_is_valid and the platform commands. Every import in the workspace is rewritten; no behaviour changes. * docs: point the command module doc at CreatePlate's new path The module doc linked [`CreatePlate::content_fingerprint`], which no longer resolves now that CreatePlate lives in model::steel::command. cargo doc with -D warnings (the CI Docs step) failed on it. * refactor: the steel bodies of the kernel move to kernel::steel (#780) * chore: stop the macOS test shards queueing for runners (#781) * refactor: the steel files of the engine move to engine::steel (#783) * refactor: the element edit, copy and array commands and the instance types are steel (#787) * chore: let pull requests save rust-cache again (#786) * refactor: compose the head once when reading references (#788) * refactor: the engine files that edit, copy and measure plates and members are steel (#791) * refactor: profile definitions, the plate cross-check and the sweep are steel (#796) * refactor: the structural steel code of model, kernel and engine is the steel crate (#799) * chore: gate CI on one job that proves every test ran (#782) * chore: stop the macOS test shards queueing for runners The account runs at most five macOS jobs at once. Every pull request asked for seven during the test phase: six test shards plus the macOS cold-release measurement. One or two shards therefore waited 30-80 s for a runner on every pull request run of 2026-10-31, on the critical path. - Run the macOS tests in five shards. They use nextest's `count:` partitioning: replaying the measured test durations, it gives a longest shard of about 60 s, against 64 s for six `hash:` shards. - Run the macOS cold-release measurement (#153) only on pushes to master and on manual runs. It is build-time evidence, not a test gate; the Linux leg still builds the release profile on every pull request. - Save rust-cache only from master. A pull request's cache serves only that pull request, competes with master's caches for the 10 GB limit, and its save step delays the shards waiting on the archive job. Linux and Windows keep eight `hash:` shards: replaying their durations, `count:` was no better. Every test still runs on every platform it ran on before. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * chore: gate CI on one job that proves every test ran Branch protection has no required checks yet, and a red pull request can be merged. Nothing proves that the shards together ran every test either: a partition mistake, a dropped matrix leg or a filter inherited by the CI profile would lose tests while every job stayed green. Later speed-ups change the sharding, so this check has to come first. - The shards run a nextest `ci` profile: the `default` profile plus a JUnit report, with retries pinned to 0. Each shard uploads its report. - Each archive job lists its platform's tests with the binaries it just built (`cargo nextest list --profile ci --message-format json`). - A new `ci-ok` job, the one check to require, runs with `always()` and fails unless every other job succeeded (a pull request may skip only the Windows jobs) and, per platform, the reports together contain every listed `(binary, test)` exactly once, nothing was filtered out, and the ignored tests are exactly those named in `.config/ci-ignored.txt` (empty today). It also fails if a job of the workflow is missing from its `needs`, so no new job can bypass it. tools/ci-gate.py holds the checks, using only the Python 3 standard library so any runner can run it; tools/test-ci-gate.py covers each failure mode, and its formats match what cargo-nextest 0.9.149 writes. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX --------- Co-authored-by: Claude <[email protected]> * ci: set project Status to In progress when 'in progress' label is added (#800) This workflow sets the project status to 'In progress' when the corresponding label is added to an issue or pull request. * docs: the roadmap keeps the gates and the scope, not the tasks (#801) ROADMAP.md listed the next work and the status of every gate package. Both duplicated the issues and drifted from them (#738: C07/C08, the #331 acceptance tail, M0.2 and A04.2 had no trace in the file). The roadmap now holds the stage, the gate verdicts and the MVP scope with its deferrals. What to work on, its order and its state are read from the issues, the milestones and the GitHub Project. CLAUDE.md and AGENTS.md say so, and ask an agent that starts work on an issue to add the label "in progress", which the "Label to Project Status" workflow turns into the Project Status. The owner decision is recorded in decisions.md. * docs: the design of the element interface (#795, slice 1) (#802) * docs: the design of the element interface (#795, slice 1) The platform still branches on the kind of element in two different places: the typed operations (copy, edit, mirror, array, evaluate), which exist as symmetric plate/member pairs, and the persistence in layer-engine, whose Shape and Change enums carry typed steel fields. The note derives one small trait for the first from the real signatures and a neutral prim description with registered validators for the second, keeps the F10 registry as it is, and composes the schema descriptor from ordered segments so its bytes stay equal. It also fixes the order of the code slices and what closes each. * docs: the element interface note records what the writers show (#795) Reading the layer-engine writers shows that the neutral attribute list would mean rewriting every writer, so the steel half moves verbatim into steel and steel depends on layer-engine. The read side needs a registry, installed once. The typed-operations trait is not on the way to the discipline kind. * docs: the hook inventory of the layer-engine move (#795) One row per role of the roughly 110 match sites on the steel variants of Shape and Change, with where each is today and the hook it becomes, so the move can go family by family with the suite green after each. * chore: skip the Rust builds for pull requests that change only prose (#792) * chore: stop the macOS test shards queueing for runners The account runs at most five macOS jobs at once. Every pull request asked for seven during the test phase: six test shards plus the macOS cold-release measurement. One or two shards therefore waited 30-80 s for a runner on every pull request run of 2026-10-31, on the critical path. - Run the macOS tests in five shards. They use nextest's `count:` partitioning: replaying the measured test durations, it gives a longest shard of about 60 s, against 64 s for six `hash:` shards. - Run the macOS cold-release measurement (#153) only on pushes to master and on manual runs. It is build-time evidence, not a test gate; the Linux leg still builds the release profile on every pull request. - Save rust-cache only from master. A pull request's cache serves only that pull request, competes with master's caches for the 10 GB limit, and its save step delays the shards waiting on the archive job. Linux and Windows keep eight `hash:` shards: replaying their durations, `count:` was no better. Every test still runs on every platform it ran on before. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * chore: gate CI on one job that proves every test ran Branch protection has no required checks yet, and a red pull request can be merged. Nothing proves that the shards together ran every test either: a partition mistake, a dropped matrix leg or a filter inherited by the CI profile would lose tests while every job stayed green. Later speed-ups change the sharding, so this check has to come first. - The shards run a nextest `ci` profile: the `default` profile plus a JUnit report, with retries pinned to 0. Each shard uploads its report. - Each archive job lists its platform's tests with the binaries it just built (`cargo nextest list --profile ci --message-format json`). - A new `ci-ok` job, the one check to require, runs with `always()` and fails unless every other job succeeded (a pull request may skip only the Windows jobs) and, per platform, the reports together contain every listed `(binary, test)` exactly once, nothing was filtered out, and the ignored tests are exactly those named in `.config/ci-ignored.txt` (empty today). It also fails if a job of the workflow is missing from its `needs`, so no new job can bypass it. tools/ci-gate.py holds the checks, using only the Python 3 standard library so any runner can run it; tools/test-ci-gate.py covers each failure mode, and its formats match what cargo-nextest 0.9.149 writes. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * chore: skip the Rust builds for pull requests that change only prose One commit in six touches only Markdown (10 of 64 since 2026-10-17). Each still ran the whole matrix: two quality jobs, two archive builds, thirteen test shards and the cold release, five of those jobs on the account's five macOS runners. None of them can change outcome when no program reads the changed files. - A `changes` job lists the pull request's files through the API. tools/ci-changes.py prints `prose=true` only when every file is Markdown that no program reads. A file counts as read when a string literal in a `.rs` or `.toml` file names it or a directory above it, resolved as the code resolves it: from the crate for `/`, `../` and `{}/`, else from the repository root. In this repository that means the F08 table, the fixture documents, `COMMANDS.md`, and every crate directory named by a path dependency. A listing error, an empty or truncated list, or any non-Markdown file means the full CI. - On `prose=true` the Rust jobs are skipped and `docs-checks` runs the three checks whose inputs are documents on Linux, in case a document became an input without the classifier knowing. - `ci-ok` allows those skips only on pull requests, only with `prose=true`, and then requires `docs-checks`. Pushes to master always run everything. tools/test-ci-changes.py covers the classifier on a synthetic repository and on this one. tools/test-ci-gate.py adds the five prose cases. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * chore: let the document checks reuse the Linux test-archive cache The prose-only probe (#793) showed document checks taking 112 s: the job runs only on prose pull requests, never on master, so no cache of its own is ever saved where pull requests can restore it. It now restores test-archive-ubuntu's cache under that job's key, with the same profile and environment, and saves nothing. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX --------- Co-authored-by: Claude <[email protected]> * refactor: build the window's integration suites into one test binary (#784) * chore: stop the macOS test shards queueing for runners The account runs at most five macOS jobs at once. Every pull request asked for seven during the test phase: six test shards plus the macOS cold-release measurement. One or two shards therefore waited 30-80 s for a runner on every pull request run of 2026-10-31, on the critical path. - Run the macOS tests in five shards. They use nextest's `count:` partitioning: replaying the measured test durations, it gives a longest shard of about 60 s, against 64 s for six `hash:` shards. - Run the macOS cold-release measurement (#153) only on pushes to master and on manual runs. It is build-time evidence, not a test gate; the Linux leg still builds the release profile on every pull request. - Save rust-cache only from master. A pull request's cache serves only that pull request, competes with master's caches for the 10 GB limit, and its save step delays the shards waiting on the archive job. Linux and Windows keep eight `hash:` shards: replaying their durations, `count:` was no better. Every test still runs on every platform it ran on before. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * chore: gate CI on one job that proves every test ran Branch protection has no required checks yet, and a red pull request can be merged. Nothing proves that the shards together ran every test either: a partition mistake, a dropped matrix leg or a filter inherited by the CI profile would lose tests while every job stayed green. Later speed-ups change the sharding, so this check has to come first. - The shards run a nextest `ci` profile: the `default` profile plus a JUnit report, with retries pinned to 0. Each shard uploads its report. - Each archive job lists its platform's tests with the binaries it just built (`cargo nextest list --profile ci --message-format json`). - A new `ci-ok` job, the one check to require, runs with `always()` and fails unless every other job succeeded (a pull request may skip only the Windows jobs) and, per platform, the reports together contain every listed `(binary, test)` exactly once, nothing was filtered out, and the ignored tests are exactly those named in `.config/ci-ignored.txt` (empty today). It also fails if a job of the workflow is missing from its `needs`, so no new job can bypass it. tools/ci-gate.py holds the checks, using only the Python 3 standard library so any runner can run it; tools/test-ci-gate.py covers each failure mode, and its formats match what cargo-nextest 0.9.149 writes. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * refactor: build the window's integration suites into one test binary Each of the window's eight integration suites was its own test binary, and each linked the whole GUI stack (eframe, egui, wgpu, winit): the costliest links in the workspace, repeated on every platform in every CI run, with eight copies in the nextest archive that every test shard downloads and unpacks. The suites now build into one `ui` binary (`tests/ui.rs`, one module per suite file), the way the CLI's suites already do. `cancel` stays its own target: it relies on being alone in its process. A guard test fails when a suite file is not listed. The shared `support` module gets its own clippy allow, which it used to inherit from the suite that declared it, and its one test now runs once instead of once in each of the four binaries that declared it. No test changes otherwise: each `<suite> <test>` is now `ui <suite>::<test>`. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX --------- Co-authored-by: Claude <[email protected]> * refactor: the write side of layer-engine goes through discipline hooks (#795) (#803) * refactor: the connection family of layer-engine is a discipline hook (#795) Shape::Weld, Shape::Bolt and Shape::BoltGroup are replaced by Shape::Discipline, whose hook answers the kind token, the category, the attributes written, the engineering bounds and the finiteness check. The arms of edit.rs and validate.rs moved unchanged into authored/connection.rs, so the bytes written and the refusals returned are the same. The CLI and the tests build the shapes with Shape::weld, Shape::bolt and Shape::bolt_group. * refactor: the bolt group edit of layer-engine is a discipline change hook (#795) Change::BoltGroup is replaced by Change::Discipline, whose hook answers the target, the kinds it may edit, the groups it reads and writes, its bounds, its attribute writes and its finiteness check. The arms moved unchanged into authored/connection.rs; the CLI builds the edit with Change::bolt_group. * refactor: the member family of layer-engine is a discipline hook (#795) Shape::Member and the Axis, Roll and Profile edits are replaced by the discipline variants. MemberShape and the three edits implement the hooks; the arms of edit.rs and validate.rs moved unchanged into authored/member.rs. The hooks gained three defaults the member needs: the owned cut records, the part class and whether an edit removes the derived result. * refactor: the plate shapes of layer-engine are discipline hooks (#795) Shape::Plate and Shape::LoftedPlate are replaced by the discipline variant. PlateShape and LoftedPlateShape implement the hook; the arms of edit.rs and validate.rs moved unchanged into authored/plate.rs. The hook gained the budget charge of an element's arrays. The tests that mutated a plate in place rebuild it with Shape::plate. * refactor: the plate, hole and cut edits of layer-engine are discipline hooks (#795) Change::Contour, Thickness, ThicknessSide, Corners, Bends, Cuts, HoleRecords and MemberHoles are replaced by the discipline variant. Eight edit types implement the change hook; the arms of edit.rs and validate.rs moved unchanged into authored/plate_change.rs. The hole records keep their own derived-field declaration through declares_mesh. * fix: export ReplaceBoltGroup so its documentation link resolves (#795) The public constructor Change::bolt_group linked to a type in a private module, which rustdoc refuses under -D warnings. * refactor: build the kernel's and steel's integration suites into one test binary each (#785) * chore: stop the macOS test shards queueing for runners The account runs at most five macOS jobs at once. Every pull request asked for seven during the test phase: six test shards plus the macOS cold-release measurement. One or two shards therefore waited 30-80 s for a runner on every pull request run of 2026-10-31, on the critical path. - Run the macOS tests in five shards. They use nextest's `count:` partitioning: replaying the measured test durations, it gives a longest shard of about 60 s, against 64 s for six `hash:` shards. - Run the macOS cold-release measurement (#153) only on pushes to master and on manual runs. It is build-time evidence, not a test gate; the Linux leg still builds the release profile on every pull request. - Save rust-cache only from master. A pull request's cache serves only that pull request, competes with master's caches for the 10 GB limit, and its save step delays the shards waiting on the archive job. Linux and Windows keep eight `hash:` shards: replaying their durations, `count:` was no better. Every test still runs on every platform it ran on before. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * chore: gate CI on one job that proves every test ran Branch protection has no required checks yet, and a red pull request can be merged. Nothing proves that the shards together ran every test either: a partition mistake, a dropped matrix leg or a filter inherited by the CI profile would lose tests while every job stayed green. Later speed-ups change the sharding, so this check has to come first. - The shards run a nextest `ci` profile: the `default` profile plus a JUnit report, with retries pinned to 0. Each shard uploads its report. - Each archive job lists its platform's tests with the binaries it just built (`cargo nextest list --profile ci --message-format json`). - A new `ci-ok` job, the one check to require, runs with `always()` and fails unless every other job succeeded (a pull request may skip only the Windows jobs) and, per platform, the reports together contain every listed `(binary, test)` exactly once, nothing was filtered out, and the ignored tests are exactly those named in `.config/ci-ignored.txt` (empty today). It also fails if a job of the workflow is missing from its `needs`, so no new job can bypass it. tools/ci-gate.py holds the checks, using only the Python 3 standard library so any runner can run it; tools/test-ci-gate.py covers each failure mode, and its formats match what cargo-nextest 0.9.149 writes. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * refactor: build the window's integration suites into one test binary Each of the window's eight integration suites was its own test binary, and each linked the whole GUI stack (eframe, egui, wgpu, winit): the costliest links in the workspace, repeated on every platform in every CI run, with eight copies in the nextest archive that every test shard downloads and unpacks. The suites now build into one `ui` binary (`tests/ui.rs`, one module per suite file), the way the CLI's suites already do. `cancel` stays its own target: it relies on being alone in its process. A guard test fails when a suite file is not listed. The shared `support` module gets its own clippy allow, which it used to inherit from the suite that declared it, and its one test now runs once instead of once in each of the four binaries that declared it. No test changes otherwise: each `<suite> <test>` is now `ui <suite>::<test>`. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * refactor: build the kernel's integration suites into one test binary The kernel's nineteen integration suites were nineteen test binaries, each linking the kernel and compiling its own copy of the shared `common*` helpers. They now build into one `kernel` binary (`tests/kernel.rs`, one module per suite), like the CLI's suites; a guard test fails when a suite file is not listed. The helpers become modules of that binary, declared once; a suite imports them with `use crate::common;`. The G06 suites named `common_mesh` as `common` through `#[path]`; they now write `use crate::common_mesh as common;`. The `fast` nextest profile and the documented `cargo test --test <suite>` commands name the new binary. No test changes otherwise: each `<suite> <test>` is now `kernel <suite>::<test>`. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX * Build the steel crate's integration suites into one test binary #799 moved eight of the kernel's suites, with copies of the shared common* helpers, into the new steel crate. The merge from master carried this branch's `use crate::common;` edits into the moved files, which only compile as modules of one binary. Steel now gets the same layout as the kernel: autotests off, one `steel` test target whose root declares the helpers and all eleven suites, and the guard test that fails when a suite file is not listed. The kernel's root drops the suites that left it. The evidence commands of A07, G07 and P03 named kernel test targets that #799 moved; they now name the suites in the steel binary. Claude-Session: https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX --------- Co-authored-by: Claude <[email protected]> * refactor: layer-engine reads parts and connections through a discipline registry (#795) (#805) * refactor: the P11 instance of layer-engine is a discipline hook (#795) Shape::Instance is replaced by the discipline variant; InstanceShape implements the hook with the arms moved unchanged from edit.rs and validate.rs. The write side of Shape now has no steel variant left. * refactor: layer-engine reads the parts and connections through a discipline registry (#795) Reading a stored layer asks a registered Discipline instead of matching plate, beam and column in the platform: which kinds are parts, the validation of a part and of a connection, and the write group of a property. The steel arms of the snapshot validator and the rebase group table moved unchanged into steel_part, steel_connection and steel_property_group; the registry holds the built-in steel discipline until the apps install it from the steel crate. * docs: say which discipline validates a connection prim (#795) * refactor: build engine, host and layer-engine suites into one binary each (#789) * docs: describe the platform and discipline split after #795 in the architecture (#806) The write side of layer-engine goes through discipline hooks (#803) and the read side through a discipline registry (#805), but the architecture still described layer-engine as writing steel directly. Record the two seams, what is done, what is left before steel becomes kind discipline, and the decisions of the element-interface note. * docs: add the wave-two design notes (#807) Design notes for the wave-two slices that #770 delivered: A14.3 bolt group placement, A14.4 generated holes, C06 compensation, D04 slice 2 section and detail views, D05 drawing sheets, V12 named views and V17 colour filters, plus the T03 and G13 note updates. Statements are verified against the merged master, so the module paths follow the current steel-crate and discipline-hook layout instead of the pre-merge fork layout. Co-authored-by: axisrow <[email protected]> * docs: extend the D01 drawing contract for the D04/D05 surfaces and the D07 slices 2-3 (#808) * docs: extend the drawing contract for the wave-two surfaces Update D01 v0.1 for the surfaces #770 delivered: the D04/D05 drawing surfaces (sections, details, sheets, whole-sheet export) and the D07 slice 2-3 drafting additions (text, hatch, symbol, free_dimension, layers and line types, MAX_LAYERS, MAX_NUMBER_TEXT_BYTES). Contract changes carry their own docs-only PR under AGENTS.md section 7, apart from the design notes. * docs: state the detail region bounds and the identity set exactly Split the detail admission bullet so the region centre reads as the signed paper cap validate_detail enforces (only the radius refuses non-positive), and name the full pairwise-distinct paper-object set - views, sections, details, drawn objects and layers - in both identity sentences, as admission checks it. --------- Co-authored-by: axisrow <[email protected]> * refactor: steel becomes a discipline above the platform (#795) (#809) The platform no longer knows steel. The steel half of layer-engine (the hooks behind Shape::Discipline and Change::Discipline, the steel readers, writers and definitions, the read-back discipline, the authoring facade and its tests) moves into steel::layer, and the Cargo edge flips: steel depends on beamcraft-layer-engine, no platform crate depends on steel, and steel is kind `discipline` in the F08 table. The registry starts empty; both apps call steel::layer::install() at start (the window reads layers through session). The remaining branching on the kind goes too. steel::engine::part::PartRecord chooses between the per-kind pairs (copy, P11 copy, array, patch with re-evaluation, quantity record), so the CLI copy, array and update-element adapters drop CopiedElement, UpdatedElement and Target. The engine's queries ask ElementKind::is_part and is_linear_member instead of matching Plate, Beam or Column. Stored bytes, refusal codes and command output are unchanged. Left on purpose, with reasons in docs/design/element-interface.md section 8: no platform trait and no ElementPatch split (the UpdateElement/v2 fingerprint), one SCHEMA_DESCRIPTOR, and the /World/Steel prim paths and F06 tokens, which are format, not behaviour. The layer-engine move and the flip are axisrow's series from their fork (axisrow/BeamCraft #394, #398-#402, #404, #407, #411, #412), ported onto master. * test: add the G11 accuracy fixtures and refuse subnormal masses (#490) (#810) * refactor: move the application layer out of the CLI (#208) (#811) * refactor: move the application layer out of the CLI (#208) #208 asks for an application API that the apps share instead of one app owning it, and for boundary checks that a layout change cannot bypass. The owner set the direction: the CLI ends up a thin layer over the commands. Move the application layer to crates/commands: - the 40 library modules of beamcraft-cli move unchanged, as git renames, into beamcraft-commands (crates/commands): plates, members, bolts, welds, definitions, grids, numbering, drawings, exports, workspaces, history, publication, rebase, the coordinator and the command registry; - the CLI keeps the binary: argument parsing, help, dispatch and JSON rendering; - F08 gains the kind `service` (layer 7). Only an app or a tool may depend on it, and it may depend on a discipline. The apps move to layer 8. Make the boundary checks fail closed on layout: - beamcraft-boundaries scanned only <pkg>/src, so moving a domain crate's sources to code/ behind `[lib] path` hid std::fs from the scan; - it now refuses target paths outside the standard layout, build scripts in domain crates, and #[path] in domain sources outside tests; - regression tests include that #208 reproduction on disk. Correct the docs: - F08 no longer claims that no package has a third-party dependency, and section 4 rows that said "planned" or "geom holds Point3 only" now match the code; - the engine crate doc no longer says that only one command exists; - development.md gives each worktree its own target directory; - design notes and contracts name beamcraft_commands and the moved paths. Behaviour, output bytes and refusals are unchanged: the full workspace suite passes with the same tests plus the new checker cases. Claude-Session: https://claude.ai/code/session_017VPNWPiS3iNL1ie8gcEgfU * chore: allow MPL for beamcraft-commands in cargo deny and optimize it in release (#208) The new crate is a BeamCraft workspace package like the others: cargo deny allows MPL-2.0 only for listed workspace packages, and the release profile lists every engineering crate for opt-level 3, which the moved code had as part of beamcraft-cli. Claude-Session: https://claude.ai/code/session_017VPNWPiS3iNL1ie8gcEgfU * refactor: merge the starter environment into blank (#812) Blank is the base environment that other environments are built from, so it must be usable on its own. Until now it was empty and a separate starter environment carried the nineteen generic profiles, which made the start window offer two environments where one is a dead end: a model on blank could place no beam or column. The generic profile library moves into environments/blank and the starter environment goes. The start window now prefers blank when no environment is saved; a saved "starter" is no longer usable, so it falls back to blank. Projects created from starter keep working because each project carries a copy of its environment. The CLI test that placed members from starter now runs on blank, the test that blank seeds no profiles goes, and the shipped-environments test checks that blank ships. The design note becomes blank-environment.md and the two ATTRIBUTION rows become one; the values stay invented and follow no standard or catalogue. * feat: model views with their own plane, properties, browser and windows (V12) (#813) * Make object properties and creation use modelling actions Remove exposed command previews and commit ceremonies, add named level assignment, and create members by two picked points and plates by contours. Keep engine admission, stale-reading guards and command publication paths, with real CLI and native-controller regression coverage. * all work with views * feat: views carry a view plane and their own display properties A named view was an orbit pose with an optional plane recipe and work area, so it could not hold a section through three points, a depth range around the plane, its own render mode or hidden kinds. The view book now stores ModelView records: an identity, a name, the view plane (the input frame), projection, visible height, depth above and below the plane, render mode, grid visibility, hidden kinds, work area, camera rotation, colour and opacity, plus browser folders and a favourite mark. Temporary views live in the book but are never written. The constructors come from the model-views branch: coordinate planes, two points (vertical or across the input plane), three points, grid axes and grid batches by family, levels and level batches, part and assembly frames by side, and picked surfaces with an optional edge. pick_visible honours the view's slab, work area and hidden kinds, and clipping() turns the slab and work area into the V15 clip record, so the renderer needs no second clipping path. The session JSON is rewritten in place (version 1): the old pose-based shape is not read any more. * feat: build and change model views headlessly The view commands follow the new view book. save-view builds a view from exactly one plane: an orbit, a coordinate plane with an offset, an explicit frame, two points (vertical or across an input plane), three points, or, reading the store, a grid axis, a level, a part or an assembly by side, or the first face a ray hits. Grid families, levels and the four part sides make batches that add every view or none and suffix repeated names. Every display property of a view has an option, and --copy-from reuses another view's. update-view changes one view in place, its plane included; remove-view and apply-view select a view by identity or name, and apply-view now returns the camera frame and the clip planes a window restores. Folders, quick access and windows stay in the window: they need no agent. * feat: model views in the window, from the browser to the view windows The window works with the new view book. The project browser lists the views by folder with search, quick access and favourites, Ctrl and Shift selection, a context menu (open, open in a window, properties, favourite, duplicate, cut, copy, close, delete with confirmation) and drag to a folder; {3D} is the free view. The View tab creates views (coordinate planes, two or three picked points, the work plane, grid axes and batches, level plans, a picked face with or without an edge, part and assembly views, the current camera), opens their properties, toggles 3D and plane (Ctrl+P), turns the camera to six directions, and opens and tiles windows. A view is applied, not copied into the window state: activating it builds a plane-aligned camera, takes its render mode, grid guides and plane as the input frame, hides its kinds from the scene, tints the model with its colour and opacity, and adds its depth slab and work area to the cut. Picking and snapping see only what the view shows. Each view keeps its session camera, Ctrl+Tab cycles the open views, and the free 3D view comes back as it was left. Up to eight windows show views with their own camera, cut, tint, hidden kinds, render mode and grids, and can be docked. Level plans and assembly views read the store, so the window builds them through save-view; everything else is built in-process by the engine. * feat: view previews, palette commands and the model-views design note The browser can show a preview of each view, drawn off screen through the view's own camera, cut, tint and hidden kinds; a preview is kept until the snapshot or the view changes, one is drawn per frame and at most 64 are kept. The command palette lists every way to create a view and the view commands. A GPU test proves the view tint reaches the pixels (a colour replaces the model colour, zero opacity draws nothing) and that a hidden kind leaves the scene without moving its origin. The V12 design note describes the view model, the session format, the commands and the window, and what stays out of this slice. --------- Co-authored-by: Yury Isachenkov <[email protected]> * feat: cut directed oblique and faceted-wall member holes (G14.2 slices 1-2) (#814) * feat: work with models from the in-model menu (#815) * feat: give the menu hierarchical navigation and drop its dead entries Diagnose & repair, Extend and Exit BeamCraft offered nothing real, and the catalog and About sub-pages had no way back to their section. Sub-pages now name a parent, show "‹ Back" and keep their parent row highlighted, and the menu owns Esc while it is open, so Esc walks up one level at a time instead of closing the whole menu from a sub-page. * feat: open the model folder from the menu The row was disabled, so reaching a model's files meant finding the folder by hand. It now opens the store in Explorer, Finder or the xdg-open handler with std::process::Command (no new crate, the links feature stays off). A missing folder or a missing launcher shows as a note under the menu heading instead of failing silently, and the child is reaped on a thread so Unix keeps no zombie. * refactor: move the new-model form out of the start page The in-model menu needs the same create form as the start page. Keeping two copies would let the name suggestion, the location check and the refusal messages drift apart, so the form, its worker and its note now live in library::NewModelForm and the start page hands its New model tab to it. The start page behaves as before. * feat: let a model window switch to another model in place The menu's New and Open should replace the open model without a detour through the library. Shell now honours an App's open_request: it opens the new model, keeps the current one with the reason on its menu when that fails, and retires the old App for one pass before freeing its textures, so no output still to be painted refers to them. The library return goes through the same slot, which stops the viewport texture entry from leaking on every round trip. * feat: create and open models from inside the menu New and Open used to drop the open model and send the user back to the library. They are now menu pages: New shows the shared new-model form and Open lists the recent models with Browse…, and both switch the window in place through open_request. While a create or open runs, the menu and the commands that would leave or change the window wait for it, so its result is never lost. The library's Recent list is re-read on return, so models opened from the menu appear there. * feat: show the project's facts on a read-only menu page Project properties only opened the Details window. It now opens a menu page with what the store's head already says about the project and what this session runs with, read on a worker so a large model never freezes the window. The page says plainly that project information fields are not stored in models yet. The design note records how the menu works and why it adds no CLI command. * Fix empty profile picker and add environment part presets (#816) * Fix empty profile picker in the window and add part presets to the blank environment The window refreshed definitions through export-view --with-definitions, which named no store, so the project's environment profiles were never listed and no profile could be picked. The call now passes the store. An environment may declare bim:environment:presets, a layer of beam, column and plate presets (name, class, material, profile or thickness). blank ships nine. definitions lists them as environment_presets, and choosing the beam, column or plate tool fills the form from the default preset; a Preset drop-down switches to another and every field stays editable. * Show only the name in the profile and material pickers The environment suffix on profiles and the density on materials added noise; the digest suffix was already hidden. Presets now match a material by its name followed by the digest suffix. * Snap markers for beam and column placement; one-click columns The member tools already snapped but showed no marker and armed nothing before the first click. The pointer now shows the snap marker and caption as for plates, from the first click on. A column is placed with one click on its base point. Its top is the form's height or a level of the model (definitions now lists levels), straight up in world Z. * feat: view windows inside the work area, view properties in the right panel (#817) * feat: project browser by view type, default view, redesigned view properties (#827) * feat: project browser by view type with a default view, 2x2 tiling The project browser gets icon tabs, a Revit-style grouping of views by kind (3D views, plans, sections and elevations) with a by-folder alternative, and no separate 3D entry. Every model has exactly one default view, which the free 3D view becomes; it cannot be removed or unmarked, and --default is available in save-view and update-view. A view without a name is temporary. Picking a part shows the part's own properties. Arrange gets a square-ish Tile grid (four windows make 2 x 2). * feat: redesign the view properties panel Sections (Camera, Extents, Appearance, Object visibility) with aligned rows, the favourite and default marks as icons beside the name, one list each for depth and work area, and Apply/Reset that wake on the first edit. Render mode and grids leave the panel: the ribbon's Display group owns them. The re-pick button goes. Rows keep slack at the end so the resizable panel no longer grows every frame. * feat: mark the default view in the named-view commands and registry (#827) * test: keep the pick and snap E2E suites, help and skill docs while the commands exist The merge kept the fork's pick/snap commands but had taken the upstream copies of their end-to-end suites, help text and skill reference, which document a removal this fork has not executed yet. Restore the fork-side versions until the interactive-assist removal lands as its own PR. --------- Co-authored-by: Yuri Isachenkov <[email protected]> Co-authored-by: axisrow <[email protected]> Co-authored-by: Yura Isachenkov <[email protected]> Co-authored-by: Claude <[email protected]> Co-authored-by: Yury Isachenkov <[email protected]>
What this changes
G14.2, slices 1–2 (task issue #492): the kernel member-hole operation cuts
directed oblique holes through flat walls, and holes through faceted walls such
as a round tube.
docs/design/g14-2-oblique-holes.mdcarries the design and theper-slice status.
MemberHoleCutter::directed(cavity, axis_y, axis_z, from, to, ordinal)sweeps the cavity along any section-plane directiond = (0, y, z)in the prism frame
(e1, e2, d),e1 = +X,e2 = d x e1;newstaysd = +Zand is reproduced bit for bit, with the G14.1 refusal set unchanged.area_signfilter re-merges a face re-triangulated by an earlier hole);strips join into chains across soft kinks (turns of at most
pi/4); thefootprint is clipped against every crossed strip region with exact
Big-integer area accounting; an opening crossing a kink edge registers one
shared vertex for both adjoining strips; each crossed strip is carved through
a generalized splice (kept outer paths + opening arc), and the removed volume
stays the exact Big-rational divergence sum of the opening polyhedron,
seal-certified.
Left unfinished on purpose
The merge-walk boundary and the hole-wider-than-facet case are left unfinished —
we followed the strict per-strip splice. A footprint wider than one band is
refused with
ResultNotCertified; where the per-strip splice ends and themerge-walk band begins is the open question in #492. The case is covered by a
test that is
#[ignore]d with that reason — kept in as a spec, not hidden.Happy to take direction or hand this over.
How you verified it
cargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warningscargo test --workspace— 26/27 G14 tests green (the merge-walk case isthe ignored one); all 23 legacy member-hole tests bit for bit;
newanddirected(_, 0, 1, _)still byte-equaldigits: 7507.1704368 =
A_poly*H - 2*tan(pi/48)*double integral |b|over thecavity's own approximation polygon (the curve approximation's segment count
depends on the radius, so the expected value is computed at the cavity's own
radius, with the band-tilt term the certified sum reflects)
New behaviour needs a test: the tube volume, the byte-exact repeat, the
swallowed-strip and non-convex refusals and the never-crash degeneracies are
covered in
crates/steel/tests/g14_member_hole.rs.Never-crash
unwrap/expect/panic!/todo!outside testswalks
get/get_mutwith the internal self-check refusal)unsafeProvenance
Anything a reviewer should look at first
The splice's kept/arc assembly in
cut_through_chains: a clip loop that startsand ends on one boundary edge counts its two endpoints as one contact, and a
notch vs a cut through the band decides between one and two assembled pieces.
The
.p1band-id suffix marks only runs past a mid-edge kink crossing (a sidecovering a whole edge keeps the bare
s{j}, the G14.1 spelling).🤖 Generated with Claude Code