Terminal UX as a Governance Interface: How Zenzic Renders Diagnostic Contracts
A linter reports violations within individual files. A governance engine verifies that a set of invariants holds across the entire document graph — and halts the pipeline when one does not.
Beyond Linting
The distinction between a linter and a governance engine is not one of depth. It is one of scope and contract type.
A linter's analysis terminates at the file boundary. It reports violations of
formatting rules — incorrect indentation, undefined references within a single
file, missing required metadata fields. Each file is processed independently,
in isolation. The diagnostic is local: a violation in file-a.md has no
bearing on the analysis of file-b.md.
Zenzic operates on a different unit: the document graph. A single invocation of
zenzic check all evaluates the entire scope defined by the adapter
configuration — every internal link, every navigation contract, every credential
surface, every suppression directive — as a unified object. No file is analyzed
in isolation, because no documentation system exists in isolation. A broken
internal link is not a property of the page that contains it. It is a property
of the relationship between two nodes in the graph. An orphaned page is not
detectable from within that page. It is detectable only when the navigation
manifest is resolved against the full file tree.
This distinction has a direct consequence for the design of the terminal output. When the unit of analysis is a graph, a single-line error message is insufficient. The interface must communicate: what the violation is, where in the file it occurs, which contract it violates, and enough surrounding context for the reader to understand the issue without opening the source file.
Zenzic exposes two orthogonal instruments for evaluating the document graph:
zenzic check— a binary gate. It returns an exit code in{0, 1, 2, 3}and a structured list of findings. The exit code is the contract with CI: deterministic, machine-readable, with semantics that hold regardless of configuration.zenzic score— a weighted penalty model. It returns a Documentation Quality Score (DQS) in the range 0–100, decomposed by diagnostic category. The output is audit-oriented: it answers not whether the documentation passes, but by how much and in which domains it deviates from the governance baseline.
Both instruments share the same analysis engine. They answer different questions.
The rest of this article examines each layer of the terminal interface that implements these contracts: the run header, the diagnostic renderer, the suppression audit model, and the exit code semantics.
Information Density in the Run Header
At the end of every zenzic check invocation, a single telemetry line is
printed below the findings list:
standalone • 20 files (14 docs, 6 assets) • 0.8s • 38 files/s
This line encodes four independent signals. Each field answers a distinct operational question.
Adapter mode (standalone). Zenzic resolves the adapter from the project
configuration at startup. Supported modes include docusaurus, mkdocs,
zensical, and standalone. The adapter determines the navigation contract: which files
constitute the document graph, how routes are resolved, and which structural
checks are active. When no recognized configuration file is present, standalone
is the default.
The adapter label carries a constraint that is not stated elsewhere in the
output. In standalone mode, the navigation manifest is absent. Checks that
require a resolved route graph — orphaned-page detection being the primary
example — are structurally inactive for that run. The label is the sole
communication of this fact.
Scope decomposition (20 files (14 docs, 6 assets)). The file count is
split into two categories: documents (.md and .mdx) and assets (images,
data files, schema definitions, and any other non-document file within the
analyzed tree). The split is not cosmetic: document checks operate on file
content; asset checks operate on the asset manifest. Different scanners activate
for each category.
The analyzed scope is bounded by docs_dir and excluded_dirs in
.zenzic.toml. Files outside this boundary are not evaluated, regardless of
their position in the repository tree. The counts in the footer reflect exactly
the scope that was evaluated — nothing more, nothing less.
Elapsed time (0.8s). Wall-clock duration from invocation to the final
diagnostic line. This includes file I/O, adapter resolution, and all analysis
passes. It is not a CPU-time measurement. On the same hardware and the same
scope, consecutive runs produce consistent values, making elapsed time a
reproducibility indicator without additional instrumentation.
Throughput (38 files/s). Derived as scope divided by elapsed time. The
value is hardware-specific and not portable across machines. Its utility is
local: establishing a baseline on a given host makes performance regressions
in the analysis pipeline detectable before they affect CI wall time.
| Field | Example | What it communicates |
|---|---|---|
| Adapter mode | standalone | Active navigation contract; which structural checks apply |
| Scope | 20 files (14 docs, 6 assets) | Exact file boundary of the analysis; nothing outside is evaluated |
| Elapsed | 0.8s | Wall-clock duration; reproducibility signal on fixed hardware |
| Throughput | 38 files/s | Analysis rate; baseline for performance regression detection |
To enumerate the full scanner manifest active for a given configuration — codes,
capabilities, and exit-code contracts — use zenzic inspect.
— No third-party plugins installed. Register rules via the zenzic.rules entry-point group.
Diagnostic Rendering
A finding is self-describing. It carries enough information for triage without opening the source file, navigating the repository, or invoking additional tools. This property is not incidental — it is a design constraint that shapes every layer of the diagnostic output.
Each finding is rendered as a three-layer block.
Layer 1 — Finding header. The first line identifies the finding unambiguously: file path, line number, column (when available), Z-code, and message. The Z-code is the machine-readable identifier of the violated contract. The message is a human-readable description of the violation.
docs/guides/install.md:47:29
✘ Z101 Broken internal link → install.md
Layer 2 — Source snippet. Five lines of source context are shown: two lines
before the error line, the error line itself, and two lines after. Context lines
are rendered with a │ gutter marker in muted style. The error line is rendered
with a ❱ gutter marker in error style.
45 │ ## Installation Prerequisites
46 │
47 ❱ See the [Installation Guide](install.md) for details.
48 │
49 │ Continue to the Configuration section when ready.
This window is the primary triage surface. The two lines of context before the error establish what the offending line belongs to — a section header, a paragraph, a list item. The two lines after establish what follows. In a CI log, this eliminates the need to reproduce the run locally, open the file, or reconstruct the surrounding context manually. The context-switching overhead between reading a pipeline failure and understanding its location is zero.
Layer 3 — Caret row. When the scanner that produced the finding also provides the exact byte offset of the matched token in the source line, a caret row is rendered immediately below the error line. The caret spans the matched token precisely.
47 ❱ See the [Installation Guide](install.md) for details.
│ ^^^^^^^^^^
The caret length equals the length of the matched string. The caret start position equals the byte offset of that string in the raw source line — the exact value returned by the pattern match, with no rounding, padding, or adjustment. If the scanner does not report a native column position, the caret row is omitted entirely. There is no fallback, no estimation, and no approximation. A caret in the output is always exact; the absence of a caret means the scanner operates at line granularity rather than token granularity.
The practical consequence: for findings where column data is available — such as credential detections and inline link violations — the operator sees the precise token that triggered the finding. No surrounding content requires manual inspection.
The three layers together form a self-contained diagnostic unit:
docs/guides/install.md:47:29
✘ Z101 Broken internal link → install.md
45 │ ## Installation Prerequisites
46 │
47 ❱ See the [Installation Guide](install.md) for details.
│ ^^^^^^^^^^
48 │
49 │ Continue to the Configuration section when ready.
The Mathematics of Suppression Debt
The zenzic:ignore inline directive tells Zenzic to skip the finding on the
annotated line. Each active directive — whether applied inline or via the
per-file suppression list in .zenzic.toml — costs exactly one point from the
Documentation Quality Score. No directive is free.
This is the flat-cost model. It replaced an allowance-based model in which a configured number of suppressions carried no penalty, and costs applied only to the excess. The allowance model produced a predictable outcome: teams treated the free allowance as a budget to fill, not a limit to avoid. A model in which the first N suppressions are free and the (N+1)th costs a point is not a governance model — it is a permission slip. The incentive it creates is to suppress freely below the free threshold and worry about governance only after that line is crossed.
Under the flat-cost model, the first suppression costs one point. The tenth
costs one point. There is no free zone. The suppression_cap value configured
in .zenzic.toml is not an allowance — it is a hard-fail ceiling. When the
suppression count exceeds the cap, the build fails regardless of the numeric
score.
The DQS formula. The Documentation Quality Score is computed in three stages. First, per-category subtotals:
Where:
- is the point cap for category : Structural (30), Navigation (25), Content (20), Brand & Governance (25).
- is the total penalty for category : the sum of per-code penalty multiplied by finding count , for all codes assigned to that category.
- is the total count of active suppression directives, each contributing exactly 1 point of deduction.
Two invariants constrain the formula:
- Category Cap Invariant. Deductions within a category cannot exceed the category cap. One thousand occurrences of a 1-point finding in the Content category deduct at most 20 points — the Content cap. The remaining categories are unaffected.
- Gravity Cap. If the Brand & Governance category is fully zeroed by findings, the category subtotal is capped at 70. Uncontrolled governance violations impose a structural ceiling on the total score regardless of how other categories perform.
Suppression debt () is applied after both invariants, as a final deduction from the adjusted subtotal.
The Quality Breakdown Ledger. The zenzic score command renders a
per-category table that exposes the deduction mechanics:
Quality Breakdown
─────────────────────────────────────────────────────────
Category Issues Weight Raw Pts Applied Pts
✔ structural 0 30% 0 0
✔ navigation 0 25% 0 0
✔ content 0 20% 0 0
✘ brand 42 25% -60 -25 (CAPPED)
─────────────────────────────────────────────────────────
Σ Subtotal 75
! Gravity Cap Enforcement (Brand = 0): -5 pts
! Technical Debt (5 suppressions): -5 pts
= Final Quality Score 65 / 100
Raw Pts is the total deduction accumulated within the category before the
cap is applied. Here, 42 brand findings produced −60 raw points. Applied Pts
is the deduction after the category cap: the Brand cap is 25 points, so the
applied penalty is −25. The (CAPPED) marker confirms that the raw deduction
was truncated by the cap boundary. The difference between Raw Pts and Applied
Pts is not recovered — it signals that the category has been fully zeroed.
The ! Gravity Cap Enforcement line appears when the zeroed Brand category
causes the subtotal to be reduced from 75 to 70, applying a 5-point structural
penalty. The ! Technical Debt line shows the flat-cost deduction: five active
suppression directives produce a deduction of five points, applied after all
category calculations.
The suppression count is also compared to governance.suppression_cap. When
the count exceeds the cap, the build fails with a distinct message:
FAILED: suppression cap exceeded (36/30).
Update governance.suppression_cap in .zenzic.toml if intentional.
This failure is Exit 1. It is overridable via --exit-zero for observe-only
runs. It cannot be resolved by adding more zenzic:ignore directives — each
additional directive increases the count and the debt simultaneously.
Exit Semantics as a CI Contract
The exit code is not a summary of the terminal output. It is the primary contract between Zenzic and the CI pipeline. The pipeline reads the exit code, not the display. The display is for operators; the exit code is for automation. This distinction determines how the exit semantics are designed.
The four exit codes and their contracts:
| Code | Trigger | Suppressible via directive? | --exit-zero effect |
|---|---|---|---|
| 0 | No error-severity findings in the analyzed scope | — | No effect |
| 1 | Error-severity findings detected; or suppression cap exceeded | Yes — via zenzic:ignore | Converts to 0 |
| 2 | Credential detected in documentation content (Z2xx) | Never | No effect |
| 3 | Path traversal to system directories detected (Z203) | Never | No effect |
Exit 0. The analyzed scope contains no error-severity findings. When
fail_under is configured, a score below the threshold also produces Exit 1 —
so Exit 0 confirms both the absence of findings and that the Documentation
Quality Score meets the configured threshold. The scope qualifier is precise:
files outside the configured docs_dir boundary are not evaluated, and their
state is not reflected in the exit code.
Exit 1. One or more error-severity findings were detected, or the
suppression count exceeded governance.suppression_cap. This is the standard
CI gate. It is the only exit code affected by --exit-zero: when that flag is
set (via CLI or .zenzic.toml), Exit 1 is converted to Exit 0 for
observe-only runs. The conversion does not suppress findings from the output —
they remain visible in the terminal. --exit-zero cannot be combined with
--strict; Zenzic rejects that combination with Exit 2 at startup.
Exit 2. A finding with security_breach severity was produced — meaning a
credential or secret was detected in the documentation source tree. This exit
code cannot be suppressed by zenzic:ignore, cannot be overridden by
--exit-zero, and cannot be silenced by per-file ignore policies. The
credential scanner (Z2xx codes) is active regardless of adapter mode,
--offline flag, or --no-external flag.
Exit 3. A path traversal to an operating-system system directory was
detected. This is a distinct severity class (security_incident) and
represents the maximum security contract: the docs_dir configuration value or
a scanned path attempted to escape the repository boundary toward system paths.
Like Exit 2, it precedes all other exit-code evaluation. --exit-zero has no
effect.
The evaluation order is fixed: Exit 3 conditions are checked first, Exit 2 second, Exit 1 third. This order ensures that security contracts are never shadowed by governance failures or score thresholds.
The four elements of the terminal interface analyzed in this article — the run header, the diagnostic block, the suppression ledger, and the exit code table — are not independent display decisions. They form a single interface that makes the documentation governance policy machine-readable, auditable, and deterministic.
A fail_under threshold, a suppression_cap, a per-category penalty model,
and an immutable exit code contract are the formal encoding of what a team
considers acceptable documentation quality. The terminal output is where that
encoding is evaluated on every run. Treating it as display-only discards that
evaluation. Treating it as a governance interface — machine-readable exit codes,
auditable debt counters, caret-precise diagnostics — makes it enforceable at
the pipeline boundary.
