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}