Skip to main content

code_ranker_plugin_api/
level.rs

1//! Level descriptors + the **semantics dictionaries** that let the core handle
2//! unknown kinds/keys without hardcoding their names, and let the UI render any
3//! language/metric set purely from data: edge kinds ([`EdgeKindSpec`]),
4//! node/edge attributes ([`AttributeSpec`], grouped via [`AttributeGroup`]),
5//! node kinds ([`NodeKindSpec`]) and cycle kinds ([`CycleKindSpec`]).
6//!
7//! The dictionaries are **maps** keyed by the kind/attribute/group name; the
8//! spec value holds only the remaining metadata.
9
10use crate::attrs::ValueType;
11use serde::{Deserialize, Serialize};
12use std::collections::BTreeMap;
13
14/// Semantics of one edge kind. Keyed by the edge `kind` in
15/// [`Level::edge_kinds`]. `flow` is the single source of truth for "is this
16/// information flow": counted in coupling/cycles AND drawn when `true`;
17/// structural (e.g. `contains`) and excluded/hidden when `false`.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct EdgeKindSpec {
20    pub flow: bool,
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub label: Option<String>,
23    /// Long human description (used as a UI tooltip).
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub description: Option<String>,
26}
27
28/// A named group of attributes (UI section). Keyed by group name in
29/// [`Level::attribute_groups`]; attributes reference it via
30/// [`AttributeSpec::group`]. Metadata only — storage stays flat.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct AttributeGroup {
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub label: Option<String>,
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub description: Option<String>,
37}
38
39/// Two-tier per-metric thresholds (at/under `info` is fine; above `warning` is
40/// likely a problem). Carried on an [`AttributeSpec`] for the advisory scorecard /
41/// viewer / prompt. `warning` is the `check` gate's own `[rules.thresholds.file]`
42/// limit, so the report shows exactly what fails the gate; `info` is an optional
43/// lower line (from a `[metrics.<key>]` spec, kept only when below `warning`).
44/// Absent when the metric has no configured gate threshold.
45#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
46pub struct Thresholds {
47    pub info: f64,
48    pub warning: f64,
49}
50
51/// Whether a metric's delta is "good" when it moves up or down — drives the
52/// green/red colouring in the viewer. `Neutral` (the default) means the metric
53/// has no agreed-good direction (raw sizes, structural counts) and is left
54/// uncoloured. `Neutral` is skipped on the wire, so a neutral metric serializes
55/// exactly as the old `Option<String>` form did: the `direction` field absent.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum Direction {
59    #[default]
60    Neutral,
61    LowerBetter,
62    HigherBetter,
63}
64
65impl Direction {
66    /// `true` for the default (no opinion) — used by serde to omit the field.
67    pub fn is_neutral(&self) -> bool {
68        matches!(self, Direction::Neutral)
69    }
70}
71
72/// Describes one attribute key (on a node or an edge). Everything the UI needs
73/// to label, explain, format, compute and threshold the metric — so the viewer
74/// hardcodes no metric by name.
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct AttributeSpec {
77    pub value_type: ValueType,
78    /// Concise display label (table grouping, popup rows).
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub label: Option<String>,
81    /// Full name used as a tooltip title (falls back to `label`).
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub name: Option<String>,
84    /// Short label for narrow table headers (falls back to `label`).
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub short: Option<String>,
87    /// Long human description (tooltip body); the `why` line in `check` diagnostics.
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub description: Option<String>,
90    /// How to fix a breach of this metric — the `fix` line in `check` diagnostics.
91    /// Data, not code: lives in the metric catalog / language config, not the CLI.
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub remediation: Option<String>,
94    /// Human-readable formula, e.g. `"sloc × (fan_in × fan_out)²"` (display only).
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub formula: Option<String>,
97    /// Evaluable JS expression over sibling attribute names + `Math`, e.g.
98    /// `"sloc * (fan_in * fan_out) ** 2"`. Lets the UI show the live derivation.
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub calc: Option<String>,
101    /// Whether higher or lower is "better" — drives delta colouring. `Neutral`
102    /// (the default) is omitted from the wire.
103    #[serde(default, skip_serializing_if = "Direction::is_neutral")]
104    pub direction: Direction,
105    /// Format large values with K/M suffixes.
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub abbreviate: Option<bool>,
108    /// Optional group this attribute belongs to, by [`AttributeGroup`] key.
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub group: Option<String>,
111    /// Optional two-tier thresholds (language-calibrated).
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub thresholds: Option<Thresholds>,
114    /// Value at which this metric carries no signal and is **omitted** from the
115    /// node (absent in the JSON, blank in the viewer). `0` for almost everything;
116    /// `1` for `cyclomatic` (McCabe's floor — a function-less file would otherwise
117    /// report a vacuous `1`). Published so the frontend knows what an absent cell
118    /// means (and may treat it as this value when sorting). Default `0` is omitted
119    /// from the wire.
120    #[serde(default, skip_serializing_if = "f64_is_zero")]
121    pub omit_at: f64,
122}
123
124/// `skip_serializing_if` helper: the default `omit_at` (`0`) stays off the wire.
125fn f64_is_zero(v: &f64) -> bool {
126    *v == 0.0
127}
128
129impl AttributeSpec {
130    /// A minimal spec with just a type + label (the common structural case).
131    pub fn new(value_type: ValueType, label: &str) -> Self {
132        Self {
133            value_type,
134            label: Some(label.to_string()),
135            name: None,
136            short: None,
137            description: None,
138            remediation: None,
139            formula: None,
140            calc: None,
141            direction: Direction::Neutral,
142            abbreviate: None,
143            group: None,
144            thresholds: None,
145            omit_at: 0.0,
146        }
147    }
148}
149
150/// One row of a declarative attribute table: a flat, named-field description of
151/// an [`AttributeSpec`]. Empty `&str` fields become `None`; `direction` defaults
152/// to [`Direction::Neutral`]. Build a whole dictionary with [`attr_dict`].
153///
154/// This replaces both the per-field `spec.x = Some(...)` boilerplate and the
155/// positional metric-row tuples that the metric/coupling crates used to carry
156/// separately, so every centrally-computed spec is declared the same way.
157#[derive(Clone)]
158pub struct SpecRow {
159    pub group: &'static str,
160    pub value_type: ValueType,
161    pub label: &'static str,
162    pub name: &'static str,
163    pub short: &'static str,
164    pub description: &'static str,
165    pub remediation: &'static str,
166    pub formula: &'static str,
167    pub calc: &'static str,
168    pub direction: Direction,
169    pub abbreviate: bool,
170    /// See [`AttributeSpec::omit_at`]. Defaults to `0`.
171    pub omit_at: f64,
172}
173
174impl Default for SpecRow {
175    fn default() -> Self {
176        SpecRow {
177            group: "",
178            value_type: ValueType::Int,
179            label: "",
180            name: "",
181            short: "",
182            description: "",
183            remediation: "",
184            formula: "",
185            calc: "",
186            direction: Direction::Neutral,
187            abbreviate: false,
188            omit_at: 0.0,
189        }
190    }
191}
192
193impl SpecRow {
194    fn into_spec(self) -> AttributeSpec {
195        let opt = |s: &str| (!s.is_empty()).then(|| s.to_string());
196        AttributeSpec {
197            value_type: self.value_type,
198            label: opt(self.label),
199            name: opt(self.name),
200            short: opt(self.short),
201            description: opt(self.description),
202            remediation: opt(self.remediation),
203            formula: opt(self.formula),
204            calc: opt(self.calc),
205            direction: self.direction,
206            abbreviate: self.abbreviate.then_some(true),
207            group: opt(self.group),
208            thresholds: None,
209            omit_at: self.omit_at,
210        }
211    }
212}
213
214/// Assemble a `key → AttributeSpec` dictionary from a declarative table.
215pub fn attr_dict(rows: Vec<(&'static str, SpecRow)>) -> BTreeMap<String, AttributeSpec> {
216    rows.into_iter()
217        .map(|(k, r)| (k.to_string(), r.into_spec()))
218        .collect()
219}
220
221/// Build an [`AttributeGroup`] from a label + description.
222pub fn group(label: &str, description: &str) -> AttributeGroup {
223    AttributeGroup {
224        label: Some(label.to_string()),
225        description: Some(description.to_string()),
226    }
227}
228
229/// Visual + label semantics of one node kind (`"file"` / `"external"` / …).
230/// Keyed by kind in [`Level::node_kinds`].
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct NodeKindSpec {
233    #[serde(default, skip_serializing_if = "Option::is_none")]
234    pub label: Option<String>,
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    pub plural: Option<String>,
237    #[serde(default, skip_serializing_if = "Option::is_none")]
238    pub fill: Option<String>,
239    #[serde(default, skip_serializing_if = "Option::is_none")]
240    pub stroke: Option<String>,
241    /// `true` marks a third-party node (a library); the UI derives "external
242    /// edge" from the endpoint kind, not from any edge flag.
243    #[serde(default, skip_serializing_if = "Option::is_none")]
244    pub external: Option<bool>,
245}
246
247/// Label + description of one cycle kind (`"mutual"` / `"chain"`).
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct CycleKindSpec {
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub label: Option<String>,
252    #[serde(default, skip_serializing_if = "Option::is_none")]
253    pub description: Option<String>,
254    /// How to fix this cycle kind — the `fix` line in `check` diagnostics. Data,
255    /// not code: lives in the shared `defaults.toml` `[cycle_kinds]`, not the CLI.
256    #[serde(default, skip_serializing_if = "Option::is_none")]
257    pub remediation: Option<String>,
258}
259
260/// How the viewer should cluster nodes in the diagram. Exactly one of `key`
261/// (group by the value of a node attribute, e.g. `crate`) or `function` (a named
262/// grouper the viewer implements, e.g. `dir` — derive the folder from the path).
263/// Absent → the viewer falls back to its default `dir` grouper.
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct Grouping {
266    #[serde(default, skip_serializing_if = "Option::is_none")]
267    pub key: Option<String>,
268    #[serde(default, skip_serializing_if = "Option::is_none")]
269    pub function: Option<String>,
270}
271
272/// An analysis level the plugin can produce, with the semantics needed to score
273/// and draw it. The orchestrator merges in centrally-computed attribute specs
274/// and the computed `ui` block before writing the snapshot.
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct Level {
277    pub name: String,
278    pub edge_kinds: BTreeMap<String, EdgeKindSpec>,
279    pub node_attributes: BTreeMap<String, AttributeSpec>,
280    pub edge_attributes: BTreeMap<String, AttributeSpec>,
281    pub attribute_groups: BTreeMap<String, AttributeGroup>,
282    /// Node-kind vocabulary (label/colour/external). Plugins seed it from
283    /// [`crate::default_node_kinds`] and may customize.
284    #[serde(default)]
285    pub node_kinds: BTreeMap<String, NodeKindSpec>,
286    /// Cycle-kind vocabulary. Plugins seed it from [`crate::default_cycle_kinds`].
287    #[serde(default)]
288    pub cycle_kinds: BTreeMap<String, CycleKindSpec>,
289    /// How the viewer should cluster nodes (defaults to `dir` when absent).
290    #[serde(default, skip_serializing_if = "Option::is_none")]
291    pub grouping: Option<Grouping>,
292}