Configuration

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 init to auto-generate rules.yml based on your detected architecture. Run radar validate after 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 fieldrules.yml field
rulesarchitecture_rules
runtime_rulesruntime_rules
reliability_rulesreliability_rules
gatesgates
scoringscoring
exceptionsexceptions

Note: The rules field in radar.yml maps to architecture_rules in rules.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:

PatternMeaning
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:

FieldTypeRequiredDescription
typestringYesallow or deny
sourcestringYesSource layer/module name, or cross-module
targetstringYesTarget layer/module name, cross-module, or __db__
throughstringNoGlob pattern for allowed import paths
descriptionstringNoHuman-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 deny without through always wins over an allow without through
  • An allow with through creates a narrow exception within a broader deny
# 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 IDWhat it detects
sync-fs-in-handlerfs.readFileSync(), fs.writeFileSync(), etc. in request handlers
sync-cryptocrypto.pbkdf2Sync(), crypto.scryptSync(), etc.
sync-compressionzlib.gzipSync(), zlib.deflateSync(), etc.
redos-vulnerable-regexRegex patterns vulnerable to catastrophic backtracking
busy-wait-loopwhile(true) spin loops that block the event loop
unhandled-promisePromises without .catch() or await inside try/catch
unbounded-json-parseJSON.parse() on untrusted input without size validation
large-json-stringifyJSON.stringify() on objects that could be arbitrarily large
cpu-heavy-loop-in-handlerNested loops or heavy computation inside route handlers
unbounded-array-operation.map(), .filter(), .reduce() on arrays without size bounds
dynamic-buffer-allocBuffer.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 IDWhat it detects
unhandled-promise-rejectionPromise chains that can reject without a handler
missing-try-catchawait expressions outside try/catch blocks
external-call-no-timeoutHTTP, gRPC, or external service calls without timeout config
retry-without-backoffRetry loops without exponential or incremental backoff
empty-catch-blockcatch (e) {} or catch (e) { /* ignore */ }
missing-error-loggingError handling paths that don't log the error
transaction-no-timeoutDatabase transactions ($transaction, getManager()) without timeout
missing-null-guardAccessing .property on values that could be null or undefined

Tip: For high-reliability services, move all reliability rules to block. The reliability-focused rule 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 IDWhat it detects
unbounded-find-manyfindMany() without take or limit on L/XL/XXL entities
find-many-no-wherefindMany({}) without any where clause
nested-include-large-relationDeep include nesting on relations with large cardinality
n-plus-one-queryQuery inside a loop that should be batched
fetch-all-filter-in-memoryFetching all rows then filtering with .filter() in application code
missing-pagination-endpointAPI endpoints returning lists without pagination parameters
unfiltered-count-large-tablecount() without where on XL/XXL tables
raw-sql-no-limitRaw 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:

MetricTypeDescription
architecture_violationscountLayer or module boundary violations in changed files
circular_dependencies_introducedcountNew circular dependency chains
runtime_risk_criticalcountCritical runtime risk findings
runtime_risk_warningcountWarning-level runtime risk findings
reliability_criticalcountCritical reliability findings
reliability_warningcountWarning-level reliability findings
critical_performance_riskcountCritical performance findings (volume-adjusted)
debt_delta_scorescoreNet change in debt score (positive = more debt)
complexity_increasescoreSum of cyclomatic complexity increases
coverage_droppercentageTest coverage decrease
maintainability_violationscountMaintainability issue count

Note: The debt_delta_score is 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 of 15 (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_critical and performance_risk_warning. To be lenient on complexity, lower complexity_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"
FieldTypeRequiredDescription
rulestringYesRule being exempted
filestringYesFile path the exception applies to
expiresstringYesExpiration date (YYYY-MM-DD format)
reasonstringYesWhy 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.

Technical Debt Radar Documentation