Rulebearing Playground Coverage Schemas GitHub

Reporters

--output-type (-T) picks the reporter on cruise and on fmt, which re-reports a saved result without extracting again, as depcruise-fmt does. dependency-cruiser's reporters are ported and byte-compared against its own test/report specs (conformance gate 1, layer 3). Source: design § Reporters; the full list with waves is the coverage tab § Output types.

Wave 1

Output type Writes Gates
err One line per violation; the default for fmt yes
err-long err, with each rule's comment and fix under its findings yes
json The result document (below) no
text One line per edge, from → to no
csv A dependency matrix no
teamcity TeamCity service messages, one inspection type per rule yes
azure-devops Azure Pipelines logging commands and a task result yes
github-annotations One GitHub workflow command per violation, so each appears on its line in the pull request yes
agent JSON for a coding agent: violations grouped by rule, cheapest fix first, within a budget yes
null Nothing; the exit code only yes

Gates means the exit code is the number of error-severity violations. The others exit 0 on a trustworthy run, as in dependency-cruiser, so a pipeline can save JSON in one step and gate in the next (ADR-0030). Every reporter exits 2 on a run that cannot be trusted and 3 on an invalid configuration (ADR-0008).

Wave 2

Output type Writes Gates
baseline Every violation as a knownViolations entry, the file rulebearing baseline writes (cli.md § Baselines) no
sarif SARIF 2.1.0 for code scanning (below) no
junit JUnit XML, one test case per rule (below) no
trx Visual Studio TRX, one unit test per rule no

The rest of wave 2 is the graph reporters (dot, ddot, archi / cdot, flat / fdot, mermaid, d2), metrics and err-html; none gates.

Wave 3

Output type Writes Gates
markdown The summary, the rules with their counts, and every violation as Markdown, for a pull-request comment or a job summary no
html The dependency matrix as one HTML page: a row and a column per module, each cell coloured by the edge and the severity of the first rule it breaks no
anon json with every module name anonymised (below) no
x-dot-webpage The dot graph drawn by GraphViz as SVG, in an HTML page with hover highlighting (below) no

Each is byte for byte dependency-cruiser 18.2.0's: gate 1 layer 3 runs upstream's test/report/{markdown,html,anon,dot-webpage} specs and renders every test/report mock through both implementations.

markdown

reporterOptions.markdown takes dependency-cruiser's keys with its defaults: showTitle, title, showSummary, showSummaryHeader, summaryHeader, showStatsSummary, showRulesSummary, includeIgnoredInSummary, showDetails, includeIgnoredInDetails, showDetailsHeader, detailsHeader, collapseDetails, collapsedMessage, noViolationsMessage, showFooter, footer, showExternalModulesUnresolved and showAliasedModulesUnresolved (coverage tab § Options). As upstream, showStatsSummary is accepted and has no effect (upstream's reporter always writes the statistics line), and a key given as null replaces its default. The default footer names dependency-cruiser 18.2.0, whose output this reproduces, and the run's time, which SOURCE_DATE_EPOCH pins.

anon

Each path element that is not a common folder or file name (src, lib, test, index.ts and the rest of upstream's list) has its part before the first dot replaced by the next word of reporterOptions.anon.wordlist, the same part by the same word throughout. dependency-cruiser bundles no word list; with none, or when the words run out, a part becomes a string of its shape (letters for letters with their case, digits for digits, -, _ and . kept). Upstream draws that string at random on every run; Rulebearing draws it from a generator keyed by a SHA-256 digest of the whole (stripped) result and the part, so two runs over the same result print the same bytes, while the string cannot be computed from the name alone: anonymising a list of candidate names does not reverse it, and the same name in two results gets two strings. That is the one difference, and a word list as long as the result needs removes it.

The result is anonymised in dependency-cruiser's shape: every Rulebearing addition is removed first, as --strict-schema removes it (The JSON document), so the code layer, namespaces, project, summary.affected, summary.plugins, the fix texts and the rest never reach the output. The identifiers of the namespaces, types, assemblies and projects those additions named are replaced wherever they occur in a path, after a dot too (src/Acme.Billing/Gateway.cs loses Billing, which upstream's rule would keep), and the object an element or slice violation names is replaced identifier by identifier. A result dependency-cruiser could have written is anonymised exactly as upstream anonymises it. As upstream, the rule set (rule names, comments, path patterns), a violation's unresolvedTo (the import as written) and optionsUsed (including the reaches expression --affected writes for a dependency-cruiser configuration) are printed as they are: name nothing secret in them, or leave them out before sharing.

x-dot-webpage

The module-level dot output, with reporterOptions.dot, is drawn by the GraphViz dot on PATH, as dependency-cruiser draws it, and wrapped in upstream's page. This output type is the only one that starts GraphViz (ADR-0053). Without GraphViz, or when dot fails, the run exits 2 with dependency-cruiser's message. rulebearing wrap-html writes the same page around an SVG drawn elsewhere (cli.md § wrap-html).

Plugin reporters

-T plugin:<path> renders through a reporter written in JavaScript, as dependency-cruiser's plugin reporters do (coverage § Output types, plan 0003, Step 7). The module's default export (module.exports for CommonJS, export default for an ES module) is called with the cruise result and returns { output, exitCode }:

// reporters/count.cjs, used as: rulebearing cruise src -T plugin:./reporters/count.cjs
module.exports = (result) => ({
  output: `${result.summary.totalCruised} modules, ${result.summary.error} errors\n`,
  exitCode: result.summary.error,
});
What How
Naming the module A path from the working directory (./reporters/count.cjs), a file:// URL or an absolute path, or a package under node_modules (plugin:my-reporter, plugin:@scope/pkg/reporter), read through its exports or main. A package can name its own exports (plugin:dependency-cruiser/sample-reporter-plugin inside dependency-cruiser's repository)
Validity As upstream's isValidPlugin: called first with a minimal cruise result, the function must return an own output and an own numeric exitCode, or the run stops with <path> is not a valid plugin. For the real result, output must be a string and exitCode a whole number of zero or more
Exit code The plugin's exitCode, as dependency-cruiser's command line exits with it (ADR-0030); --exit-code-mode strict shifts it to 10 + n, and fmt uses it only with --exit-code. A run that cannot be trusted still exits 2
Errors Exit 3, naming the plugin and the fix: a missing module (Could not find reporter plugin '<name>' (or it isn't valid)), an invalid one, a sandbox refusal, a time or memory overrun, or an exception the plugin throws
Receipt The result the plugin receives carries summary.plugins: ["reporters/count.cjs"], the module relative to the repository root. With --strict-schema the plugin receives dependency-cruiser's shape instead, every addition stripped

The plugin runs in the same QuickJS sandbox as a JavaScript configuration (ADR-0006), never in Node:

The sandbox gives The sandbox refuses
require and import of JavaScript and JSON files inside the repository, and of packages under its node_modules Any file outside the repository, by path, by ../, by file:// URL or by a symbolic link
The pure path and url modules (ADR-0027), JSON, Math and the rest of the ECMAScript standard library Every Node built-in (fs, http, child_process, ...), process, Buffer, fetch, XMLHttpRequest, WebSocket and timers
30 seconds and 512 MiB per report Running past either
Package exports targets inside their package A target or a * match with a ., .., empty or node_modules segment, as Node refuses them

The repository is the nearest folder above the working directory that holds a .git, or the working directory itself outside a work tree. A sandbox that would be rooted at the filesystem root or the home directory is refused with exit 3, for a plugin and a JavaScript configuration alike: run from inside the project, or git init it first.

A refusal stops the run with <path> is not a valid plugin: the reporter sandbox refused it: <what it reached for>. A package that imports a Node built-in at load time, as watskeburt imports child_process, is therefore not a valid plugin here, where under Node it would load and fail later. A plugin that needs the network or the filesystem is not portable to Rulebearing; render its input with -T json and run it with Node instead.

diff renderings

rulebearing diff renders its own document, not a cruise result, and takes its own three output types (cli.md § diff):

Output type Writes
json The diff document: base, head, addedEdges, removedEdges, newViolations, resolvedViolations, ratchets, pretty-printed
markdown A heading, the two sides when known, a line of counts, then one table per section and one line for an empty section; the body of the pull-request comment
agent One line per new violation (new <id> <severity> <rule>: <from:line:column> -> <to>. Fix: <fix>), then one line of counts

Options

Option Reporter Effect
reporterOptions.err.showExternalModulesUnresolved, showAliasedModulesUnresolved err, err-long Print the specifier rather than the resolved path for an unresolved import
reporterOptions.text.highlightFocused text Mark the modules --focus selected
--prefix, --suffix all that link Around each module path in a link
--strict-schema json, plugin:<path> Strip every Rulebearing addition
--max-findings N agent Show at most N violations per rule
--color auto|always|never err, err-long, text Colour; auto honours NO_COLOR
reporterOptions.plantuml.*, --from plantuml What the diagram draws (below)

The JSON document

json writes dependency-cruiser's cruise-result document with its field names unchanged, plus additions that are only ever added (ADR-0004):

Where Additions
module language
dependency line, column, dependencyKind
violation id (stable, ADR-0015), fix, decision
summary inspected (files and modules read per language), vacuousRules (each with "severity": "warn" when the run only warned, ADR-0032), ratchets (ADR-0029), expired (rules and known violations past their date, ADR-0031), plugins (the plugin reporter the result was handed to)

--strict-schema removes all of them, and the output then validates against dependency-cruiser 18.2.0's schema (layer 4). The graph document's own schema is schema/v1.json, generated from the types. Two runs over the same inputs serialise byte for byte. optionsUsed.baseDir is the working folder, as upstream writes it, and teamcity stamps each message with the time, which SOURCE_DATE_EPOCH pins.

Declared edges

A fleet run adds the edges edges.yaml and OpenAPI clients declare, each marked declared: true, dependencyKind: declared and declaredKind (cli.md § Fleets and declared edges, ADR-0078). Every reporter that prints dependencies or violations tells a declared edge from a detected one (FR-REACH-03; ADR-0014: declared, never inferred). An edge between two modules is declared when every dependency of the one on the other is; when a detected edge joins the same two modules, a finding stands on the detected edge and carries no mark. A result without declared edges, which is every cruise result and every result dependency-cruiser writes, renders byte for byte as before, so every upstream byte comparison is unchanged.

Reporter A declared edge
err, err-long, err-html, markdown, teamcity, azure-devops, github-annotations, junit, trx (declared: <kind>) after the violation's target; in a cycle or a reachability path, after the step the declared edge reaches
text (declared: <kind>) at the end of the dependency's line
csv the cell reads true (declared: <kind>), or the severity with the mark
html the cell's title has the mark after the edge, and the cell carries the class cell-declared and a dashed outline
json declared, dependencyKind: declared, declaredKind and comment on the dependency; --strict-schema strips them, and the edge stays
anon declared: true on the dependency, the one addition the anonymiser keeps; the kind and comment are the declaration's own words and may name a service, so they are left out
agent dependencyKind: declared, declared: true and declaredKind on the finding
sarif the mark in the result's message, and declared: true and declaredKind in its properties
baseline declared: true and declaredKind on the entry, before the lifecycle fields; knownViolations keeps keys it does not read and matches the entry by its rule and ends as before
dot, ddot, archi / cdot, flat / fdot, x-dot-webpage the edge drawn dashed and labelled declared: <kind>, in an attribute list after the theme's, so no theme hides it; at folder or collapsed granularity an edge is declared when every module edge drawn on it is
mermaid a dotted link labelled with the kind: A-.->|"declared: http"|B
d2 style.stroke-dash: 3 and the label declared: <kind>, under the rule names when the edge breaks a rule
plantuml in the component forms, Ci --> Cj : declared: <kind> when every module edge the arrow stands for is declared, which adhereTo reads as the arrow it is. The type, namespace and slice forms draw the code layer, which a declared edge never enters (the union carries no code layer, ADR-0078 § 6), so they have no declared edge to draw

<kind> is the declaration's kind (http for an OpenAPI edge); several, joined by , , when the edge stands for declarations of different kinds; (declared) when none has one.

Two reporters print no edge and so carry no mark, and a test holds each to that: null prints nothing, and metrics prints each module's and folder's coupling and instability, counts in which a declared edge is one more dependency, with no line per edge or per violation.

github-annotations

::error file=src/domain/model.ts,line=1,col=1,title=domain-not-to-web::src/domain/model.ts -> src/web/view.ts: The domain stays independent of the web layer Fix: Move the shared type into src/domain

warning for warn and notice for info; ignore is not printed. line and col come from the edge; a module finding has none.

sarif

One SARIF rule per configuration rule: the name as shortDescription, the comment as help.text, and the comment followed by the fix in help.markdown, which code scanning shows as the recommendation. One result per violation at the rule's level (warning for warn, note for info), located at from with the edge's line and column, and fingerprinted with partialFingerprints["rulebearing/v1"], the stable id, so an alert survives line churn. A known violation is reported with an external suppression; a vacuous rule and an expired entry are configuration notifications. Paths are from the repository root, as for github-annotations. The output validates against the OASIS SARIF 2.1.0 schema (crates/rb-report/tests/sarif_schema.rs).

junit and trx

One test case per rule of every family, and one per ratchet. An error-severity violation fails the case: the message is the fix and the first five violations with their id, from, to and line, and the body lists every violation, one per object for an element rule. A vacuous rule, an expired rule or known violation and a ratchet without a budget are errors, not failures, because the rule could not be checked. Warn, info and known findings are listed in the case's output without failing it. The receipt (summary.inspected) is the test suite's properties in junit and the run's output in trx. junit validates against the Jenkins xUnit plugin's junit-10.xsd, and trx has the structure Visual Studio's vstst.xsd requires (crates/rb-report/tests/xml_schemas.rs).

agent

Each violation carries a cost: edgesToMove (one for a dependency, the steps of a cycle or a reachability path, none for a module finding) and targetFanIn (how many modules depend on the target), summed into score. Violations are ordered by score, then from and to; rules by their cheapest violation. Each rule group carries its fix and decision token; for not-in-allowed, which names the allowed list rather than a rule, it is the fix of the entry that applies to the cheapest violation, and a violation another entry applies to carries that entry's fix itself (below). count keeps the total, and budget.truncated says whether --max-findings cut anything. With the hooks from agents.md, this is what an agent reads when a turn ends.

allowed and fix

dependency-cruiser reports a dependency that no allowed entry matches as not-in-allowed: the violation names the list, not an entry, and the allowed entries in summary.ruleSetUsed carry no name, so no output field gains one. Each violation carries the fix of the entry that applies to it, the first entry whose from selects the violating module and that has a fix, since those entries say what the module may depend on; its decision token comes from that entry's comment. When no such entry has a fix, the violation carries the list's text: each entry's fix once, in order, joined by a space, with the decision token of the first entry's comment. err-long, github-annotations, sarif's results and agent print the violation's own fix; a junit or trx failure message opens with each distinct fix of the case's violations, one per line; sarif's rule descriptor, which stands for the whole list, carries the list's text. agent gives the not-in-allowed group the fix of its cheapest violation, and a finding whose own fix differs carries it.

plantuml

plantuml (wave 3) writes the graph as a PlantUML diagram, generated the way ArchUnitNET's PlantUmlDefinition.ComponentDiagram() generates one, so a diagram can be written once, committed, and then enforced with an adhereTo diagram rule (rules.md, design § Diagram rules, coverage § PlantUML). It exits 0, as every drawing reporter does. The options take ArchUnitNET's names (plan 0003 § 1.5):

options:
  reporterOptions:
    plantuml:
      from: slices               # slices | types | namespaces | folders; --from on the command line wins
      Matching: "RiverBooks.(*)" # the slice pattern (or MatchingWithPackages)
      LimitDependencies: false
      C4Style: false
      FocusOn: "^RiverBooks\\.Books\\."
      IncludeDependenciesToOther: false
      DependencyFilters: ["IgnoreDependenciesToChildrenAndParents", "^System\\."]
Key Effect
from What the nodes are: slices of a pattern, types, namespaces, or folders of modules (Rulebearing's, for TypeScript and Python). Without it: slices when a pattern is given, else namespaces for a graph with .NET types, else folders
Matching, MatchingWithPackages The slice pattern, the argument of ArchUnitNET's SliceRuleDefinition.Slices().Matching(...) or MatchingWithPackages(...), grouped as a slice rule groups: a namespace pattern (RiverBooks.(*)) by ., a path pattern (src/(*), src/(**)) by /. As in ArchUnitNET, a slice deeper than the pattern's number of (*) is not drawn. Only with from: slices
LimitDependencies Draw only arrows between nodes at the same depth (and, with packages, under the same parent)
C4Style Draw C4 containers inside boundaries; needs MatchingWithPackages
FocusOn A regular expression over full names (a type's, or a module's path): keep a dependency when exactly one end matches, ArchUnitNET's DependencyFilters.FocusOn
IncludeDependenciesToOther Also draw dependencies on what lies outside the selection: from types, the other types; in the component forms, a node for each namespace (folder) depended on
DependencyFilters Each entry is ArchUnitNET's IgnoreDependenciesToParents, IgnoreDependenciesToChildren or IgnoreDependenciesToChildrenAndParents, or a regular expression whose matching targets are left out

An unknown key, or an option that does not apply to the chosen form (a slice pattern without from: slices, LimitDependencies or C4Style from types, IncludeDependenciesToOther with packages, C4Style without them), is refused with exit 3 rather than ignored. --from with any output type but plantuml is refused the same way; fmt --from also keeps its older values rulebearing and dependency-cruiser.

The nodes and arrows are the ones ArchUnitNET's PlantUmlFileBuilder selects; gate 2 proves the builder against ArchUnitNET's own output over the committed fixtures (conformance/README.md). How they are written depends on the form:

Form Written as
from: types; from: slices with MatchingWithPackages ArchUnitNET's text byte for byte, header (the C4 !include and HIDE_STEREOTYPE()) included. A picture, not an adhereTo diagram: a stereotype places a type by its namespace, so types cannot be components, and adhereTo refuses an !include
from: slices with Matching, from: namespaces, from: folders An adhereTo diagram: hide stereotype in place of the C4 include, one [Name] <<pattern>> as C1 component per node and one C1 --> C2 line per arrow, by alias (a circle as two lines, where ArchUnitNET draws <-[#red]>, which the parser does not read as a dependency)
the same with LimitDependencies, FocusOn or DependencyFilters A partial picture. These options leave arrows out, so the file could never pass its own round trip: its first comment line says it is not for adhereTo, and its components carry no stereotype, so adhereTo refuses it rather than enforcing a partial diagram

A component's stereotype is generated, not meant to be edited: a regular expression that matches the namespaces the node holds and the full name of every type one segment below them, and no other namespace the graph knows, so no two components intersect and a namespace the diagram leaves out lies in none. A type in the global namespace lies in the component (global). Arrows name components by alias, so any folder name reads back: a space stays, and [ and ] (a Next.js app/[slug] folder) are drawn as ( and ) in the name while the stereotype keeps the exact path. With IncludeDependenciesToOther, the extra components are targets: adhere the types of the slices, not theirs; one whose name a slice already has (System beside a slice App.System) is named System (other), so no two components share a name.

The round trip is the reporter's acceptance test: write the diagram, then enforce it, and the same graph reports nothing.

rulebearing cruise -T plantuml -f architecture.puml

with the pattern in the configuration (--from slices is the default once a pattern is given):

options:
  reporterOptions:
    plantuml:
      Matching: "RiverBooks.(*)"

and the diagram rule over it:

rules:
  diagrams:
    - name: modules-follow-the-diagram
      comment: "The committed component diagram. adr:0009"
      select: { kind: type, where: { resideInNamespaceMatching: "^RiverBooks\\.[^.]+$" } }
      adhereTo: architecture.puml

It is proven over every committed .NET graph, from namespaces and from slices, and over a TypeScript tree for src/(*), src/(*)/ and src/(**) (crates/rb-cli/tests/plantuml.rs), and nightly over each .NET oracle (testbeds/oracles/dotnet.sh). A dependency the diagram does not draw is a violation, which is the point: a new edge between two modules fails until the diagram is regenerated and the change reviewed.