Developers / Codebase development standards

Codebase development standards

Good compiler work should be easy to read, easy to test and hard to misinterpret. These references cover implementation style, test design and the validation gates.

All of these must be followed by all contributors to meet the criteria for an acceptable PR.

Codebase style guide

This document owns implementation style for new and refactored compiler code.

Test strategy and test construction guide.

Final technical gates and completion checks guide.

Priorities:

  1. Readability
  2. Modularity
  3. Correctness and diagnostics

No user-input panics

Diagnostics

For test assertion policy, see testing.mtf.

Naming

format! and printing

Use the saying library macro say!() for user-facing stdout that may need colour styling. Use variables directly in format! strings whenever possible.

Imports

Use inline imports only when they are one level deep and improve local reasoning. Import long paths at the top, including types used once in signatures.

// ACCEPTABLE: terse and locally clear.
let type_id = builtin_type_ids::BOOL

// BAD: import this long path at the top.
let type_id = crate::compiler_frontend::datatypes::ids::builtin_type_ids::BOOL

Avoid aliasing unless it clearly improves readability.

Code style and organisation

For test-file placement, see testing.mtf.

Readable Rust style

Compiler code should optimise for fast review. Dense code is harder to scan and easier to extend badly.

Prefer code that reads as a sequence of named steps and makes data flow obvious before every detail is understood. The main function for a module or pass should also read as a sequence of named steps.

Use named intermediate values, short single-purpose functions, explicit multi-step control flow, narrow helpers, context/input structs, enums for meaningful states, comments as landmarks and visible spacing. Avoid dense expression chains, clever iterator nesting, large inline matches and logic compressed into one expression. Brevity is less important than clarity.

Vertical spacing

Use vertical spacing to show structure.

Required:

Prefer this:

let source_file = header.source_file.clone();
let visibility = binding_environment.visibility_for(&source_file)?;

// Resolve type aliases before constants because constant folding may depend on alias-expanded types.
self.resolve_type_aliases(&headers, visibility, string_table)?;

self.resolve_constant_headers(&headers, visibility, string_table)?;

Comments as reading landmarks

Comments must explain non-local intent, not restate the code. Write for programmers reading the codebase for the first time so they can understand local code without first reading distant owners. Stay comment-positive: keep the comments that carry ownership, invariants and failure context, and drop the ones that only narrate syntax.

File-level docs are the primary place for subsystem role, ownership, exclusions and detailed WHAT/WHY context. A reader should learn the architecture from the file header and mod.rs map, then use item comments only where local code is not enough.

Detailed item comments belong on complex functions, stage joins, ordering constraints, invariants, unusual diagnostics and non-obvious data flow. Tiny accessors, constructors, fields, forwarding methods and obvious private helpers need no comment, or at most one short sentence.

WHAT/WHY headings are optional landmarks, not a required template. Use them when they make a non-local contract easier to find. Do not add them to every item.

Keep comments aligned with the current design. Migration history belongs in Git. Do not justify obsolete adapters, compatibility wrappers or deleted owners in comments.

Test-only implementation belongs with tests. Production files must not grow test-only constructors, mutators, semantic variants or convenience lookups just to support fixtures.

Be grammatical and readable. Do not narrate syntax, repeat a descriptive name or paraphrase a clear signature.

// Register same-file declarations before dependencies so aliases cannot shadow local names.
for declaration in same_file_declarations {
    visible_names.register_same_file_declaration(declaration)?;
}

// Prelude names are registered as reserved names first, then inserted only if not shadowed.
for symbol in external_package_registry.prelude_symbols() {
    visible_names.reserve_prelude_symbol(symbol)?;
}

Production layering and stage ownership

Stage separation is a dependency rule, not only a naming preference. Production code must respect these directions.

Prefer narrowed Rust visibility for these rules wherever the module tree can express them. Use a repository source check only for dependency directions Rust visibility cannot encode.

Tests may target stage-local APIs from their owning module. Later build, link and backend code may still consume completed HIR, function IDs, link facts and borrow results where the architecture documents that handoff. The rule bans external orchestration of the stages, not legitimate consumption of their final data.

Refactor moves

When moving code across compiler stages, do not copy the old module shape unchanged. Use the move to correct ownership, names, APIs, comments and data flow.

Moved code must:

Do not preserve a bad API shape because it already exists.

API breakage

Type ordering

Order types in a file from higher-level abstractions to lower-level supporting types.

Iterators versus loops

Function size

Functions should usually stay under ~200 lines. Longer functions are fine when they still represent one coherent operation, such as a compiler transformation, state machine or tightly coupled sequential process.

Split functions when they mix unrelated responsibilities, are hard to test or no longer match their name.

Match readability

Large matches should be grouped by meaning.

Use blank lines and short comments to make each group obvious. If a match grows too large, extract branch handling into named helpers.

Prefer:

match resolution {
    // Constants create dependency edges.
    ConstantReference::SourceConstant { path, source_file } => {
        add_constant_dependency(header, path, source_file)?;
    }

    // Type names are valid to resolve but do not create value dependency edges.
    ConstantReference::SourceTypeAlias { .. } => {}

    // These are structurally invalid in constant initializers.
    ConstantReference::SourceNonConstant { path }
    | ConstantReference::ExternalNonConstant { .. } => {
        errors.push(non_constant_reference_error(path, location.clone()));
    }

    // Unknown names may produce better diagnostics during AST expression parsing.
    ConstantReference::Unknown => {}
}

Avoid long ungrouped matches where every branch looks equally important.

Avoid clever Rust

Don't use advanced Rust features just to make code shorter.

Avoid:

Prefer named types and straightforward flow:

pub(crate) struct DependencyResolution {
    pub(crate) target: ResolvedDependencyTarget,
    pub(crate) local_name: StringId,
    pub(crate) location: SourceLocation,
}

Instead of:

Result<(ResolvedDependencyTarget, StringId, SourceLocation), CompilerError>

A few more lines are fine when the result is easier to inspect and harder to misuse.

Macros

Keep macro usage minimal. Use small declarative macros only where they clearly reduce repetition and avoid creating procedural macros.

Warnings and lints

Section banners

Use section banners only for real phase boundaries in long functions, large files or complex orchestration such as parsing, lowering, template folding, dependency resolution, diagnostic rendering or build flow.

Use exactly this format:

// ------------------------
//  Resolve template slots
// ------------------------

The banner has three lines. The title starts with // and uses title case or a clear imperative name. The top and bottom start with // and their dashes extend equally beyond the title on both sides. Do not use banners around small helpers or to hide a file that should be split.

Returning errors

The diagnostic lane rules above apply to every local error boundary.

// Typed diagnostic constructor.
return Err(CompilerDiagnostic::invalid_assignment_target(
    InvalidAssignmentTargetReason::ImmutableVariable,
    variable_name,
    location,
));

// Internal bug path.
return_compiler_error!(
    "Unsupported AST node type: {:?}",
    node_type; {
        CompilationStage => "AST Processing",
        PrimarySuggestion => "This is a compiler bug - please report it"
    }
);

Codebase documentation

This section covers codebase doc comments and small requested documentation edits. For substantial documentation work, follow the task's dedicated writing guidance and the surrounding source.

For small edits:

Testing standards

This document owns test selection, test structure, assertions, fixture design and test pruning for compiler work.

It does not define the final validation command. For completion gates, read docs/src/developer-docs/style-guide/validation.mtf.

The primary goal is end-to-end language and artifact correctness. Prefer real usage patterns and complete Moth snippets over narrow implementation-shaped tests.

Task-reading guide

Heading paths use > to name nested sections. Read the selected heading through the next heading of the same or higher level, including nested subsections unless the route narrows further. Read this document in full for test infrastructure, suite policy, broad fixture cleanup or audits.

Task

Read

Choosing the test owner, level or location

  • Default testing preference
  • Supporting rules
  • Test ownership
  • Test location and module layout

Unit or subsystem-invariant coverage

  • Unit tests
  • HIR and internal-IR assertions when IR or side-table facts are involved
  • Fixtures and temporary files

User-visible language or project behaviour

  • Integration cases
  • Successful backend intent
  • Focused integration selection and inventory

Runtime output or reactivity behaviour

  • Runtime output assertions
  • Backend matrices and artifact assertions when structure also matters

Threads, shared state, timing or repeated execution

Concurrency and repetition

Diagnostics or warnings

  • Diagnostics assertions
  • Integration cases

Backend structure, target parity, Wasm validation or goldens

  • Successful backend intent
  • Backend matrices and artifact assertions
  • HIR and internal-IR assertions only for hidden compiler invariants

Manifest policy, suite inventory, filtering or audit behaviour

  • Successful backend intent
  • Focused integration selection and inventory

Adding or changing a Cargo feature, or a feature- or platform-gated test

Feature and platform lanes

Asserting on source text, or adding an architecture ban

Source-text assertions

Fixtures, temporary paths or test support

Fixtures and temporary files

Removing duplicate or obsolete coverage

  • Pruning and duplicate coverage
  • Benchmarks are not correctness tests

Final coverage review

Testing checklist

Default testing preference

Integration tests written in Moth are the default for user-visible language behaviour. They exercise the real compiler path and usually provide the strongest regression coverage.

Supporting rules

Test ownership

Behavior

Owner

Pure data or invariant behavior

Focused unit test near the owning subsystem

Compiler-stage invariant

Stage-local unit test naming the invariant

Stage-boundary orchestration

Minimal pipeline/build smoke test

User-facing language behavior

Integration case under tests/cases/

Backend artifact behavior

Backend-specific artifact assertions or contractual goldens

Side-table or hidden compiler fact

Focused unit test if external output cannot expose it

Cross-backend semantic parity

One integration input with backend-specific assertions

Each behavior should have one primary owner. Secondary coverage is justified only when it protects a distinct boundary, target, diagnostic lane or internal invariant.

Test location and module layout

src/compiler_frontend/hir/tests/
src/compiler_tests/
tests/cases/

Unit tests

Retain unit tests for:

Prefer integration tests for user-visible behavior. Once a subsystem is stable, prune superseded unit tests. Rewrite obsolete tests rather than preserving old API shapes. A unit test should name the invariant it protects. Do not use unit coverage as a substitute for an end-to-end case when user behavior changed.

Integration cases

Successful backend intent

Every successful HTML and HTML-Wasm backend run applies its universal backend baseline. The baseline protects harness and target invariants. It is not the case's authored semantic contract.

Use success_contract = "acceptance_only" only when that backend has no stronger case-specific semantic, artifact, golden, absence or expected-warning assertion.

[backends.html]
mode = "success"
warnings = "forbid"
success_contract = "acceptance_only"

Acceptance-only intent does not disable the baseline. The audit records both baseline_applied = true and acceptance_only = true, with backend_baseline and acceptance_only in assertion_kinds.

Do not combine acceptance-only intent with artifact assertions, golden mode, rendered-output assertions, artifact-absence assertions or an authored expected-warning contract. A whole case whose backends are all acceptance-only uses role = "smoke". A mixed-backend case may use a boundary or backend role when another backend owns a stronger contract.

A successful backend with only the universal baseline is invalid. Typed fixture loading rejects it before policy evaluation, listing, audit, or execution.

For new type-system syntax, include:

Canonical fixture shape:

tests/cases/
├── manifest.toml
└── case_name/
    ├── input/
    │   ├── main.moth
    │   └── helper.moth
    ├── expect.toml
    └── golden/
        ├── html/
        └── html_wasm/

Manifest entry shape:

[[case]]
id = "borrow_conflict_resolved_by_reordering"
path = "borrow_conflict_resolved_by_reordering"
tags = ["integration", "borrows"]
contract = "language.bindings.alias_final_use_allows_mutable_rebinding"
role = "primary"

Focused integration selection and inventory

Use the canonical case ID and retained metadata for local iteration:

cargo run --quiet -- tests --case arithmetic_operator_precedence --backend html
cargo run --quiet -- tests --tag borrows --tag diagnostics --backend html
cargo run --quiet -- tests --contract <contract-id>
cargo run --quiet -- tests --list --tag borrows

Use the audit mode to validate and inventory the complete canonical suite without compiling cases:

cargo run --quiet -- tests --audit

Audit writes its JSON inventory before returning hard-policy failure. Normal listing and execution use the same policy evaluator and reject hard findings before compiling a case. Missing roles, missing contracts on non-smoke cases, primary cases without contracts and duplicate primary owners are hard findings. Contractless whole-case acceptance-only smoke cases are valid.

Audit cannot be combined with filters or --list. It writes the reporting-owned JSON inventory to target/test-reports/integration_suite_inventory.json, including canonical metadata, backend expectation facts, hard-policy violations and primary-less contract-family advisories. The report is review input only; it never deletes, merges or rewrites fixtures.

The schema 8 inventory reports acceptance-only, rendered-output, artifact, golden, absence and expected-warning backend counts, plus the zero baseline-only invariant. It reports exact, ordered and exact-once runtime contracts separately. Per-backend fields keep baseline_applied, acceptance_only, assertion_kinds, golden_present, golden_mode, every rendered-output form, exact warning codes, diagnostic match mode and any contains-mode reason separate so the report matches what ran.

Schema 7 added the weak-contract review fields. smoke_role_cases, warning_ignore_backend_blocks and diagnostic_contains_backend_blocks count the three legal weak contracts, and weak_contract_review_backend_blocks counts blocks carrying at least one of them. Every backend block lists its own weak_contract_reviews. These are review fields, not policy: a smoke case, an ignored warning contract and a contains-mode failure all remain valid. The canonical suite currently declares none of them, so any non-zero count is a new weaker contract to review rather than an inherited one to explain.

Schema 8 added report identity. Every report carries run — a run id, the command, the host OS and architecture, the features the binary was built with, the runner thread count and whether the run finished — and the inventory replaced repository_commit with repository_revision, which reports a discovered commit, not_a_repository, or unknown with the reason discovery failed. A failed discovery and a genuine absence are different facts, and a report that names neither its command nor its build cannot be told apart from a stale one.

Reports are written to a sibling temporary file and renamed into place, so a reader never sees a partial file. Before the work begins, each report is replaced by one whose run says `completed: false`; an interrupted run therefore leaves a report that says it did not finish rather than the previous run's passing output.

Hard policy has one suite-policy owner. Missing roles, missing non-smoke contracts, duplicate primary contracts, a primary without a contract and a whole-case acceptance-only fixture without role = "smoke" are hard findings. Normal list and execution modes reject hard findings before selection or compilation. Audit still writes the complete JSON report, then returns failure. Primary-less contract-family advisories remain non-fatal.

Runtime output assertions

Runtime assertions use one Node harness. It records console output and HTML fragment inserts in one event array when each event happens, then joins their text with newlines in that same order. Channel views are derived from the event array. They never reconstruct chronology after execution.

The harness runs only when a backend declares a rendered-output assertion. It executes the emitted page scripts and waits for one documented microtask tick so scheduled reactive updates can flush. Do not make extra scheduler turns or incidental microtask counts contractual.

Choose the narrowest field that owns the behaviour:

[backends.html]
mode = "success"
warnings = "forbid"
rendered_output_contains = ["active-root"]
rendered_output_not_contains = ["imported-root"]
rendered_output_contains_in_order = ["before-loop", "after-loop"]
rendered_output_contains_exactly_once = ["active-root", "mounted"]

Use exact output for small deterministic behaviours such as loop or map operation results. Use ordered and exact-once fragments when unrelated output may coexist. Prefer these runtime contracts over generated JavaScript text or a whole-page golden when execution is the real owner.

An exact mismatch is reported escaped, post-normalization, with the byte offset of the first difference, so a whitespace-only difference does not print as two identical lines.

What the harness will execute

The harness parses the emitted page for a supported script shape and rejects everything else rather than guessing:

Harness failure classes

A harness problem is never reported as a case failure. RenderHarnessError names which boundary broke: Artifact, Workspace, Spawn, Timeout, ExitStatus, OutputDecoding, OutputProtocol, ScriptShape or Cleanup. Execution is bounded by a 30-second deadline with kill-and-reap, stdout and stderr are drained on their own threads under a 4 MiB bound, and a stderr decode failure never replaces the boundary that actually failed.

Goldens carry their own artifact kind

A golden file's extension decides the artifact kind it expects, and a cross-kind match is rejected before content is read. Identical bytes emitted as a different artifact kind do not satisfy a golden. Text goldens require strict UTF-8; invalid UTF-8 is a harness failure, not a rewrite.

Concurrency and repetition

A test that waits for a thread to reach a state must observe that state, not guess how long reaching it takes.

Stateful owners — the timing collector, the frontend counter stores, output writes, current-directory guards and the Node harness — only misbehave under a thread schedule or a repeat that one default-parallelism run never produces. That schedule has an owned command:

just stress

It repeats the unit and integration suites at one thread, default parallelism and sixteen threads, and keeps running after a failing lane so the whole matrix is visible.

Diagnostics assertions

[backends.html]
mode = "failure"
warnings = "forbid"
diagnostic_codes = ["MOTH-RULE-0033", "MOTH-RULE-0033"]

Use diagnostic_assertions when a broad code needs a compiler-owned reason or when source remapping is part of the contract. Each table selects one diagnostic by stable code and one-based occurrence. A unique code may omit occurrence. Repeat the code in diagnostic_codes to preserve its exact multiplicity.

[[backends.html.diagnostic_assertions]]
code = "MOTH-RULE-0057"
reason = "invalid_generic_instantiation.recursive_function_instantiation"
path = "input/@page.moth"
line = 3
count = 1

[[backends.html.diagnostic_assertions.secondary_labels]]
occurrence = 1
path = "input/helpers.moth"
line = 2

Use diagnostic_match = "contains" only when intentional independent recovery or cascades make additional diagnostics part of the accepted current behaviour. Every contains-mode block needs a substantive diagnostic_match_reason. The suite audit reports the reason and treats a missing or blank reason as a hard policy finding.

diagnostic_match = "contains"
diagnostic_match_reason = "Independent recovery may emit additional diagnostics from another source unit."

Warnings use a separate backend-local contract:

[backends.html]
mode = "success"
warnings = "exact"
warning_codes = ["MOTH-RULE-0022", "MOTH-RULE-0022"]
rendered_output_contains = ["warning-contract result=one"]

Backend matrices and artifact assertions

Use one expect.toml per case with backend-specific assertion blocks.

entry = "."

[backends.html]
mode = "success"
warnings = "forbid"
artifacts_must_not_exist = ["unexpected.html"]

[backends.html_wasm]
mode = "success"
warnings = "forbid"

[[backends.html_wasm.artifact_assertions]]
path = "page.wasm"
kind = "wasm"
validate_wasm = true
must_export = ["memory", "moth_str_ptr", "moth_str_len", "moth_release"]
golden/<backend>/

HIR and internal-IR assertions

Feature and platform lanes

A feature-gated test that no command runs is not a test. Every declared Cargo feature belongs to a lane. Standard lanes run in the feature matrix. Deliberately expensive developer systems may use an opt-in lane with its own named command:

just test-feature-matrix

Lane

Features

Only this lane covers

default

none

the shipped configuration and every cfg(not(feature = ...)) branch

timers

timers

the timing collector, boundary identities and command/build timing tests

detailed-timers

detailed_timers

AST substage timings and the detailed-only summary shape

counters

benchmark_counters

counter-only builds, where counters record without a timing collector

timers-counters

timers, benchmark_counters

collector-backed counters and the counter summary carried by a timing session

scoped-blocks

checked_blocks, async_blocks

the deferred-feature diagnostics for checked: and async: blocks

dev-output

every show_* feature

the developer stage-dump branches, which no other lane compiles

xtask

none

the benchmark, profiling and process-runner tests in the xtask package

boracle

boracle

the reference-solver developer gate, owned by just boracle and excluded from the standard matrix

boracle-campaign

boracle, boracle_campaign

the measured generated differential campaign, owned by just boracle-campaign: 2048 deterministic generated shapes per cyclic mode, 4096 problems and 8192 comparisons, classified through the same differential comparison the boracle lane uses

Lanes are package-scoped, and that is not a stylistic choice. Cargo unifies features across one resolve graph, and xtask depends on moth with features = timers, so cargo test --workspace always builds the compiler with timers enabled. It can never run the default configuration, and --features benchmark_counters on a workspace command is really timers plus counters. Only cargo test -p moth with an explicit feature list configures the crate the way a lane claims.

Feature coverage is checked mechanically, without running a lane:

just feature-lane-check

The check reads Cargo.toml and every cfg attribute in src and xtask/src, then fails when a declared feature has no lane, when a lane names a feature its package does not declare, or when a cfg names a feature that does not exist. A misspelled feature name in a cfg is a test that can never compile and never reports, which is why the check reads the tree instead of a list in prose. It writes target/test-reports/feature_lane_coverage.json, which names, for every feature, its standard lanes, opt-in lanes and the files whose cfg attributes mention it. That report is a coverage map and states no lane outcome, because the check that writes it runs no lane. Running the standard lanes is just test-feature-matrix, which writes their outcomes to target/test-reports/feature_matrix_results.json. The opt-in boracle and boracle-campaign lanes run only through their named just boracle and just boracle-campaign commands, not through the standard lane set.

Adding a Cargo feature means adding it to a standard or opt-in lane in the same change. The gate fails otherwise.

Platform-gated tests are owned by the platform that has the API:

Gate

Runs on

Owns

cfg(unix)

Linux and macOS

symlink identity and retargeting, non-regular nodes, unreadable paths and non-UTF-8 names

cfg(windows)

Windows

extended-length path prefixes, Windows canonicalisation and hard-link inspection

cfg(any(unix, windows))

all three

the portable policy shared by both families, asserted on each

cfg(target_os = "linux")

Linux

non-UTF-8 filesystem identity, which macOS rejects at the filesystem boundary

CI runs every gate on Linux, macOS and Windows, so a platform-gated owner executes wherever its API exists. A platform-specific test may be absent only when the underlying API does not exist there; an equivalent portable policy test should still run everywhere. Windows results are currently non-blocking — validation.mtf owns that status and the conditions that end it.

Source-text assertions

A test that reads source text and asserts on it proves what the text says, not what the code does. An alias, a reformat or an equivalent reimplementation all pass it.

Fixtures and temporary files

Pruning and duplicate coverage

Benchmarks are not correctness tests

Benchmark fixtures are performance evidence, not correctness coverage.

Do not use benchmark coverage as a substitute for unit or integration tests. Negative diagnostics belong in tests/cases, not benchmark fixtures.

Use bench-check to detect broad performance regressions after correctness has already been established by ordinary tests.

Testing checklist

Before considering test coverage complete, check:

Validation gates

This document defines the technical gates a completed compiler or documentation slice must pass.

The required final gate depends on whether the slice changes implementation or is strictly documentation-only.

Task-reading guide

Start every completion decision with Choose the final gate by scope. Heading paths use > to name nested sections. Read the selected heading through the next heading of the same or higher level, including nested subsections unless the route narrows further. Read this document in full when changing validation infrastructure or auditing the complete gate policy.

Task

Read

Select the required final gate

Choose the final gate by scope

Choose direct compiler or Cargo invocation

Compiler invocation

Complete or review code-bearing work

  • Code-bearing final gate
  • Formatting
  • Manual architecture audits when its trigger applies
  • Progress-matrix review
  • Benchmark safety
  • Failure handling
  • Code-bearing completion checklist

Complete or review documentation-only work

  • Documentation-only release-build gate
  • Generated documentation
  • Failure handling
  • Documentation-only completion checklist

Add or change a Cargo feature, or a feature-gated test

Feature lanes

Add or change a broad-source architecture ban

Source audit

Read, change or interpret a CI gate

  • CI gates
  • Platform policy

Pick a targeted iteration command

Fast iteration commands

Performance or benchmark validation

  • Benchmark safety
  • Code-bearing final gate when the slice is being completed

Review a failed gate or a validation claim

  • Failure handling
  • the checklist for the selected final gate

Compiler invocation

Use an up-to-date release build of the compiler when one is available. The moth command in PATH is the usual direct form:

moth check docs --terse
moth build docs --release

If a suitable release build is not available, run the same operations through Cargo from the repository root:

cargo run --quiet -- check docs --terse
cargo run --quiet -- build docs --release

Choose the final gate by scope

Slice

Required final gate

Code-bearing or mixed change

just validate

Strictly documentation-only change

moth build docs --release or the equivalent Cargo invocation

The justfile is the executable authority for the full code-bearing gate.

Code-bearing final gate

A slice is code-bearing if it changes Rust, compiler or build-system sources, libraries, tests, benchmarks, manifests, scripts, configuration or any other non-documentation implementation file.

Before declaring a non-trivial code-bearing slice complete, run:

just validate

What just validate runs

Gate

Current command

Purpose

Clippy

just ci-clippy-native

Native-host linting of every workspace target under the explicit standard feature set, warnings denied

Feature lane coverage

just feature-lane-check

Proves every declared Cargo feature has an executing lane, and that no cfg names a feature that does not exist

Rust unit tests

cargo test --workspace --quiet -- --format terse

Rust unit and subsystem tests across the complete workspace, in the workspace-unified feature configuration

Compiler integration tests

cargo run --quiet -- tests --terse

Canonical suite policy followed by end-to-end compiler cases

Documentation check

cargo run --quiet -- check docs --terse

Moth docs-source checking inside the full gate

Benchmark sanity check

cargo run --package xtask --bin xtask -- bench-ci

Complete benchmark preflight followed by bounded non-recording measurements

Complexity budgets

cargo run --package xtask --bin xtask -- bench-scaling

Fits the growth exponent of every declared scaling series against its declared input sizes and fails when one exceeds its budget

Source audit

just source-audit

The one broad-source architecture audit: cfg! timer checks, direct clocks, collector and runtime calls outside the facade, closure wrappers, unguarded timer-only fields, obsolete timer macros, provisional and hard-coded metric names, and the removed-name tripwires. Reports typed findings and writes source_audit.json

Timer erasure gate

just timers-erasure-check

Builds a no-timer release binary and proves that no timer-only marker, and no name from the schema-owned metric inventory, survives into its bytes

The integration command loads the complete canonical suite and applies the shared hard-policy evaluator before selection or compilation. This makes missing ownership, duplicate primaries and other hard suite-policy findings part of the normal validation gate.

Test honesty audit

The campaign that hardened this suite is owned by one command:

just test-honesty-audit

It writes the canonical inventory to target/test-reports/test_honesty_inventory.json, and it is the single owner of what that file says. The inventory has four parts:

The tracked durable copy under docs/roadmap/evidence/ is refreshed by the same owner, never by hand:

just test-honesty-evidence

What a report guarantees

Every machine-readable report is written to a sibling temporary file and renamed into place, so a reader never observes a partial file and a failed write leaves the previous report intact.

Every report also carries the identity of the run that produced it: an id unique within the process without depending on the wall clock, the command, the OS and architecture, the features the linked compiler was built with, a thread count where the command owns one, and whether the run finished.

Completion state is what separates the reports, and the distinction is real:

Source audit

An architecture ban whose scope is "no file anywhere may contain this" is not a behaviour claim, and a unit test that reads one file's text and asserts on it is not behaviour evidence. Those bans have one owner:

just source-audit

It walks src and xtask/src once, applies every broad-source rule, and reports typed findings with the file each was found in. Only the audit's own implementation files are exempt, because they necessarily contain the fragments the rules search for; their rules are proved against fixture text instead. A source file that cannot be read is a finding, not a file the audit skips.

Add a broad-source ban here rather than as a test. A ban that lives in a test reads one file, calls that evidence, and stops applying the moment the code moves.

Feature lanes

just validate runs one standard feature configuration. The standard lanes run all standard configurations:

just test-feature-matrix

Eight package-scoped standard lanes cover the default configuration, timers, detailed_timers, benchmark_counters, timers with benchmark_counters, the scoped-block feature lane, every show_* feature and the xtask package. testing.mtf > Feature and platform lanes owns the lane table and what each lane uniquely covers.

The boracle feature is an opt-in developer lane. just boracle owns its formatting check, feature-enabled Clippy and smoke test. The boracle-campaign lane, using boracle_campaign, is a separate opt-in developer lane owned by just boracle-campaign. It was measured at roughly 61 seconds against roughly 8 seconds for the default lane, so it cannot belong in the default lane. Both lanes are intentionally excluded from just validate, just test-feature-matrix and ordinary CI gates.

Lanes use cargo test -p <package> rather than --workspace because Cargo unifies features across one resolve graph. xtask requires moth with timers, so no workspace-wide command can run the default configuration.

just feature-lane-check proves the mapping without running a lane, and is part of just validate. The report distinguishes standard and opt-in lane owners. Adding a Cargo feature means adding it to a standard or opt-in lane in the same change; the check fails otherwise.

CI gates

just validate is the local gate and is deliberately fail-fast: the first failure stops the run.

CI does not work that way. Each validation family is its own job, so one failure never stops another from reporting:

Gate

Command

clippy

just ci-gate-clippy

unit-tests

just ci-gate-unit-tests

feature-matrix

just ci-gate-feature-matrix

integration

just ci-gate-integration

docs

just ci-gate-docs

benchmarks

just ci-gate-benchmarks

scaling

just ci-gate-scaling

timers-erasure

just ci-gate-timers-erasure

source-audit

just ci-gate-source-audit

honesty-audit

just ci-gate-honesty-audit

Each gate runs on Linux, macOS and Windows. The workflow result is still failed when any blocking gate fails. The integration, feature-matrix and honesty-audit gates upload target/test-reports/ whether they passed or failed, because a failed run's report is the evidence a reviewer needs most.

unit-tests and feature-matrix overlap on purpose. unit-tests runs the workspace-unified command a developer runs locally; feature-matrix runs the configurations that command cannot reach.

The release workflow keeps the single fail-fast just validate. A tag build only needs to know whether the revision is releasable, and the split gates have already reported on every push to the default branch.

Platform policy

Linux and macOS gates are blocking. Windows gates are non-blocking: a Windows failure is reported but does not fail the workflow or hold back deployment.

Windows becomes blocking when both hold:

The change is then removing continue-on-error from the gate job. Until that happens, a Windows result is evidence to act on, not a gate that can be ignored: a Windows-only failure belongs in the ledger like any other.

A platform-specific test may be absent only when the underlying API does not exist on that platform. An equivalent portable policy test should still run everywhere. `testing.mtf > Feature and platform lanes` owns which gate owns which platform behaviour.

Documentation-only release-build gate

A slice is documentation-only only when every modified file is documentation source or generated documentation.

Allowed examples include:

A documentation-only slice must not change Rust, tests, libraries, build configuration, manifests, scripts, benchmark code or fixtures, compiler configuration or other executable project sources.

For a strictly documentation-only slice, run one documentation release build using a current compiler.

The direct release-binary form is:

moth build docs --release

If no suitable release build is available, use cargo run --quiet -- build docs --release instead.

Then:

Do not additionally run just validate, Clippy, unit tests, integration tests or benchmark checks for a documentation-only slice. A Cargo invocation of the compiler is an alternative way to run the same documentation gate, not an extra gate.

If any non-documentation file changed, the documentation-only exception does not apply. Use the full code-bearing gate.

Fast iteration commands

Task

Command

Unit-test iteration

cargo test --workspace --quiet -- --format terse

Integration iteration

moth tests

Clippy iteration

just ci-clippy-native

Feature lane coverage

just feature-lane-check

Every standard feature lane

just test-feature-matrix

Boracle feature lane

just boracle

Boracle campaign lane

just boracle-campaign

Broad-source architecture audit

just source-audit

Test honesty audit and inventory

just test-honesty-audit

Durable honesty evidence refresh

just test-honesty-evidence

Docs-source iteration

moth check docs --terse

Bounded benchmark sanity

just bench-ci

Benchmark-only preflight

just bench-validate

Full CLI benchmark check

just bench-check

Full frontend benchmark check

just bench-frontend-check

Documentation release-build iteration

moth build docs --release

Complete code-bearing gate

just validate

Passing one targeted command does not imply a code-bearing slice passes the complete gate.

Formatting

The ship recipe formats before validation, while validate itself does not.

Generated documentation

Do not edit files under docs/release/** directly.

The documentation release build is:

moth build docs --release

A generated diff must result from documentation source changes, not manual HTML edits.

Manual architecture audits

When a code-bearing slice changes stage ownership, frontend boundaries, HIR, diagnostics, types or backend handoff, perform a manual architecture audit in addition to the automated gate.

Audit that:

This full architecture audit is not required for a prose-only documentation change.

Progress-matrix review

Review the progress matrix whenever behaviour, support, rejection, backend coverage or test coverage changes.

Update it when current status changed. Do not make a meaningless edit for a pure refactor or prose-only correction.

Benchmark safety

Failure handling

Code-bearing completion checklist

Before declaring a non-trivial code-bearing slice complete, check:

Documentation-only completion checklist

Before declaring a documentation-only slice complete, check: