fallow_output/audit_focus.rs
1//! Audit focus-map output contracts.
2
3use serde::Serialize;
4
5/// The focus label for a review unit. `Skip` is the SAFE explicit-skip label and
6/// is runtime-backed ONLY: it is producible solely on the paid runtime path
7/// and solely for a unit runtime-proves cold with zero risk signals. Free mode
8/// (no runtime evidence) emits only `ReviewHere` / `NotPrioritized`, never `Skip`
9/// (the build-focus-map labeller cannot reach the `Skip` arm without runtime
10/// input), so the free-tier "rank but never skip" stance holds by construction.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
12#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
13#[serde(rename_all = "kebab-case")]
14pub enum FocusLabel {
15 /// Review this unit.
16 ReviewHere,
17 /// Not prioritized, but still visible in the escape-hatch list.
18 NotPrioritized,
19 /// Safe to skip: runtime evidence proves the unit cold (only `safe_to_delete`
20 /// findings, no hot path) AND it carries no risk signal. Runtime-backed only.
21 Skip,
22}
23
24impl FocusLabel {
25 /// The wire token.
26 #[must_use]
27 pub const fn token(self) -> &'static str {
28 match self {
29 Self::ReviewHere => "review-here",
30 Self::NotPrioritized => "not-prioritized",
31 Self::Skip => "skip",
32 }
33 }
34}
35
36/// A per-unit confidence flag. The EXACT panel-decided strings: a dynamically-
37/// wired or re-export-heavy unit carries one so its static-reachability signal is
38/// not trusted as complete (the anti-silent-de-prioritization guard). The flag
39/// NEVER lowers the score; it is advisory provenance.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
41#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
42#[serde(rename_all = "kebab-case")]
43pub enum ConfidenceFlag {
44 /// The unit is dynamically wired.
45 DynamicDispatch,
46 /// The unit's reachability runs through re-export barrels.
47 ReExportIndirection,
48}
49
50impl ConfidenceFlag {
51 /// The wire message for this flag.
52 #[must_use]
53 pub const fn message(self) -> &'static str {
54 match self {
55 Self::DynamicDispatch => "low: dynamic dispatch detected",
56 Self::ReExportIndirection => "low: re-export indirection",
57 }
58 }
59}
60
61/// The composite attention score, with the deterministic component sub-scores
62/// kept on the wire so the runtime layer adds its weight without recomputing the
63/// signals.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
65#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
66pub struct FocusScore {
67 /// Fan-in/out blast-radius component.
68 pub fan_io: u32,
69 /// Security source -> sink taint-touch component (0 until a security pass is
70 /// threaded onto the brief path; the seam is built and tested).
71 ///
72 /// Omitted from the wire while it is zero, the same treatment `runtime`
73 /// gets. Publishing a permanently-zero component as a required field made
74 /// it read as a measurement that found nothing, when nothing measured it.
75 /// A consumer that sums components must read an absent component as zero.
76 #[serde(default, skip_serializing_if = "is_zero")]
77 #[cfg_attr(feature = "schema", schemars(default))]
78 pub security_taint: u32,
79 /// Risk-zone component (boundary / public-API / security-sensitive).
80 pub risk_zone: u32,
81 /// Change-shape component (new/widened export, signature change proxy).
82 pub change_shape: u32,
83 /// Runtime-weight component (paid): a hot path (runtime evidence of high
84 /// invocation) adds an invocation-bucketed weight so it amplifies the blast
85 /// and outranks an otherwise-equal cold unit. `0` in free mode (no runtime
86 /// input), so the free-tier total stays the four deterministic components and
87 /// is byte-identical to the no-runtime baseline.
88 #[serde(default, skip_serializing_if = "is_zero")]
89 #[cfg_attr(feature = "schema", schemars(default))]
90 pub runtime: u32,
91 /// The summed total of every present component (the four deterministic ones
92 /// plus the runtime weight).
93 pub total: u32,
94}
95
96/// Whether a `u32` is zero (serde skip predicate so the `runtime` component is
97/// omitted from the wire in free mode, keeping the no-runtime JSON byte-identical).
98#[expect(
99 clippy::trivially_copy_pass_by_ref,
100 reason = "serde skip predicate signature"
101)]
102fn is_zero(value: &u32) -> bool {
103 *value == 0
104}
105
106/// One review unit on the focus map: its file, composite score, label, human
107/// reason, and any confidence flags.
108#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
109#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
110pub struct FocusUnit {
111 /// Root-relative path of the changed file this unit covers.
112 pub file: String,
113 /// The composite attention score and its component breakdown.
114 pub score: FocusScore,
115 /// The focus label (`review-here` / `not-prioritized`, or the runtime-backed
116 /// `skip` on the paid path).
117 pub label: FocusLabel,
118 /// A human-readable reason for the label, built from the present signals.
119 pub reason: String,
120 /// Confidence flags (advisory; never lower the score). Sorted, deduped.
121 #[serde(default, skip_serializing_if = "Vec::is_empty")]
122 pub confidence: Vec<ConfidenceFlag>,
123}
124
125/// The weighted focus map: the ranked `review-here` units plus the FULL
126/// `deprioritized` escape-hatch list, so nothing is hidden.
127///
128/// Completeness invariant (the escape-hatch done-condition): the two lists
129/// partition the unit set, so `review_here.len() + deprioritized.len()` equals
130/// the total unit count by construction.
131#[derive(Debug, Clone, Default, Serialize)]
132#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
133pub struct FocusMap {
134 /// Units labeled `review-here`, ranked by composite score (descending), ties
135 /// broken by path for determinism.
136 pub review_here: Vec<FocusUnit>,
137 /// EVERY de-prioritized unit (`not-prioritized`, plus runtime-backed `skip`
138 /// units on the paid path) -- the escape hatch. Always present and fully
139 /// enumerated so a reviewer can always "show me what you de-prioritized"; the
140 /// human brief collapses it by default and re-expands under
141 /// `--show-deprioritized`. Nothing is ever hidden, including a `skip`.
142 pub deprioritized: Vec<FocusUnit>,
143}
144
145impl FocusMap {
146 /// Total number of units.
147 #[must_use]
148 pub fn total_units(&self) -> usize {
149 self.review_here.len() + self.deprioritized.len()
150 }
151}