Rulebearing Playground Coverage Schemas GitHub

Rulebearing

Deterministic guardrails for agentic engineering. Architecture rules your coding agent cannot talk its way past, with the fix attached to every finding, for TypeScript (TypeScript 7 included), JavaScript, .NET and Python in one rule file.

Status: waves 1 and 2 built, first release in progress. Wave 1, the drop-in for repositories already using dependency-cruiser, builds and runs from source and supports TypeScript 7 (how it is proven); its v0.1.0 release is being cut. Wave 2, .NET and Python, is built, with its nightly evidence and the NuGet and PyPI packages still to finish. If the problem below is yours, star or watch the repository; the roadmap says what lands when.

The problem with telling an agent the rules

Most teams working with coding agents have written the same file. It is called AGENTS.md or CLAUDE.md, it started as a page, and it is now forty kilobytes of prose rules, each one recording a mistake that already cost something. "Apps only talk to each other over HTTP." "Nothing in Domain may reference Infrastructure." "Features must not import each other."

Prose rules do not hold. Not because agents ignore them, but because of how agents actually work:

The tools that could enforce these rules already exist, one per language, and none of them was built with an agent as the reader of its findings. A finding names two files and no line. The fix-it advice lives in a comment only one output mode prints. Asking "may this file import that one" costs a thirteen-second full run. And a rule that matches nothing passes.

What a deterministic guardrail looks like

Rulebearing turns the paragraph into a fence the agent runs into before it finishes its turn, with the way out attached.

# rulebearing.yaml
rules:
  dependencies:
    forbidden:
      - name: no-cross-app-imports
        comment: "Apps share only packages/* and HTTP. adr:0003"
        fix: "Call the other app over its API, or move the shared code into packages/*."
        severity: error
        from: { path: "^apps/([^/]+)/" }
        to:   { path: "^apps/([^/]+)/", pathNot: "^apps/$1/" }
        examples:
          forbidden: ["apps/web/src/x.ts -> apps/worker/src/y.ts"]
          allowed:   ["apps/web/src/x.ts -> packages/format/src/index.ts"]

Every part of that rule is load-bearing for an agent:

Field What it does for the agent
name A stable id a decision record can cite and a review comment can reference
comment with adr:0003 The why, linked to the decision. A rule without a decision token is rejected
fix The imperative the agent follows when the rule fires. Printed in every output an agent reads
examples Executable. rulebearing test proves the rule fires before CI has to
from and to Match the resolved target, so switching import style cannot dodge the fence

And when it fires, the agent gets something it can act on rather than a paragraph to interpret:

// rulebearing cruise --affected HEAD --output-type agent   (shape, illustrative)
{
  "violations": [{
    "id": "RB-4f2a9c1e",
    "rule": "no-cross-app-imports",
    "from": "apps/web/src/checkout/total.ts", "line": 3, "column": 1,
    "to": "apps/worker/src/pricing/index.ts",
    "fix": "Call the other app over its API, or move the shared code into packages/*.",
    "decision": "adr:0003"
  }],
  "inspected": { "files": 212, "modules": 212 }
}

A line and a column. A stable id, so "fix RB-4f2a9c1e" means the same thing on every run. The fix text. The decision it serves. A receipt of what was looked at, so "the check passed" is distinguishable from "the check looked at nothing". Token-budgeted with --max-findings, grouped by rule, no prose to parse.

Inside the loop, not beside it

A check the agent has to remember to run is a check that gets skipped. Rulebearing installs itself into the loops agents already run.

rulebearing hooks install --claude-code

That writes three hooks for Claude Code:

Hook What happens Why it matters
SessionStart Injects a token-budgeted architecture brief: tiers, hot boundaries, open violations, ratchet headroom The agent starts every session knowing the architecture, not just the task
PreToolUse on Edit and Write Runs impact on the file about to change The agent is told before editing that the file sits on a boundary, not after
Stop Runs the affected check and feeds violations back before the turn ends Sub-two-second budget, so it stays on. A thirteen-second check gets disabled

For pipelines and editors, the same rules and the same graph feed a pre-commit hook, one test case per rule in xUnit, NUnit, pytest or vitest, an ESLint rule that flags a boundary violation inline as the agent types, a Roslyn analyzer that fails dotnet build with the line, an MCP server so the architecture is available as tools rather than as a document the agent may not have read (rulebearing hooks install --claude-code --mcp), and a language server that shows each finding in the editor with its fix as the quick-fix title (serve.md).

Before it writes the import, the agent can ask:

rulebearing can-import apps/web/src/x.ts apps/worker/src/y.ts   # yes or no, and which rule decides, in milliseconds
rulebearing place --imports a,b --imported-by c --language ts    # where would a new module with these edges be legal
rulebearing impact src/Domain/Entities/Order.cs                  # what depends on this, what rules mention it, is it on a cycle
rulebearing explain no-cross-app-imports                         # the rule, its fix, what it matches today, the first ten edges

Guardrails on the guardrails

An agent asked to "stop Domain from reaching Web" will write the regex. The tool makes that safe rather than trusting it.

One rule file for the whole repository

The rules above work the same way whether the edge is a TypeScript import, a .NET type reference read from the compiled assembly, or a Python import. Rulebearing is a strict superset of dependency-cruiser for TypeScript and JavaScript and of ArchUnitNET for .NET, with import-linter's contract kinds mapped one to one for Python. Every rule attribute, option, flag and reporter of the incumbents is accounted for; nothing is dropped, and the claim is proven by running their own test suites against this tool as required checks (conformance).

So a repository with a TypeScript front end, a .NET service and a Python pipeline keeps one rule file, one gate, one graph other scripts can read, and one answer to "may this file import that one". If you already have a .dependency-cruiser.js, it runs unchanged on day one.

Try it

Now, from source. Wave 1, the TypeScript drop-in, is built: dependency-cruiser's whole rule language and option set, both configuration formats, the wave 1 reporters, and the agent commands. It matches dependency-cruiser with zero differences on dependency-cruiser's own repository, langfuse and FluidFramework (conformance). Until the first release is published:

cargo build --release                               # target/release/rulebearing
./target/release/rulebearing init                   # reads the repo and proposes rules that already pass
./target/release/rulebearing cruise src             # or: replace `depcruise` with it in your pipeline
./target/release/rulebearing hooks install --claude-code

From the first release, v0.1.0. In a TypeScript repository:

npm install --save-dev rulebearing
npx rulebearing init
npx rulebearing cruise --output-type agent

or in GitHub Actions, uses: benbahrenburg/rulebearing@v0.1.0 with args: --config rulebearing.yaml src. A repository already on dependency-cruiser keeps its configuration: replace depcruise with rulebearing cruise and change nothing else, or run rulebearing adopt for a baseline, a CI step and one green pull request.

The guides: configuration, rules, reporters, the command line and Rulebearing for coding agents.

.NET and Python (wave 2). The .NET extractor reads the compiled assemblies and their portable PDBs, so build first; the Python extractor reads the source, without running an interpreter. Once the wave 2 release is published, install with dotnet tool install -g Rulebearing or pip install rulebearing; from source, use ./target/release/rulebearing as above.

dotnet build -p:DebugType=portable                  # .NET: the assemblies and PDBs the extractor reads
rulebearing init                                    # finds the solution or pyproject.toml and proposes rules that pass
rulebearing cruise --output-type agent

Existing rules come across as a command, not a rewrite, with the original kept as a comment beside each rule:

rulebearing import archunit tests/Architecture.Tests --out rulebearing.yaml      # ArchUnitNET or NetArchTest chains
rulebearing import import-linter --from pyproject.toml --out rulebearing.yaml     # or .importlinter, setup.cfg
rulebearing import eslint --from eslint.config.js --out rulebearing.yaml          # no-restricted-paths, eslint-plugin-boundaries

The same rules run as tests in the suite you already have: Rulebearing.TestAdapter for xUnit, NUnit, MSTest and TUnit, pytest-rulebearing, and rulebearing/vitest, one test per rule with the fix in the failure message; eslint-plugin-rulebearing flags a boundary in the editor. ArchUnitNET's predicates are keys of element rules, with each key's meaning in every language in the generated reference.

TypeScript 7

TypeScript 7 is supported. Rulebearing parses with oxc and resolves with oxc_resolver; it never loads the typescript package, so the compiler version a repository installs does not matter to it. What TypeScript 7 changes for an extractor is the tsconfig, and that is what is tested:

What How it is proven
paths without baseUrl, in the tsconfig itself or inherited through extends from another folder, and ${configDir} fixture ts7-config
moduleResolution: bundler and nodenext, .js specifiers for .ts files, # subpath imports the same fixture
The tsconfig's module and target deciding whether an import reaches the rules as import or require when tsPreCompilationDeps is off, as dependency-cruiser's transpileModule step decides it fixture ts-config-module, every expectation taken from dependency-cruiser 18.2.0 with TypeScript 6.0.3
A real repository on TypeScript 7 gate 1 layer 5 on langfuse, whose web package is on typescript ^7.0.2 with a baseUrl-free tsconfig: 3,832 modules and 369 violations, zero differences from dependency-cruiser

dependency-cruiser cannot load TypeScript 7 itself (its manifest accepts typescript >=2.0.0 <7.0.0), so a repository that keeps it beside Rulebearing installs TypeScript 6 for it, as langfuse does with @typescript/typescript6. Two resolutions follow TypeScript rather than dependency-cruiser, whose tsconfig-paths 4.2.0 departs from the compiler once baseUrl is gone: inherited paths resolve (dependency-cruiser leaves them unresolved), and a bare src/x with no baseUrl stays unresolved (dependency-cruiser resolves it against the tsconfig's folder). module: amd, umd, system and none, which TypeScript 7 removes, stop a run with exit 2 unless tsPreCompilationDeps is true. The reasoning is ADR-0040.

Roadmap

Five waves of part-time work, TypeScript first because that is where the largest set of validation repositories is.

Wave Weeks What lands for you Exit criterion Status
0 Spike 4 The TypeScript extractor, matching dependency-cruiser on 292 of the 296 cases its own extraction suite records (0.9865); the .NET metadata reader, attributing 99.29% of the .NET oracles' types to a source file, so the C# fallback is not needed (ADR-0022); both conformance harnesses; the nightly test beds; the name held on four registries Fixtures at 95%; 99% of .NET types attributed to a source file, or the fallback extractor is invoked Done
1 TypeScript 10 The drop-in: full dependency-cruiser parity, TypeScript 7 included; the agent reporter, fix and examples, line-precise findings, liveness by default, init, adopt, hooks install, attest, can-import, explain, test; npm package and GitHub Action Zero difference against dependency-cruiser on its own repository, langfuse and FluidFramework Built; the v0.1.0 release is in progress
2 .NET and Python 10 Both extractors; ArchUnitNET's full vocabulary as declarative element rules; propose, impact, place, docs; importers; test-runner adapters; the ESLint rule; SARIF and JUnit Every imported .NET test agrees with dotnet test; every Python contract reproduces Built; the nightly oracle evidence and the NuGet and PyPI publishing are in progress
3 Inner loop 8 Caching and --affected; a source mode for .NET that answers without a build; guard --watch; the Roslyn analyzer; MCP and LSP servers; framework presets; the public rule library Stop hook under two seconds on a large .NET solution; all reporters byte-compared Not started
4 Reach 8 Browser playground; pull-request app; fix --plan; rules across a fleet of repositories Funded only if the numbers below move Not started

Beside the waves, five tracks. Two run alongside waves 1 and 2. The other three add languages and incumbents once wave 2 is done, under the same promise: each incumbent's own test suite becomes a conformance gate before its row says Parity.

Track Weeks What lands for you Exit criterion Status
Guard catalogue 5 Quality, convention and lifecycle guards (cycles, dead code, licences, layer fences, budgets that only fall, exceptions that expire) as executable recipes: one fixture per guard, and the cookbook page generated from the fixtures so it cannot drift from the engine Every recipe a green fixture in CI, none vacuous; the page checked against the fixtures Started
Rulebearing on Rulebearing 5 This repository gated by its own tool: one graph over its five languages, the gate in CI, the test runners, the editor and the agent loop, and the first month of signals The self-check green on main; the test runners agree with the gate Started
Java and ArchUnit 9 Java as the fourth language, read from compiled classes; ArchUnit's vocabulary and library rules (layered and onion architectures, GeneralCodingRules); an importer for existing ArchUnit tests; JUnit adapter and a Maven Central package ArchUnit's own tests pass as conformance gate 3; the Java oracles agree Not started
.NET, complete 6 VB.NET and F# projects read as C# ones are; NetArchTest's own definitions as additive options, so a NetArchTest suite migrates without a changed verdict NetArchTest's ported cases at 388 with ArchUnitNET's unchanged; the three .NET oracles that compare nothing today compared Not started
Rust 9 Rust as the fifth language, with arch-lint, archaven and TangleGuard as the incumbents; importers for their configurations; a crates.io package; this repository's own rules on the Rust graph instead of a script Their suites pass as conformance gate 4; zero difference on the Rust oracles, or each difference recorded Not started

This is measured, not believed. From wave 1, six signals are tracked on the repositories where agent-authored pull requests can be seen: the share that pass the boundary check on their first CI run (target above 90%), the median turns from a violation to green (target one), rules caught by the authoring guardrails, Stop-hook latency, the share of rules carrying fix text, and budget raises merged (target zero). If the first two do not move, the agent surface is cut back to the reporter and the hook, and what remains is a faster dependency-cruiser that also covers .NET and Python. Saying that in advance is cheaper than discovering it later.

Validated nightly against real repositories

Every night the repositories in testbeds/manifest.yaml are cloned at a pinned commit and their incumbent tool runs with its own configuration: dependency-cruiser, NetArchTest, ArchUnitNET or import-linter. From wave 1 Rulebearing runs beside it, and the table shows whether the two agree to the finding and how long each took (testbeds/README.md). The latest full run is on the testbeds-results branch; the table below is refreshed from it by pull request. In it, failed means the incumbent itself reports failing tests or broken contracts at the pinned commit, and error means the repository does not build there. From wave 2 the table has two more kinds of row with a Rulebearing time and no incumbent: the greenfield init proof on microsoft/semantic-kernel and microsoft/autogen, ok when the configuration init writes cruises back with exit 0 and matches its committed fixture, and the scale rows n8n-io/n8n, grafana/grafana, elastic/kibana, dotnet/aspnetcore, jellyfin/jellyfin and home-assistant/core, timed in the scale table below.

Rows: 52; error 2, failed 4, idle 19, ok 27. Zero-diff and the Rulebearing timings start in wave 1.

Repository Role Incumbent Status Incumbent time Incumbent peak memory Rulebearing time Zero diff
abpframework/abp greenfield none idle
apache/superset greenfield none idle
getsentry/sentry greenfield none idle
jasontaylordev/CleanArchitecture greenfield none idle
microsoft/autogen greenfield none idle
microsoft/semantic-kernel greenfield none idle
PostHog/posthog greenfield none idle
umbraco/Umbraco-CMS greenfield none idle
zulip/zulip greenfield none idle
ag-grid/ag-grid oracle dependency-cruiser ok 0.32 s 85 MB
ardalis/RiverBooks oracle archunitnet ok 1.53 s 150 MB
aws/aws-toolkit-vscode oracle dependency-cruiser ok 0.69 s 91 MB
BenMorris/NetArchTest oracle netarchtest failed 1.32 s 123 MB
bridgecrewio/checkov oracle import-linter ok 0.3 s 46 MB
DrJohnMelville/Pdf oracle netarchtest error
evolutionary-architecture/evolutionary-architecture-by-example oracle netarchtest ok 1.61 s 124 MB
google/langextract oracle import-linter ok 0.2 s 31 MB
HKUDS/DeepTutor oracle import-linter ok 0.3 s 43 MB
infinitered/ignite oracle dependency-cruiser ok 0.69 s 99 MB
invertase/react-native-firebase oracle dependency-cruiser ok 0.36 s 91 MB
karaoke-dev/karaoke oracle archunitnet ok 8.93 s 610 MB
kedro-org/kedro oracle import-linter ok 0.19 s 33 MB
langfuse/langfuse oracle dependency-cruiser ok 0.25 s 98 MB
langgenius/dify oracle none idle
liveblocks/liveblocks oracle dependency-cruiser ok 0.19 s 88 MB
microsoft/FluidFramework oracle dependency-cruiser ok 0.88 s 98 MB
microsoft/promptflow oracle import-linter ok 0.25 s 41 MB
nager/Nager.Date oracle archunitnet ok 1.73 s 129 MB
napari/napari oracle import-linter ok 0.28 s 42 MB
NeVeSpl/NetArchTest.eNhancedEdition oracle netarchtest ok 3.6 s 309 MB
nolar/kopf oracle import-linter failed 0.18 s 28 MB
onebeyond/monaco oracle archunitnet error
online-ml/river oracle import-linter ok 0.22 s 34 MB
open-metadata/OpenMetadata oracle none idle
openedx/openedx-platform oracle import-linter ok 0.75 s 77 MB
phongnguyend/Practical.CleanArchitecture oracle netarchtest ok 1.61 s 124 MB
remult/remult oracle dependency-cruiser ok 0.53 s 99 MB
seddonym/import-linter oracle import-linter ok 0.22 s 30 MB
sqlfluff/sqlfluff oracle import-linter ok 0.23 s 40 MB
sverweij/dependency-cruiser oracle dependency-cruiser ok 1.28 s 168 MB
TNG/ArchUnitNET oracle archunitnet failed 20.1 s 2223 MB
tsparticles/tsparticles oracle dependency-cruiser ok 0.6 s 85 MB
wemake-services/wemake-python-styleguide oracle import-linter ok 0.21 s 32 MB
benbahrenburg/ai-sdk-otel-logger own none idle
benbahrenburg/rulebearing own none idle
dotnet/aspnetcore scale none idle
elastic/kibana scale none idle
grafana/grafana scale none idle
home-assistant/core scale none idle
jellyfin/jellyfin scale none idle
n8n-io/n8n scale none idle

Scale: wall-clock and peak memory on large repositories

Each night the scale repositories are built at their pinned commits, given the configuration rulebearing init writes, and cruised three times; the median run's wall-clock time and peak resident memory are below, aspnetcore read both from its compiled assemblies and from source, with the Stop hook's p95 on aspnetcore (testbeds/bench/scale_table.py, plan 0003 Step 24). The runner is GitHub's standard Linux runner: 4 CPUs, 16 GB of memory. A row more than 20% slower or larger than the previous published one, on the same commit, fails the night; a row that did not complete keeps its last measurement, marked. The nightly rewrites this table by pull request.

Repository .NET read as Wall-clock Peak memory Modules Measured in run
n8n-io/n8n - 11.46 s 2020 MB javascript 1500, python 168, typescript 21244, unknown 4102 38109169995
grafana/grafana - 0.61 s 182 MB javascript 52, typescript 2077, unknown 351 38109169995
elastic/kibana - 7.06 s 611 MB javascript 271, typescript 6825, unknown 390 38109169995
dotnet/aspnetcore compiled 33.65 s 5613 MB dotnet 10131, javascript 268, typescript 207, unknown 69 38109169995
dotnet/aspnetcore source 8.32 s 524 MB dotnet 10131, javascript 268, typescript 207, unknown 69 38109169995
jellyfin/jellyfin compiled 4.7 s 974 MB dotnet 2317 38109169995
home-assistant/core - 9.47 s 2254 MB javascript 3, python 22920, typescript 2, unknown 1 38109169995

Stop hook on dotnet/aspnetcore in source mode: p95 1.818 s over 200 edits on 4 CPUs, run 38109169995.

Oracle agreement: migrated rules against the incumbent

For the .NET and Python oracles the question is sharper than whether the tools agree on a graph: does a team's existing rule set, migrated with rulebearing import archunit or rulebearing import import-linter, give the verdict the incumbent gives, test by test and contract by contract. The harness in testbeds/oracles runs both sides at the pinned commit and writes one file per repository; stays is a test or contract the importer leaves with the incumbent (a custom predicate or contract type), not imported one it could not translate, each with its reason in the file. The table is generated from those files by python3 testbeds/oracles/table.py --readme README.md, and CI fails when it is stale.

Repository Incumbent Compared Agree Disagree Stays Not imported Errors Graph (grimp vs Rulebearing) Result
ardalis/RiverBooks archunitnet 2 0 0 0 2 0 nothing compared: every row stays or is not imported
bridgecrewio/checkov import-linter 6 6 0 0 0 0 5499 = 5499 agrees
DrJohnMelville/Pdf netarchtest error: dotnet build Src/Melville.Pdf.DataModelTests/Melville.Pdf.DataModelTests.csproj failed (see build.log)
evolutionary-architecture/evolutionary-architecture-by-example netarchtest 16 2 0 0 14 0 agrees
google/langextract import-linter 3 3 0 0 0 0 127 vs 154, 0 unexplained agrees
HKUDS/DeepTutor import-linter 3 3 0 0 0 0 3485 vs 3613, 0 unexplained agrees
karaoke-dev/karaoke archunitnet 5 0 0 0 5 0 nothing compared: every row stays or is not imported
kedro-org/kedro import-linter 4 4 0 0 0 0 222 vs 230, 0 unexplained agrees
langgenius/dify import-linter 37 37 0 0 0 0 6429 vs 8581, 0 unexplained agrees
microsoft/promptflow import-linter 1 1 0 0 0 0 557 vs 574, 0 unexplained agrees
nager/Nager.Date archunitnet 0 0 0 0 0 0 error: dotnet test ran no tests (see incumbent.log)
napari/napari import-linter 4 4 0 0 0 0 2423 vs 2764, 0 unexplained agrees
NeVeSpl/NetArchTest.eNhancedEdition netarchtest 355 0 0 0 355 0 nothing compared: every row stays or is not imported
nolar/kopf import-linter 11 10 0 1 0 0 373 = 373 agrees
onebeyond/monaco archunitnet 11 6 0 3 2 0 agrees
online-ml/river import-linter 1 1 0 0 0 0 1237 = 1237 agrees
open-metadata/OpenMetadata import-linter error: import-linter could not run: ModuleNotFoundError: No module named 'pydantic'
openedx/openedx-platform import-linter 4 3 0 1 0 0 7098 vs 7097, 0 unexplained agrees
phongnguyend/Practical.CleanArchitecture netarchtest 5 5 0 0 0 0 agrees
seddonym/import-linter import-linter 2 2 0 0 0 0 84 = 84 agrees
sqlfluff/sqlfluff import-linter 4 4 0 0 0 0 985 = 985 agrees
wemake-services/wemake-python-styleguide import-linter 6 6 0 0 0 0 530 = 530 agrees

Built the way it asks you to build

This repository holds itself to the bar it proposes for yours. Every gate below was required and green before the first extractor line was written, so the extractors were built against the finished harness rather than alongside it.

Gate What it enforces
cargo lint Four languages behind one command: rustfmt and clippy, eslint and prettier, ruff and mypy, dotnet format
Documentation links Every relative link and anchor resolves, checked inside every compile. A broken reference fails cargo build
Coverage 70% of lines per crate, not per workspace
Mutation testing A surviving mutant is a missing assertion and fails the build. The gate starts at zero survivors
Conformance The incumbents' own test suites, as ratchets that may only tighten
Supply chain Licences, advisories, allowed registries; every action pinned by commit SHA
Reproducibility Minimum Rust version, every feature combination, determinism asserted byte for byte

The reasoning behind each is a numbered decision record in docs/adr/, and the repository's own rulebearing.yaml cites them the way it asks yours to.

Repository map

Path What is there
docs/prd.md Requirements with fixed identifiers the code cites
docs/architecture.md Stages, crates, the graph document, extractors, security, performance
docs/adr/ Every decision, numbered
docs/config.md, rules.md, reporters.md, cli.md, agents.md The user guides
docs/perf.md, docs/adoption.md The performance and adoption measurements
crates/ The Rust workspace: model, config, rules, three extractors, ingest, reporters, CLI, Node binding
conformance/, testbeds/ The upstream suites and the pinned repositories validated nightly
wrappers/, adapters/, frontends/ npm, NuGet and pip wrappers; test-runner adapters; the ESLint plugin and Roslyn analyzer

Building

cargo build --release          # target/release/rulebearing
cargo test --workspace --all-features
cargo lint                     # all four languages, plus documentation links
cargo mutants --package rb-model --package rb-rules --package xtask
git config core.hooksPath .githooks   # formatting, links and tests before each commit and push

Contributing and licence

CONTRIBUTING.md is short and points at the four documents that govern everything else. One person builds this in evenings; the reviewer of record is a pair of upstream test suites anyone can run, and a second maintainer is the goal by the end of wave 2.

MIT. Security reports go through the security policy.