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.
For report routing, partial-output, profile, and cancellation workflows, see Adopt or Upgrade ArchLinterNet.
Release forensics report output¶
arch-linter-net history analyze --from <rev> --to <rev> writes the version-1
Release Architecture Forensics JSON report to standard output. Its canonical
bytes use UTF-8 without BOM, LF, two-space indentation, exactly one terminal LF,
fixed schema/property order, exact integer fields, and nine-place canonical
real values. Artifact equality is byte equality, not semantic JSON equality.
Use --format markdown for the deterministic human reading view. It summarizes
the range/configuration, hotspots, co-change clusters, bottlenecks, OCP pressure,
candidates, enrichment, and interpretation limits; it never changes the JSON
artifact. Git-only analysis remains valid when enrichment is not requested,
inapplicable, or unavailable. Failed canonical analysis writes a separate stable
diagnostic and no partial report, ranking, or candidate set.
Policy context output¶
arch-linter-net policy context --format json writes one deterministic
architecture-policy-context document with schema_version: 5. It is a
policy-only artifact for coding-agent context: it describes effective declared
policy facts and portable provenance, and does not report an architecture
validation result. --format markdown renders the same model as a compact
prompt-ready summary. Neither format includes local absolute paths, build
receipts, target-assembly results, or runtime environment values.
Version 5 additionally records native declared-topology facts. It also retains the resolved waiver-lifecycle profile and every structured waiver's exact target fingerprint, lifecycle metadata, and portable provenance alongside typed ignored-violation matchers. It is not backward-compatible as a weakening-comparison input: regenerate base and current contexts with the same supported CLI version rather than treating a missing section as empty.
Policy weakening output¶
arch-linter-net policy weakening --base-context <path> --current-context <path> compares two separately generated policy-context JSON artifacts. It
does not load YAML or perform project/assembly analysis. The normalized result
has kind architecture-policy-weakening, schema version 1, the current
configured severity, and deterministically ordered findings. Every finding
contains a stable identity, weakening kind, control identity, semantic versus
impact_not_proven classification, base/current values and provenance,
optional canonical affected subjects, and existing schema-backed rationale.
Project include/exclude glob changes are emitted as impact_not_proven unless
complete resolved project membership is supplied; they are never treated as
literal-string inventories. Prefix/glob/call-pattern facts and cross-field
location unions are also impact_not_proven until their effective membership
or containment is proven. A required source expansion made empty-tolerant is a
semantic finding.
Changes to authored analysis target_assemblies, projects, and source_roots
are likewise impact_not_proven until effective discovery/scanner scope is
available as trusted evidence.
Adding a structured waiver or extending its expiry is semantic weakening;
changing its target fingerprint is impact_not_proven, never silently accepted
as equivalent.
Human, JSON, and SARIF project that same result. JSON is suitable for CI and
SARIF uses one ArchLinterNet.PolicyWeakening.<kind> rule for each weakening
kind. An error finding exits 1; warn and off findings remain visible and
exit 0. Invalid, incomplete, or incompatible context input exits 2 instead of
being interpreted as a clean comparison.
Architecture debt gate output¶
arch-linter-net gate --policy <path> --baseline <path> composes complete
current persistent-debt comparison with optional explicitly exported base/current
policy contexts. Its result has kind architecture-debt-gate and three
independent sections: evaluation, persistent_debt, and
policy_weakening. Persistent entries retain the exact baseline identity and
new/matched/resolved/stale/ambiguous/configuration-error lifecycle
status. Weakening entries retain their own identity, classification, severity,
values, provenance, and rationale; they never get a baseline status.
Human output is a readable sectioned report. JSON is one deterministic document
for automation. SARIF emits gate_section: persistent_debt or
gate_section: policy_weakening result properties with separate rule
namespaces. A matched entry is still visible, but only new or untrusted
persistent-debt state fails the debt dimension. The command is read-only and
does not add a ratchet validation mode.
Architecture health output¶
arch-linter-net health --policy <path> --baseline <path> projects the canonical
architecture-health/v1 summary from current evaluation, applicability, coverage, topology,
metrics, external evidence, audit evidence, policy inventory, baseline debt, waiver debt, new
debt, and policy weakening authorities. History is explicitly not_configured until an
advisory-only history input is added. The command does not parse another command's output or
recompute any dimension in the CLI.
When the selected policy declares external_evidence, supply its repository-local SARIF inputs
through repeatable --external-evidence id=<id>,path=<path> bindings and supply the current
producer context with --evidence-repository, --evidence-revision, and, when required,
--evidence-scope. Health binds those artifacts through the canonical trust authority before it
projects JSON, so a matching zero-result SARIF receipt remains current rather than becoming an
unavailable report authority.
Human output is Core's readable projection. JSON is one health document with schema_id, gate,
health, and ordered dimensions; each dimension retains ordered reasons. The health model is
non-compensating and deliberately has no score, percentage, letter grade, badge, PR rendering, or
SARIF representation. Valid gate: unassessable evidence remains a health document, not a
command_error envelope.
Every reason has stable code and source fields. When the owning authority supplies them, JSON
also carries family, control_identity, policy_identity, and evidence_identity; these are
canonical drill-down references, not formatted display text. Family applicability state reports
whether a control can be assessed, while the family health state separately projects authoritative
topology violations, metric-budget breaches, and imported external findings. Waiver health projects
the evaluated lifecycle profile and its blocking states rather than aggregate debt counts. A
resolved baseline entry is reported as resolved_baseline_hygiene on the passing
new_architecture_debt dimension, while the independent gate can still require baseline pruning.
Coverage follows its existing severity authority: analysis.coverage: error produces a failing
coverage dimension, while warn retains the finding as non-blocking degrading evidence. When
--mode audit is requested, ordinary audit diagnostics are retained in the non-blocking
audit_evidence dimension, so a clean result remains distinguishable from an audit-only result.
Architecture pull-request report output¶
arch-linter-net report pr --health <architecture-health.json> --change <architecture-change.json> renders deterministic Markdown from two canonical local artifacts. The
Health input is an architecture-health/v1 document with versioned canonical reporting evidence;
the change input is the versioned architecture-change report. Both must carry the same non-empty
workflow execution identifier and condition-set scope when the Health envelope is present.
Malformed, incomplete supplied envelopes and incompatible artifacts fail closed. A legacy Health
artifact that has no reporting-evidence envelope remains a valid input, but the report renders its
headline with report availability unavailable and no fabricated evidence detail. The command consumes
those artifacts only: it does not rerun or recreate analysis, reopen snapshots, inspect GitHub, or
publish a pull-request comment. Use --output <architecture-pr-report.md> to write the Markdown
file; otherwise it is written to standard output.
An automated producer may supply one trusted, optional navigation set:
arch-linter-net report pr \
--health architecture-health.json \
--change architecture-change.json \
--repository-url https://github.com/example/repository \
--head-sha "$GITHUB_PR_HEAD_SHA" \
--artifact-url https://github.com/example/repository/actions/runs/123456789 \
--output architecture-pr-report.md
The repository URL, current head SHA, and GitHub Actions run/artifact URL are transport context,
not governance evidence. The CLI accepts a link only when the HTTPS GitHub Actions URL is bound to
the supplied repository and run context; it never uses the values to recalculate Gate, Health,
coverage, debt, or change semantics. The report keeps the resulting full-report link outside the
ordinary bounded detail lists, so it remains available when --max-details omits rows. Local or
historical invocations may omit the set; the report then labels full bundle navigation
unavailable instead of inventing a URL. An invalid supplied context fails closed.
Use --max-details <positive-count> to bound each detailed evidence family independently. Canonical
totals and omitted counts remain visible, and ordering is stable across runs. The report combines
neither evidence nor authority: its headline repeats direct Health/projection gate and health
facts and is not a score, percentage, grade, or compensating quality calculation. Gate and Health
remain independent: gate=pass can legitimately accompany health=debt or health=degrading.
Blockers contains only canonical blocking reasons; a separate Health explanation identifies every
non-healthy dimension, including advisory debt, without relabeling it as a Gate failure.
Effective rule counts, applicability completeness, topology evidence, external evidence, waiver lifecycle detail, architecture change, remediation, and canonical navigation remain separate sections. Each ordinary detail section is bounded independently and reports its total, shown rows, and omitted rows; bounded Markdown is not a replacement for the canonical JSON artifacts. The full immutable report bundle/run link is transport navigation and is always shown when validated, outside those bounds. A missing required authority remains unavailable or unassessable, never an implied zero or pass. Change details likewise retain the canonical added, continuing, and resolved evidence supplied by the change report.
Missing or incomplete canonical evidence is represented as unavailable or unassessable, never as a zero count or passing state. Input/schema incompatibility fails closed rather than being interpreted as a clean report. Canonical family, control, policy, evidence, and navigation identities remain source references for drill-down; the renderer does not manufacture replacements.
GitHub comment publication, workflow/event orchestration, and security permissions remain outside this command's boundary. This repository's separate completed-CI publisher consumes the exact rendered file only after validating a bounded manifest, current PR head, producer run identity, and report hash; the CLI itself remains the local architecture-report projection.
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.
Remediation hints¶
When a diagnostic contains enough typed policy and analysis evidence, its
normalized finding can include an optional deterministic remediation hint. A
Human report appends a concise remediation: <category>: <summary> clause; JSON
exposes the full structured value as remediation_guidance; and SARIF retains the
same normalized value under properties.arch_linter_net.remediation_guidance.
The existing port-boundary remediation_hint string remains available unchanged
for v1 consumers, including in typed details.
Hints are guidance, not edits. They never create code changes, rewrite YAML,
baselines, or reviewed public-API snapshots, and SARIF output does not emit
fixes for them.
The category is a finite machine-readable token:
move_code— move code to an already-evidenced architectural owner;depend_on_abstraction/invert_dependency— only when policy evidence already establishes the required abstraction or direction;introduce_adapter/use_declared_port— use an already-declared adapter or port seam;fix_classification/fix_policy_input— correct role, location, coverage, build, or policy input facts before changing structure;narrow_exception— a precise exception may need explicit review;remove_or_replace_dependency— remove a forbidden dependency when no approved seam is evidenced;review_contract— existing evidence is insufficient to prescribe a safe structural repair.
Every populated hint carries its category, summary, stable contract identity, structured canonical finding identity, ordered evidence, optional expected seam or direction, caveat, and review flag. The structured identity keeps same-named subjects from different assemblies distinct; never use the display text as identity.
For example, a port-boundary result with a declared seam includes compact data like this:
"remediation_guidance": {
"category": "use_declared_port",
"summary": "Use the declared port seam instead of the direct cross-context dependency.",
"contract_identity": "orders-boundary",
"finding_identity": { "source_assembly": "App", "source_type": "App.Orders.OrderService" },
"evidence": [
{ "kind": "evidence_kind", "value": "direct_edge" },
{ "kind": "expected_seam", "value": "role:Port, name: Orders" }
],
"expected_seam_or_direction": "role:Port, name: Orders",
"caveat": "The declared seam is the only supported alternative; do not add a broad exception.",
"requires_review": false
}
Treat a hint as an evidence-backed starting point, not permission to make the
policy easier to satisfy. In particular, do not respond by adding broad ignores
or exclusions, expanding allow-lists merely to permit the observed edge,
reducing governed scope, baselining new debt, changing strict to audit, or
deleting a contract without evidence that it is wrong. When no safe specialized
hint is present, keep the existing diagnostic unchanged and review the contract
and policy context.
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 by default. Use --report json=<path> to write JSON to a file while routing a different format to stdout. When --timings is also enabled, timings are written to stderr so stdout remains parseable.
For baseline-configuration and public-API snapshot or build-state failures after a command has selected --format json, stdout remains one parseable JSON error document and the existing exit code is retained. Those newly unified paths use a common envelope containing schema_version: 1, status: "error", kind: "command_error", and an error object with category, message, and typed details when the command has diagnostic evidence. Validation, policy-check, graph, and explain preserve their existing structured JSON error documents. Human output remains on stderr for the same failures.
arch-linter-net measure --format json is a distinct read-only report rather
than a validation-result document. Its schema_id: "architecture-metrics-report/v1" and schema_version: 1 envelope has a
complete or unassessable status and ordered measurements. Each measurement
records its ID, metric kind, native subject, effective scope, typed
applicability state, exact numeric value only when evaluable, and contributor
evidence. A bounded contributor list always includes contributor_count and
contributors_truncated; --all-contributors disables the bound. The report
also carries the shared applicability completion/projection so an incomplete
scope cannot be mistaken for a trustworthy low value. An unassessable
measurement serializes value, contributor_count, contributors, and
contributors_truncated as null: it has not proven an empty contributor
universe. It does not contain
healthy metric values as violations or SARIF findings.
Current JSON output is a single top-level object with these result fields:
violationscyclescoverage_findingsunmatched_ignored_violationswaiverspolicy_inventorypolicy_consistency_findingscoverage_summary
waivers is present when the policy has manual ignores. Each entry has an ID,
lifecycle state, exact target_fingerprint when structured, owner, issue,
lifecycle dates, the single evaluation_date used for the run, matching status,
and policy provenance. Structured waivers are active, stale, or expired;
legacy compatibility entries are metadata_incomplete. Invalid metadata fails
policy loading instead of producing a potentially trusted record.
policy_inventory is present when Core produced effective-policy inventory
evidence for the validation result. It is the canonical
architecture-policy-inventory/v1 projection: effective_rule_count counts
configured effective controls once (rather than findings, YAML lines, or
source-set aliases). It is repository-level for the selected effective policy,
so its strict, audit, and coverage rules partition is the same whether the
current run evaluates strict or audit findings. Its ignore_debt and
waivers use that same selected repository scope, retaining each configured
manual waiver lifecycle record once. The mode-local waivers result outside
policy_inventory remains the evidence used for that mode's validation gate.
Inventory waiver records retain canonical lifecycle IDs, state, target,
remediation metadata, and portable provenance for drill-down.
Baseline finding debt, ordinary findings, and intended scope exclusions are not
waiver debt. Consumers must preserve a missing policy_inventory as missing
evidence rather than interpreting it as zero rules or zero waivers.
The Health report's waiver_lifecycle.evaluation_date is retained even when
records is empty, so a zero-waiver run still carries the explicit evaluation
date needed to establish a finite publication horizon.
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_inventory": {
"schema": "architecture-policy-inventory/v1",
"effective_rule_count": 42,
"rules": { "strict": 31, "audit": 7, "coverage": 4 },
"ignore_debt": {
"total": 7,
"active": 5,
"stale": 1,
"expired": 1,
"metadata_incomplete": 0,
"invalid": 0
},
"waivers": []
},
"waivers": [
{
"id": "ARCH-IGN-042",
"state": "active",
"target_fingerprint": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"owner": "architecture-team",
"issue": "ARCH-231",
"introduced": "2026-08-01",
"expires": "2026-10-01",
"evaluation_date": "2026-08-02",
"matches_governed_finding": true
}
],
"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.- Strict waiver-lifecycle policies fail for stale or expired structured waivers. Supply
--waiver-evaluation-date yyyy-MM-ddto make an expiry-boundary run reproducible; otherwise the CLI captures one UTC date for the full invocation.
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. Use --report sarif=<path> to write SARIF to a file directly instead of redirecting stdout (also works in PowerShell):
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 --report json=architecture-violations.json
- name: Upload architecture violations
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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 --report json=architecture-audit.json
For combined strict + audit with multi-sink output:
- name: Validate architecture (strict + audit)
run: arch-linter-net --mode strict,audit --ensure-built \
--report json=architecture-results.json \
--report sarif=architecture-results.sarif
- name: Upload architecture results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: architecture-results
path: |
architecture-results.json
architecture-results.sarif
Use this combined invocation when the workflow requires both strict and audit
results from the same build state. --ensure-built preparation belongs to one
immutable analysis snapshot, including any post-build receipt verification,
and both modes are evaluated from that snapshot. The command fails when either
requested mode fails. JSON and SARIF report sinks render the completed mode
outcomes; adding sinks does not run analysis again.
If audit is intentionally advisory, retain separate strict-blocking and non-blocking-audit steps instead. Those are independent CLI processes and do not reuse prepared state across processes.