# ADR-0012: Three-Level Issue Severity with `--strict` Exit-Code Promotion

## Status

✅ Accepted

## Context

omni-dev validates commit messages against configurable guidelines and reports every
issue it finds. Issues are not all equally important: a missing conventional commit type
is a structural violation that should block CI; not including a body on a large change is
advisable but not critical; using a period at the end of the subject line is a style
preference. A single severity category forces an all-or-nothing choice: either the tool
blocks on every issue (over-strict, noisy) or no issue blocks anything (under-strict,
useless for CI enforcement).

Four approaches were evaluated:

- **Binary pass/fail.** Each commit either passes or fails. The pass threshold is a
  single boolean that teams must tune at the tool level. Teams wanting to enforce body
  guidelines must accept that style nits also block; teams tolerant of style nits cannot
  also enforce body guidelines through the same mechanism. The model cannot express
  graduated importance.

- **Advisory-only, no exit-code signalling.** All issues are informational. CI pipelines
  receive no machine-readable signal; enforcement is left entirely to human review.
  This loses the primary value of automated commit checking.

- **Per-rule severity configuration.** Each individual rule carries its own severity,
  configured separately. This offers maximum flexibility but requires teams to configure
  every rule, creates a large configuration surface, and makes it hard to read a guidelines
  file and reason about its CI behaviour at a glance.

- **Section-level severity tiers with an opt-in strict mode.** Severity is declared once
  per guideline section, not per rule. A `--strict` flag lets teams promote warnings to
  blocking. A third, non-blocking tier (`info`) carries suggestions that are always
  advisory. This is the approach taken by ESLint (`error`/`warn`/`off`), the TypeScript
  compiler (`--strict`), mypy (`--strict`), and GCC/Clang (`-Werror`).

## Decision

We will define three severity levels — `error`, `warning`, and `info` — and map them to
exit codes through a `--strict` flag.

### Severity levels

| Level     | Meaning                                          | Exit code without `--strict` | Exit code with `--strict` |
|-----------|--------------------------------------------------|------------------------------|---------------------------|
| `error`   | Structural violation; always blocks              | 1                            | 1                         |
| `warning` | Advisory issue; blocks only in strict mode       | 0                            | 2                         |
| `info`    | Suggestion; never affects exit code              | 0                            | 0                         |

A clean run (no issues, or only `info` issues) always exits 0.

### Severity declaration

Severity is declared in the guidelines file, not in application code. The
`default-commit-guidelines.md` template opens with a `Severity Levels` table that maps
each guideline section to one of the three levels. Project-specific guidelines files
follow the same convention. This keeps the severity policy co-located with the rules it
governs and allows per-project overrides without changing the binary.

The default mapping is:

| Severity  | Sections                                                                |
|-----------|-------------------------------------------------------------------------|
| `error`   | Commit Format, Types, Scopes, Subject Line, Accuracy, Breaking Changes  |
| `warning` | Body Guidelines                                                         |
| `info`    | Subject Line Style                                                      |

### Implementation

`IssueSeverity` (`src/data/check.rs`) is the canonical Rust type. `CheckReport::exit_code(strict: bool)`
implements the mapping: it returns `1` if `error_count > 0`, returns `2` if `strict &&
warning_count > 0`, and returns `0` otherwise. `info_count` is never consulted for exit
code purposes.

Severity values arrive from the AI response as strings. `IssueSeverity::from_str` parses
them case-insensitively; an unrecognised value defaults to `Warning` and emits a
`tracing::debug!` log rather than failing hard.

The `--strict` flag is part of the public CLI contract. It is also used internally: the
`omni-dev git commit message check` invocation within the `commit-twiddle` flow passes
`--strict --quiet` to ensure warnings surface in that automated context.

## Consequences

**Positive:**

- **CI pipelines have a fine-grained contract.** Teams can fail on `error` only (normal
  mode) or on `error` and `warning` (strict mode) without changing the guidelines file.
  The exit code distinction (1 vs 2) allows CI scripts to differentiate the two failure
  classes if needed.

- **`info` is permanently non-blocking.** Suggestions and educational notes reach the
  developer without ever triggering a CI failure, regardless of `--strict`. This
  preserves the value of informational output without creating noise-driven pressure to
  suppress it.

- **Severity policy is readable at a glance.** The `Severity Levels` table at the top
  of every guidelines file is the single source of truth. A contributor can determine
  what will block CI by reading two lines of Markdown, not by tracing application code.

- **Per-project override is built in.** Because severity is defined in the guidelines
  file rather than in code, any project can promote or demote a section's severity by
  supplying a custom `.omni-dev/commit-guidelines.md` (see
  [`omni-dev-directory.md`](../omni-dev-directory.md#commit-guidelinesmd) for the file's
  current contract). No configuration keys or application changes are required.

- **The pattern is immediately familiar.** Developers who have used ESLint, TypeScript,
  mypy, or GCC with `-Werror` already understand the mental model. Onboarding friction
  is low.

**Negative:**

- **Severity granularity is at the section level, not the rule level.** Every rule within
  a section shares the same severity. A team that wants to treat one body-guideline rule
  as an error while leaving others as warnings must either fork the guidelines file into
  multiple sections or accept the coarser granularity.

- **`info` cannot be promoted, even with `--strict`.** The strict flag only promotes
  `warning` to blocking. Teams wanting `info`-level issues to block must relabel them as
  `warning` in their guidelines file. This may surprise users who expect `--strict` to
  mean "everything blocks".

**Neutral:**

- **Unknown severity strings default to `warning`.** An unrecognised severity value in
  an AI response does not cause a parse failure; it silently becomes a `warning`. This
  keeps the tool resilient against model variation but means a typo in a custom prompt
  or guidelines file will produce warnings rather than an error surfacing the typo.

- **`--strict` is part of the internal CLI contract.** The `commit-twiddle` flow
  hard-codes `--strict` when invoking `omni-dev git commit message check`. Changes to
  the strict-mode exit code semantics therefore affect both external CI usage and
  internal tool composition.
