Rulebearing Playground Coverage Schemas GitHub

Configuration

Rulebearing reads two configuration formats into one model: dependency-cruiser's, unchanged, and its own native format, which is a superset. Decisions: ADR-0005 (the two formats), ADR-0006 (JavaScript in a sandbox), ADR-0016 and ADR-0028 (regular expressions). Source: design § Configuration. The rule language itself is in rules.md.

Finding the file

--config FILE names it; --config - reads stdin; --no-config runs without one. With --config and no value, or with no flag at all on the commands that need a configuration, the first of these found in the working directory is used:

Order Name Format
1 rulebearing.yaml, .yml, .json, .jsonc, .toml native
2 .dependency-cruiser.js, .cjs, .mjs, .json, .yaml, .yml dependency-cruiser

--config-format native or --config-format dependency-cruiser settles a name that says neither.

dependency-cruiser's format, as it is

A .dependency-cruiser.js that works with dependency-cruiser 18.2.0 works here without edits: forbidden, allowed, allowedSeverity, required, options and extends, with every key, attribute and option of the coverage tab. Unknown keys are refused, as dependency-cruiser refuses them.

JavaScript configurations run in an embedded QuickJS sandbox with no filesystem, network or process access, and with CommonJS and ES module syntax. require and import resolve only inside the repository, and the pure path and url modules are provided (ADR-0027). A configuration that reads files, such as one that lists a folder to build a pattern, is refused with a message naming --config-via-node, which evaluates it with the local Node instead.

extends takes a file, a package, or one of the bundled presets. A package named <package>/<path> is looked for in this order, so a rule library installed from any one registry resolves under one name (ADR-0020, plan 0003 Step 23):

Layout Where extends: rulebearing-rules/nextjs is found
npm node_modules/rulebearing-rules/nextjs.yaml, in the configuration's folder or one above it
NuGet <global packages>/rulebearing.rules/<version>/rulebearing-rules/nextjs.yaml; the global packages folder is $NUGET_PACKAGES, else ~/.nuget/packages; the version is the one installed, else the one a project under the repository root references, and several installed with none referenced is an error naming them
PyPI <site-packages>/rulebearing_rules/nextjs.yaml, in $VIRTUAL_ENV, else a .venv or venv folder in the configuration's folder or one above it

The rule library itself is rulebearing-rules on npm and PyPI and Rulebearing.Rules on NuGet, published with each release at the release version and built from the presets below (wrappers/rules, ADR-0071).

Presets

Preset What it is
dependency-cruiser/configs/recommended, recommended-strict, recommended-warn-only dependency-cruiser's own, vendored with their licence (presets/dependency-cruiser)
rulebearing:typescript The TypeScript and JavaScript defaults: type-only imports kept as edges, export maps resolved as Node and bundlers resolve them, node_modules recorded but not followed, build output (dist/, build/, coverage/, .next/, out/) left out, the npm and Node rules (no-non-package-json, no-deprecated-core, no-duplicate-dep-types), and no-orphans with the toolchain's entry files exempt (presets/rulebearing/typescript.yaml)
rulebearing:dotnet The .NET exclusions and nothing else: C# sources under obj/ and bin/, *.g.cs, *.Designer.cs left out; Program.cs, Startup.cs, AssemblyInfo.cs and Migrations/ not orphans (presets/rulebearing/dotnet.yaml)
rulebearing:python The Python exclusions and nothing else: .venv/, venv/, site-packages/, __pycache__/ and .pyi stubs left out; __main__.py and conftest.py not orphans (presets/rulebearing/python.yaml)
rulebearing:recommended Composes the three (extends: [rulebearing:typescript, rulebearing:dotnet, rulebearing:python]) and adds no-circular and not-to-unresolvable; every rule has a fix (presets/rulebearing/recommended.yaml)
rulebearing:nextjs An opinion, off unless named: route entries and API handlers are not imported, components/, hooks/ and lib/ outside the route folders never import a route; type-only imports are never findings (presets/frameworks/nextjs.yaml)
rulebearing:clean-architecture An opinion, off unless named: Domain <- Application <- Infrastructure and Presentation, read from path segments (a module's layer is the last one in its path), for a .NET solution or a TypeScript or Python tree (presets/frameworks/clean-architecture.yaml)
rulebearing:django An opinion, off unless named: models are innermost, views are reached through URL configurations, migrations are not imported (presets/frameworks/django.yaml)
rulebearing:fastapi An opinion, off unless named: routers, services, repositories, each importing only the layers beneath it, with models and schemas innermost (presets/frameworks/fastapi.yaml)
rulebearing:vertical-slices An opinion, off unless named: a slice under features/ or slices/ never imports another, and the shared kernel never imports a slice (presets/frameworks/vertical-slices.yaml)

The merge replaces a rule's from and an option's value whole, so rulebearing:recommended lists the union of the three presets' no-orphans exclusions and exclude patterns, and a test keeps it the union. A repository with one language puts its own preset first, so that its exclusions win and no other language's are carried (design § What stays honest across the boundary):

extends: [rulebearing:python, rulebearing:recommended]

rulebearing init writes that line for the language it finds, and extends: rulebearing:recommended when it finds several (cli.md).

The framework presets are opinions, and nothing extends one unless a configuration names it (design § The developer relations hat: "off by default, each a documented opinion"). Each reads module paths only, declares its own rules (named with its prefix, so none replaces a language preset's) and no options, and goes after the language presets. Every rule's comment carries the decision token plan:rulebearing-<preset>, which --require-comment-token accepts, and its examples pass rulebearing test. The opinion behind each rule is in presets/frameworks/README.md.

extends: [rulebearing:typescript, rulebearing:recommended, rulebearing:nextjs]

Rules merge by name, as dependency-cruiser merges them: a rule in the extending file with the name of an extended rule replaces the attributes it names.

The native format

$schema: https://benbahrenburg.github.io/rulebearing/schema/config-v1.json
extends: rulebearing:recommended
defines:
  legacyApps: { fromJson: eng/legacy-apps.json, select: "apps[*].name" }
languages:
  typescript:
    tsConfig: { fileName: tsconfig.json }
    tsPreCompilationDeps: true
rules:
  dependencies:
    forbidden:
      - name: no-app-to-app
        comment: "An app never imports another app. adr:0003"
        fix: "Move the shared code into a package under packages/ and import it from both apps."
        severity: error
        from: { path: "^apps/([^/]+)/", pathNot: "^apps/(${legacyApps})/" }
        to: { path: "^apps/([^/]+)/", pathNot: "^apps/$1/" }
        examples:
          forbidden: ["apps/web/src/a.ts -> apps/admin/src/b.ts"]
          allowed: ["apps/web/src/a.ts -> apps/web/src/b.ts"]
  layers:
    - name: clean
      layers: ["^src/web/", "^src/application/", "^src/domain/"]
  independence:
    - name: features
      pattern: "^src/features/([^/]+)/"
  ratchets:
    - name: routes-via-service
      from: { path: "^apps/([^/]+)/src/app/.*/(page|route)\\.tsx?$" }
      to: { path: "^apps/$1/src/(server|domain)/" }
      budget: eng/routes-via-service-budget.json
options:
  doNotFollow: { path: node_modules }

The top level holds $schema, extends, defines, languages, options, rules, and dependency-cruiser's forbidden, allowed, allowedSeverity and required, which are aliases for rules.dependencies.*. schema/config-v1.json is generated from the model and checked by a test, so an editor with YAML schema support completes and validates the file.

Key Holds
languages.typescript The per-language options: tsConfig, tsPreCompilationDeps, babelConfig, webpackConfig, enhancedResolveOptions, moduleSystems, parser, baseDir and the rest of the TypeScript extractor's options. dependency-cruiser's flat option names are accepted at the top of options too, as aliases
languages.dotnet The .NET extractor's options: solution, configuration, targetFramework, excludeProjects, the loader keys (assemblies, includeDependencies, directories, namespaces), and mode: compiled (the default) or source, which reads the .cs files without a build (source-mode.md)
rules.dependencies dependency-cruiser's rule set: forbidden, allowed, allowedSeverity, required (rules.md)
rules.layers Paths from the highest layer to the lowest; each lower layer gets one forbidden rule per higher layer, named <name>:<lower>-to-<higher>
rules.independence A pattern with one capturing group; a module in one group may not import a module in another
rules.ratchets A count of direct edges that may only fall, against a budget file { "ceiling": n } (ADR-0029)
defines.<name> A value read from a JSON file (fromJson), selected with dot-separated keys, [*] and [n] (select), escaped, and joined with joinWith (default |); ${name} in any pattern is replaced by it
options Everything else of dependency-cruiser's options, including knownViolations
allowEmpty Rules and ratchets allowed to match nothing, by name (allowed[N] for the Nth allowed entry), including rules from an extended dependency-cruiser file, which cannot carry the key itself. A name that is no rule is an error, so an exception cannot outlive its rule (ADR-0032)

rulebearing config expand FILE prints a native file with defines substituted and the shorthands expanded into the rules they stand for.

webpackConfig (fileName, env, arguments, or --webpack-config [FILE]) is evaluated in the same sandbox as a JavaScript configuration, or by Node under --config-via-node. A function-shaped configuration is called with env and arguments, an array's first element is taken, and the resolve block's alias, aliasFields, conditionNames, exportsFields, extensions, mainFields, mainFiles, modules and symlinks reach the resolver, winning over enhancedResolveOptions as they do in dependency-cruiser; any other key is named in a warning. A configuration that loads webpack plugins from node_modules needs Node: evaluate it elsewhere and pass the JSON (the configuration, or its resolve block) with --webpack-config-json FILE.

Markdown fences. In a native file, listing .md in extraExtensionsToScan also reads the js, ts, jsx, tsx, javascript and typescript code fences of those files, with each dependency's line and column pointing into the Markdown. A dependency-cruiser file keeps dependency-cruiser's behaviour, which never reads a file of extraExtensionsToScan (ADR-0036).

The cache

options.cache turns on the extraction cache cruise --cache uses (cli.md; coverage § Options row cache). It takes dependency-cruiser's forms in both formats, and optionsUsed records it normalised, as dependency-cruiser does:

Written Means
true on, in .graph/cache (node_modules/.cache/dependency-cruiser in a dependency-cruiser file)
"tmp/cache" on, in that folder
{ folder, strategy, compress } each given; strategy is metadata (default) or content, compress: true stores the extraction zlib-compressed
false off
options:
  cache:
    folder: .graph/cache
    strategy: metadata
    compress: true

--cache, --cache-strategy and --no-cache replace it for one run.

Fleets and declared edges

rulebearing fleet (cli.md) reads two files beside each other (ADR-0078; plan 0004 § 1.6). Their schemas are schema/fleet-v1.json and schema/edges-v1.json.

fleet.yaml names the repositories and the fleet-wide rules:

repos:
  - { name: web,      path: ../web,      config: rulebearing.yaml }
  - { name: api,      path: ../api,      config: rulebearing.yaml }
  - { name: pipeline, path: ../pipeline }
rules:
  dependencies:
    forbidden:
      - name: web-never-reaches-pipeline
        comment: "adr:0021"
        from: { path: "^web/" }
        to:   { path: "^pipeline/" }
Key Meaning
repos[].name the prefix of the repository's module paths: letters, digits, ., _ and -, unique
repos[].path the repository's folder, relative to the manifest
repos[].config its configuration, relative to the repository; without it, the one cruise would find
rules the native format's rules, over the prefixed paths: dependencies, layers and independence. Element, slice and diagram rules and ratchets belong in a repository's own configuration and are refused (exit 3)

edges.yaml declares the edges no extractor can see, such as an HTTP call or a queue between two services (ADR-0014):

declared:
  - from: web/src/api/client.ts
    to: api/src/Web/Endpoints/Orders.cs
    kind: http
    comment: "OrdersClient calls POST /orders. adr:0007"
openapi:
  - client: web/src/api/generated/
    server: api/openapi.json

Paths are the union's module paths, prefixed with the repository's name. Each declared entry becomes one dependency of its from module with declared: true, dependencyKind: declared, declaredKind (the kind) and comment.

An openapi pair declares an edge for each operation the server document records both an operationId and a handler for, from each module under client whose code layer declares a function, class or member named exactly as the operationId (a generated client declares one per operation), to the handler. The handler is the operation's x-rulebearing-handler extension, a file path relative to the repository the document is in:

"/orders": { "post": { "operationId": "createOrder", "x-rulebearing-handler": "src/Web/Endpoints/Orders.cs" } }

Nothing is guessed: an operation without an operationId, or without a handler, or that no client module declares, declares no edge, and summary.declared.openapiSources[] counts each case and says why when a pair declares nothing. When a server framework cannot write the extension, declare the edges in declared instead.

Rule metadata

Every rule, in either format, may carry these fields dependency-cruiser does not have. They are what explain, err-long and the agent reporter print (design § Rule metadata).

Field Meaning
fix The imperative to follow when the rule fires
examples forbidden and allowed edges, from -> to, that rulebearing test checks against the rule
owner Who answers for the rule
expires A date after which the rule fails the run: for a temporary exception
since The release the rule arrived in
deprecated The release the rule was deprecated in; changelog lists it as retired
replacedBy The rule that takes over from this one
allowEmpty Opt the rule out of liveness: it may match nothing without failing the run (ADR-0007). dependency-cruiser's schema refuses it on a rule, so a rule in a .dependency-cruiser.* file is named in the native file's top-level allowEmpty list instead (ADR-0032)

Lifecycle fields. since, deprecated and replacedBy are how a rule file shrinks honestly (design § The architect's hat). They are legal on dependency, element, slice and diagram rules, on the layers and independence shorthands (copied onto every rule an entry expands to, which config expand shows) and on ratchets. They change nothing about how a rule is evaluated: a deprecated rule still reports its violations, and one that matches nothing still fails the run unless it has allowEmpty (ADR-0007). The versions are strings and are not validated, since a repository may release as 1.2.0, v1.2.0 or 2026.09; they are compared as semver when they are semver (cli.md § Snapshots). rules --json prints all three, changelog reports the deprecations, and config lint checks them:

Finding When
replaced-by-unknown replacedBy names no rule, layers or independence entry, or ratchet of the configuration
replaced-by-self replacedBy names the rule itself
since-after-deprecated since is later than deprecated, both semver

A deprecated without a replacedBy is not a finding: a rule may be retired with nothing in its place.

- name: no-legacy-http-client
  comment: "adr:0012"
  since: "1.2.0"
  deprecated: "2.0.0"
  replacedBy: no-http-client-outside-gateway
  severity: warn

A decision token in comment, adr:NNNN or plan:<slug>, links the rule to the decision behind it. --require-comment-token makes a rule without one a configuration error (exit 3).

Regular expressions

Patterns are JavaScript regular expressions, run on the linear-time regex crate through a translation table. Backreferences are supported (ADR-0028); lookaround is refused with a message. $1 in a to pattern takes the first capture of from.path, escaped so it matches only itself. --strict-compat refuses what dependency-cruiser would refuse, including the nested quantifiers its safe-regex check rejects, and any Rulebearing addition, so a file that passes it also runs under dependency-cruiser.

Between the formats

Command Does
rulebearing config convert FILE dependency-cruiser to native, losslessly
rulebearing config convert FILE --to dependency-cruiser native to dependency-cruiser, and prints what it dropped: ratchets, shorthands (expanded instead), defines (substituted), languages other than TypeScript, fix, examples and the other metadata
rulebearing config expand FILE the native file with nothing hidden
rulebearing config lint rules that can never match (with --graph), rules shadowed by an earlier rule, overlapping allowed entries, an allowed list that admits everything, a severity below error on a rule with no violations, rules without a fix or whose fix restates the name, and missing decision tokens under --require-comment-token