code_ranker_graph/attrs.rs
1//! Shared attribute helpers used by every enrichment pass: numeric rounding,
2//! the `f64 → AttrValue` bridge, typed attribute reads, and the external-node
3//! predicate. This is a leaf module — it depends only on the plugin API, never
4//! on the crate root, so the enrichment passes can pull helpers from here
5//! without creating a `submodule → crate-root` back-edge.
6
7use code_ranker_plugin_api::{attrs::AttrValue, node::Node};
8
9/// Truncate to 3 significant digits (matching the historical `sig3` serializer):
10/// values ≥ 1 are truncated to 3 decimals, values < 1 to 3 significant figures.
11/// Non-finite values collapse to 0 (JSON has no NaN/Inf).
12pub fn round_sig3(x: f64) -> f64 {
13 if !x.is_finite() || x == 0.0 {
14 return 0.0;
15 }
16 let abs = x.abs();
17 let sign = if x < 0.0 { -1.0 } else { 1.0 };
18 let truncated = if abs >= 1.0 {
19 (abs * 1000.0).floor() / 1000.0
20 } else {
21 let d = abs.log10().floor() as i32;
22 let factor = 10f64.powi(2 - d);
23 (abs * factor).floor() / factor
24 };
25 truncated * sign
26}
27
28/// Round a metric and pick the natural JSON scalar: an integral value becomes
29/// an `Int` (so `1.0` serializes as `1`), otherwise a `Float`. This is the
30/// single bridge metric producers use before inserting into `attrs`.
31pub fn num_attr(x: f64) -> AttrValue {
32 let r = round_sig3(x);
33 if r.fract() == 0.0 && r.abs() < i64::MAX as f64 {
34 AttrValue::Int(r as i64)
35 } else {
36 AttrValue::Float(r)
37 }
38}
39
40/// Read a numeric node attribute as `f64` (from either `Int` or `Float`).
41pub(crate) fn attr_f64(node: &Node, key: &str) -> Option<f64> {
42 match node.attrs.get(key) {
43 Some(AttrValue::Int(i)) => Some(*i as f64),
44 Some(AttrValue::Float(f)) => Some(*f),
45 _ => None,
46 }
47}
48
49/// Is this node an external dependency (a library node, not a project file)?
50/// Derived from `kind == "external"` or an explicit `external: true` attribute.
51pub(crate) fn is_external(node: &Node) -> bool {
52 node.kind == "external" || matches!(node.attrs.get("external"), Some(AttrValue::Bool(true)))
53}