Skip to content

Violation Reporting

When an architecture rule fails, ts-archunit produces rich violation messages with code frames, file paths, line numbers, and optional context about why the rule exists and how to fix it.

What You See

A violation includes:

  1. Rule ID (if provided via .rule())
  2. Violation message -- what was found and why it's wrong
  3. File path and line number -- exact location
  4. Code frame -- surrounding source code with the violating line highlighted
  5. Why -- reason the rule exists (from .because() or .rule({ because }))
  6. Fix -- suggested remediation (from .rule({ suggestion }))
  7. Docs -- link to documentation (from .rule({ docs }))

Example output:

Architecture Violation [repo/typed-errors]

  WebhookRepository.findById contains new 'Error' at line 42
  at src/repositories/webhook.repository.ts:42

    41 |     if (!result) {
  > 42 |       throw new Error(`Webhook '${id}' not found`)
    43 |     }

  Why: Generic Error loses context and prevents consistent error handling
  Fix: Replace new Error(msg) with new NotFoundError(entity, id)
  Docs: https://example.com/adr/011#error-handling

.check() vs .warn()

.check()

Throws an ArchRuleError when violations are found. The test fails, CI blocks the PR.

typescript
classes(p).that().extend('BaseRepository').should().notContain(call('parseInt')).check()

.warn()

Logs violations to stderr but does not throw. The test passes. Use for advisory rules.

typescript
classes(p).that().haveDecorator('Deprecated').should().notExist().warn()

When to Use Which

ScenarioMethod
Hard constraint the team agreed on.check()
Aspirational rule being gradually adopted.warn()
New rule with many existing violations.warn() or use baseline mode
Deprecated code tracking.warn()

Rule Metadata with .rule()

When a rule fails, developers need to know not just what broke but why it exists and how to fix it. The .rule() method attaches structured metadata -- ID, rationale, fix suggestion, and docs link -- that appears directly in violation output. This turns cryptic failures into actionable guidance.

Attach context to any rule:

typescript
classes(p)
  .that()
  .extend('BaseRepository')
  .should()
  .notContain(newExpr('Error'))
  .rule({
    id: 'repo/typed-errors',
    because: 'Generic Error loses context and prevents consistent error handling',
    suggestion: 'Replace new Error(msg) with new NotFoundError(entity, id)',
    docs: 'https://example.com/adr/011#error-handling',
  })
  .check()

All fields are optional:

FieldDescriptionShown in output as
idUnique rule identifierHeader: [repo/typed-errors]
becauseWhy the rule existsWhy: ...
suggestionHow to fix a violationFix: ...
docsLink to documentationDocs: ...

.because() Shorthand

For simple reasons without the full .rule() object:

typescript
classes(p)
  .that()
  .extend('BaseRepository')
  .should()
  .notContain(call('parseInt'))
  .because('BaseRepository provides extractCount() for safe type coercion')
  .check()

Excluding Intentional Violations

Not every rule violation is a bug. Some code legitimately needs to break a general rule -- a wrapper that constructs URLSearchParams, a legacy adapter that calls parseInt. Exclusions let you suppress these known-good violations permanently while keeping the rule enforced everywhere else. Unlike baseline mode (which tracks temporary debt), exclusions are for code that is correct as-is.

Some violations are intentional -- they'll never be "fixed" because the code is correct. Use exclusions to suppress them while keeping the rule enforced for everything else.

Chain-level exclusion

Suppress specific violations in the rule definition. Patterns match against the violation's element name, file path, or message:

typescript
// Match by element name
functions(p)
  .that()
  .resideInFolder('**/wrappers/**')
  .should()
  .notContain(newExpr('URLSearchParams'))
  .excluding('Asset.getImageUrl', 'Environment.sync')
  .check() // enforced — excluded elements silently skipped
typescript
// Match by file path (useful for defineCondition violations)
functions(p)
  .should()
  .satisfy(routeMustHavePreHandler())
  .excluding(/images\.ts/, /platform\/index\.ts/)
  .check()
typescript
// Match by message content
classes(p)
  .should()
  .notContain(call('parseInt'))
  .excluding(/LegacyRepo/, /extractCount/)
  .check()

Supports exact strings and regex patterns. String matching is exact (===). Use regex for partial matching.

Patterns are tested against three fields — the first match wins:

  • violation.element — qualified name like MyService.doWork, Config.constructor, or handler (for standalone functions). Inner AST nodes (e.g., AsExpression) are resolved to their nearest enclosing class/method/function.
  • violation.file — absolute file path
  • violation.message — full violation description

If an exclusion pattern matches zero violations, a warning is emitted to help detect stale exclusions after renames.

Inline exclusion comments

Inline comments let you suppress a violation directly in the source file, right next to the code. This is better than chain-level exclusions when the exception is tightly coupled to a specific line -- it survives renames and moves with the code during refactors.

Exclude at the code level -- the exclusion moves with the code:

typescript
// ts-archunit-exclude sdk/no-manual-urlsearchparams: builds image transform URL, not list pagination
async getImageUrl() {
  const params = new URLSearchParams()  // <- not flagged
}

Block exclusions cover a range of lines:

typescript
// ts-archunit-exclude-start sdk/no-manual-urlsearchparams: image URL builder
async getImageUrl() {
  const params = new URLSearchParams()
  return params.toString()
}
// ts-archunit-exclude-end

Multiple rule IDs on one line:

typescript
// ts-archunit-exclude rule-a, rule-b: shared reason for both rules
doSomething()

Requires a .rule({ id }) -- exclusion comments reference the rule by ID. Requires a reason -- undocumented exclusions are flagged as warnings.

Exclusions vs Baseline

MechanismPurposeWhere defined
.excluding()Permanent intentional exceptionsTest file (rule definition)
Inline commentsPermanent exceptions at code levelSource file
BaselineTemporary violations to fix over timearch-baseline.json

Output Formats

Terminal (Default)

Colored output with code frames, used when running locally:

typescript
// Automatically detected
classes(p).should().notContain(call('eval')).check()

GitHub Actions Annotations

When running in GitHub Actions, violations appear as inline annotations on PR diffs:

typescript
import { detectFormat } from '@nielspeter/ts-archunit'

const format = detectFormat() // 'github' in CI, 'terminal' locally

classes(p).should().notContain(call('eval')).check({ format })

JSON

Machine-readable output for custom integrations:

typescript
classes(p).should().notContain(call('eval')).check({ format: 'json' })

Programmatic Format Detection

typescript
import { detectFormat, isCI } from '@nielspeter/ts-archunit'

const format = detectFormat() // auto-detects environment
const ci = isCI() // true in any CI environment

Error Structure

When .check() throws, it throws an ArchRuleError:

typescript
import { ArchRuleError } from '@nielspeter/ts-archunit'

try {
  classes(p).that().extend('BaseRepository').should().notContain(call('parseInt')).check()
} catch (error) {
  if (error instanceof ArchRuleError) {
    console.log(error.violations) // ArchViolation[]
    console.log(error.message) // Formatted violation report
  }
}

ArchViolation Shape

Each violation contains:

PropertyTypeDescription
rulestringHuman-readable rule description from the fluent chain
ruleIdstring | undefinedUnique rule identifier from .rule({ id })
elementstringElement identifier, e.g. "OrderService" or "parseConfig"
filestringAbsolute path to the source file
linenumberLine number where the violating element starts
messagestringHuman-readable description of what went wrong
becausestring | undefinedRationale provided via .because()
suggestionstring | undefinedActionable suggestion for fixing the violation
docsstring | undefinedLink to documentation (ADR, wiki, style guide)
codeFramestring | undefinedSource code snippet around the violation line

Programmatic Access

For custom reporting, catch the error and process violations:

typescript
import {
  ArchRuleError,
  formatViolations,
  formatViolationsPlain,
  formatViolationsJson,
} from '@nielspeter/ts-archunit'

try {
  classes(p).that().extend('BaseRepository').should().notContain(call('parseInt')).check()
} catch (error) {
  if (error instanceof ArchRuleError) {
    // Re-format violations
    const plain = formatViolationsPlain(error.violations)
    const json = formatViolationsJson(error.violations)

    // Send to external system
    await reportToSlack(plain)

    // Or just count them
    console.log(`Found ${error.violations.length} violations`)
  }
}

Code Frame Customization

The generateCodeFrame() utility can be used directly for custom formatting:

typescript
import { generateCodeFrame } from '@nielspeter/ts-archunit'

const frame = generateCodeFrame(sourceText, lineNumber, {
  // options
})

Released under the MIT License.