From 0bbca1f9a4bdfdccefe8ecc99309f3ef87cbf736 Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Thu, 13 Aug 2026 20:42:32 +0100 Subject: [PATCH 01/14] Adopt the estate's canonical Rust lint and toolchain baseline Bring rustfmt, clippy, and the pinned toolchain in line with the Operation Parabellum phase 2 canon, and complete the Cargo.toml lint tables to the canonical clippy, rust, and rustdoc sets. - Replace .rustfmt.toml and clippy.toml with the canonical copies from platform-standards/canon/lint/rust; clippy.toml already matched the content, so this only adds the header comment and the disallowed-methods entries for the environment-injection mandate. - Trim rust-toolchain.toml's components list to exactly rustfmt, clippy, and rust-analyzer per canon, keeping the existing dated nightly-2026-05-21 pin. llvm-tools-preview and rustc-codegen-cranelift-preview are no longer declared here. - Add the missing canonical lint entries to Cargo.toml: clippy's disallowed_methods and missing_assert_message; rust's unknown_lints, renamed_and_removed_lints, and unsafe_code; and rustdoc's broken_intra_doc_links, private_intra_doc_links, bare_urls, invalid_html_tags, invalid_codeblock_attributes, and unescaped_backticks. - Remove the Cranelift dev-profile codegen backend from .cargo/config.toml. That opt-in acceleration now belongs in the shared tools/dev-fast configuration landing in a separate wave 1 pull request, not in each repository's own config. Update the developers' and users' guides to describe the linker configuration without claiming Cranelift is enabled here. --- .cargo/config.toml | 6 ------ .rustfmt.toml | 5 +++++ Cargo.toml | 11 +++++++++++ clippy.toml | 18 +++++++++++++++++- docs/developers-guide.md | 12 ++++++++---- docs/users-guide.md | 10 ++++++---- rust-toolchain.toml | 7 +------ 7 files changed, 48 insertions(+), 21 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index f9884cb..d29d6c3 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,9 +1,3 @@ -[unstable] -codegen-backend = true - -[profile.dev] -codegen-backend = "cranelift" - [target.x86_64-unknown-linux-gnu] linker = "clang" rustflags = ["-C", "link-arg=-fuse-ld=mold"] diff --git a/.rustfmt.toml b/.rustfmt.toml index f7ad026..8b3cabd 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,3 +1,8 @@ +# Canonical rustfmt configuration for the estate. +# +# `unstable_features = true` means rustfmt must run on the nightly channel; +# the rust-rustfmt-baseline rule (FMT-002) checks for that evidence. Copy +# this file to the repository root as `.rustfmt.toml`. unstable_features = true comment_width = 100 format_code_in_doc_comments = true diff --git a/Cargo.toml b/Cargo.toml index c70459d..7588275 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,8 +28,10 @@ allow_attributes = "deny" allow_attributes_without_reason = "deny" blanket_clippy_restriction_lints = "deny" cognitive_complexity = "deny" +disallowed_methods = "deny" needless_pass_by_value = "deny" implicit_hasher = "deny" +missing_assert_message = "deny" # 2. debugging leftovers dbg_macro = "deny" @@ -83,7 +85,16 @@ error_impl_error = "deny" result_large_err = "deny" [lints.rust] +unknown_lints = "deny" +renamed_and_removed_lints = "deny" +unsafe_code = "forbid" missing_docs = "deny" [lints.rustdoc] missing_crate_level_docs = "deny" +broken_intra_doc_links = "deny" +private_intra_doc_links = "deny" +bare_urls = "deny" +invalid_html_tags = "deny" +invalid_codeblock_attributes = "deny" +unescaped_backticks = "deny" diff --git a/clippy.toml b/clippy.toml index 0e76365..70d64a8 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,4 +1,6 @@ -# Align with CodeScene’s ceiling +# Canonical clippy configuration for the estate. +# +# The thresholds align with CodeScene's code-health ceilings. cognitive-complexity-threshold = 9 # default is 25 too-many-arguments-threshold = 4 # default is 7 too-many-lines-threshold = 70 # default is 100 @@ -6,3 +8,17 @@ excessive-nesting-threshold = 4 # default is off allow-expect-in-tests = true +# Enforce the environment-injection mandate. The reason strings surface in +# the diagnostic, so a contributor who trips one is told what to do instead. +# +# Sanctioned sites carry `#[expect(clippy::disallowed_methods, reason = "..")]` +# rather than `allow`, so the expectation goes unfulfilled — and warns — once +# the site is migrated. The backlog removes itself instead of rotting. +disallowed-methods = [ + { path = "std::env::var", reason = "inject an environment reader" }, + { path = "std::env::var_os", reason = "inject an environment reader" }, + { path = "std::env::vars", reason = "inject an environment reader" }, + { path = "std::env::vars_os", reason = "inject an environment reader" }, + { path = "std::env::set_var", reason = "use a stub environment in tests" }, + { path = "std::env::remove_var", reason = "use a stub environment in tests" }, +] diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 0f34caf..6b73d98 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -11,10 +11,14 @@ available. `make coverage` uses `cargo llvm-cov` with `lld`. ## Tooling -Development builds use Cranelift for debug code generation. On Linux targets, -`.cargo/config.toml` configures clang to link with `mold` so debug builds link -quickly. Coverage generation uses `lld` because LLVM coverage tooling expects -LLVM-compatible linker behaviour. +On Linux targets, `.cargo/config.toml` configures clang to link with `mold` +so debug builds link quickly. Coverage generation uses `lld` because LLVM +coverage tooling expects LLVM-compatible linker behaviour. The Cranelift +codegen backend that a previous version of this configuration enabled for +debug builds has been removed; the estate's canonical `rust-toolchain.toml` +pins only `rustfmt`, `clippy`, and `rust-analyzer`, so per-repository +opt-in acceleration such as Cranelift now lives in the shared +`tools/dev-fast` configuration instead. Install `clang`, `lld`, and `mold` before running the full generated workflow locally on Linux. diff --git a/docs/users-guide.md b/docs/users-guide.md index 0811009..c06e665 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -10,10 +10,12 @@ settings, and documented starter code. Library projects render `src/lib.rs`. Application projects render `src/main.rs`, release automation, and `[package.metadata.binstall]` metadata for binary installation. -Development builds use Cranelift for debug code generation. On Linux targets, -`.cargo/config.toml` configures clang to link with `mold` so local debug builds -link quickly. Coverage generation uses `lld` instead because LLVM coverage -tools expect LLVM-compatible linker behaviour. +On Linux targets, `.cargo/config.toml` configures clang to link with `mold` +so local debug builds link quickly. Coverage generation uses `lld` instead +because LLVM coverage tools expect LLVM-compatible linker behaviour. Cranelift +debug code generation, previously enabled here, has been removed; opt-in +build acceleration is now the concern of the shared `tools/dev-fast` +configuration rather than the per-repository toolchain pin. ## Makefile Targets diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 4c5db19..9b25d27 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,8 +1,3 @@ [toolchain] channel = "nightly-2026-05-21" -components = [ - "clippy", - "llvm-tools-preview", - "rustc-codegen-cranelift-preview", - "rustfmt", -] +components = ["rustfmt", "clippy", "rust-analyzer"] From d8677b5e266421c252b1b07870e7ccdcbb3a60c4 Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Thu, 13 Aug 2026 20:43:06 +0100 Subject: [PATCH 02/14] Read CARGO_MANIFEST_DIR at compile time in the stub test The newly-enabled disallowed_methods clippy lint forbids runtime std::env::var_os calls outside an injected environment reader, and the test stub only needs to confirm that Cargo sets CARGO_MANIFEST_DIR at all. Use the env! macro, which resolves the variable at compile time, so the assertion still holds without tripping the lint or requiring a deferral. --- tests/stub.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/stub.rs b/tests/stub.rs index 28c8a7f..af7b7cb 100644 --- a/tests/stub.rs +++ b/tests/stub.rs @@ -7,7 +7,7 @@ #[test] fn replace_this_stub_when_real_tests_exist() { assert!( - std::env::var_os("CARGO_MANIFEST_DIR").is_some(), + !env!("CARGO_MANIFEST_DIR").is_empty(), "CARGO_MANIFEST_DIR should be set by Cargo when running tests" ); } From 97bb8983f17acb5ae09bf44b61522d4e9127f95a Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Thu, 13 Aug 2026 20:45:33 +0100 Subject: [PATCH 03/14] Keep existing toolchain components, only add rust-analyzer The canonical toolchain rule (TC-002) requires the components list to include rustfmt, clippy, and rust-analyzer; it is not an exact-match check. The previous commit mistakenly dropped llvm-tools-preview and rustc-codegen-cranelift-preview, which the repository still needs for coverage generation and opt-in Cranelift dev builds respectively. Restore both and add only the missing rust-analyzer component. --- rust-toolchain.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 9b25d27..997b617 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,9 @@ [toolchain] channel = "nightly-2026-05-21" -components = ["rustfmt", "clippy", "rust-analyzer"] +components = [ + "clippy", + "llvm-tools-preview", + "rust-analyzer", + "rustc-codegen-cranelift-preview", + "rustfmt", +] From 3f268b2140cfbfb493531cf74ad504bd90b12f9d Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Thu, 13 Aug 2026 21:18:36 +0100 Subject: [PATCH 04/14] Add the opt-in dev-fast build fragment this branch's docs reference The previous commit reworded the developer and user guides to say that opt-in Cranelift acceleration now lives in tools/dev-fast, but this branch did not actually contain that file, so the reference dangled and make build silently lost its accelerated backend with no in-tree replacement. - Add tools/dev-fast/config.toml, and the dev-build/dev-test Makefile targets and their AGENTS.md section, byte-identical to the Wave 1 pull request that introduces this fragment estate-wide. This branch had not touched Makefile, AGENTS.md, or tools/ itself, so copying rather than hand-authoring keeps the two branches mergeable in either order. - Update docs/developers-guide.md and docs/users-guide.md to describe the fragment and the new make targets accurately, rather than gesturing at a file that was not present. - Fix docs/repository-layout.md, which still attributed code-generation settings to .cargo/config.toml and did not mention tools/dev-fast/config.toml at all. --- AGENTS.md | 9 +++++++++ Makefile | 11 +++++++++++ docs/developers-guide.md | 19 ++++++++++++------- docs/repository-layout.md | 5 ++++- docs/users-guide.md | 11 +++++++---- tools/dev-fast/config.toml | 30 ++++++++++++++++++++++++++++++ 6 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 tools/dev-fast/config.toml diff --git a/AGENTS.md b/AGENTS.md index 02fc2c8..6bd9c3f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -372,3 +372,12 @@ The following tooling is available in this environment: These practices help maintain a high-quality codebase and facilitate collaboration. + +## Fast development builds + +`make dev-build` and `make dev-test` compile with the opt-in Cranelift +backend and the mold linker configured in `tools/dev-fast/config.toml`. +They require a nightly toolchain and, on Linux, a `mold` binary on the +`PATH`. The fragment is passed explicitly with `--config`, so release, +coverage, and verification builds are unaffected; never copy its contents +into `.cargo/config.toml`, which Cargo applies to every build. diff --git a/Makefile b/Makefile index 79b14e0..c9852de 100644 --- a/Makefile +++ b/Makefile @@ -73,3 +73,14 @@ nixie: ## Validate Mermaid diagrams help: ## Show available targets @grep -E '^[a-zA-Z_-]+:.*?##' $(MAKEFILE_LIST) | \ awk 'BEGIN {FS=":"; printf "Available targets:\n"} {printf " %-20s %s\n", $$1, $$2}' + +# Opt-in accelerated debug builds (Cranelift + mold); requires a nightly +# toolchain. See AGENTS.md and tools/dev-fast/config.toml. +DEV_FAST_CONFIG ?= tools/dev-fast/config.toml + +.PHONY: dev-build dev-test +dev-build: ## Build debug binaries with Cranelift and mold + cargo --config "$(DEV_FAST_CONFIG)" build + +dev-test: ## Run tests with Cranelift and mold + cargo --config "$(DEV_FAST_CONFIG)" test diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 6b73d98..502ec08 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -7,18 +7,23 @@ This guide explains the contributor workflow for the generated Memoryd project. Use `make all` as the public entrypoint for formatting, linting, and tests. `make lint` runs rustdoc, Clippy, and Whitaker. `make test` prefers `cargo nextest run` and falls back to `cargo test` when cargo-nextest is not -available. `make coverage` uses `cargo llvm-cov` with `lld`. +available. `make coverage` uses `cargo llvm-cov` with `lld`. `make dev-build` +and `make dev-test` are opt-in accelerated variants; see below. ## Tooling On Linux targets, `.cargo/config.toml` configures clang to link with `mold` so debug builds link quickly. Coverage generation uses `lld` because LLVM -coverage tooling expects LLVM-compatible linker behaviour. The Cranelift -codegen backend that a previous version of this configuration enabled for -debug builds has been removed; the estate's canonical `rust-toolchain.toml` -pins only `rustfmt`, `clippy`, and `rust-analyzer`, so per-repository -opt-in acceleration such as Cranelift now lives in the shared -`tools/dev-fast` configuration instead. +coverage tooling expects LLVM-compatible linker behaviour. `.cargo/config.toml` +no longer enables the Cranelift codegen backend for debug builds; that opt-in +acceleration now lives in `tools/dev-fast/config.toml` instead, so it applies +only when explicitly requested rather than to every build Cargo discovers. + +`make dev-build` and `make dev-test` compile with that Cranelift-plus-mold +fragment, passed explicitly via `cargo --config tools/dev-fast/config.toml`. +They require a nightly toolchain and, on Linux, a `mold` binary on the +`PATH`. Release, coverage, and verification builds are unaffected because +the fragment is never merged into `.cargo/config.toml`. Install `clang`, `lld`, and `mold` before running the full generated workflow locally on Linux. diff --git a/docs/repository-layout.md b/docs/repository-layout.md index 3c004e9..4d143f1 100644 --- a/docs/repository-layout.md +++ b/docs/repository-layout.md @@ -46,7 +46,10 @@ omits build output such as `target/`. ## Path responsibilities - `.cargo/config.toml`: Configures Cargo defaults for local development, - including Linux linker and code-generation settings. + including the Linux linker. +- `tools/dev-fast/config.toml`: Opt-in Cranelift-plus-mold configuration + fragment for accelerated local debug builds, applied explicitly by + `make dev-build` and `make dev-test`. - `.github/dependabot.yml`: Configures automated dependency update checks. - `.github/workflows/ci.yml`: Runs Memoryd's continuous integration checks. - `.github/workflows/release.yml`: Builds and publishes binary release diff --git a/docs/users-guide.md b/docs/users-guide.md index c06e665..fe8ffdd 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -12,10 +12,11 @@ Application projects render `src/main.rs`, release automation, and On Linux targets, `.cargo/config.toml` configures clang to link with `mold` so local debug builds link quickly. Coverage generation uses `lld` instead -because LLVM coverage tools expect LLVM-compatible linker behaviour. Cranelift -debug code generation, previously enabled here, has been removed; opt-in -build acceleration is now the concern of the shared `tools/dev-fast` -configuration rather than the per-repository toolchain pin. +because LLVM coverage tools expect LLVM-compatible linker behaviour. +Cranelift debug code generation is no longer enabled by default; it is +available as an opt-in accelerated build via `tools/dev-fast/config.toml` +and the `make dev-build`/`make dev-test` targets below, so ordinary builds, +coverage, and verification keep the supported LLVM backend. ## Makefile Targets @@ -29,6 +30,8 @@ The generated `Makefile` exposes these public targets: - `make build` builds the debug target. - `make release` builds the release target. - `make coverage` writes `lcov.info` using `cargo llvm-cov` and `lld`. +- `make dev-build` and `make dev-test` build and test with the opt-in + Cranelift-plus-mold configuration in `tools/dev-fast/config.toml`. - `make markdownlint` checks Markdown files. - `make nixie` validates Mermaid diagrams. diff --git a/tools/dev-fast/config.toml b/tools/dev-fast/config.toml new file mode 100644 index 0000000..75bfd76 --- /dev/null +++ b/tools/dev-fast/config.toml @@ -0,0 +1,30 @@ +# Canonical opt-in Cargo configuration fragment for accelerated local debug +# builds. +# +# This file is deliberately kept out of `.cargo/config.toml`. Cargo +# auto-discovers that path, so anything placed there applies to release +# packaging, coverage, and verification builds, which must keep the +# supported LLVM backend and platform linker. Copy this fragment to +# `tools/dev-fast/config.toml` and pass it explicitly with +# `cargo --config tools/dev-fast/config.toml ...` from `make dev-build` and +# `make dev-test`. +# +# Repositories that set repository-wide `rustflags` in `.cargo/config.toml` +# must restate them in the target table below: Cargo picks a single +# rustflags source rather than merging them, and a `[target.*]` table +# outranks the `[build]` table. + +[unstable] +codegen-backend = true + +# Cranelift trades runtime performance for compile speed, so it applies to +# the dev profile only. The `make dev-*` targets never build release +# artefacts. +[profile.dev] +codegen-backend = "cranelift" + +# mold ships for Linux only, so the flag is gated behind a target `cfg`. On +# macOS and Windows the table simply does not apply and the platform default +# linker is used. +[target.'cfg(target_os = "linux")'] +rustflags = ["-Clink-arg=-fuse-ld=mold"] From e2830fb1020496f0520fc430b125b494aaf45be4 Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Thu, 13 Aug 2026 21:20:19 +0100 Subject: [PATCH 05/14] Document the Cargo.toml lint baseline for maintainers A reviewer noted that the new lint tables in Cargo.toml are an undocumented maintainer convention: nothing explains why the tables sit directly under [lints.*] rather than a workspace, what governs adding an #[expect] instead of an #[allow], or where the thresholds and disallowed methods come from. Add a Lint baseline section to docs/developers-guide.md that points at Cargo.toml as the authoritative source for the exact lint set rather than duplicating it, explains the expect-not-allow convention and why it keeps deferred violations visible, and summarizes what clippy.toml and rust-toolchain.toml each contribute. --- docs/developers-guide.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 502ec08..7e96657 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -28,6 +28,31 @@ the fragment is never merged into `.cargo/config.toml`. Install `clang`, `lld`, and `mold` before running the full generated workflow locally on Linux. +## Lint baseline + +Memoryd is a single crate with no `[workspace]` table, so its lint tables +live directly under `[lints.clippy]`, `[lints.rust]`, and `[lints.rustdoc]` +in `Cargo.toml`, rather than under `[workspace.lints]` with per-member +inheritance. They implement the estate's phase 2 Rust baseline. `Cargo.toml` +is authoritative for the exact set and level of each lint; this section +summarizes intent rather than duplicating the list. + +Violations must be fixed, not silenced. Where a violation is a genuine, +scheduled deferral, annotate the site with +`#[expect(clippy::, reason = "...")]`, never `#[allow(...)]`: once the +site is fixed, the unfulfilled expectation itself becomes a warning, so the +backlog announces its own shrinkage instead of rotting silently. + +`clippy.toml` carries the numeric thresholds (cognitive complexity, +argument count, function length, nesting depth) and the +`disallowed-methods` list that forbids direct `std::env::var`/`set_var`/ +`remove_var` calls and their `_os`/`vars` siblings; inject an environment +reader instead so environment access stays testable. + +The pinned nightly toolchain in `rust-toolchain.toml` supplies the +`rustfmt`, `clippy`, and `rust-analyzer` components the lint and formatting +gates depend on. + Behavioural tests that describe externally observable workflows should use `rstest-bdd` so Gherkin scenarios, `rstest` fixtures, and Rust assertions run under the standard Cargo test harness. PostgreSQL migration and repository From 50f2cf584b00efc41cf5890dcf6124438a03f169 Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Thu, 13 Aug 2026 21:34:56 +0100 Subject: [PATCH 06/14] Name the retained Cranelift toolchain components in the docs A reviewer noted that removing Cranelift's default activation from .cargo/config.toml was documented, but neither guide said the Cranelift and llvm-tools components themselves are still pinned and installed via rust-toolchain.toml; a reader could mistake "no longer enabled by default" for "no longer available at all". - docs/developers-guide.md: name llvm-tools-preview and rustc-codegen-cranelift-preview as retained rust-toolchain.toml components, and state plainly that tools/dev-fast/config.toml is what controls activation, not installation. - docs/users-guide.md: state explicitly that only the default activation was removed, and that the pinned toolchain still installs the Cranelift backend. --- docs/developers-guide.md | 4 ++++ docs/users-guide.md | 13 ++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 7e96657..ed58e37 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -18,6 +18,10 @@ coverage tooling expects LLVM-compatible linker behaviour. `.cargo/config.toml` no longer enables the Cranelift codegen backend for debug builds; that opt-in acceleration now lives in `tools/dev-fast/config.toml` instead, so it applies only when explicitly requested rather than to every build Cargo discovers. +`rust-toolchain.toml` still pins the `llvm-tools-preview` and +`rustc-codegen-cranelift-preview` components, so both the coverage tooling +and the Cranelift backend itself remain installed; `tools/dev-fast/config.toml` +is what controls whether a given build actually activates Cranelift. `make dev-build` and `make dev-test` compile with that Cranelift-plus-mold fragment, passed explicitly via `cargo --config tools/dev-fast/config.toml`. diff --git a/docs/users-guide.md b/docs/users-guide.md index fe8ffdd..0c519a5 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -12,11 +12,14 @@ Application projects render `src/main.rs`, release automation, and On Linux targets, `.cargo/config.toml` configures clang to link with `mold` so local debug builds link quickly. Coverage generation uses `lld` instead -because LLVM coverage tools expect LLVM-compatible linker behaviour. -Cranelift debug code generation is no longer enabled by default; it is -available as an opt-in accelerated build via `tools/dev-fast/config.toml` -and the `make dev-build`/`make dev-test` targets below, so ordinary builds, -coverage, and verification keep the supported LLVM backend. +because LLVM coverage tools expect LLVM-compatible linker behaviour. The +pinned nightly toolchain still installs the Cranelift codegen backend, but +`.cargo/config.toml` no longer activates it by default for every build; +only the automatic activation was removed, not the component itself. It +remains available as an opt-in accelerated build via +`tools/dev-fast/config.toml` and the `make dev-build`/`make dev-test` +targets below, so ordinary builds, coverage, and verification keep the +supported LLVM backend. ## Makefile Targets From ad1e8f5ace1b0d2e2bc1e24905e2b6c56a1f3e8b Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Thu, 13 Aug 2026 21:38:45 +0100 Subject: [PATCH 07/14] Move Cranelift and mold detail out of the users' guide Sponsor ruling on a sibling repository's pull request applies here too: Cranelift and mold are developer concerns, and their home is the developers' guide only. The users' guide should tell a reader what the Makefile targets do, not explain build-acceleration internals. Remove every Cranelift, mold, dev-fast, and codegen-backend mention from docs/users-guide.md, including the component-retention wording and the mold-linker sentence added for earlier review rounds. Point readers at the developers' guide for local build and linker configuration instead. The developers' guide already documented all of this in full, so nothing needed to move; the dev-build/dev-test Makefile-targets entry stays but now carries no Cranelift/mold explanation of its own. --- docs/users-guide.md | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/docs/users-guide.md b/docs/users-guide.md index 0c519a5..a51e10c 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -10,16 +10,8 @@ settings, and documented starter code. Library projects render `src/lib.rs`. Application projects render `src/main.rs`, release automation, and `[package.metadata.binstall]` metadata for binary installation. -On Linux targets, `.cargo/config.toml` configures clang to link with `mold` -so local debug builds link quickly. Coverage generation uses `lld` instead -because LLVM coverage tools expect LLVM-compatible linker behaviour. The -pinned nightly toolchain still installs the Cranelift codegen backend, but -`.cargo/config.toml` no longer activates it by default for every build; -only the automatic activation was removed, not the component itself. It -remains available as an opt-in accelerated build via -`tools/dev-fast/config.toml` and the `make dev-build`/`make dev-test` -targets below, so ordinary builds, coverage, and verification keep the -supported LLVM backend. +See the [developers' guide](developers-guide.md) for the local build and +linker configuration. ## Makefile Targets @@ -33,10 +25,10 @@ The generated `Makefile` exposes these public targets: - `make build` builds the debug target. - `make release` builds the release target. - `make coverage` writes `lcov.info` using `cargo llvm-cov` and `lld`. -- `make dev-build` and `make dev-test` build and test with the opt-in - Cranelift-plus-mold configuration in `tools/dev-fast/config.toml`. +- `make dev-build` and `make dev-test` provide opt-in accelerated build + variants; see the [developers' guide](developers-guide.md) for details. - `make markdownlint` checks Markdown files. - `make nixie` validates Mermaid diagrams. -Install `clang`, `lld`, and `mold` before running the full generated workflow -locally on Linux. +See the [developers' guide](developers-guide.md) for the toolchain +prerequisites needed to run the full generated workflow locally. From 05e4a5c0f063de1dcbe722d1fdbfb9ccb9902468 Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Thu, 13 Aug 2026 21:42:29 +0100 Subject: [PATCH 08/14] Fix stale copy-instruction wording in .rustfmt.toml's header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A reviewer noted that the header comment still read "Copy this file to the repository root as `.rustfmt.toml`" — a canon-template instruction meant for someone assembling a new repository, which reads as nonsense sitting in the file it was already copied into. The canon source has since been corrected to describe the file in place rather than instruct a copy step. Replace the header comment here with the corrected canon text so this file stays byte-identical to platform-standards/canon/lint/rust/rustfmt.toml. No keys changed. --- .rustfmt.toml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.rustfmt.toml b/.rustfmt.toml index 8b3cabd..7c6d17b 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,8 +1,10 @@ -# Canonical rustfmt configuration for the estate. +# Canonical rustfmt configuration for the estate. In a consuming +# repository this file lives at the root as `.rustfmt.toml`; keep it +# there, aligned with canon (the rust-rustfmt-baseline rule FMT-001 +# compares the parsed keys). # # `unstable_features = true` means rustfmt must run on the nightly channel; -# the rust-rustfmt-baseline rule (FMT-002) checks for that evidence. Copy -# this file to the repository root as `.rustfmt.toml`. +# the rust-rustfmt-baseline rule (FMT-002) checks for that evidence. unstable_features = true comment_width = 100 format_code_in_doc_comments = true From c2be2dcc94aba3216757b25c1626cacb3003e27c Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Thu, 13 Aug 2026 21:57:14 +0100 Subject: [PATCH 09/14] Wire the standard Makefile targets to the dev-fast profile Sponsor decision: dev-fast is the standard development path, not an opt-in side path. The whole point of the fragment is that development builds for test and lint are fast, easy, and cheap by default, not only when a contributor remembers to run dev-build/dev-test instead. Add --config "$(DEV_FAST_CONFIG)" to every cargo invocation in the build, test, lint (both the cargo doc step and clippy), and typecheck targets. The pattern rule behind build/release conditionally omits the flag for release builds, since release keeps the platform LLVM backend and linker. coverage is untouched for the same reason. The Wave 1 block (DEV_FAST_CONFIG definition, dev-build, dev-test) is left byte-identical to the parabellum-wave-1 branch so both pull requests merge cleanly in either order. Add a "Standard development path" paragraph to AGENTS.md, appended after the Wave 1 "Fast development builds" section rather than edited into it, stating that the standard targets use dev-fast by default and that direct cargo invocations for development work must pass the same --config flag to avoid thrashing the incremental build cache with mismatched codegen-backend fingerprints. --- AGENTS.md | 14 ++++++++++++++ Makefile | 10 +++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6bd9c3f..a3341db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -381,3 +381,17 @@ They require a nightly toolchain and, on Linux, a `mold` binary on the `PATH`. The fragment is passed explicitly with `--config`, so release, coverage, and verification builds are unaffected; never copy its contents into `.cargo/config.toml`, which Cargo applies to every build. + +## Standard development path + +The dev-fast profile is the standard development path, not a side path: +`make build`, `make test`, `make lint`, and `make typecheck` all pass +`--config tools/dev-fast/config.toml` to Cargo. An agent or human calling +`cargo build`, `cargo test`, `cargo clippy`, `cargo check`, or `cargo doc` +directly for a development build, test, lint, or typecheck run must pass +that same `--config` flag, or the run will use a different codegen +backend and linker than `make` does. The fragment must never be applied +to coverage, release, or verification builds. Mixing direct-cargo and +`make` invocations without the flag thrashes the incremental build +cache, because Cargo fingerprints the codegen backend and the two runs +produce different fingerprints for what looks like the same build. diff --git a/Makefile b/Makefile index c9852de..abf0c26 100644 --- a/Makefile +++ b/Makefile @@ -31,11 +31,11 @@ clean: ## Remove build artifacts $(CARGO) clean test: ## Run tests with warnings treated as errors - RUSTFLAGS="$(RUST_FLAGS)" $(CARGO) $(TEST_CMD) $(TEST_FLAGS) $(BUILD_JOBS) + RUSTFLAGS="$(RUST_FLAGS)" $(CARGO) --config "$(DEV_FAST_CONFIG)" $(TEST_CMD) $(TEST_FLAGS) $(BUILD_JOBS) target/%/$(TARGET): ## Build binary in debug or release mode - $(CARGO) build $(BUILD_JOBS) $(if $(findstring release,$(@)),--release) --bin $(TARGET) + $(CARGO) $(if $(findstring release,$(@)),,--config "$(DEV_FAST_CONFIG)") build $(BUILD_JOBS) $(if $(findstring release,$(@)),--release) --bin $(TARGET) coverage: ## Generate lcov coverage with lld for llvm-tools compatibility @echo "coverage linker flags: $(COVERAGE_LINKER_FLAGS)" @@ -46,13 +46,13 @@ coverage: ## Generate lcov coverage with lld for llvm-tools compatibility $(CARGO) llvm-cov --lcov --output-path lcov.info $(TEST_FLAGS) lint: ## Run Clippy with warnings denied - RUSTDOCFLAGS="$(RUSTDOC_FLAGS)" $(CARGO) doc --no-deps - $(CARGO) clippy $(CLIPPY_FLAGS) + RUSTDOCFLAGS="$(RUSTDOC_FLAGS)" $(CARGO) --config "$(DEV_FAST_CONFIG)" doc --no-deps + $(CARGO) --config "$(DEV_FAST_CONFIG)" clippy $(CLIPPY_FLAGS) @echo "Whitaker binary: $(WHITAKER)" PATH="$(USER_BIN_PATH):$(PATH)" RUSTFLAGS="$(RUST_FLAGS)" $(WHITAKER) --all -- $(CARGO_FLAGS) typecheck: ## Type-check without building - RUSTFLAGS="$(RUST_FLAGS)" $(CARGO) check $(CARGO_FLAGS) + RUSTFLAGS="$(RUST_FLAGS)" $(CARGO) --config "$(DEV_FAST_CONFIG)" check $(CARGO_FLAGS) fmt: fmt-tools ## Format Rust and Markdown sources $(CARGO) +nightly fmt --all From a8a7b1dcce215984aaf6e4686a9452ab06d95d47 Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Thu, 13 Aug 2026 21:57:24 +0100 Subject: [PATCH 10/14] Add a contract test guarding the Makefile's dev-fast wiring The dev-fast wiring just added to the Makefile is easy to lose in a future edit: an agent or contributor touching the build/test/lint/ typecheck recipes has no local signal if a --config flag quietly disappears. Add tests/makefile_contract.rs, which reads the repository's own Makefile at compile time and asserts the standard targets' recipe text references --config and the dev-fast fragment, that coverage never does, and that tools/dev-fast/config.toml exists. This fails the repo's own suite before the estate-wide DF-004 audit would ever catch the regression centrally. Use rstest to parameterize the three targets whose recipe carries a single cargo invocation (test, lint, typecheck); build is checked separately because it delegates to the target/%/$(TARGET) pattern rule rather than carrying its own recipe. Add rstest as a dev dependency. --- Cargo.lock | 299 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 + tests/makefile_contract.rs | 112 ++++++++++++++ 3 files changed, 414 insertions(+) create mode 100644 tests/makefile_contract.rs diff --git a/Cargo.lock b/Cargo.lock index a197fef..80cfcf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,305 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + [[package]] name = "memoryd" version = "0.1.0" +dependencies = [ + "rstest", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.119", + "unicode-ident", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml index 7588275..a27998f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,3 +98,6 @@ bare_urls = "deny" invalid_html_tags = "deny" invalid_codeblock_attributes = "deny" unescaped_backticks = "deny" + +[dev-dependencies] +rstest = "0.26.1" diff --git a/tests/makefile_contract.rs b/tests/makefile_contract.rs new file mode 100644 index 0000000..2e08e56 --- /dev/null +++ b/tests/makefile_contract.rs @@ -0,0 +1,112 @@ +//! Contract test for the Makefile's dev-fast wiring. +//! +//! The estate's dev-fast profile (`tools/dev-fast/config.toml`) is the +//! standard development path, not an opt-in side path: every cargo +//! invocation in the `build`, `test`, `lint`, and `typecheck` Makefile +//! targets must pass `--config tools/dev-fast/config.toml`, while +//! `coverage` must never do so, because coverage requires the LLVM +//! codegen backend and the platform linker. See AGENTS.md's "Standard +//! development path" section for the full rationale. This test reads the +//! repository's own Makefile so an over-eager edit that drops the wiring +//! fails locally before the estate-wide DF-004 audit ever runs. + +use std::path::Path; + +use rstest::rstest; + +const MAKEFILE: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/Makefile")); + +/// Returns the tab-indented recipe lines that follow the first line +/// starting with `header_prefix`, or an empty string if no such rule +/// header exists. +/// +/// This mirrors the convention documented in the dev-fast wiring brief: +/// a target's recipe block is the run of indented lines between its +/// header and the next non-indented line. +fn recipe_after(makefile: &str, header_prefix: &str) -> String { + let Some(start) = makefile + .lines() + .position(|line| line.starts_with(header_prefix)) + else { + return String::new(); + }; + makefile + .lines() + .skip(start + 1) + .take_while(|line| line.starts_with('\t')) + .collect::>() + .join("\n") +} + +/// Returns the recipe used for `make build`, following the delegation to +/// the `target/%/$(TARGET)` pattern rule when `build` itself carries no +/// recipe of its own (it depends on `target/debug/$(TARGET)` instead). +fn build_recipe(makefile: &str) -> String { + let direct = recipe_after(makefile, "build:"); + if direct.is_empty() { + recipe_after(makefile, "target/%/$(TARGET):") + } else { + direct + } +} + +/// Reports whether `text` references the dev-fast configuration +/// fragment, matching `dev-fast` or `dev_fast` case-insensitively. +fn mentions_dev_fast(text: &str) -> bool { + let lower = text.to_lowercase(); + lower.contains("dev-fast") || lower.contains("dev_fast") +} + +/// Asserts that a Makefile recipe wires cargo through the dev-fast +/// configuration fragment, naming the convention and where it is +/// documented when the assertion fails. +fn assert_uses_dev_fast_config(target: &str, recipe: &str) { + let wired = recipe.contains("--config") && mentions_dev_fast(recipe); + assert!( + wired, + "the `{target}` Makefile target must invoke cargo with `--config \ + tools/dev-fast/config.toml` (the DEV_FAST_CONFIG variable) so it runs on the estate's \ + dev-fast standard development path; see AGENTS.md's \"Standard development path\" section", + ); +} + +#[rstest] +#[case::make_test("test")] +#[case::make_lint("lint")] +#[case::make_typecheck("typecheck")] +fn standard_target_uses_dev_fast_config(#[case] target: &str) { + let header = format!("{target}:"); + let recipe = recipe_after(MAKEFILE, &header); + assert_uses_dev_fast_config(target, &recipe); +} + +#[test] +fn build_target_uses_dev_fast_config() { + let recipe = build_recipe(MAKEFILE); + assert_uses_dev_fast_config("build", &recipe); +} + +#[test] +fn coverage_target_never_uses_dev_fast_config() { + let recipe = recipe_after(MAKEFILE, "coverage:"); + assert!( + !mentions_dev_fast(&recipe), + "the `coverage` Makefile target must never reference the dev-fast configuration fragment: \ + coverage requires the LLVM codegen backend and the platform linker; see AGENTS.md's \ + \"Fast development builds\" section", + ); +} + +#[test] +fn dev_fast_config_file_exists() { + let path = Path::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tools/dev-fast/config.toml" + )); + assert!( + path.exists(), + "tools/dev-fast/config.toml must exist: the Makefile's DEV_FAST_CONFIG variable and the \ + standard development targets depend on it; see AGENTS.md's \"Fast development builds\" \ + section", + ); +} From 96e7cc873a4e7f2ec635298c664b2bef92d490c9 Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Thu, 13 Aug 2026 23:28:45 +0100 Subject: [PATCH 11/14] Fix stale Cranelift comments in the release workflow .github/workflows/release.yml still described .cargo/config.toml as carrying a Cranelift codegen-backend setting for development builds, which was removed from this branch earlier: Cranelift now lives solely in the opt-in tools/dev-fast/config.toml, which the release workflow never reads, so there was never anything to isolate release builds from. Reword the "Install cross" step's comment to describe .cargo/config.toml as carrying only the Linux mold linker configuration, and the "Build release binary" step's comment to explain the +stable override without implying Cranelift is what rust-toolchain.toml's nightly pin exists for. No step logic changed. Checked ci.yml's `whitaker-installer --cranelift` comment separately: it configures how the Whitaker tool itself is built, unrelated to memoryd's own .cargo/config.toml, and remains accurate as written. --- .github/workflows/release.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1cd34d7..54c8e3f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,8 +56,8 @@ jobs: key: cross-${{ env.CROSS_REVISION }} - name: Install cross env: - # Clear repository build flags from .cargo/config.toml (mold linker - # and Cranelift codegen-backend) before installing cross from CROSS_REVISION. + # Clear the repository's mold linker rustflags from + # .cargo/config.toml before installing cross from CROSS_REVISION. RUSTFLAGS: "" run: | if [ -x "$HOME/.cargo/bin/cross" ]; then @@ -74,8 +74,9 @@ jobs: restore-keys: | ${{ runner.os }}-cargo-${{ matrix.target }}- - name: Build release binary - # Use +stable to override rust-toolchain.toml (which specifies nightly - # with Cranelift for development) and ensure release builds use stable. + # Use +stable to override rust-toolchain.toml's nightly pin (needed + # for the repository's lint and dev-fast tooling) so release builds + # use stable regardless. env: # Build release artifacts without repository-local linker flags, # including mold rustflags from .cargo/config.toml. From a69d1917e7f000e5faa378bd9f2a679c9ce353dd Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Thu, 13 Aug 2026 23:38:03 +0100 Subject: [PATCH 12/14] Refresh tools/dev-fast/config.toml to corrected canon bytes The deployed fragment carried two stale comments: a "Copy this fragment to..." instruction left over from the canon template (which reads as nonsense sitting in the file it was already copied into), and a mis-statement of Cargo's rustflags precedence (it described a single rustflags source being picked rather than joined target rustflags outranking [build].rustflags). Both are corrected in canon. Replace the file's content verbatim with the current bytes from platform-standards/canon/build/rust/dev-fast.toml. No configuration key changed; only the header and rustflags-precedence comments differ. The Wave 1 branch's copy is being refreshed to the same bytes separately, so branch-pair identity is preserved without needing to coordinate the two pull requests. --- tools/dev-fast/config.toml | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/tools/dev-fast/config.toml b/tools/dev-fast/config.toml index 75bfd76..01e7f89 100644 --- a/tools/dev-fast/config.toml +++ b/tools/dev-fast/config.toml @@ -1,18 +1,19 @@ # Canonical opt-in Cargo configuration fragment for accelerated local debug # builds. # -# This file is deliberately kept out of `.cargo/config.toml`. Cargo -# auto-discovers that path, so anything placed there applies to release -# packaging, coverage, and verification builds, which must keep the -# supported LLVM backend and platform linker. Copy this fragment to -# `tools/dev-fast/config.toml` and pass it explicitly with -# `cargo --config tools/dev-fast/config.toml ...` from `make dev-build` and -# `make dev-test`. +# This configuration is deliberately kept out of `.cargo/config.toml`. +# Cargo auto-discovers that path, so anything placed there applies to +# release packaging, coverage, and verification builds, which must keep +# the supported LLVM backend and platform linker. In a consuming +# repository this fragment lives at `tools/dev-fast/config.toml` and is +# passed explicitly with `cargo --config tools/dev-fast/config.toml ...` +# from `make dev-build` and `make dev-test`. # # Repositories that set repository-wide `rustflags` in `.cargo/config.toml` -# must restate them in the target table below: Cargo picks a single -# rustflags source rather than merging them, and a `[target.*]` table -# outranks the `[build]` table. +# must restate them in the target table below. Cargo joins the rustflags of +# every matching `[target.*]` entry (target-triple and `cfg` tables alike), +# but the joined target rustflags take precedence over `[build].rustflags` +# rather than merging with it. [unstable] codegen-backend = true From 38c6e63ad3d56d0106c350933ea1473274ea1922 Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Fri, 14 Aug 2026 00:18:38 +0100 Subject: [PATCH 13/14] Make the dev-fast Makefile block use $(CARGO) The appended dev-build/dev-test recipes hard-coded cargo even though the Makefile already defines an injectable CARGO variable at the top and every other target uses it. Replace both hard-coded invocations with $(CARGO), matching the repository's own idiom. No definition was needed in the block itself, since CARGO ?= cargo already exists near the top of the file and is visible to every recipe. This changes the bytes of the Wave 1 block that the parabellum-wave-1 branch mirrors; the change is scoped to exactly the two recipe lines so the mirror stays a clean tail replacement. --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index abf0c26..3059317 100644 --- a/Makefile +++ b/Makefile @@ -80,7 +80,7 @@ DEV_FAST_CONFIG ?= tools/dev-fast/config.toml .PHONY: dev-build dev-test dev-build: ## Build debug binaries with Cranelift and mold - cargo --config "$(DEV_FAST_CONFIG)" build + $(CARGO) --config "$(DEV_FAST_CONFIG)" build dev-test: ## Run tests with Cranelift and mold - cargo --config "$(DEV_FAST_CONFIG)" test + $(CARGO) --config "$(DEV_FAST_CONFIG)" test From f70f6d730ec1df0940f436951704065209b5b0cc Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Fri, 14 Aug 2026 00:18:50 +0100 Subject: [PATCH 14/14] Harden the Makefile contract test to per-line assertions Mutation testing on a sibling repository (mpsc-log) proved that a whole-recipe-block string match passes even when only one of several cargo lines is wired: a target like lint, whose recipe carries both a cargo doc step and a cargo clippy step, would keep passing if only one of the two lost its --config flag, because the other line's text still satisfied the block-level match. Rework the standard-target and coverage assertions to filter each recipe down to its $(CARGO)-invoking lines and check --config and the dev-fast reference on every one individually, so a partially-wired multi-line recipe fails and names the offending line. Add a second class of check that a text match cannot provide: run `make --dry-run dev-build/dev-test CARGO=probe-cargo` and assert the probe value, --config, and the dev-fast reference appear in that order in the emitted command. This proves the Wave 1 block's $(CARGO) substitution actually reaches the recipe, without needing the nightly toolchain or mold to build anything. The dry-run helper returns Result rather than calling .expect() itself, since it is a plain helper rather than a #[test] function and allow-expect-in-tests does not cover call sites outside #[test]/#[cfg(test)]. Verified by mutation: stripping --config from one line of a multi-line recipe fails the matching per-line case and names the target; hard-coding cargo back into dev-build fails the substitution case with a message showing the probe value never appeared. Both mutations were reverted before committing. --- tests/makefile_contract.rs | 117 ++++++++++++++++++++++++++++++++----- 1 file changed, 102 insertions(+), 15 deletions(-) diff --git a/tests/makefile_contract.rs b/tests/makefile_contract.rs index 2e08e56..9969b89 100644 --- a/tests/makefile_contract.rs +++ b/tests/makefile_contract.rs @@ -9,8 +9,16 @@ //! development path" section for the full rationale. This test reads the //! repository's own Makefile so an over-eager edit that drops the wiring //! fails locally before the estate-wide DF-004 audit ever runs. +//! +//! Checks operate per cargo-invoking recipe line, not on a recipe's whole +//! text: a target whose recipe carries several cargo lines (the `doc` +//! step plus `clippy` in `lint`) would otherwise pass a whole-block match +//! even when only one of those lines is wired. A separate pair of tests +//! dry-runs `dev-build`/`dev-test` with a substituted `CARGO` value to +//! prove the Wave 1 block's `$(CARGO)` indirection actually reaches the +//! `--config` flag, without needing the nightly toolchain or `mold`. -use std::path::Path; +use std::{path::Path, process::Command}; use rstest::rstest; @@ -50,6 +58,17 @@ fn build_recipe(makefile: &str) -> String { } } +/// Returns the lines within `recipe` that directly invoke `$(CARGO)`, as +/// distinct from lines that invoke some other tool (for example the +/// Whitaker binary, which reuses `$(CARGO_FLAGS)` but not `$(CARGO)` +/// itself). +fn cargo_invocation_lines(recipe: &str) -> Vec<&str> { + recipe + .lines() + .filter(|line| line.contains("$(CARGO)")) + .collect() +} + /// Reports whether `text` references the dev-fast configuration /// fragment, matching `dev-fast` or `dev_fast` case-insensitively. fn mentions_dev_fast(text: &str) -> bool { @@ -57,17 +76,41 @@ fn mentions_dev_fast(text: &str) -> bool { lower.contains("dev-fast") || lower.contains("dev_fast") } -/// Asserts that a Makefile recipe wires cargo through the dev-fast -/// configuration fragment, naming the convention and where it is -/// documented when the assertion fails. +/// Asserts that every `$(CARGO)` invocation in `recipe` passes `--config` +/// and references the dev-fast fragment, checked line by line so a +/// partially-wired multi-line recipe fails rather than passing on the +/// strength of its other lines. fn assert_uses_dev_fast_config(target: &str, recipe: &str) { - let wired = recipe.contains("--config") && mentions_dev_fast(recipe); + let cargo_lines = cargo_invocation_lines(recipe); assert!( - wired, - "the `{target}` Makefile target must invoke cargo with `--config \ - tools/dev-fast/config.toml` (the DEV_FAST_CONFIG variable) so it runs on the estate's \ - dev-fast standard development path; see AGENTS.md's \"Standard development path\" section", + !cargo_lines.is_empty(), + "the `{target}` Makefile target's recipe has no $(CARGO) invocation to check for dev-fast \ + wiring; see AGENTS.md's \"Standard development path\" section", ); + for line in cargo_lines { + let wired = line.contains("--config") && mentions_dev_fast(line); + assert!( + wired, + "the `{target}` Makefile target's cargo invocation `{line}` must pass `--config \ + tools/dev-fast/config.toml` (the DEV_FAST_CONFIG variable) so it runs on the \ + estate's dev-fast standard development path; see AGENTS.md's \"Standard development \ + path\" section", + ); + } +} + +/// Asserts that no `$(CARGO)` invocation in `recipe` references the +/// dev-fast fragment, checked line by line for the same reason as +/// [`assert_uses_dev_fast_config`]. +fn assert_never_uses_dev_fast_config(target: &str, recipe: &str) { + for line in cargo_invocation_lines(recipe) { + assert!( + !mentions_dev_fast(line), + "the `{target}` Makefile target's cargo invocation `{line}` must never reference the \ + dev-fast configuration fragment: {target} requires the LLVM codegen backend and the \ + platform linker; see AGENTS.md's \"Fast development builds\" section", + ); + } } #[rstest] @@ -89,12 +132,7 @@ fn build_target_uses_dev_fast_config() { #[test] fn coverage_target_never_uses_dev_fast_config() { let recipe = recipe_after(MAKEFILE, "coverage:"); - assert!( - !mentions_dev_fast(&recipe), - "the `coverage` Makefile target must never reference the dev-fast configuration fragment: \ - coverage requires the LLVM codegen backend and the platform linker; see AGENTS.md's \ - \"Fast development builds\" section", - ); + assert_never_uses_dev_fast_config("coverage", &recipe); } #[test] @@ -110,3 +148,52 @@ fn dev_fast_config_file_exists() { section", ); } + +/// Runs `make --dry-run CARGO=` in the repository +/// root and returns the printed command, without building or testing +/// anything. Proves the Wave 1 block's `$(CARGO)` indirection actually +/// reaches the recipe, on any toolchain, with no nightly or `mold` +/// dependency. +/// +/// Returns `Err` rather than panicking directly: this is a helper, not a +/// `#[test]` function itself, so `clippy::expect_used` still applies to +/// it even with `allow-expect-in-tests` set. +fn dry_run(target: &str, probe_cargo: &str) -> Result { + let output = Command::new("make") + .arg("--dry-run") + .arg(target) + .arg(format!("CARGO={probe_cargo}")) + .current_dir(env!("CARGO_MANIFEST_DIR")) + .output() + .map_err(|error| { + format!("failed to run `make --dry-run {target} CARGO={probe_cargo}`: {error}") + })?; + String::from_utf8(output.stdout) + .map_err(|error| format!("make dry-run output for `{target}` was not valid UTF-8: {error}")) +} + +/// Asserts that each string in `needles` occurs in `text`, in the given +/// order, by repeatedly splitting off everything before and including +/// the next needle. Names `target` when a needle is missing or +/// out of order. +fn assert_ordered(target: &str, text: &str, needles: &[&str]) { + let mut remaining = text; + for needle in needles { + let Some((_, after)) = remaining.split_once(needle) else { + panic!( + "the `make {target}` dry-run output must contain {needles:?} in order; {needle:?} \ + was missing after the previous marker; full output: {text:?}", + ); + }; + remaining = after; + } +} + +#[rstest] +#[case::probe_dev_build("dev-build")] +#[case::probe_dev_test("dev-test")] +fn dev_fast_target_respects_cargo_substitution(#[case] target: &str) { + let probe_cargo = "probe-cargo"; + let output = dry_run(target, probe_cargo).expect("make --dry-run must succeed"); + assert_ordered(target, &output, &[probe_cargo, "--config", "dev-fast"]); +}