Rulebearing Playground Coverage Schemas GitHub

Rules

The rule language is dependency-cruiser 18.2.0's, whole: every rule shape and attribute it accepts is accepted here with the same meaning, and its own test suites prove it (conformance gate 1). The exhaustive list, with each row's status, is the coverage tab § Rules; this page explains the shapes and what Rulebearing adds. Source: design § Dependency rules. Where rules live and how files are found is in config.md.

The four families

Family A violation is Example
forbidden an edge, or a module, that matches the rule from: { path: "^src/domain/" }, to: { path: "^src/web/" }
allowed an edge that no allowed entry covers; one rule, not-in-allowed, with allowedSeverity from: { path: "^src/" }, to: { path: "^(src|node_modules)/" }
required a module matching module that does not import (or, with to.reachable: true, does not reach) a module matching to every page must reach src/auth.ts
rules.ratchets (native) a count of matching edges above the ceiling in the budget file route files importing server code, counted per app

forbidden has three variants beside the plain from and to edge rule:

from.orphan: true matches modules that import nothing and are imported by nothing. scope: folder applies circular to folders rather than modules.

scope: project (native configurations only) compares projects: the .csproj, or Python package, each module belongs to. from.path and to.path match project names, and to.circular and to.moreUnstable read the project layer, projects[], which a run writes beside folders[] when metrics are on. A dependency on a package or a framework assembly counts toward the project's efferent coupling, as a folder's does, and appears as a sink. A violation that is neither a cycle nor an instability has type project. In a dependency-cruiser configuration the scope is exit 3, and config convert leaves such a rule out and says so (ADR-0066).

rules:
  dependencies:
    forbidden:
      - name: stable-projects-depend-on-stable-projects
        comment: "adr:0066"
        severity: warn
        scope: project
        from: { path: "^src/Domain/" }
        to: { moreUnstable: true }

The metrics reporter lists projects as rows of type project; reporterOptions.metrics.hideProjects: true hides them.

Element, slice and diagram rules

Native configurations have three more families under rules, which carry ArchUnitNET's vocabulary to every language (plan 0002, Steps 5 and 6). Gate 2 holds them to ArchUnitNET's and NetArchTest's own tests (conformance/README.md).

rules:
  elements:
    - name: services-are-sealed
      comment: "adr:0004"
      select: { kind: class, where: { haveNameEndingWith: Service } }   # That()
      should: { beSealed: true }                                         # Should()
      because: services are composed, never extended
  slices:
    - name: bounded-contexts-do-not-know-each-other
      comment: "adr:0005"
      matching: "RiverBooks.(*)"
      should: [notDependOnEachOther, beFreeOfCycles]
  diagrams:
    - name: matches-the-context-diagram
      comment: "adr:0001"
      select: { kind: type, where: { resideInNamespaceMatching: "^Shop\\." } }
      adhereTo: docs/architecture/components.puml

Keys across languages

A native dependency rule may also narrow from and to by language, namespace / namespaceNot, project / projectNot and assembly / assemblyNot, and to by dependencyKind / dependencyKindNot (inherits, implements, attribute, ...). They read what the extractors record on each module and edge, and a .dependency-cruiser.* configuration that uses one is exit 3 (plan 0002, Step 8). to.license reads npm, NuGet and Python licences alike.

Not every extractor records every property:

Property (keys) .NET TypeScript, JavaScript Python
namespaces (namespace, namespaceNot) the namespaces the file declares not recorded the dotted module name
project (project, projectNot) the .csproj (or loaded assembly) path not recorded the top-level package
assembly (assembly, assemblyNot) the assembly of the file's types not recorded not recorded
dependencyKind (dependencyKind, dependencyKindNot) inherits, implements, attribute, ... import import

A rule whose from or to narrows by a property that a language the side can select does not record is exit 3 naming the rule, the side, the key and the language, because the key would be false for every module of that language (ADR-0014). Add that side's language to leave the language out: from: { language: dotnet, namespace: "^Shop\\." }. to can select the targets of the edges from what from can select, since no edge crosses languages. A core or external module, which no extractor analysed, has none of these properties and matches neither a key nor its Not form. The table is data in rb-config (capability::records).

Declared edges

A rule across languages matches nothing until an edge exists, because no extractor invents one (ADR-0014). In a fleet, edges.yaml declares the edges between services (config.md), and a rule over the prefixed paths then matches them like any other edge. Each declared edge carries declared: true and dependencyKind: declared, so a rule can also select them, or leave them out:

- name: no-new-http-calls-to-billing
  from: { path: "^web/" }
  to: { path: "^billing/", dependencyKind: declared }

Captures

A capturing group in from.path is available in to as $1 to $9 (and $0 for the whole match), escaped so it matches only itself. The common fence is one rule:

- name: apps-are-independent
  from: { path: "^apps/([^/]+)/" }
  to: { path: "^apps/([^/]+)/", pathNot: "^apps/$1/" }

rulebearing explain apps-are-independent --plain says it back as "Files under apps/<x>/ may not import files under apps/<y>/ unless x = y."

Severity and the exit code

error, warn, info and ignore. A gating reporter (err, err-long, null, teamcity, azure-devops, github-annotations, agent) exits with the number of error violations, capped at 255; json, csv and text exit 0, as dependency-cruiser's do, so a pipeline can save JSON in one step and gate in the next (ADR-0008, ADR-0030). Exit 2 means the run cannot be trusted and exit 3 means the configuration is invalid, whatever the reporter.

Liveness

A rule whose selecting side matches no module checks nothing, and a gate that reads green because a rule went stale is worse than no rule. So every rule whose from (or module) matches nothing is listed in summary.vacuousRules and named on stderr (ADR-0007). What that does to the run depends on the configuration (ADR-0032):

Configuration Default A vacuous rule
rulebearing.*, including one that extends a dependency-cruiser file strict fails the run with exit 2
.dependency-cruiser.*, run as it is warn a warning; the exit code is dependency-cruiser's, and the entry carries "severity": "warn"

--liveness strict|warn|off overrides the default, and --no-liveness is off, which dependency-cruiser's own test suites need. A rule that may match nothing is excused by name: allowEmpty: true on the rule, or, for a rule that lives in a dependency-cruiser file, its name in the native file's top-level list:

extends: ./.dependency-cruiser.cjs
allowEmpty: [plugins-stay-apart]   # matches nothing until the first plugin lands

A name in that list that is no rule is a configuration error. rulebearing adopt writes the list for the rules that match nothing on the day it runs.

What Rulebearing adds to a rule

Addition What it does
fix The imperative printed under every finding by err-long, explain, can-import and the agent reporter
examples.forbidden, examples.allowed Edges rulebearing test builds into a graph and checks the rule against, so a rule's regex is tested like code
owner, expires Who answers for a temporary rule, and the day after which it fails the run
since, deprecated, replacedBy When the rule arrived, when it was deprecated and what replaces it; reported by rules --json and changelog, checked by config lint, and without effect on evaluation (config.md § Rule metadata)
A decision token in comment adr:NNNN or plan:<slug>; required by --require-comment-token
A stable id on every violation RB- and eight hex digits from the rule, the two ends and the kind of edge (ADR-0015), so a baseline entry survives unrelated edits
line and column on every TypeScript edge Where the import is, so a finding points at a line

Baselines and ratchets

Known violations. options.knownViolations lists findings that are accepted for now. An entry written by rulebearing adopt or init carries the violation's id, its type and, for a cycle or a reachability chain, the modules in it, plus expires and owner; an entry written by dependency-cruiser (from, to, rule, cycle, via) matches the way dependency-cruiser matches it. A known finding is reported with severity ignore. An entry past its expires date fails the run the day after. An element or slice entry without an id matches on its rule and whichever of from and to it names, so an entry naming only a type (to) or only a slice (from) covers every finding of it, as ArchUnitNET's frozen rules do. rulebearing baseline writes and maintains the file (cli.md § Baselines).

Ratchets. A rules.ratchets entry counts the direct edges that match its from and to (with $1 captures) and compares the count with { "ceiling": n } in its budget file. cruise reports every ratchet in summary.ratchets: an exceeded one is one error, and a missing budget exits 2 (ADR-0029). rulebearing count --from ... --to ... --budget FILE --write lowers the ceiling to the current count and refuses to raise it.

Checking the rules themselves

Command Answers
rulebearing test Does each rule flag its forbidden examples and pass its allowed ones?
rulebearing config lint Can a rule never match? Is it shadowed by an earlier one? Does an allowed list admit everything? Does every rule have a fix that says more than its name? Does every replacedBy name a rule?
rulebearing rules --json Every rule with its family, severity, and how many modules each side matched
rulebearing rules --unused Which rules matched nothing in each of the last releases' snapshots (cli.md)
rulebearing explain <rule> The rule in a sentence, its reason and fix, and the first edges it matched