ADR-0032: Liveness follows the configuration's format, with named exceptions in the native file
- Status: Accepted
- Date: 2026-09-23
- Derives from: design ยง Why ("superset, precisely; nothing is dropped"), ADR-0007, ADR-0030 (a drop-in exits as dependency-cruiser does), the private monorepo measurement in docs/perf.md
- Constrains:
crates/rb-config(allowEmptyat the top level of a native file),crates/rb-model(summary.vacuousRules[].severity),crates/rb-cli(--liveness,cruise,fmt --exit-code,adopt) - Supersedes: the clause of ADR-0007 that keeps the check failing "in dependency-cruiser compatibility mode". Its default for native files, its
allowEmpty: trueon a rule,--no-liveness, andsummary.vacuousRulesstand. - Implemented by: Wave 1 plan, Steps 7 and 17
- Requirements: FR-CORE-05, NFR-CONF-01
Context
ADR-0007 fails a run when a rule's selecting side matches no module, in every mode. On a real dependency-cruiser repository that makes Rulebearing exit 2 where dependency-cruiser exits 0: the private monorepo of docs/perf.md has one such rule. The way out ADR-0007 gives, allowEmpty: true on the rule, cannot be written in a .dependency-cruiser.* file. dependency-cruiser's configuration schema rejects any key it does not define on a rule, so the file would stop working with dependency-cruiser. What is left is --no-liveness, which turns the check off for every rule, the opposite of what the check is for.
A drop-in that fails where dependency-cruiser passes is not a drop-in, and ADR-0030 already settled the same question for the error count.
Decision
- Three modes.
--liveness strictfails the run with exit 2 (ADR-0007's behaviour).--liveness warnreports each vacuous rule insummary.vacuousRules[], with"severity": "warn", and as awarning:on stderr, without changing the exit code.--liveness offdoes not check.--no-livenessstays as the spelling ofoff, and cannot be combined with--liveness. - The default follows the root file. A
.dependency-cruiser.*configuration runs underwarn, so it exits as dependency-cruiser exits. Arulebearing.*configuration runs understrict, including one thatextendsa dependency-cruiser file, which is whatadoptwrites. A repository opts into the check by adopting. - Named exceptions. A native file may carry
allowEmpty: [names]at the top level: rule names, ratchet names, andallowed[N]for the Nthallowedentry. Each named rule is exempt, as if it carriedallowEmpty: true. The list merges throughextends. A name that is no rule or ratchet of the configuration is a configuration error (exit 3), so an exception cannot outlive its rule. In a dependency-cruiser file the key is a native addition: a warning, and an error under--strict-compat, like the others (ADR-0005). adoptlists the rules that match nothing today underallowEmptyin therulebearing.yamlit writes, and names them in its output and in the pull request, instead of refusing to adopt.- Saved results.
fmt --exit-codecounts only thevacuousRulesentries without"severity": "warn", so a saved result gates as its cruise did (ADR-0031).severityis additive and absent understrict.
Consequences
- A dependency-cruiser configuration runs under Rulebearing with dependency-cruiser's exit code, and a stale rule is still named on every run.
- A repository that adopts gets the check on every rule, and the exceptions are a reviewed list in one file rather than a global switch.
- Someone who uses Rulebearing only as a faster dependency-cruiser is warned about a dead rule, not stopped by it, which is what dependency-cruiser gives them today.
--liveness strictrestores the failure without adopting. - Conformance gate 1 still passes
--no-liveness, because dependency-cruiser's specs cover rules that match nothing on purpose.
Alternatives considered
- Keep ADR-0007 in every mode. Rejected: it keeps dependency-cruiser users on
--no-liveness, which disables the check for every rule. - Warn in every mode. Rejected: ADR-0007's reason stands for a repository that has chosen Rulebearing. Four rules matched nothing for months while warnings went unread.
- Accept
allowEmptyon rules in a dependency-cruiser file. Rejected: the file would then fail dependency-cruiser's own validation, and a repository that runs both tools could not use it.