# ADR-0008: Deterministic Pre-Validation Before AI Analysis

## Status

✅ Accepted

## Context

omni-dev's `check` command sends commit data to an AI model for message quality
analysis. Some properties of a commit message — whether a scope identifier exists in the
configured scope list, whether a multi-scope string uses the correct comma-without-space
format — are entirely deterministic. They can be evaluated locally by string matching or a
list lookup, with no ambiguity and no need for language understanding.

Delegating deterministic checks to the AI has two costs:

- **Token waste**: The AI must re-derive facts that are already known. Prompt space consumed
  by re-checking "is `cli` a valid scope?" is prompt space unavailable for semantic analysis.

- **Non-determinism risk**: AI models are probabilistic. A check that has a single correct
  answer may occasionally receive a wrong answer if evaluated by the model rather than by
  code. Deterministic code is always correct for deterministic questions.

Three alternative approaches were considered:

- **AI-only validation**: Delegate all checks, including scope validity, to the model. Simple
  to implement, but wastes tokens on checks the model may get wrong and the code could get
  right every time.

- **Separate validation pass**: Run deterministic checks independently, report their results
  separately from the AI analysis, and present two parallel result sets to the user. This
  adds a distinct reporting path with its own UX and output format.

- **Pre-validation with in-payload reporting**: Run deterministic checks before the AI call,
  embed *passing* results in the YAML payload as authoritative facts, and let the AI focus on
  everything else. Failing checks are not recorded — the AI may surface them, or they may
  not warrant a finding.

## Decision

We will run deterministic checks on each commit immediately before constructing the AI
payload, and include *passing* checks in the `pre_validated_checks` field of
`CommitInfoForAI`. The AI is instructed to treat entries in this list as authoritative and
to skip re-verifying them.

### Data structure

`CommitInfoForAI` carries the results as a `Vec<String>` alongside the commit data it
already holds:

```rust
pub struct CommitInfoForAI {
    #[serde(flatten)]
    pub base: CommitInfo<CommitAnalysisForAI>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub pre_validated_checks: Vec<String>,
}
```

The field is omitted from the serialised YAML when empty, so commits with no passing checks
produce no extra output.

### Checks performed

`CommitInfoForAI::run_pre_validation_checks()` accepts the project's `valid_scopes` list
and performs two checks:

| Check                  | Condition recorded                                                                    |
|------------------------|---------------------------------------------------------------------------------------|
| Multi-scope format     | Scope contains commas with no surrounding spaces (e.g. `cli,git`)                    |
| Scope validity         | All scope parts are present in the configured scope list (when the list is non-empty) |

Only *passing* checks are recorded. A failing check produces no entry; the AI handles it as
it would any other potential issue.

### Call site

The checks are applied in `src/claude/client.rs`, after the AI view is constructed and
before the system prompt is generated:

```rust
for commit in &mut ai_repo_view.commits {
    commit.run_pre_validation_checks(valid_scopes);
}
```

The results are then serialised into the YAML payload the AI receives, so the model sees
them alongside the commit message and diff.

## Consequences

**Positive:**

- **Token efficiency.** Scope validity and format checks consume a few bytes of input rather
  than requiring the model to reason about the scope list. The AI can apply its capacity to
  semantic concerns: tone, clarity, description completeness.

- **Guaranteed correctness for deterministic facts.** A scope that passes the local check is
  in the list. The AI cannot contradict that fact with a probabilistic guess. This eliminates
  a class of spurious findings on valid scopes.

- **Seamless integration with existing AI analysis.** Pre-validated results arrive in the
  same YAML structure as the rest of the commit data. No separate reporting path, no
  additional command output, no user-facing change for the common case.

- **Omitted when empty.** `skip_serializing_if = "Vec::is_empty"` keeps the YAML compact for
  commits where no checks pass (no scope present, unconfigured scope list).

**Negative:**

- **Passing-only recording is asymmetric.** Failing checks are invisible to the caller of
  `run_pre_validation_checks()`. Code that needs to know which checks failed must re-run the
  check logic independently. This is acceptable today because only the AI output path needs
  the results, but would require revisiting if a separate human-facing validation report were
  added.

- **Silent no-op on empty scope list.** Scope validity is only checked when `valid_scopes`
  is non-empty. Projects without a configured scope list get no local scope validation. This
  is correct behaviour, but the silence may be surprising when debugging a missing scope
  configuration.

**Neutral:**

- **Checks grow with project needs.** `run_pre_validation_checks()` is a single method with
  no extension point. Adding a new deterministic check means adding code to that method and
  updating tests. For the current small set of checks this is fine; a registry or trait-based
  approach would add abstraction without benefit at this scale.
