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 ofanalysis.coverageseverity;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:
violationscyclescoverage_findingsunmatched_ignored_violationspolicy_consistency_findingscoverage_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|offcontrols whethercoverage_findingsfail 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|offcontrols whetherpolicy_consistency_findingsfail the run, report without failing, or are suppressed.analysis.unmatched_ignored_violations: error|warn|offcontrols 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.nameidentifies the CLI, andtool.driver.ruleslists every contract ID that produced a result, deduplicated by rule ID.- Each
result.ruleIdis the violating contract's ID (or a normalized fallback derived from its name when no ID is set). - Each
result.leveliserrorin--mode strictandwarningin--mode audit— SARIF severity reflects the run's mode uniformly, not a per-contract setting. - Method-body violations (source-scanned forbidden calls) include a
physicalLocationwith 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 alogicalLocationsentry 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