Skip to main content

aqc_file_engine_core/
finding.rs

1//! Structured findings emitted by engines and linter adapters.
2//!
3//! One enum, every layer speaks it. Engines emit `Mismatch` /
4//! `UnwritableRequiredKey` / `ParseError`, plus the two requirement-level
5//! failures from the merge/write path: `ConflictingRequirements` and
6//! `InvalidRequirements`. Anything else catastrophic that's still
7//! recoverable becomes `InternalError`.
8//!
9//! GUARDRAILS CAPABILITY BLOCK (R2) -- DO NOT REMOVE. Every `key` here is a
10//! location inside a CONFIG file (a TOML/JSON key), never a source-code span,
11//! AST node, line/column into `.rs`/`.ts`/`.js`, or any code construct.
12//! Guardrails validates config and is NOT a linter; a source-location variant
13//! would be the first step to reimplementing one. Do not add a source span /
14//! code-location / AST payload to any variant unless the repo owner authorizes
15//! it in writing with reasoning. See guardrails-capability-boundary (R2).
16
17use crate::types::{Provenance, Severity};
18
19/// Rendered policy contributors for a requirement-level finding.
20pub type RenderedContributors = Vec<(String, String)>;
21
22/// A structured finding emitted by a `FileEngine` or a linter adapter.
23#[derive(Debug, Clone)]
24pub enum Finding {
25    /// A key on disk disagrees with what the requirement asserts.
26    Mismatch {
27        key: String,
28        current: Option<String>,
29        expected: String,
30        /// Free-form policy-authored explanation of the mismatch: what is
31        /// wrong, why it's wrong, what should be done instead. Comes from
32        /// the assertion entry that produced this finding.
33        message: String,
34        severity: Severity,
35        attribution: Vec<Provenance>,
36    },
37    /// Reconcile knew where to write but the file role forbids it.
38    UnwritableRequiredKey {
39        key: String,
40        expected: String,
41        attribution: Vec<Provenance>,
42    },
43    /// The merged requirement set resolves per key but is JOINTLY unwritable:
44    /// the tool itself would reject the resulting file, so the engine refuses
45    /// to produce it. Always `Severity::Error`; hard failure; NOT waivable
46    /// (there is no on-disk value to keep -- the policy set is wrong).
47    ///
48    /// Preference order before reaching for this variant:
49    /// 1. Make the invalidity unrepresentable in the requirement types where
50    ///    that is natural.
51    /// 2. Model a genuine either/or decision as ONE key, so the merge
52    ///    surfaces disagreement as `ConflictingRequirements`.
53    /// 3. Use this variant only for relational constraints across keys.
54    ///
55    /// The `Finding` closed-set manifest row blocks ad hoc alternatives.
56    InvalidRequirements {
57        /// The in-file key the constraint is anchored at.
58        key: String,
59        /// Which relational rule the set violates.
60        message: String,
61        /// Each policy whose requirement participates in the invalid set.
62        contributors: RenderedContributors,
63    },
64    /// The file isn't valid in its native grammar (e.g. malformed TOML).
65    ParseError { message: String, severity: Severity },
66    /// Two or more requirements disagree on the same key in the same file
67    /// (the policies that issued them are the attribution). Produced by the
68    /// engine's merge phase, per key, naming each disagreeing policy and its
69    /// value. Always `Severity::Error`; the field is dropped (not written);
70    /// not waivable.
71    ConflictingRequirements {
72        /// The in-file key (e.g. `[workspace.lints.clippy].unwrap_used`).
73        key: String,
74        /// Each disagreeing policy id + its rendered value.
75        contributors: RenderedContributors,
76        /// Which rule fired (scalar-disagree / set-key-disagree / exact-mismatch).
77        reason: String,
78    },
79    /// Engine- or adapter-internal failure (panic-class, but caught
80    /// before it actually panics). Always `Severity::Error`.
81    InternalError { message: String },
82}