Enforcement Rules (rules.yml)
Complete reference for rules.yml — the enforcement configuration file that defines architecture rules, runtime safety checks, reliability rules, merge gates, and scoring.
Enforcement Rules: rules.yml
The rules.yml file defines what to enforce on every pull request. It contains architecture dependency rules, runtime safety checks, reliability requirements, performance detection rules, merge gate thresholds, scoring weights, and exceptions.
# rules.yml — generated by radar init
architecture_rules:
- deny: "domain -> infrastructure"
- deny: "domain -> api"
- deny: "cross-module direct imports"
- allow: "cross-module through 'src/**/contracts/**'"
gates:
block_merge:
- architecture_violations > 0
- runtime_risk_critical > 0
- debt_delta_score > 15
Tip: Run
radar initto auto-generaterules.ymlbased on your detected architecture. Runradar validateafter any manual edits.
Two-File vs. Single-File Mode
Technical Debt Radar supports two configuration approaches:
Two-file mode (recommended): radar.yml defines your project structure, rules.yml defines enforcement. When both files exist, rules.yml takes precedence for all overlapping fields.
Single-file mode (legacy): Everything lives in radar.yml. Fields like rules, runtime_rules, reliability_rules, gates, scoring, and exceptions are read directly from radar.yml. This mode is backward-compatible with v4 configurations.
When migrating from single-file to two-file mode, move enforcement-related fields from radar.yml into rules.yml:
radar.yml field | rules.yml field |
|---|---|
rules | architecture_rules |
runtime_rules | runtime_rules |
reliability_rules | reliability_rules |
gates | gates |
scoring | scoring |
exceptions | exceptions |
Note: The
rulesfield inradar.ymlmaps toarchitecture_rulesinrules.yml. The field was renamed for clarity.
Full Schema Reference
architecture_rules
Dependency rules that govern which layers and modules can import from each other. Same syntax as the rules field in radar.yml.
architecture_rules:
# Shorthand — human-readable arrow syntax
- deny: "domain -> infrastructure"
- deny: "domain -> api"
- deny: "domain -> application"
- deny: "api -> infrastructure"
- deny: "cross-module direct imports"
- allow: "cross-module through 'src/**/contracts/**'"
# Structured — machine-friendly format
- type: deny
source: domain
target: infrastructure
description: "Domain must not depend on infrastructure"
Shorthand syntax patterns:
| Pattern | Meaning |
|---|---|
deny: "A -> B" | Files in layer A cannot import from layer B |
deny: "A <- B" | Files in layer B cannot import from layer A (reverse arrow) |
allow: "A -> B" | Explicitly permits imports from A to B (overrides a broader deny) |
deny: "cross-module direct imports" | Forbids all direct imports across module boundaries |
allow: "cross-module through 'glob'" | Permits cross-module imports only through files matching the glob |
Structured syntax fields:
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | allow or deny |
source | string | Yes | Source layer/module name, or cross-module |
target | string | Yes | Target layer/module name, cross-module, or __db__ |
through | string | No | Glob pattern for allowed import paths |
description | string | No | Human-readable description shown in reports |
The virtual target __db__ matches direct ORM/database package imports (e.g., import { PrismaClient } from '@prisma/client'). Use it to enforce that only certain layers touch the database:
architecture_rules:
- deny: "controllers -> __db__" # Controllers cannot access DB directly
- deny: "domain -> __db__" # Domain must be pure
Rule precedence: When the same source-target pair has both deny and allow rules:
- A
denywithoutthroughalways wins over anallowwithoutthrough - An
allowwiththroughcreates a narrow exception within a broaderdeny
# Common pattern: deny all cross-module, allow through contracts
architecture_rules:
- deny: "cross-module direct imports"
- allow: "cross-module through 'src/**/contracts/**'"
runtime_rules
Controls which Node.js runtime safety patterns block merges and which produce warnings.
runtime_rules:
block:
- sync-fs-in-handler
- sync-crypto
- sync-compression
- redos-vulnerable-regex
- busy-wait-loop
- unhandled-promise
warn:
- unbounded-json-parse
- large-json-stringify
- cpu-heavy-loop-in-handler
- unbounded-array-operation
- dynamic-buffer-alloc
Moving a rule from warn to block makes it a merge blocker. Moving it from block to warn downgrades it. Removing a rule entirely disables that check.
All available runtime rules:
| Rule ID | What it detects |
|---|---|
sync-fs-in-handler | fs.readFileSync(), fs.writeFileSync(), etc. in request handlers |
sync-crypto | crypto.pbkdf2Sync(), crypto.scryptSync(), etc. |
sync-compression | zlib.gzipSync(), zlib.deflateSync(), etc. |
redos-vulnerable-regex | Regex patterns vulnerable to catastrophic backtracking |
busy-wait-loop | while(true) spin loops that block the event loop |
unhandled-promise | Promises without .catch() or await inside try/catch |
unbounded-json-parse | JSON.parse() on untrusted input without size validation |
large-json-stringify | JSON.stringify() on objects that could be arbitrarily large |
cpu-heavy-loop-in-handler | Nested loops or heavy computation inside route handlers |
unbounded-array-operation | .map(), .filter(), .reduce() on arrays without size bounds |
dynamic-buffer-alloc | Buffer.alloc() or Buffer.allocUnsafe() with dynamic size |
reliability_rules
Controls which reliability patterns block merges and which produce warnings.
reliability_rules:
block:
- unhandled-promise-rejection
warn:
- missing-try-catch
- external-call-no-timeout
- retry-without-backoff
- empty-catch-block
- missing-error-logging
- transaction-no-timeout
- missing-null-guard
All available reliability rules:
| Rule ID | What it detects |
|---|---|
unhandled-promise-rejection | Promise chains that can reject without a handler |
missing-try-catch | await expressions outside try/catch blocks |
external-call-no-timeout | HTTP, gRPC, or external service calls without timeout config |
retry-without-backoff | Retry loops without exponential or incremental backoff |
empty-catch-block | catch (e) {} or catch (e) { /* ignore */ } |
missing-error-logging | Error handling paths that don't log the error |
transaction-no-timeout | Database transactions ($transaction, getManager()) without timeout |
missing-null-guard | Accessing .property on values that could be null or undefined |
Tip: For high-reliability services, move all reliability rules to
block. Thereliability-focusedrule pack does exactly this.
performance_rules
Lists ORM and query performance anti-patterns to detect. Unlike runtime and reliability rules, performance rules use a flat list (severity is determined by the entity's volume tier).
performance_rules:
detect:
- unbounded-find-many
- find-many-no-where
- nested-include-large-relation
- n-plus-one-query
- fetch-all-filter-in-memory
- missing-pagination-endpoint
- unfiltered-count-large-table
- raw-sql-no-limit
All available performance rules:
| Rule ID | What it detects |
|---|---|
unbounded-find-many | findMany() without take or limit on L/XL/XXL entities |
find-many-no-where | findMany({}) without any where clause |
nested-include-large-relation | Deep include nesting on relations with large cardinality |
n-plus-one-query | Query inside a loop that should be batched |
fetch-all-filter-in-memory | Fetching all rows then filtering with .filter() in application code |
missing-pagination-endpoint | API endpoints returning lists without pagination parameters |
unfiltered-count-large-table | count() without where on XL/XXL tables |
raw-sql-no-limit | Raw SQL queries without LIMIT clause |
Performance rule severity is volume-aware. An unbounded-find-many on an S-tier entity is suppressed, but the same pattern on an XL-tier entity blocks the merge. See Volume Configuration.
gates
Merge gate conditions that determine PR outcomes. The block_merge conditions prevent merging; the warn conditions post comments but allow merging.
gates:
block_merge:
- architecture_violations > 0
- circular_dependencies_introduced > 0
- runtime_risk_critical > 0
- reliability_critical > 0
- critical_performance_risk > 0
- debt_delta_score > 15
warn:
- complexity_increase > 5
- coverage_drop > 2%
- debt_delta_score > 8
Each condition follows the format: metric operator value
Supported operators: >, >=, <, <=, ==
Percentage values (like 2%) are converted to their numeric equivalent -- coverage_drop > 2% becomes coverage_drop > 2.
Available metrics:
| Metric | Type | Description |
|---|---|---|
architecture_violations | count | Layer or module boundary violations in changed files |
circular_dependencies_introduced | count | New circular dependency chains |
runtime_risk_critical | count | Critical runtime risk findings |
runtime_risk_warning | count | Warning-level runtime risk findings |
reliability_critical | count | Critical reliability findings |
reliability_warning | count | Warning-level reliability findings |
critical_performance_risk | count | Critical performance findings (volume-adjusted) |
debt_delta_score | score | Net change in debt score (positive = more debt) |
complexity_increase | score | Sum of cyclomatic complexity increases |
coverage_drop | percentage | Test coverage decrease |
maintainability_violations | count | Maintainability issue count |
Note: The
debt_delta_scoreis computed from all findings using the scoring weights. A PR that fixes more issues than it introduces can have a negative delta (good). The threshold of15(default) allows small regressions while blocking large ones.
scoring
Override default scoring weights. Each finding type has a point value that contributes to the debt_delta_score.
scoring:
architecture_violation: 5
circular_dependency: 10
runtime_risk_critical: 8
runtime_risk_warning: 3
performance_risk_critical: 8
performance_risk_warning: 3
reliability_critical: 5
reliability_warning: 3
complexity_point: 1
missing_tests: 3
coverage_drop_per_pct: 2
ai_concern: 2
violation_fixed: -5
runtime_risk_fixed: -8
complexity_reduced: -1
reliability_fixed: -3
Negative values are credits. When a PR fixes an existing violation, the negative weight reduces the debt delta, rewarding cleanup work.
Tip: To make your team prioritize performance fixes, increase
performance_risk_criticalandperformance_risk_warning. To be lenient on complexity, lowercomplexity_point.
exceptions
Temporary exemptions for specific files. Every exception has a mandatory expiration date.
exceptions:
- rule: "domain -> infrastructure"
file: "src/billing/domain/legacy-adapter.ts"
expires: "2026-06-01"
reason: "Legacy migration — tracked in JIRA-1234"
- rule: "sync-fs-in-handler"
file: "src/config/bootstrap.ts"
expires: "2026-03-30"
reason: "Config file read at startup only — refactoring in sprint 42"
| Field | Type | Required | Description |
|---|---|---|---|
rule | string | Yes | Rule being exempted |
file | string | Yes | File path the exception applies to |
expires | string | Yes | Expiration date (YYYY-MM-DD format) |
reason | string | Yes | Why the exception exists (include ticket reference) |
Expired exceptions are silently ignored during analysis. The PR comment and dashboard both surface upcoming expirations (within 30 days).
Warning: When regenerating rules with
radar init --regenerate-rules, existing exceptions are preserved. Your team's exception agreements are never overwritten.
Generated Rules
When you run radar init, the CLI detects your architecture pattern and generates rules.yml with appropriate rules. The generated file includes comments explaining each section:
# rules.yml — Auto-generated for DDD architecture
# Pattern: ddd | Generated: 2026-03-18
#
# You can modify this file:
# - Comment out rules you don't want
# - Add custom rules
# - Adjust gate thresholds
# - Add exceptions
# Run: radar validate to verify changes
# -- Architecture Rules (DDD) --
architecture_rules:
- deny: "domain -> infrastructure" # Domain must not depend on infrastructure
- deny: "domain -> api" # Domain must not depend on API/controllers
- deny: "domain -> application" # Domain must not import from application layer
- deny: "api -> infrastructure" # Controllers must not directly access infrastructure
- deny: "cross-module direct imports" # Modules must not directly import from each other
- allow: "cross-module through 'src/**/contracts/**'"
# -- Runtime Safety (Node.js) --
runtime_rules:
block:
- sync-fs-in-handler
- sync-crypto
# ... remaining rules
You can freely edit the generated file. Comment out rules you don't want, add custom rules, adjust thresholds, and add exceptions. Run radar validate after changes.
Custom Rules
You can add custom dependency rules beyond what the presets generate. Custom rules use the same syntax:
architecture_rules:
# Generated rules (from preset)
- deny: "domain -> infrastructure"
# Custom rules (added by your team)
- deny: "services -> controllers" # Services cannot call controllers
- deny: "repositories -> services" # Repositories cannot depend on services
- allow: "infrastructure -> domain" # Infra may reference domain entities
- deny: "controllers -> __db__" # No direct DB access from controllers
Rules are evaluated in order. When a file import is checked, the engine finds the first matching rule and applies it.
Combining with Rule Packs
Rule packs provide pre-built rules.yml configurations. When you install a pack, it writes the full rules.yml. You can then customize it:
# Install a rule pack
radar pack install nestjs-prisma-ddd-strict
# Customize the generated rules.yml
# Then validate
radar validate
The pack's rules become your starting point. Any edits you make are preserved across radar validate runs.