Skip to content

Output Formats

ArchLinterNet supports human-readable output for local development, JSON output for CI artifacts and downstream automation, and SARIF output for code-scanning viewers.

Human output

Use human output when reading diagnostics in a terminal or CI log:

arch-linter-net --mode strict --format human

Example shape:

- [application-not-infrastructure] [application-must-not-depend-on-infrastructure] MyApp.Application.Services.LegacyService -> MyApp.Infrastructure: MyApp.Infrastructure.Repositories.UserRepository

Human output is optimized for readability, not machine parsing.

When enabled and non-empty, supplemental diagnostics are emitted in dedicated sections:

  • Coverage findings: for namespace, rule-input, project, assembly, and dependency-edge coverage contracts;
  • Coverage summary: for the per-contract coverage counts described in Coverage contracts — printed whenever any coverage contract ran, regardless of analysis.coverage severity;
  • Unmatched ignored violations: for stale baseline/ignore entries;
  • Policy consistency findings: for internal contradictions in the policy document.

Example supplemental section:

Coverage findings:
- [feature-namespace-coverage] [feature-namespace-coverage] MyApp.Features.Payments -> uncovered namespace: MyApp.Features.Payments.PaymentsRepresentative
- [layer-edge-coverage] [layer-edge-coverage] MyApp.Cli.Commands -> MyApp.Testing.Fixtures -> uncovered dependency edge: MyApp.Cli.Commands.DeployCommand

Coverage summary:
- [feature-namespace-coverage] [feature-namespace-coverage] scope: namespace covered=4 excluded=1 uncovered=1 stale=0 unknown=0
    uncovered: MyApp.Features.Payments (MyApp.Features.Payments.PaymentsRepresentative)
- [layer-edge-coverage] [layer-edge-coverage] scope: dependency_edge covered=1 excluded=0 uncovered=1 stale=0 unknown=0
    uncovered: MyApp.Cli.Commands -> MyApp.Testing.Fixtures (MyApp.Cli.Commands.DeployCommand)

JSON output

Use JSON output for CI artifacts, dashboards, or automation:

arch-linter-net --mode strict --format json > architecture-violations.json

Shortcut:

arch-linter-net --strict --json > architecture-violations.json

JSON output is written to stdout. When --timings is also enabled, timings are written to stderr so stdout remains parseable.

Current JSON output is a single top-level object with these arrays:

  • violations
  • cycles
  • coverage_findings
  • unmatched_ignored_violations
  • policy_consistency_findings
  • coverage_summary

Example shape:

{
  "passed": false,
  "mode": "strict",
  "violations": [],
  "cycles": [],
  "coverage_findings": [
    {
      "contract": "feature-namespace-coverage",
      "contract_id": "feature-namespace-coverage",
      "source": "MyApp.Features.Payments",
      "forbidden_namespace": "uncovered namespace",
      "forbidden_references": ["MyApp.Features.Payments.PaymentsRepresentative"]
    },
    {
      "contract": "layer-edge-coverage",
      "contract_id": "layer-edge-coverage",
      "source": "MyApp.Cli.Commands -> MyApp.Testing.Fixtures",
      "forbidden_namespace": "uncovered dependency edge",
      "forbidden_references": ["MyApp.Cli.Commands.DeployCommand"]
    }
  ],
  "unmatched_ignored_violations": [],
  "policy_consistency_findings": [
    {
      "kind": "policy_consistency",
      "check_kind": "duplicate-id",
      "contract": "domain-boundaries",
      "contract_id": "domain-boundaries",
      "reason": "Contract ID is used more than once.",
      "conflicting_contract_ids": ["domain-boundaries", "domain-boundaries"],
      "conflicting_contract_names": ["domain-boundaries", "domain-boundaries-copy"],
      "layers": []
    }
  ],
  "coverage_summary": [
    {
      "contract": "feature-namespace-coverage",
      "contract_id": "feature-namespace-coverage",
      "scope": "namespace",
      "counts": { "covered": 4, "excluded": 1, "uncovered": 1, "stale": 0, "unknown": 0 },
      "excluded_items": [
        { "item": "MyApp.Features.Video.Generated", "reason": "Generated code is excluded from manual architecture coverage." }
      ],
      "uncovered_items": [
        { "item": "MyApp.Features.Payments", "evidence": "MyApp.Features.Payments.PaymentsRepresentative" }
      ],
      "stale_items": [],
      "unknown_items": [],
      "covered_items": [
        { "item": "MyApp.Features.Billing", "evidence": "MyApp.Features.Billing.BillingRepresentative" }
      ]
    },
    {
      "contract": "layer-edge-coverage",
      "contract_id": "layer-edge-coverage",
      "scope": "dependency_edge",
      "counts": { "covered": 1, "excluded": 0, "uncovered": 1, "stale": 0, "unknown": 0 },
      "excluded_items": [],
      "uncovered_items": [
        { "item": "MyApp.Cli.Commands -> MyApp.Testing.Fixtures", "evidence": "MyApp.Cli.Commands.DeployCommand" }
      ],
      "stale_items": [],
      "unknown_items": [],
      "covered_items": [
        { "item": "MyApp.Cli.Commands -> MyApp.Core.Deployment", "evidence": "MyApp.Cli.Commands.DeployCommand" }
      ]
    }
  ]
}

Every coverage_summary entry always includes uncovered_items, stale_items, unknown_items, and covered_items; only the array(s) matching the contract's scope are ever non-empty (uncovered_items for scope: namespace/scope: project/scope: assembly/scope: dependency_edge; unknown_items additionally for scope: project; stale_items/unknown_items for scope: rule_input) — they are kept distinct so a stale finding can't be mistaken for an unknown one or vice versa. covered_items names the specific units found covered with supporting evidence, for every scope — this is the only positive evidence of coverage in the JSON output; a unit's absence from every list (including covered_items) does not mean it is covered, it means no configured contract's scope/roots include that unit at all.

coverage_summary is always present as an array (empty when no coverage contracts ran) and is reported independent of analysis.coverage severity, since it summarizes state rather than gating the run. See Coverage contracts — Coverage summary for the count semantics, including how scope: rule_input maps to stale/unknown.

Behavior for non-violation finding families is controlled separately:

  • analysis.coverage: error|warn|off controls whether coverage_findings fail the run, report without failing, or are suppressed — this applies uniformly across every implemented coverage scope (namespace, rule_input, project, assembly, dependency_edge), not just namespace/rule-input coverage.
  • analysis.policy_consistency: error|warn|off controls whether policy_consistency_findings fail the run, report without failing, or are suppressed.
  • analysis.unmatched_ignored_violations: error|warn|off controls whether stale ignore entries fail the run, report without failing, or are suppressed.

SARIF output

Use SARIF output to feed violations into GitHub code scanning or other standard static-analysis viewers:

arch-linter-net --mode strict --format sarif > architecture-violations.sarif

SARIF output is a single SARIF 2.1.0 document (version: "2.1.0", with a $schema pointing at the SARIF 2.1.0 schema) containing one run:

  • tool.driver.name identifies the CLI, and tool.driver.rules lists every contract ID that produced a result, deduplicated by rule ID.
  • Each result.ruleId is the violating contract's ID (or a normalized fallback derived from its name when no ID is set).
  • Each result.level is error in --mode strict and warning in --mode audit — SARIF severity reflects the run's mode uniformly, not a per-contract setting.
  • Method-body violations (source-scanned forbidden calls) include a physicalLocation with the source file and line number. Every other violation kind (dependency/layer, external-dependency, package-dependency, type-placement, IL-scanned method-body calls, etc.) includes a logicalLocations entry naming the type, namespace, assembly, or package involved, since no file position is available for those checks.

SARIF output only covers violations and cycles. Coverage findings, unmatched-ignored violations, and policy-consistency findings — the same supplemental categories shown in the human and JSON output above — are not included in SARIF results, since they describe the policy configuration itself rather than a violation found in scanned code. If a run fails (exit code 1) because of one of those categories with zero violations or cycles, the SARIF document will report an empty results array even though the run failed. Use --format json (or human output) alongside SARIF if you need visibility into those categories in CI.

CI artifact pattern

- name: Validate architecture
  run: arch-linter-net --strict --json > architecture-violations.json

- name: Upload architecture violations
  if: failure()
  uses: actions/upload-artifact@v4
  with:
    name: architecture-violations
    path: architecture-violations.json

For audit runs, keep the job non-blocking and always upload the artifact:

- name: Architecture audit
  if: always()
  continue-on-error: true
  run: arch-linter-net --audit --json > architecture-audit.json