Cargo Bylaw
Cargo Bylaw is an architecture enforcement framework for Rust, inspired by
ArchUnit. It builds a semantic dependency graph with rust-analyzer and checks
rules from either normal Rust tests or bylaw.toml.
cargo install cargo-bylaw
cargo bylaw check
If Cargo cannot find the installed subcommand, follow the PATH troubleshooting steps.
The initial rule library supports:
- Forbidden and allow-listed dependencies.
- Layered architecture.
- Cycle detection across modules, crates, or named slices.
- Actual source references and Cargo manifest declarations.
- Workspace modules, workspace crates, external crates, and toolchain crates.
Analysis runs on stable Rust and fails closed when semantic information is incomplete unless the user explicitly allows warnings.
Guardrails for coding agents
Cargo Bylaw gives coding agents an executable definition of the architecture. Agents can run the same check as CI, receive source-level dependency violations, and revise their changes instead of relying on architecture prose alone.
See Keeping coding agents aligned for a recommended agent instruction, workflow, and CI gate.
Documentation
- Start with Getting started.
- Add guardrails for coding agents.
- See the runnable model boundaries example.
- Read Semantic analysis for cfg, macro, and build-script behavior.
- Extend the framework with Custom rules.
API documentation
Published crates receive API documentation from docs.rs:
Getting started
Install the CLI
cargo install cargo-bylaw
cargo bylaw --version
Cargo discovers installed subcommands through PATH. Cargo installs binaries
into $CARGO_HOME/bin, which defaults to ~/.cargo/bin.
If Cargo reports no such command: bylaw on macOS or Linux, add the directory
to PATH, persist the same setting in the shell profile, and restart the shell:
export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:$PATH"
cargo bylaw --version
For PowerShell on Windows:
$env:Path += ";$HOME\.cargo\bin"
cargo bylaw --version
For architecture rules written as Rust tests, add the library as a development dependency:
[dev-dependencies]
bylaw = "0.1"
Configure the CLI
Create bylaw.toml beside the workspace Cargo.toml:
version = 1
[selectors.domain]
modules = ["shop::domain::**"]
[selectors.persistence]
modules = ["shop::persistence::**"]
[[rule]]
id = "domain-is-internal"
kind = "forbid-dependencies"
from = "domain"
to = "persistence"
scope = "both"
because = "domain policy must not depend on infrastructure"
Run the check. The check subcommand is required; cargo bylaw alone does not
run architecture rules.
cargo bylaw check
Write a Rust architecture test
#![allow(unused)]
fn main() {
use bylaw::analyzer::{AnalysisOptions, analyze_workspace};
use bylaw::prelude::*;
#[test]
fn architecture_is_valid() -> Result<(), Box<dyn std::error::Error>> {
let manifest_path = format!("{}/Cargo.toml", env!("CARGO_MANIFEST_DIR")).into();
let graph = analyze_workspace(&AnalysisOptions {
manifest_path,
..AnalysisOptions::default()
})?;
rules()
.forbid_dependencies(
"domain-is-internal",
modules(["shop::domain::**"]),
modules(["shop::persistence::**"]),
)
.check(&graph)?
.assert();
Ok(())
}
}
The graph is imported once and can be evaluated by any number of built-in or custom rules.
Analyze a different build configuration
Architecture depends on the selected features, target, and Cargo targets:
[analysis]
features = ["postgres"]
target = "x86_64-unknown-linux-gnu"
target_kinds = ["library", "binary"]
See Configuration for every option.
Keeping coding agents aligned
Coding agents are effective at making focused changes, but architecture is a workspace-wide concern. An agent can solve the immediate task while accidentally:
- Importing persistence types into the domain model.
- Exposing internal entities through a public API contract.
- Bypassing an application layer to call an adapter directly.
- Adding a convenient dependency that creates a cycle.
Architecture prose helps an agent understand intent, but prose alone is advisory. Cargo Bylaw turns that intent into executable feedback that applies to every change, regardless of whether it was written by a person or an agent.
The agent feedback loop
Keep bylaw.toml or Rust architecture tests in the repository:
- The agent reads the same architecture rules as the team.
- It makes the requested code change.
- It runs
cargo bylaw check. - Cargo Bylaw reports the violated rule, dependency direction, and source location.
- The agent revises the change until the architecture check passes.
This gives the agent an objective completion condition instead of relying on it to remember every boundary across a large codebase.
Add the requirement to agent instructions
Add guidance like this to AGENTS.md, .github/copilot-instructions.md, or the
instruction file used by the coding-agent platform:
## Architecture
- Treat `bylaw.toml` and the architecture tests as the source of truth for
dependency boundaries.
- Before completing any code change, run `cargo bylaw check`.
- Fix architecture violations rather than weakening, excluding, or deleting
rules.
- Do not use `--allow-incomplete` unless the task explicitly requires and
documents the exception.
If the project uses the Rust API instead of bylaw.toml, name the architecture
test command explicitly:
Before completing a change, run:
cargo test -p architecture-tests
Enforce the same rule in CI
Local agent instructions provide fast feedback. CI makes the boundary non-optional:
- name: Check architecture
run: cargo bylaw check
The default fail-closed behavior is important for automated changes. An unresolved path or unavailable macro expansion is an analysis failure rather than a success-shaped result that could let an agent introduce an unseen dependency.
Keep rules architectural
Rules are most useful to agents when they describe durable system boundaries, not temporary implementation details. Prefer rules such as:
- The domain cannot depend on persistence or transport models.
- API handlers cannot access database adapters directly.
- Feature slices must remain acyclic.
- Only the composition root may depend on every adapter.
The model boundaries example demonstrates these constraints in
both bylaw.toml and a normal Rust architecture test.
Model boundaries example
The repository includes a runnable application with separate domain, persistence, public contract, API, and composition-root crates:
shop-persistence --> shop-domain
shop-api ---------> shop-domain
shop-api ---------> shop-contract
shop-app ---------> all model and adapter crates
From the repository root, run the Rust architecture test:
cargo test \
--manifest-path examples/model-boundaries/Cargo.toml \
-p architecture-tests
Run the equivalent TOML rules through the CLI:
cargo run -p cargo-bylaw -- check \
--config examples/model-boundaries/bylaw.toml
The expected result is:
architecture checks passed (6 rules)
The test suite also contains real passing and intentionally failing Rust architecture tests:
cargo test -p bylaw --test architecture_test_e2e
The parent harness verifies that the passing test succeeds and that the failing test exits non-zero with the expected boundary and cycle diagnostics.
Architecture
Cargo Bylaw separates stable public contracts from the rust-analyzer integration:
bylaw-core <--- bylaw-config
^ ^
| |
bylaw-analyzer |
^ |
\ /
bylaw
^
|
cargo-bylaw
bylaw-core owns the immutable architecture graph, selectors, extension traits,
built-in rule specifications, and structured reports. It has no dependency on
Cargo or rust-analyzer.
bylaw-analyzer imports a Cargo workspace into that graph. All ra_ap_* types
remain private so a rust-analyzer upgrade cannot break downstream rule code.
bylaw-config strictly parses versioned bylaw.toml files and lowers built-in
rules into the same BuiltInRuleSpec values used by the Rust DSL.
bylaw provides the fluent test API, assertion integration, report formatting,
and re-exports needed by custom rules. cargo-bylaw is the stock CLI.
Graph model
Components are workspace crate targets, Rust modules, or external/toolchain crates. Canonical IDs distinguish Cargo package names, Rust crate names, and target kinds.
Edges have one of two scopes:
actual: a semantic source reference resolved by rust-analyzer.declared: a Cargo manifest dependency.
An edge retains all known evidence spans instead of duplicating graph edges. Rules can evaluate actual dependencies, declared dependencies, or both.
Analysis completeness is part of the graph. Unresolved paths, unavailable macro expansions, and skipped requested targets are diagnostics rather than silent omissions.
Configuration
Cargo Bylaw discovers bylaw.toml from the current directory upward, or accepts
--config. Unknown fields and unsupported configuration versions are errors.
Analysis
version = 1
[analysis]
manifest_path = "Cargo.toml"
packages = ["shop-api"]
features = ["postgres"]
target_kinds = ["library", "binary"]
incomplete = "deny"
proc_macros = true
build_scripts = true
[output]
format = "human"
Library and binary targets are analyzed by default. Tests, examples, benches,
build scripts, and proc-macro targets must be selected explicitly. incomplete = "deny" is the default; cargo bylaw check --allow-incomplete changes
incompleteness to warnings for that invocation.
Output is human by default and can be set to json; --format overrides the
configuration for one invocation. JSON output uses a top-level version field
and either a structured report or error object.
cargo bylaw check exits with 0 for a successful check, 1 for architecture
violations, 2 for configuration errors, and 3 for analyzer failures.
Selectors
[selectors.domain]
packages = ["shop-domain"]
[selectors.domain-modules]
crates = ["shop"]
modules = ["shop::domain::**"]
[selectors.serialization]
external_crates = ["serde", "serde_json"]
Values within a field are alternatives. Different fields are combined, so the
domain-modules selector means modules matching the path in the shop crate.
Rust path patterns use * for one :: segment and ** for zero or more.
Dependency rules
[[rule]]
id = "domain-is-internal"
kind = "forbid-dependencies"
from = "domain"
to = ["persistence", "api"]
scope = "both"
because = "domain policy must not depend on adapters"
[[rule]]
id = "domain-allowlist"
kind = "only-dependencies"
from = "domain"
allowed = ["domain", "serialization"]
scope = "actual"
allow_toolchain = true
allow_self = true
scope is actual, declared, or both.
Layers
[[rule]]
id = "application-layers"
kind = "layers"
scope = "both"
[[rule.layers]]
name = "domain"
selector = "domain"
[[rule.layers]]
name = "persistence"
selector = "persistence"
[[rule.dependencies]]
from = "persistence"
may_depend_on = ["domain"]
Omitting a layer from may_depend_on forbids that direction. Dependencies
within the same layer are allowed.
Cycles
[[rule]]
id = "model-crates-are-acyclic"
kind = "no-cycles"
within = ["domain", "persistence", "api"]
grouping = "crates"
scope = "actual"
Grouping can be components, modules, crates, or named slices.
Semantic analysis
Cargo Bylaw uses cargo metadata for workspace, target, and declared dependency
information and embedded rust-analyzer libraries for source semantics. It runs
on stable Rust and does not use rustc_private, nightly flags, or rustdoc JSON.
The importer resolves aliases, re-exports, qualified paths, active cfg
branches, declarative macros, workspace dependencies, renamed dependencies, and
external/toolchain crates where rust-analyzer provides a semantic target.
Configuration-dependent graphs
Features, target triples, target kinds, build-script output, and proc-macro availability change the imported graph. Architecture checks should use the same configuration as the build they protect.
Build-script analysis can execute project build scripts and write normal Cargo
artifacts. Proc-macro analysis can execute project procedural macros through
rust-analyzer’s proc-macro server. Disable either behavior in bylaw.toml when
that is inappropriate for the environment.
Incomplete analysis
Enforcement fails closed by default. Any unresolved semantic path, unavailable
macro expansion, or requested target that cannot be loaded is emitted as an
error diagnostic. Use --allow-incomplete or incomplete = "allow" only when
warnings and possible false negatives are acceptable; every omission remains in
human and JSON reports.
The project pins its ra_ap_* dependencies exactly because rust-analyzer’s
library APIs are not semver-stable. Upgrades must run the semantic fixture suite
before changing the pin.
Custom rules
Custom Rust rules use the analyzer-independent bylaw-core API. No
rust-analyzer type is exposed.
Most rules use three concepts:
DescribedSelector: chooses graph components and composes withand,or, andnot.DescribedCondition: evaluates selected component IDs and emitsConditionEventvalues.Rule: combines metadata, a selector, and a condition.
#![allow(unused)]
fn main() {
use bylaw::core::{
Candidate, ConditionEvent, DescribedCondition, DescribedSelector, Rule,
RuleMetadata,
};
let selector = DescribedSelector::new("workspace modules", |candidate: Candidate<'_>| {
matches!(candidate.component(), bylaw::core::Component::Module(_))
});
let condition = DescribedCondition::new(
"have at least one outgoing dependency",
|graph, selected| {
selected
.iter()
.filter(|id| graph.outgoing(id).next().is_none())
.map(|id| ConditionEvent::new(format!("{id} has no outgoing dependencies")))
.collect()
},
);
let rule = Rule::new(RuleMetadata::new("modules-have-dependencies"), selector, condition);
let report = bylaw::rules().with_custom(rule).check(&graph)?.into_report();
Ok::<(), bylaw::Error>(())
}
Implement Selector, Condition, or ArchitectureRule directly when a closure
is not sufficient. Public extension traits are Send + Sync, allowing rule sets
to remain shareable. Arbitrary custom Rust rules run in a test or purpose-built
binary; the stock TOML CLI supports serializable built-in rules only.
Releasing
Cargo Bylaw publishes its guide to GitHub Pages and its five workspace packages to crates.io.
GitHub Pages
The Docs workflow builds this mdBook and deploys the generated book/
directory with GitHub’s official Pages actions.
In the GitHub repository:
- Open Settings → Pages.
- Set Source to GitHub Actions.
- Push to
mainor run theDocsworkflow manually.
Build the site locally with:
cargo install mdbook --version 0.5.4 --locked
mdbook serve --open
crates.io
Create a crates.io API token and save it as the GitHub Actions repository secret
CARGO_REGISTRY_TOKEN.
All workspace crates use the same version. To publish 0.1.0:
git tag v0.1.0
git push origin v0.1.0
The Publish crates workflow verifies that the tag matches every workspace
package and publishes in dependency order:
bylaw-core
bylaw-analyzer
bylaw-config
bylaw
cargo-bylaw
The script waits for each version to reach the crates.io index before publishing its dependents. It also skips versions that already exist, so a partially completed release can be rerun safely.
Before the first release, validate the core package plus every dependent package’s manifest and file set without publishing:
PUBLISH_DRY_RUN=1 bash scripts/publish-crates.sh v0.1.0
Upgrading rust-analyzer
Cargo Bylaw embeds rust-analyzer through exactly pinned ra_ap_* crates. Those
library APIs do not provide normal semver compatibility, so update every
ra_ap_* crate in one change.
- Change all pins to the same release.
- Keep all adaptation inside
bylaw-analyzer; nora_ap_*type may appear in another crate’s public or private API. - Compile the focused analyzer fixtures first.
- Run the module-boundary end-to-end tests, the model-boundaries example, and the root self-enforcement check.
- Compare human and JSON diagnostics for source-span or canonical-name changes.
- Confirm the new crates’ MSRV before changing the workspace
rust-version.
Required validation:
cargo test -p bylaw-analyzer
cargo test -p bylaw --test model_boundaries
cargo test -p bylaw --test architecture_test_e2e
cargo test -p cargo-bylaw --test cli
cargo test --manifest-path examples/model-boundaries/Cargo.toml
cargo run -p cargo-bylaw -- check
Do not replace a failed semantic resolution with a silent syntax-only fallback. Add a completeness diagnostic or reject the workspace instead.