Skip to content

chore: stop the macOS test shards queueing for runners - #781

Merged
Blogbotana merged 1 commit into
masterfrom
claude/eloquent-knuth-n9oprq
Oct 31, 2026
Merged

Blogbotana merged 1 commit into
masterfrom
claude/eloquent-knuth-n9oprq

Conversation

@axisrow

@axisrow axisrow commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

What this changes

This is phase 0 of a CI speed-up that drops no test. It changes only workflow configuration and docs, no Rust code.

Every pull request run is limited by its macOS path. The account runs at most five macOS jobs at once. During the test phase a pull request asked for seven: six test shards plus the macOS cold-release measurement. In every one of the five pull request runs measured below, one or two shards waited 28–79 s (median 56 s) for a runner, on the critical path.

  • macOS tests: six hash: shards become five count: shards. I replayed the measured per-test durations from the shard logs of run 41208825628. The replay reproduces the six observed shard times to within 0.1 s. Five count: shards give a longest shard of about 60 s, against 64 s for the current six. nextest still runs every test exactly once.
  • The macOS cold-release measurement (Cut the cold release build of the two apps from 5m to at most 2m #153) runs only on pushes to master and on manual runs. It is build-time evidence, not a test gate. The Linux cold release still builds the release profile on every pull request.
  • Swatinem/rust-cache saves only from master. A cache saved from a pull request is visible only to that pull request. It also competes with master's caches for the 10 GB limit, and its save step delays the shards that wait on the archive job.
  • Unchanged: Linux and Windows keep eight hash: shards. Replaying their durations, count: was no better: the Windows longest shard is bounded by one 48 s test.

Considered and left out: pinning the toolchain version in CI. The runners preinstall the toolchain under the name stable, so pinning 1.99.1 would make rustup download a toolchain in every job, roughly 5–10 s each, on the critical path. It is a stability measure, not a speed-up.

Before / after

"Before" is the median and range of the five most recent successful pull_request runs with code changes (41223870484, 41223814708, 41220867905, 41220126706, 41219459431) and the four most recent push runs on master (41221282045, 41220691933, 41216788817, 41212529358), computed from the Actions jobs API. Wall time runs from the first job created to the last job completed, so it includes queueing.

"After" is this PR's own run 41225159297, a single sample. The GitHub integration here cannot re-run or dispatch workflows, so more samples come from later pushes; the stacked #782 run will add one. Hosted runners vary by about 10–20 % between runs.

Pull request run Before (n=5) After (n=1) Change
Wall time 242 s (222–273) 202 s −40 s (−17 %)
macOS shard queue (longest wait) 56 s (28–79) 13 s −43 s
macOS longest shard (test step) 60 s (56–68) 66 s +6 s (5 shards instead of 6)
macOS archive build 93 s (61–105) 78 s cache and runner variance
macOS jobs per run 9 7 −2
Tests run all all (same partitions, nextest-complete) —

What the numbers say: the macOS shards no longer queue. The longest shard is slightly longer with five shards, as the replay predicted, and the net gain is about 40 s per pull request run.

master push run Before (n=4) After
Wall time 276 s (263–349) not measured yet
Windows path end 274 s (263–283) expected unchanged

The master path is not measured yet: only a push to master or a manual run (gh workflow run ci.yml --ref claude/eloquent-knuth-n9oprq) runs the Windows jobs. That path is still bounded by Windows; later phases target it.

How you verified it

  • cargo fmt --all -- --check: not run locally, because no Rust file changes. CI ran it: green.
  • cargo clippy --workspace --all-targets -- -D warnings: not run locally, same reason. CI: green.
  • cargo test --workspace: not run locally, same reason. CI ran every test on Linux and macOS: green.
  • ci.yml parses as YAML, and both JSON literals of the build-timings matrix parse.
  • The shard replay that motivated count: reproduces the observed shard times to within 0.1 s before it predicts anything.

Never-crash

No Rust code changes.

Provenance

  • Nothing taken from Tekla Structures or any Trimble product
  • No GPL/AGPL code copied in
  • No new assets

Anything a reviewer should look at first

The macOS cold release no longer runs on pull requests. If a pull request breaks only the macOS release-profile build, that now shows up on master, not on the pull request. The Linux release build and the macOS test-profile build still run on every pull request.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HF6nmZH7s6U6F43va227NX

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
@axisrow
axisrow marked this pull request as ready for review October 31, 2026 17:46
@axisrow
axisrow requested a review from Blogbotana as a code owner October 31, 2026 17:46
@Blogbotana
Blogbotana merged commit d96c1b3 into master Oct 31, 2026
22 checks passed
@Blogbotana
Blogbotana deleted the claude/eloquent-knuth-n9oprq branch November 1, 2026 23:31
axisrow added a commit to axisrow/BeamCraft that referenced this pull request Nov 2, 2026
…odel views (#413)

* refactor: dispatch CLI commands from the registry and list access and parameters (slices 2-3 of Apibim-com#753) (Apibim-com#760)

* refactor: dispatch CLI commands from the registry (second slice of Apibim-com#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 Apibim-com#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 Apibim-com#753, first part) (Apibim-com#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 Apibim-com#757) and
create-project --environment (added by Apibim-com#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 (Apibim-com#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 Apibim-com#753) (Apibim-com#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
Apibim-com#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 Apibim-com#753) (Apibim-com#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 Apibim-com#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 (Apibim-com#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 (Apibim-com#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 Apibim-com#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 (Apibim-com#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 (Apibim-com#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 (Apibim-com#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 (Apibim-com#780)

* chore: stop the macOS test shards queueing for runners (Apibim-com#781)

* refactor: the steel files of the engine move to engine::steel (Apibim-com#783)

* refactor: the element edit, copy and array commands and the instance types are steel (Apibim-com#787)

* chore: let pull requests save rust-cache again (Apibim-com#786)

* refactor: compose the head once when reading references (Apibim-com#788)

* refactor: the engine files that edit, copy and measure plates and members are steel (Apibim-com#791)

* refactor: profile definitions, the plate cross-check and the sweep are steel (Apibim-com#796)

* refactor: the structural steel code of model, kernel and engine is the steel crate (Apibim-com#799)

* chore: gate CI on one job that proves every test ran (Apibim-com#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 (Apibim-com#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 (Apibim-com#801)

ROADMAP.md listed the next work and the status of every gate package. Both
duplicated the issues and drifted from them (Apibim-com#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 (Apibim-com#795, slice 1) (Apibim-com#802)

* docs: the design of the element interface (Apibim-com#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 (Apibim-com#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 (Apibim-com#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 (Apibim-com#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 (Apibim-com#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 (Apibim-com#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 (Apibim-com#795) (Apibim-com#803)

* refactor: the connection family of layer-engine is a discipline hook (Apibim-com#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 (Apibim-com#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 (Apibim-com#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 (Apibim-com#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 (Apibim-com#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 (Apibim-com#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 (Apibim-com#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

Apibim-com#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 Apibim-com#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 (Apibim-com#795) (Apibim-com#805)

* refactor: the P11 instance of layer-engine is a discipline hook (Apibim-com#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 (Apibim-com#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 (Apibim-com#795)

* refactor: build engine, host and layer-engine suites into one binary each (Apibim-com#789)

* docs: describe the platform and discipline split after Apibim-com#795 in the architecture (Apibim-com#806)

The write side of layer-engine goes through discipline hooks (Apibim-com#803) and the
read side through a discipline registry (Apibim-com#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 (Apibim-com#807)

Design notes for the wave-two slices that Apibim-com#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 (Apibim-com#808)

* docs: extend the drawing contract for the wave-two surfaces

Update D01 v0.1 for the surfaces Apibim-com#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 (Apibim-com#795) (Apibim-com#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) (Apibim-com#810)

* refactor: move the application layer out of the CLI (#208) (Apibim-com#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 (Apibim-com#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) (Apibim-com#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]>

---------

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]>
axisrow added a commit to axisrow/BeamCraft that referenced this pull request Nov 3, 2026
The fork keeps no tracked python (the #178/#182 rule). The four files belong
to the upstream CI restructure (Apibim-com#792/Apibim-com#782/Apibim-com#781) that this sync deliberately
does not adopt; no fork workflow or tool references them.
axisrow added a commit to axisrow/BeamCraft that referenced this pull request Nov 3, 2026
…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]>
axisrow added a commit to axisrow/BeamCraft that referenced this pull request Nov 4, 2026
…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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants