Rulebearing: target architecture
Status: to-be architecture, version 1, 2026-09-20. This document describes the system the plans build. It derives from the design document and the two coverage tabs (dependency-cruiser 18.2.0, ArchUnitNET 0.13.4); every decision it relies on is an ADR. Requirements are in the PRD.
Contents
- Purpose and scope
- Architectural drivers
- System context
- The five stages
- Crate layout
- The graph document
- Extractors
- Configuration and the rule language
- The rule engine
- Outputs and CI contract
- Agent surface
- Distribution
- Technology choices
- Security posture
- Performance model
- Verification strategy
- Repository layout
- Risks and their mitigations
- Glossary
Purpose and scope
Rulebearing is one open-source command-line tool and one rule language that does what dependency-cruiser does for TypeScript and JavaScript, what ArchUnitNET does for .NET, and what import-linter does for Python, over a single graph. A repository with more than one language keeps one set of architecture rules, one gate, and one answer to "may this file import that one" (design § Why).
In scope: the binary, its three extractors, the graph document, the rule engine, every reporter, the subcommands, the npm, NuGet and pip wrappers, the test-runner adapters, the ESLint plugin, the Roslyn analyzer, the MCP and LSP servers, and the conformance harness. Out of scope: any cross-language edge the languages do not declare (ADR-0014), and arbitrary-code predicates, which stay in ArchUnitNET.
Architectural drivers
| Driver | Source | Architectural consequence |
|---|---|---|
| Superset, precisely. Every dependency-cruiser 18.2.0 and ArchUnitNET 0.13.4 feature has a row with a status; nothing is dropped. | design § Why, coverage tabs | Two conformance gates are required checks (ADR-0009); the module layer is the cruise-result schema unchanged (ADR-0004). |
Drop-in for a heavy dependency-cruiser user. The config file, the JSON field names, fmt, the err reporter and the exit code must be identical on day one. |
design § What a heavy dependency-cruiser user needs | Both config formats load into one model (ADR-0005); JavaScript configs run in an embedded sandbox (ADR-0006). |
| The graph is a first-class artefact. Other guards read it. | design § The run and its consumers | One extraction feeds the gate, every ratchet, the MCP server and fmt. |
| Liveness is checked. A rule matching nothing fails. | design § Why | Vacuous rules exit 2 by default in a native configuration and warn in a dependency-cruiser one (ADR-0007, ADR-0032). |
| One binary, no runtime. Must run in a TypeScript, a .NET and a Python pipeline. | design § Language decision | Rust, thin wrappers (ADR-0002, ADR-0020). |
Agent-shaped. Line-precise findings, fix text, a token-budgeted reporter, sub-two-second affected runs. |
design § Rules an agent can implement and follow | Stable ids and line/column on every edge (ADR-0015); the CLI is the primary agent surface (ADR-0021). |
| One maintainer. | design § What Rust does not solve | The suites are the reviewer; the crate layout keeps each piece recognisable to a specialist in its language (ADR-0010); 70% coverage is a floor (ADR-0018). |
System context
flowchart LR
subgraph Inputs
TS["TypeScript / JavaScript sources<br/>tsconfig, package.json, Babel, webpack"]
DN["Built .NET assemblies + portable PDBs<br/>sln, csproj"]
PY["Python sources<br/>pyproject"]
CFG["rulebearing.yaml<br/>or .dependency-cruiser.*"]
end
RB["rulebearing<br/>(one static binary)"]
TS --> RB
DN --> RB
PY --> RB
CFG --> RB
subgraph Consumers
CI["CI gate<br/>exit code, err, github-annotations, sarif, junit"]
JSON["cruise.json<br/>ratchets, ADR guards, importer finders"]
AG["Coding agents<br/>agent reporter, hooks, can-import, MCP"]
IDE["Editors<br/>LSP, ESLint plugin, Roslyn analyzer"]
TR["Test runners<br/>xUnit / NUnit / pytest / vitest adapters"]
end
RB --> CI
RB --> JSON
RB --> AG
RB --> IDE
RB --> TR
UP["Upstream conformance suites<br/>dependency-cruiser tests, ArchUnitNET TestAssembly"] -.validate.-> RB
The binary is the only runtime component. Everything else either produces an input it reads or consumes an output it writes. The wrappers and adapters are shells over the binary and the JSON.
The five stages
The pipeline inside rulebearing cruise, from design § The five stages:
flowchart LR
D["1 Discover<br/>workspace, solution, roots"] --> X["2 Extract<br/>module layer + code layer"]
X --> G["3 Build graph document<br/>modules, folders, code, summary"]
G --> E["4 Evaluate rules<br/>dependency, element, slice, diagram, ratchet"]
E --> R["5 Report<br/>reporters, exit code"]
G -. "saved as json" .-> F["fmt: re-report without extracting"]
F --> R
- Discover walks from
baseDirwithout building anything. TypeScript:package.jsonworkspaces, thetsconfig.jsonchain,.babelrc.json,webpack.config.js. .NET:.slnor.slnx, every.csproj,Directory.Build.props,Directory.Packages.props, yielding project nodes andProjectReference/PackageReferenceedges. Python:pyproject.toml, source roots, package layout. Discovery alone supports project-level rules and is fast enough for a pre-commit hook. - Extract fills both layers per language (see Extractors).
- Build the graph document (see The graph document).
- Evaluate rules (see The rule engine).
- Report. Exit code is the count of error-severity violations (ADR-0008).
fmtre-reports a saved JSON without re-extracting.
Crate layout
Fixed by ADR-0010. Arrows point from dependant to dependency.
flowchart BT
model["rb-model"]
config["rb-config"] --> model
rules["rb-rules"] --> model
rules --> config
xts["rb-extract-ts"] --> model
xdn["rb-extract-dotnet"] --> model
xpy["rb-extract-python"] --> model
ingest["rb-ingest"] --> model
report["rb-report"] --> model
report --> rules
cli["rb-cli"] --> config
cli --> rules
cli --> xts
cli --> xdn
cli --> xpy
cli --> ingest
cli --> report
node["rb-node"] --> cli
| Crate | Owns | Wave |
|---|---|---|
rb-model |
Graph document types (module layer = cruise-result schema, code layer), the published JSON schema, serde, the violation-id hash, per-language option structs, the Files every extractor reads through (the disk, or a tree in memory: ADR-0073) |
0, 4 |
rb-config |
Native and dependency-cruiser config parsing, extends, presets, defines, $0–$9 captures, the QuickJS evaluator (the js-config feature, which rb-cli turns on), config lint / convert / expand |
1 |
rb-rules |
Matchers, Tarjan cycles, breadth-first reachability, dependents, instability, element predicates and conditions, slices, PlantUML adherence, ratchets, liveness, violation summary | 1, 2 |
rb-extract-ts |
Workspace discovery, oxc_parser, oxc_resolver, npm classification, licence and deprecation from package.json, Vue / Svelte splitting, JSDoc and triple-slash parsing, sidecar dispatch |
0, 1, 2, 3 |
rb-extract-dotnet |
MSBuild discovery, ECMA-335 and portable PDB readers, IL operand scan, edge projection to files, --mode source |
0, 2, 3 |
rb-extract-python |
ruff_python_parser, import resolver, per-version stdlib list, TYPE_CHECKING detection |
2 |
rb-ingest |
dependency-cruiser JSON, ArchUnitNET-style JSON and the C# fallback extractor's output in; graph document out | 1, 2 |
rb-report |
Every dependency-cruiser reporter plus sarif, github-annotations, junit, trx, agent, plantuml |
1, 2, 3 |
rb-cli |
cruise, fmt, baseline, rules, count, diff, explain, can-import, place, impact, test, docs, config, init, adopt, hooks, attest, import, propose, decisions, guard, snapshot, changelog, serve; cache; --affected |
1, 2, 3 |
rb-node |
napi-rs binding exposing cruise() and format() with dependency-cruiser's signatures |
3 |
rb-wasm |
The playground's facade: the library crates compiled to wasm32-unknown-unknown, evaluate, cruise_ts, cruise_py, explain and version exported as strings in and out, the extractors reading the dropped files from memory; no crate depends on it, and it never links rb-cli (ADR-0067, ADR-0073) |
4 |
Outside the workspace: wrappers/npm, wrappers/nuget, wrappers/pip; adapters/dotnet (Rulebearing.TestAdapter), adapters/python (pytest-rulebearing), adapters/vitest; frontends/eslint-plugin-rulebearing, frontends/Rulebearing.Analyzer; conformance/; testbeds/.
Each extractor is a Cargo feature of rb-cli (extract-ts, extract-dotnet, extract-python), on by default. The extractors are the only crates that read files other than the config, and none depends on rb-config or rb-rules.
The graph document
Fixed by ADR-0004 and ADR-0015. One JSON with two layers.
Module layer (dependency-cruiser's cruise-result schema, unchanged): modules[] with source, dependencies[], dependents[], orphan, valid, rules[], reachable[], reaches[], instability, couldNotResolve, coreModule, followable, matchesDoNotFollow, matchesFocus, matchesReaches, matchesHighlight, consolidated, checksum, license, dependencyTypes, experimentalStats; each dependency with module, resolved, moduleSystem, dependencyTypes, dynamic, exoticallyRequired, exoticRequire, followable, coreModule, couldNotResolve, matchesDoNotFollow, circular, cycle[], valid, rules[], preCompilationOnly, typeOnly, protocol, mimeType, license, instability; folders[] with the coupling metrics; summary with violations[], the severity counts, totalCruised, totalDependenciesCruised, optionsUsed, ruleSetUsed, environment; revisionData.
Additions, all additive:
| Where | Field | Meaning |
|---|---|---|
| module | language |
typescript, javascript, dotnet, python |
| module | project, namespaces[] |
the .csproj or package that owns the file; the namespaces it declares |
| module | attribution |
pdb, inferred, none (.NET only) |
| dependency | line, column |
from the AST span, PDB sequence point or Python node |
| dependency | dependencyKind |
import, inherits, implements, field, signature, body, attribute, generic-argument, typeof, call, declared |
| dependency | member |
the member reference that formed the edge (TodoItemsController.Get calls ApplicationDbContext.SaveChanges) |
| dependency | sidecar, declared |
edge produced by the Node sidecar; edge declared rather than detected (wave 4) |
| dependency | declaredKind, comment |
what a declared edge stands for (http, queue) and why it exists (ADR-0078) |
| violation | id |
RB- plus eight hex characters of SHA-256 over rule, from, to, dependencyKind |
| violation | fix, decision |
the rule's fix text and the decision token parsed from comment |
| summary | inspected |
counts of files, assemblies and modules per language: the receipt |
| summary | vacuousRules[] |
rules whose selection was empty |
| summary | fleet, declared |
a fleet run's repositories with each one's own counts, and its declared edges by source (ADR-0078) |
| top level | code |
types[], members[], attributes[], calls[], each with language, file, line, column, and the properties the element predicates read (visibility, sealed, abstract, static, readonly, record, virtual, base types, interfaces, attribute arguments, return type, accessors) |
--strict-schema strips every addition and the result validates against the pinned 18.2.0 schema. The superset schema is generated from rb-model with schemars, committed under schema/, and served at https://benbahrenburg.github.io/rulebearing/schema/v1.json.
Extractors
From design § What each extractor has to get right and § One engine, three languages. Each extractor's whole job is to produce modules, dependencies and code elements in the document shape; nothing after it is language-aware.
| Concern | TypeScript (ADR-0012) | .NET (ADR-0011, ADR-0003) | Python (ADR-0013) |
|---|---|---|---|
| Discovery | package.json workspaces, tsconfig chain, Babel and webpack configs |
.sln / .slnx, .csproj, Directory.*.props; IsTestProject, OutputPath, TargetFramework |
pyproject.toml, src/ layout, setup.cfg |
| Module identity | the file | the PDB document path, normalised; /_/ deterministic-build prefixes unmapped through SourceRoot |
the file; __init__.py stands for its package |
| Edge source | every dependency form dependency-cruiser extracts | metadata tables and IL operands | import, from ... import, __all__, literal importlib.import_module |
| Resolution | oxc_resolver with tsconfig, webpack and Babel alias tables |
TypeRef to TypeDef across solution assemblies; external to package or framework |
roots, relative, stdlib list, installed distributions, unresolved |
dependencyTypes |
dependency-cruiser's forty | local, project, package, framework, test-only, signature-only, unresolved |
local, stdlib, site, type-only, dynamic, unresolved |
| Code layer | classes, interfaces, enums, type aliases, functions, exports, decorators, extends / implements, accessors, modifiers, visibility |
everything ArchUnitNET reads | classes, functions, methods, decorators, bases, @property, @staticmethod, @classmethod, @dataclass, underscore visibility |
dynamic |
import() |
literal Assembly.Load, Type.GetType, Activator.CreateInstance |
literal importlib.import_module, __import__ |
| Default excludes | node_modules/, .next/, dist/, coverage/ |
obj/, bin/, *.g.cs, *.Designer.cs, GlobalUsings.g.cs |
.venv/, site-packages/, __pycache__/, *.pyi unless --stubs |
| Default orphan exclusions | framework entry files (Next.js page.tsx, route.ts, config files) |
Program.cs, Startup.cs, AssemblyInfo.cs, migrations |
__main__.py, conftest.py, console-script targets |
| Not native | CoffeeScript, LiveScript: --sidecar node runs the repository's dependency-cruiser for those files and the walk continues from their edges, marked sidecar: true (ADR-0017) |
source mode is approximate and never the gate | none |
The .NET reader. Eight ECMA-335 tables are needed for the edge set (TypeDef, TypeRef, MemberRef, MethodDef, Field, InterfaceImpl, CustomAttribute, TypeSpec, plus the blob and string heaps), and the portable PDB's Document and MethodDebugInformation tables for attribution. IL is scanned for call, callvirt, newobj, ldfld, stfld, ldtoken, box, castclass, isinst operands. The reader is about 3,000 lines against two stable specifications and lives entirely in rb-extract-dotnet; the fallback in ADR-0003 replaces it with a C# program writing the same document.
Configuration and the rule language
Fixed by ADR-0005, ADR-0006, ADR-0016. Both formats load into one internal model in rb-config.
flowchart LR
DC[".dependency-cruiser.{json,yaml,cjs,js,mjs}"] --> DCF["dependency-cruiser front-end"]
NAT["rulebearing.{yaml,json,jsonc,toml}"] --> NATF["native front-end"]
DCF --> JS["QuickJS sandbox<br/>(rquickjs)"]
JS --> M["Config model<br/>rules × families, options, languages, defines"]
DCF --> M
NATF --> M
M --> EXT["extends: files, npm packages, bundled presets"]
M --> LINT["config lint / convert / expand"]
M --> RE["rb-rules"]
Four rule families over one graph, sharing name, comment, severity, fix, examples, owner, expires and the liveness default:
| Family | Vocabulary | Reads |
|---|---|---|
rules.dependencies (forbidden, allowed, required) |
the whole of dependency-cruiser 18.2.0 (coverage § Rules), plus language, namespace, project, assembly, dependencyKind on from and to |
module layer |
rules.elements (select … should) |
ArchUnitNET's selectors, predicates and conditions in camelCase (coverage), all / any / not, nested selectors, because, allowEmpty |
code layer |
rules.slices |
matching with (*) / (**), notDependOnEachOther, beFreeOfCycles, ignore, where |
both |
rules.diagrams |
adhereTo: <file>.puml with <<pattern>> stereotypes |
both |
rules.ratchets |
from, to, budget file; the ceiling may only fall |
module layer |
Shorthands layers, independence |
expand to forbidden rules; config expand shows the expansion |
module layer |
languages holds per-language settings; dependency-cruiser's flat option names are accepted at the top level as aliases. defines reads a named value from a JSON file with a small path expression and substitutes ${name} into any pattern, replacing computed JavaScript.
The rule engine
rb-rules reads the document and the config model and writes violations[], rules[] on modules and dependencies, and vacuousRules[]. It contains no match language.
| Concern | Mechanism |
|---|---|
| Path, namespace, name matching | regex crate, linear-time (ADR-0016); $0–$9 substituted escaped |
| Cycles | Tarjan's strongly connected components with the path enumerated, at module and folder scope |
Reachability (reachable, reaches, via*) |
breadth-first search from the from matches, with maxDepth |
| Dependents, orphans, instability, folder metrics | consolidated folder graph; afferent and efferent couplings |
| Element predicates and conditions | one function per ArchUnitNET predicate over code; a per-language capability table decides whether a predicate is answerable, and an unanswerable one is a validation error (ADR-0014) |
| Slices | group by the capture of matching; evaluate pairwise dependencies and cycles |
| Diagram adherence | parse a PlantUML component diagram; map components to types by stereotype; fail undrawn dependencies |
| Ratchets | count direct edges; compare to the budget file; --write may only lower it |
| Liveness | every rule reports fromMatches and toMatches; zero on the selecting side is vacuous (ADR-0007); strict, warn or off by the configuration's format (ADR-0032) |
| Severity | error, warn, info, ignore; only error counts toward the exit code |
| Baseline | knownViolations keyed by the stable id, with expires, owner, reason; shrink-only fails when a baselined entry no longer occurs |
| Determinism | modules, dependencies and violations are sorted before output so a local run and CI agree byte for byte |
Outputs and CI contract
From design § Outputs and CI integration. The contract is four things: an exit code (ADR-0008), a json document, fmt, and a reporter the host's review surface understands.
| Reporter group | Reporters | Wave |
|---|---|---|
| Terminal and data | err, err-long, text, csv, json, null |
1 |
| Review annotations | teamcity, azure-devops, github-annotations |
1 |
| Agent | agent (token-budgeted, fix-cost ordered, --max-findings) |
1 |
| Code scanning and tests | sarif, junit, trx |
2 |
| Graphs | dot, ddot, archi / cdot, flat / fdot, mermaid, d2 |
2 |
| Data | metrics, baseline, err-html |
2 |
| Remaining | x-dot-webpage, html, markdown, anon, plugin:<path>, plantuml, wrap-html |
3 |
Every reporter is byte-compared against dependency-cruiser's test/report fixtures where one exists (ADR-0009). Every report carries the receipt (inspected) and every violation its stable id, fix and decision link.
Subcommands a guard reaches for: cruise, fmt, rules --json, count, diff, explain, baseline, config convert | lint | expand, test, docs, can-import, place, impact, serve.
Agent surface
Fixed by ADR-0021. The CLI with --output-type agent is the primary surface; every other front-end reads the same config and the same cached graph, so none can disagree with the gate.
flowchart TB
G[("cached graph<br/>.graph/cruise.json")]
CLI["rulebearing CLI<br/>agent reporter, can-import, explain, impact, place, test, propose"] --> G
HOOK["Claude Code hooks<br/>SessionStart brief · PreToolUse impact · Stop affected cruise"] --> CLI
MCP["serve --mcp<br/>rules, explain, can_import, place, impact, count, query, diff"] --> G
LSP["serve --lsp<br/>diagnostics with fix as quick-fix"] --> G
ESL["eslint-plugin-rulebearing<br/>rulebearing/boundaries"] --> G
ROS["Rulebearing.Analyzer<br/>RB0001 diagnostics at compile time"] --> CFG["rulebearing.yaml"]
CLI --> CFG
DOCS["docs --format agents-md | contributing | skill"] --> CFG
Authoring guardrails: a rule needs a decision token (--require-comment-token); a vacuous rule fails; a rule ships with examples that test runs; propose drafts a rule with current match counts; config lint catches patterns that match nothing, shadowed rules, permit-everything lists and unanswerable predicates; ratchets only fall; runs are hermetic.
Distribution
Fixed by ADR-0002 and ADR-0020.
| Host | Package | Mechanism |
|---|---|---|
| TypeScript repository | rulebearing on npm |
platform binaries under optionalDependencies, the ruff and oxc pattern; rulebearing/vitest reporter; rb-node napi binding for library callers |
| .NET repository | Rulebearing on NuGet |
dotnet tool wrapper carrying the binary under runtimes/; Rulebearing.TestAdapter; Rulebearing.Analyzer |
| Python repository | rulebearing on PyPI |
pip wheel wrapper per platform; pytest-rulebearing |
| Any | crates.io rulebearing, GitHub Releases, a GitHub Action, Homebrew (later) |
static binaries for macOS (arm64, x64), Linux (x64, arm64, musl), Windows (x64) |
Release is one tag: cargo-dist builds the binaries, and the three wrappers are published from the same workflow with the same version.
Technology choices
| Concern | Choice | ADR |
|---|---|---|
| Language, packaging | Rust 2024 edition, Cargo workspace, cargo-dist |
0002 |
| TypeScript parsing and resolution | oxc_parser, oxc_resolver |
0012 |
| .NET metadata | hand-written ECMA-335 and portable PDB reader; fallback System.Reflection.Metadata in C# |
0011, 0003 |
| Python parsing | ruff_python_parser |
0013 |
| JavaScript config evaluation | QuickJS via rquickjs, sandboxed |
0006 |
| Regex | regex crate |
0016 |
| Serialisation, schema | serde, serde_json, serde_yaml, toml, schemars |
0004 |
| CLI | clap with derive; --help byte-compared for the shared flags |
0008 |
| Hashing | sha2 for violation ids and attest |
0015 |
| Node binding | napi-rs |
0002 |
| .NET source mode | tree-sitter-c-sharp |
0011 |
| Licence policy | cargo deny |
0019 |
| Coverage | cargo-llvm-cov, vitest, coverlet, pytest-cov, 70% floor |
0018 |
| Linters | rustfmt and clippy, eslint and prettier, ruff and mypy, dotnet format, behind cargo xtask lint |
0023 |
| Test quality | cargo-mutants, proptest, committed snapshots |
0024 |
| CI and supply chain | least privilege, SHA-pinned actions, cargo deny sources, cargo-hack, Dependabot, typos, actionlint, shellcheck |
0025 |
| Documentation links | xtask/src/doclinks.rs, run by xtask/gate/build.rs on every workspace-wide compile |
0023, 0058 |
Security posture
- No network, ever. The binary makes no outbound connection. Opt-in usage counts (wave 4) are off by default, counted on the machine with salted rule hashes and printed by
--usage-counts print; there is no endpoint yet, so no send path is built and nothing is retained remotely (ADR-0076; the payload schema and the local retention are in usage-counts.md). A test fails if the binary links a network call. - No code execution outside the sandbox. The QuickJS evaluator has no filesystem access beyond the repository, no
process, no timers (ADR-0006). It runs JavaScript configurations andplugin:<path>reporters alike (reporters § Plugin reporters). Escaping it is a test case that must fail. - The sidecar is explicit. Node is spawned only with
--sidecar nodeor--config-via-node, and the report records that it was. - Inputs are untrusted. Assemblies, PDBs and source files are parsed defensively; a malformed input produces exit 2 with a named reason, never a panic. Fuzz targets exist for the metadata reader and the config parsers.
- Hermetic runs. Deterministic ordering, no ambient state, so
attestcan hash config, inputs and results and CI can verify the hash againstHEAD. - Supply chain.
cargo denyfor licences and advisories; dependencies pinned; release binaries built in CI from a tag.
Performance model
| Target | Figure | Source |
|---|---|---|
| Full cruise of a 5,500-module TypeScript monorepo | 1 to 2 s (13 s today) | design § Three pipelines |
Stop hook with --affected, p95 |
under 2 s, including aspnetcore in source mode | design § How to know |
can-import from the cached graph |
milliseconds | design § Questions an agent can ask |
guard --watch re-check of a saved file |
under 100 ms | design § The agentic engineering hat |
| Nightly scale table regression | fail over 20% | design § Test beds |
Mechanisms: parallel parsing with rayon; a content-addressed cache keyed by file hashes (and assembly plus PDB hashes for .NET) under .graph/, worktree-aware; --affected computes the changed files' dependent closure from the cache; the MCP and LSP servers keep the graph warm.
Verification strategy
Fixed by ADR-0009 and ADR-0018.
| Layer | What | Where | Required |
|---|---|---|---|
| Unit and property tests | every crate; fixed-vector test for the violation hash; regex compatibility table | crates/*/src, crates/*/tests |
yes, ≥ 70% lines per crate |
| Conformance gate 1 | dependency-cruiser 18.2.0: extract fixtures, validate and graph-utl specs unmodified, report fixtures, schema, oracle zero-diff and mutation branch | conformance/dependency-cruiser/ |
yes, ratcheting excluded.json |
| Conformance gate 2 | ArchUnitNET 0.13.4 and NetArchTest: TestAssembly fixture, ported element tests, oracle agreement with dotnet test |
conformance/archunitnet/ |
yes, ratcheting unported count |
| Test beds | oracle zero-diff, greenfield init fixtures, scale timing |
testbeds/manifest.yaml, nightly workflow |
nightly; timing regression fails |
| Wrapper and adapter tests | vitest, xUnit with coverlet, pytest-cov | wrappers/, adapters/, frontends/ |
yes, ≥ 70% lines |
| Documentation links | every relative link and #anchor in Markdown and in Rust doc comments resolves; runs inside every workspace-wide compile, in cargo xtask lint and as its own CI job (ADR-0023, ADR-0058) |
xtask/src/doclinks.rs, xtask/gate/build.rs |
yes |
| Linters, all languages | rustfmt and clippy; eslint and prettier; ruff and mypy; dotnet format. One entry point, cargo xtask lint --strict |
eslint.config.mjs, pyproject.toml, Directory.Build.props, .editorconfig, rustfmt.toml |
yes |
| Self-check | this repository's own rulebearing.yaml enforcing the crate boundary, citing ADRs |
repo root | yes, from wave 1 |
| Mutation testing | cargo mutants over rb-model, rb-rules and xtask; a surviving mutant fails (ADR-0024) |
.cargo/mutants.toml |
yes |
| Property tests | invariants for the violation id, the anchor slug and path normalisation | beside the example tests | yes |
| Snapshots and determinism | the command-line help is a committed snapshot; serialising the same graph twice gives the same bytes | crates/rb-cli/tests/, crates/rb-model/src/lib.rs |
yes |
| Reproducibility | minimum Rust version, every feature combination, rustdoc with warnings denied, doc tests | .github/workflows/ci.yml |
yes |
| Supply chain | licences, advisories, banned crates, allowed registries; actions pinned by SHA; Dependabot (ADR-0025) | deny.toml, .github/dependabot.yml |
yes |
| Spelling and scripts | typos, actionlint, shellcheck |
.typos.toml |
yes |
| Fuzzing | metadata reader, config parsers | fuzz/ |
nightly |
Repository layout
Fixed by ADR-0001 and ADR-0010.
rulebearing/
├── CLAUDE.md # working agreement for humans and agents
├── Cargo.toml # workspace
├── rulebearing.yaml # this repo's own rules (wave 1)
├── crates/
│ ├── rb-model/ rb-config/ rb-rules/
│ ├── rb-extract-ts/ rb-extract-dotnet/ rb-extract-python/
│ ├── rb-ingest/ rb-report/ rb-cli/ rb-node/ rb-wasm/
├── wrappers/ npm/ nuget/ pip/
├── adapters/ dotnet/ python/ vitest/
├── frontends/ eslint-plugin-rulebearing/ Rulebearing.Analyzer/
├── xtask/ doclinks.rs (link gate) and the one lint entry point; gate/ runs the link check on every workspace build
├── conformance/ dependency-cruiser/ archunitnet/ excluded.json
├── testbeds/ manifest.yaml
├── schema/ v1.json (generated)
├── presets/ recommended, typescript, dotnet, python, framework presets
├── docs/
│ ├── architecture.md prd.md
│ ├── adr/ NNNN-*.md
│ ├── artifacts/ design.md and the two coverage tabs
│ └── plans/ pending/ implemented/
├── .cargo/config.toml # cargo lint, cargo check-links, cargo ci
├── .cargo/mutants.toml # mutation-testing scope and exclusions
├── .githooks/ # opt-in pre-commit and pre-push
├── .typos.toml deny.toml # spelling; licences, advisories, sources
├── eslint.config.mjs .prettierrc.json tsconfig.base.json package.json
├── pyproject.toml # ruff, mypy, pytest
├── Directory.Build.props .editorconfig # C# analyzers and style
└── .github/workflows/ ci.yml nightly-testbeds.yml release.yml
Every language's linter is configured at the root and runs from one command, and the documentation link check runs inside every compile; both are fixed by ADR-0023.
Risks and their mitigations
| Risk | Likelihood | Impact | Mitigation | Owner plan |
|---|---|---|---|---|
| The Rust .NET metadata reader cannot attribute 99% of types | medium | high | Measured trigger and a pre-decided C# fallback (ADR-0003) | Wave 0 |
oxc_resolver diverges from enhanced-resolve on an edge case |
medium | medium | dependency-cruiser's own test/extract suite replayed case by case (296 recorded cases from its 480 tests, conformance/README.md); divergences filed upstream and pinned |
Wave 0, Wave 1 |
| A JavaScript config needs more than the sandbox allows | low | medium | --config-via-node, --webpack-config-json, defines |
Wave 1 |
| Regex semantics differ between JavaScript and Rust | medium | medium | compatibility table, exit 3 on unsupported syntax, --strict-compat (ADR-0016) |
Wave 1 |
| .NET Framework projects without portable PDBs | medium | low | measured in the spike; DebugType=portable guidance; attribution: none handled |
Wave 0 |
| Single maintainer stalls | medium | high | suites as reviewer, public nightly, small crates, second maintainer by end of wave 2 | all |
| Name lost on a registry | low | medium | placeholder 0.0.1 on all four on day one (ADR-0020) |
Wave 0 |
| Agents ignore the tool | medium | high | six measured adoption signals; cut back if the first two do not move (ADR-0021) | Wave 1 onward |
| Stop hook exceeds 2 s on a large solution | medium | medium | cache, --affected, --mode source, guard --watch |
Wave 3 |
Glossary
| Term | Meaning |
|---|---|
| Cruise | one extraction plus evaluation run; the verb dependency-cruiser uses |
| Graph document | the JSON with the module layer and the code layer |
| Module layer | files and the edges between them; dependency-cruiser's model |
| Code layer | types, members, attributes, calls; ArchUnitNET's model |
| Vacuous rule | a rule whose selecting side matches nothing; fails by default |
| Oracle repository | a public repository that already carries one of the three tools' configs; Rulebearing must reproduce its findings with zero difference |
| Greenfield repository | a public repository with no architecture tool; exercises init and propose |
| Scale repository | a public repository used only for timing |
| Parity, Parity+, Kept, Sidecar, Stays | the statuses used in the coverage tabs |
| Ratchet | a numeric ceiling that may only fall |
| Receipt | summary.inspected, the proof a run looked at something |
| Decision token | adr:NNNN or plan:<slug> in a rule's comment |