Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

src/knowledge/symbols.rs

//! The symbol-scan ROW MODEL (re-exported from `nornir-warehouse-trait`), the
//! `call_edges` derivation vocabulary, and the build-free `.rs` FILE DISCOVERY.
//!
//! The `syn` scanner that used to live here was removed with Mímir v2
//! (PLAN-MIMIR-V2 §7.3b): the rustc-driver dump (lane 3) is the one parser.
//! File discovery stays because it is not a parser — a walkdir over every
//! `Cargo.toml`'s `src/ tests/ benches/ examples/`.

use std::path::Path;

use walkdir::WalkDir;

// The symbol-scan MODEL PODs (`SymbolScan`/`SymbolRow`/`CallEdgeRow`/
// `TestDefRow`/`FeatureGateRow`) live in the `nornir-warehouse-trait` edda leaf
// so a consumer can use them without depending on nornir. Re-exported at their
// old path (`crate::knowledge::symbols::…`).
pub use nornir_warehouse_trait::{CallEdgeRow, FeatureGateRow, SymbolRow, SymbolScan, TestDefRow};

/// **Derivation PROVENANCE** of a call/flow edge — *how* it was found, an axis
/// ORTHOGONAL to its semantic kind (`CallEdgeRow.call_kind` / [`crate::arch::ArchEdgeKind`]).
/// Two edges can both be `Calls` yet be found by completely different machinery,
/// and a reader needs to know which: a `syn` AST edge is a name-match (precise
/// about syntax, blind to trait/macro resolution), an ELF binary edge is a
/// *linkage* fact (a real relocation, no source needed), and a resolved
/// call-graph edge is IDE-grade.
///
/// The variants are the derivation methods the code ACTUALLY runs (the wish's
/// "regex" is not one of them — nornir has no regex edge scanner, so inventing
/// the variant would be a fake column):
///
///  - [`Ast`](Self::Ast)       — the (removed) build-free `syn` walker: `call`,
///    `method`, `macro` edges + the accessor→table access facts.
///  - [`CallGraph`](Self::CallGraph) — a RESOLVED index (SCIP): trait/macro/type
///    resolution included.
///  - [`Binary`](Self::Binary) — ELF PLT/GOT relocations
///    ([`crate::graph::extract::binary`]): `caller object → callee symbol`.
///
/// `Default` is [`Ast`](Self::Ast): every edge written before this axis existed
/// came from the `syn` scanner (the only producer that persisted to the
/// `call_edges` warehouse table), so a NULL cell means "pre-axis", never
/// "unknown" — the same MEASURED-default contract as the language axis
/// ([`crate::warehouse::iceberg_schema::LANG_DEFAULT`]).
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
)]
pub enum EdgeDerivation {
    Ast,
    CallGraph,
    Binary,
}

impl Default for EdgeDerivation {
    fn default() -> Self {
        EdgeDerivation::Ast
    }
}

impl EdgeDerivation {
    /// The stable wire token stored in the warehouse `derived_by` column and in
    /// the serialized [`crate::arch::ArchGraph`]. Never renumber/rename — the
    /// column is resolved by this string.
    pub fn as_str(self) -> &'static str {
        match self {
            EdgeDerivation::Ast => "ast",
            EdgeDerivation::CallGraph => "callgraph",
            EdgeDerivation::Binary => "binary",
        }
    }

    /// Parse a stored token back, defaulting a NULL/empty/unknown cell to the
    /// documented [`Default`](Self::default) (`ast`) so a pre-axis row is never a
    /// third, undefined provenance state.
    pub fn parse(s: &str) -> Self {
        match s {
            "callgraph" => EdgeDerivation::CallGraph,
            "binary" => EdgeDerivation::Binary,
            _ => EdgeDerivation::Ast,
        }
    }

    /// Infer the provenance of a [`CallEdgeRow`] from the semantic `call_kind`
    /// its producer stamped. This is the ONE channel that is producer-legible
    /// today without a per-row field on the (edda-owned) `CallEdgeRow` POD:
    ///
    ///  - `plt:<reloc>` — the binary extractor's marker
    ///    ([`crate::graph::extract::binary::BinaryGraph::to_call_edge_rows`],
    ///    where "the marker is implicit in `call_kind`") → [`Binary`](Self::Binary).
    ///  - `call` / `method` / `macro` (and anything else) → [`Ast`](Self::Ast).
    ///
    /// LIMIT (documented, not faked): the SCIP producer stamps `call_kind =
    /// "call"`, byte-identical to the `syn` producer's, so `call_kind` alone
    /// CANNOT separate [`CallGraph`](Self::CallGraph) from [`Ast`](Self::Ast).
    /// Labelling SCIP edges truthfully requires threading `derived_by` on the
    /// `CallEdgeRow` model itself (a cross-repo edda change) — the tracked
    /// follow-up. This inference is correct for the two producers whose markers
    /// DO differ, and never mislabels a binary edge as source.
    pub fn for_call_kind(call_kind: &str) -> Self {
        if call_kind.starts_with("plt:") {
            EdgeDerivation::Binary
        } else {
            EdgeDerivation::Ast
        }
    }
}

/// A single discovered scannable `.rs` file with its crate + section. `size` is
/// the on-disk byte length (already stat'd by the discovery walk).
#[derive(Debug, Clone)]
pub struct DiscoveredFile {
    pub path: std::path::PathBuf,
    pub crate_name: String,
    pub section: &'static str,
    pub size: u64,
}

/// Public front for the (otherwise private) discovery walk: every scannable `.rs`
/// under `repo_root`, each tagged with its crate + section.
pub fn discover_files(repo_root: &Path) -> Vec<DiscoveredFile> {
    let mut files: Vec<(std::path::PathBuf, String, &'static str, u64)> = Vec::new();
    discover_repo_files(repo_root, &mut files);
    files
        .into_iter()
        .map(|(path, crate_name, section, size)| DiscoveredFile {
            path,
            crate_name,
            section,
            size,
        })
        .collect()
}

/// Append every scannable `.rs` file of every crate under `repo_root` as
/// `(file, crate, section, size)`. Pure directory walk (no parsing). A
/// vanished/again-unreadable dir entry is skipped (best-effort), never fatal.
fn discover_repo_files(
    repo_root: &Path,
    files: &mut Vec<(std::path::PathBuf, String, &'static str, u64)>,
) {
    for entry in WalkDir::new(repo_root)
        .into_iter()
        .filter_entry(|e| !is_skipped_dir(&e.file_name().to_string_lossy()))
    {
        let Ok(entry) = entry else { continue };
        if entry.file_name() == "Cargo.toml" {
            collect_crate_files(entry.path(), files);
        }
    }
}

fn is_skipped_dir(name: &str) -> bool {
    // `.claude/` holds agent tooling — notably `.claude/worktrees/`, which nests
    // FULL checkouts of other branches (each with its own `src/…`). Scanning into
    // them double-counts symbols and lets a duplicate file (e.g. a worktree's
    // `src/bin/nornir-server.rs`) shadow the canonical one. Prune it like `.git`.
    matches!(
        name,
        "target" | ".git" | "node_modules" | ".nornir" | ".claude"
    )
}

/// Collect every scannable `.rs` file of the crate at `cargo_toml` as
/// `(file, crate_name, section, size)` tuples.
fn collect_crate_files(
    cargo_toml: &Path,
    files: &mut Vec<(std::path::PathBuf, String, &'static str, u64)>,
) {
    let Ok(text) = std::fs::read_to_string(cargo_toml) else {
        return;
    };
    let Ok(doc) = text.parse::<toml::Table>().map(toml::Value::Table) else {
        return;
    };
    let Some(crate_name) = doc
        .get("package")
        .and_then(|p| p.get("name"))
        .and_then(|n| n.as_str())
    else {
        return;
    };
    let Some(crate_dir) = cargo_toml.parent() else {
        return;
    };

    for sub in &["src", "tests", "benches", "examples"] {
        let dir = crate_dir.join(sub);
        if !dir.is_dir() {
            continue;
        }
        for entry in WalkDir::new(&dir)
            .into_iter()
            .filter_entry(|e| !is_skipped_dir(&e.file_name().to_string_lossy()))
        {
            let Ok(entry) = entry else { continue };
            if entry.file_type().is_file()
                && entry.path().extension().and_then(|e| e.to_str()) == Some("rs")
            {
                // Stat the size now (the discovery walk is already touching the
                // dir entry) so a consumer can schedule heaviest-file-first.
                let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
                files.push((
                    entry.path().to_path_buf(),
                    crate_name.to_string(),
                    *sub,
                    size,
                ));
            }
        }
    }
}

/// The `file` column value for `file` under `repo_root` — the repo-root-relative
/// path.
pub fn rel_path(repo_root: &Path, file: &Path) -> String {
    file.strip_prefix(repo_root)
        .unwrap_or(file)
        .to_string_lossy()
        .to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Edges from DIFFERENT derivation paths carry the correct provenance label
    /// (the wish's core invariant). The two producers whose `call_kind` markers
    /// differ — the `syn` AST scanner (`call`/`method`/`macro`) and the ELF binary
    /// extractor (`plt:<reloc>`, see [`crate::graph::extract::binary`]) — map to
    /// [`EdgeDerivation::Ast`] and [`EdgeDerivation::Binary`] respectively, and are
    /// never confused. Documents the SCIP limit: `scip` stamps `call`, so it is
    /// (correctly, not falsely) read as `Ast` until per-row provenance lands on
    /// the `CallEdgeRow` model.
    #[test]
    fn edge_derivation_is_per_producer_marker() {
        // syn AST producer markers.
        assert_eq!(EdgeDerivation::for_call_kind("call"), EdgeDerivation::Ast);
        assert_eq!(EdgeDerivation::for_call_kind("method"), EdgeDerivation::Ast);
        assert_eq!(EdgeDerivation::for_call_kind("macro"), EdgeDerivation::Ast);
        // ELF binary extractor markers (`format!("plt:{}", reloc)`).
        assert_eq!(
            EdgeDerivation::for_call_kind("plt:R_X86_64_JUMP_SLOT"),
            EdgeDerivation::Binary
        );
        assert_eq!(
            EdgeDerivation::for_call_kind("plt:R_X86_64_GLOB_DAT"),
            EdgeDerivation::Binary
        );
        // Wire tokens round-trip; unknown/pre-axis → the `ast` default.
        assert_eq!(EdgeDerivation::Binary.as_str(), "binary");
        assert_eq!(EdgeDerivation::CallGraph.as_str(), "callgraph");
        assert_eq!(EdgeDerivation::parse("binary"), EdgeDerivation::Binary);
        assert_eq!(EdgeDerivation::parse("nonsense"), EdgeDerivation::default());
    }
}