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

nordisk/edda

crates/nornir-release-core/src/skew_warehouse.rs

//! WAREHOUSE-NATIVE API-skew — the *Mimir join* form of [`crate::api_skew`].
//!
//! # What this is
//!
//! [`api_skew`](crate::api_skew) answers "does member `M` call a symbol from dep
//! `D` that the PUBLISHED `D` does not export?" by *grepping* two blobs of Rust
//! source (the member's `src/**/*.rs` and the published tarball's). That is a
//! deliberately coarse first cut. This module states the SAME question as a
//! **relational join over already-extracted warehouse facts** (PODs), so the check
//! becomes a set operation over Mimir relations instead of a source scan:
//!
//! * the LEFT relation `references(member, dep, symbol)` is derived from the
//!   member's snapshot `call_edges` — the `callee_ident`s that resolve into `dep`;
//! * the RIGHT relation `published_api(dep, version, symbol)` is derived from the
//!   INGESTED published-`dep` `symbol_facts` snapshot (the one immutable fact the
//!   ecosystem still has to ingest — crates.io tarball rustdoc-json/SCIP, ingested
//!   ONCE per (crate, version); PLAN "published-crate API ingest").
//!
//! The skew is the **anti-join** LEFT minus RIGHT:
//!
//! ```datalog
//! % a member references `sym` from `dep`, witnessed by a concrete call edge
//! references(M, D, Sym, File, Line) :-
//!     call_edge(M, Caller, Callee, File, Line),
//!     resolves_into(Callee, D, Sym).
//!
//! % the published dep exports `sym`
//! published_api(D, V, Sym) :-
//!     symbol_fact(D, V, Sym, Vis), public(Vis).
//!
//! % SKEW: referenced but NOT published  (stratified negation)
//! skew(M, D, V, Sym) :-
//!     references(M, D, Sym, _, _),
//!     published_snapshot(D, V),
//!     not published_api(D, V, Sym).
//! ```
//!
//! [`assess_warehouse_skew`] evaluates exactly that rule over POD inputs. It is a
//! PURE SEAM: no `&dyn Warehouse`, no scan — the caller feeds the two already-read
//! relations (`&[CallEdgeRow]` for the member, `&[SymbolRow]` for the published
//! dep). Wiring the live warehouse (`knowledge::load_latest` for the member,
//! `Warehouse::scan_filtered(symbol_facts, dep@version)` for the ingested API) sits
//! ABOVE this seam and is intentionally out of scope here, so the join core is
//! unit-testable on fixtures and reused unchanged by both nornir's live path and
//! the offline advisory doctor.
//!
//! # Reuse, not a twin (LAW)
//!
//! The set-difference heart is [`api_skew::missing_symbols`] and the finding shape
//! is [`api_skew::ApiSkew`] — both consumed verbatim. This module only supplies the
//! two *relations* (extracted from PODs instead of scanned from source) and the
//! **provenance** that the relational form makes cheap. It adds no second copy of
//! the diff.
//!
//! # Why the warehouse form is STRICTLY better (the glob win)
//!
//! [`api_skew::assess_pair`] must BAIL to "inconclusive" when the published source
//! contains a glob re-export (`pub use inner::*;`) — a coarse scan cannot enumerate
//! what the glob pulls in, so flagging would risk a false block. The warehouse form
//! has NO such blind spot: `symbol_facts` is produced by rustdoc-json/SCIP ingest,
//! which has already RESOLVED every glob into concrete exported rows. So the join
//! sees the true public surface and never needs the inconclusive escape hatch.
//!
//! # The "why" provenance (Datalog derivation trees)
//!
//! Because `references` carries the witnessing `call_edge` (caller path + file:line),
//! every derived `skew(M, D, V, Sym)` fact has a **derivation tree**: the `references`
//! tuple that fired the rule IS the proof of *why* the symbol is flagged. A pure
//! Datalog engine (crepe/ascent/datafrog — Phase-3, PLAN Mimir) would recover this
//! for free via provenance semirings; here we materialize it eagerly as
//! [`RefWitness`] rows attached to each finding, so the Gate-A report can say not
//! just "`ingest_multi` is missing" but "...and here are the exact call sites
//! (`nornir/src/x.rs:42` in `run_ingest`) that would fail to compile". That is the
//! actionable half the coarse `explain()` cannot give.

use std::collections::{BTreeMap, BTreeSet};

use nornir_warehouse_trait::{CallEdgeRow, SymbolRow};

use crate::api_skew::{self, ApiSkew};
use crate::published_ingest::PublishedCrateApi;

/// One witnessing `call_edge` behind a referenced symbol — the provenance the
/// Datalog derivation of a `skew` fact rests on. Records WHERE the member reaches
/// into the dep so a flagged symbol points at the exact call site(s).
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct RefWitness {
    /// The item under the dep's crate root that is referenced (the join key `Sym`).
    pub symbol: String,
    /// The calling function's path (`caller_path` of the edge).
    pub caller_path: String,
    /// Source file of the call site.
    pub file: String,
    /// 1-based line of the call site.
    pub line: u32,
}

/// A warehouse-native skew finding: the reused [`ApiSkew`] verdict plus the
/// per-symbol provenance the relational form makes available.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WarehouseSkew {
    /// The finding, in the SHARED [`api_skew`] shape (same `explain()`, same
    /// downstream handling as the coarse path — no twin type).
    pub skew: ApiSkew,
    /// The call-edge witnesses for the missing symbols, sorted, deduped. Each row
    /// is a leaf of the Datalog derivation tree for `skew(member, dep, _, symbol)`.
    pub witnesses: Vec<RefWitness>,
}

impl WarehouseSkew {
    /// The witnesses for one missing `symbol` — the "why" provenance: the concrete
    /// call sites whose compile would fail against the published dep.
    pub fn why(&self, symbol: &str) -> Vec<&RefWitness> {
        self.witnesses.iter().filter(|w| w.symbol == symbol).collect()
    }

    /// [`ApiSkew::explain`] plus the first witness site per missing symbol — the
    /// full actionable Gate-A line the coarse path cannot produce.
    pub fn explain_with_sites(&self) -> String {
        let mut out = self.skew.explain();
        for sym in &self.skew.missing {
            if let Some(w) = self.why(sym).into_iter().next() {
                out.push_str(&format!("\n  - `{sym}` at {}:{} (in `{}`)", w.file, w.line, w.caller_path));
            }
        }
        out
    }
}

/// The RIGHT relation `published_api(dep, version, symbol)`: the set of exported
/// item names of the ingested published dep, from its `symbol_facts` rows. Filters
/// to the crate-visible public surface via [`is_published_public`] — OVER-collects
/// (unknown/blank visibility counts as public) so a name that IS in the API is
/// never a false skew, matching [`api_skew::public_api_names`]'s discipline. The
/// rustdoc-json/SCIP ingest that produces these rows has already resolved globs,
/// so there is no glob blind spot to guard.
pub fn published_api_from_symbols(published: &[SymbolRow]) -> BTreeSet<String> {
    published
        .iter()
        .filter(|s| is_published_public(&s.visibility))
        .map(|s| s.item_name.clone())
        .filter(|n| !n.is_empty())
        .collect()
}

/// The LEFT relation `references(member, dep, symbol)` with provenance: for each
/// `call_edge` of the member, resolve the `callee_ident` into an item under `dep`'s
/// crate root and, if it resolves, record the symbol plus the witnessing edge.
///
/// Resolution REUSES [`api_skew::referenced_symbols`] over the single
/// `callee_ident` string, so the path-boundary + first-segment-capture semantics
/// are IDENTICAL to the coarse path (`dep::mod::item` -> `mod`, `foo_dep::x` !=
/// `dep::x`). A bare, unqualified callee (`ingest_multi` with no `dep::` prefix)
/// does not resolve here — the coarse fact carries no crate attribution; a fully
/// resolved SCIP moniker join (callee def-moniker -> crate) is the richer future and
/// slots in at this exact seam without touching the diff.
pub fn references_with_provenance(
    dep_alias: &str,
    member_edges: &[CallEdgeRow],
) -> (BTreeSet<String>, Vec<RefWitness>) {
    let mut symbols = BTreeSet::new();
    let mut witnesses = Vec::new();
    for e in member_edges {
        for sym in api_skew::referenced_symbols(&e.callee_ident, dep_alias) {
            symbols.insert(sym.clone());
            witnesses.push(RefWitness {
                symbol: sym,
                caller_path: e.caller_path.clone(),
                file: e.file.clone(),
                line: e.line,
            });
        }
    }
    (symbols, witnesses)
}

/// Evaluate the anti-join `skew(member, dep, version, sym)` over POD relations: the
/// member's `call_edges` (LEFT) against the published dep's `symbol_facts` (RIGHT).
/// Returns `None` when there is no skew (empty difference). The verdict is the
/// SHARED [`api_skew::missing_symbols`] set difference; the added value over the
/// coarse path is the [`RefWitness`] provenance and the glob-free published surface.
pub fn assess_warehouse_skew(
    member: &str,
    dep: &str,
    published_version: &str,
    dep_alias: &str,
    member_edges: &[CallEdgeRow],
    published_symbols: &[SymbolRow],
) -> Option<WarehouseSkew> {
    let published = published_api_from_symbols(published_symbols);
    assess_skew_against_api(member, dep, published_version, dep_alias, member_edges, &published)
}

/// The RIGHT relation `published_api(dep, version, symbol)` lifted from an INGESTED
/// [`PublishedCrateApi`] POD (the [`published_ingest`](crate::published_ingest)
/// output) instead of raw `symbol_facts` rows. The POD's `symbols` ARE already the
/// extracted, sorted, de-duplicated public surface (`api_skew::public_api_names` ran
/// at ingest), so this is a direct lift — no visibility filter to re-apply, no second
/// definition of "the public API".
pub fn published_api_from_pod(published: &PublishedCrateApi) -> BTreeSet<String> {
    published.symbols.iter().cloned().collect()
}

/// POD ADAPTER — close the `published_ingest` -> `skew_warehouse` loop into an
/// end-to-end PURE pipeline: evaluate the anti-join over the member's `call_edges`
/// (LEFT) and an already-INGESTED [`PublishedCrateApi`] POD (RIGHT), with NO live
/// warehouse, NO source grep, NO network. The POD is self-describing, so `dep` and
/// `published_version` come from its `krate`/`version`.
///
/// # Enumerability honored (no false block)
///
/// Unlike `symbol_facts` from rustdoc-json/SCIP (which resolve globs and are always
/// enumerable — the "glob win" [`assess_warehouse_skew`] enjoys), a
/// [`PublishedCrateApi`] is produced by the COARSE ingest scan. When the published
/// crate glob-re-exports, its surface is NOT fully enumerable and the POD records
/// that as [`PublishedCrateApi::api_enumerable`] `= false`. In that case a "symbol
/// missing" verdict is INCONCLUSIVE, so this returns `None` (never a false block) —
/// exactly as [`api_skew::assess_pair`] bails on a glob, leaving the verify build as
/// the backstop. When the surface IS enumerable the join is exact, with the same
/// [`RefWitness`] provenance as the `symbol_facts` path.
pub fn assess_skew_from_published_api(
    member: &str,
    dep_alias: &str,
    member_edges: &[CallEdgeRow],
    published: &PublishedCrateApi,
) -> Option<WarehouseSkew> {
    // COARSE-INGEST BLIND SPOT: a non-enumerable (glob) surface makes any "missing"
    // verdict inconclusive — emit no skew rather than risk a false block.
    if !published.api_enumerable {
        return None;
    }
    let api = published_api_from_pod(published);
    assess_skew_against_api(
        member,
        &published.krate,
        &published.version,
        dep_alias,
        member_edges,
        &api,
    )
}

/// The shared anti-join heart, over a member's `call_edges` (LEFT) and an
/// already-materialized published-API name set (RIGHT). Reused verbatim by BOTH the
/// `symbol_facts` path ([`assess_warehouse_skew`]) and the POD path
/// ([`assess_skew_from_published_api`]) so the diff + provenance exist ONCE (LAW).
/// Returns `None` on an empty difference.
fn assess_skew_against_api(
    member: &str,
    dep: &str,
    published_version: &str,
    dep_alias: &str,
    member_edges: &[CallEdgeRow],
    published: &BTreeSet<String>,
) -> Option<WarehouseSkew> {
    let (referenced, provenance) = references_with_provenance(dep_alias, member_edges);
    let missing = api_skew::missing_symbols(&referenced, published);
    if missing.is_empty() {
        return None;
    }
    let mut witnesses: Vec<RefWitness> =
        provenance.into_iter().filter(|w| missing.contains(&w.symbol)).collect();
    witnesses.sort();
    witnesses.dedup();
    Some(WarehouseSkew {
        skew: ApiSkew {
            member: member.to_string(),
            dep: dep.to_string(),
            published_version: published_version.to_string(),
            missing,
        },
        witnesses,
    })
}

/// True when a `symbol_facts.visibility` string denotes an EXPORTED item. Only the
/// explicitly restricted visibilities (`pub(crate)`, `pub(super)`, `pub(in ...)`,
/// `pub(self)`, and private) are excluded; everything else — `"pub"`, `"public"`,
/// and blank (SCIP/rustdoc rows carry no `pub` token but are public-by-construction)
/// — counts as exported. Inclusive by design: never drop a truly-public name, or the
/// join would raise a FALSE block.
pub fn is_published_public(visibility: &str) -> bool {
    let v = visibility.trim();
    if v.is_empty() {
        return true; // resolved/ingested rows: public-by-construction.
    }
    let restricted = v == "priv"
        || v == "private"
        || v.starts_with("pub(crate")
        || v.starts_with("pub(super")
        || v.starts_with("pub(self")
        || v.starts_with("pub(in");
    !restricted
}

// ─────────────────────────── the LIVE dispatcher ───────────────────────────
//
// The pure seams above ([`assess_warehouse_skew`] over `symbol_facts`,
// [`assess_skew_from_published_api`] over a coarse-ingest POD) each take an
// already-materialized RIGHT relation. The DISPATCHER below sits one level up: for
// every (member, dep@version) reference target it picks the RIGHT source — PREFERRING
// glob-resolved `symbol_facts` from a live warehouse (the "glob win", always
// enumerable), FALLING BACK to the coarse published-API ingest POD when that dep@version
// is not in the warehouse. The LEFT relation (the member's `call_edges`) comes from a
// live warehouse via `knowledge::load_latest`; when NO live warehouse is present the leg
// is INCONCLUSIVE for that member (a note, never a false block). Both relation sources
// are injected as traits so the dispatcher is unit-testable on stubs — no warehouse, no
// network — exactly like the pure seams it drives.

/// One reference target the dispatcher resolves: member `member` (in repo `repo`)
/// references dep `dep` under source alias `dep_alias`, and the version it will resolve
/// against at publish time is `published_version`. `repo` rides along only for
/// [`Issue`](crate::model::Issue) attribution at the lowering seam.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkewTarget {
    /// The repo owning `member` — for preflight `Issue::repo` attribution.
    pub repo: String,
    /// The member crate whose `call_edges` are the LEFT relation.
    pub member: String,
    /// The published dep whose public API is the RIGHT relation.
    pub dep: String,
    /// The identifier the member's source uses in a `dep_alias::sym` path (the dep-table
    /// key, `-`→`_`), which may differ from `dep` under a `package = …` rename.
    pub dep_alias: String,
    /// The already-published version the member resolves against.
    pub published_version: String,
}

/// The RIGHT-relation source's answer for one dep@version — which materialization of the
/// published public API is available, in preference order.
pub enum PublishedApi {
    /// Glob-resolved `symbol_facts` rows from a live warehouse (the strictly-better
    /// path — no glob blind spot).
    Symbols(Vec<SymbolRow>),
    /// The coarse published-API ingest POD (crates.io tarball scan). Its
    /// `api_enumerable` flag is honored: a non-enumerable (glob) surface is inconclusive.
    Ingested(PublishedCrateApi),
    /// Neither source could produce the API (no warehouse rows, fetch/decompress failed)
    /// ⇒ INCONCLUSIVE for this target, never a false block.
    Unavailable,
}

/// The LEFT-relation source: a member's `call_edges` from a LIVE warehouse (typically
/// [`knowledge::load_latest`](crate::knowledge::load_latest)). `None`
/// when there is no live warehouse or no snapshot for the member — the dispatcher then
/// records the target INCONCLUSIVE rather than fabricating a block.
pub trait MemberEdgeSource {
    /// The member crate's call edges, or `None` when no live snapshot is available.
    fn member_call_edges(&self, member: &str) -> Option<Vec<CallEdgeRow>>;
}

/// The RIGHT-relation source: resolve the published public API of `dep@version`,
/// preferring glob-resolved warehouse `symbol_facts` and falling back to a coarse ingest.
pub trait PublishedApiSource {
    /// Resolve the published API materialization for `dep` at `version`.
    fn published_api(&self, dep: &str, version: &str) -> PublishedApi;
}

/// A finding from the dispatcher: the warehouse-native skew plus the repo it belongs to,
/// ready to lower into a preflight [`Issue`](crate::model::Issue).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkewFinding {
    /// The repo owning the flagged member (for `Issue::repo`).
    pub repo: String,
    /// The warehouse-native verdict + call-site provenance.
    pub skew: WarehouseSkew,
}

/// A target the dispatcher could NOT decide — surfaced as an informational NOTE, never a
/// blocker. The three reasons map to the design's fall-safe seams: no live LEFT relation,
/// a non-enumerable (glob) coarse RIGHT relation, or an unavailable RIGHT relation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkewInconclusive {
    /// The member the target concerns.
    pub member: String,
    /// The dep the target concerns.
    pub dep: String,
    /// Why the target is inconclusive (human-readable).
    pub reason: String,
}

/// The dispatcher's output: the surfaced [`SkewFinding`]s (real skews to block on) and
/// the [`SkewInconclusive`] targets (notes only, never blocks).
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SkewDispatch {
    /// Real anti-join hits — a referenced symbol absent from the published API.
    pub findings: Vec<SkewFinding>,
    /// Targets left undecided (fall-safe), reported as notes.
    pub inconclusive: Vec<SkewInconclusive>,
}

/// LIVE Gate-A skew dispatcher: for each [`SkewTarget`], load the member's `call_edges`
/// (LEFT) once via `edges`, then resolve the dep@version public API (RIGHT) via `apis`
/// preferring glob-resolved `symbol_facts` and falling back to the coarse ingest POD, and
/// evaluate the SHARED anti-join seam. Every fall-safe branch (no live LEFT relation, a
/// glob-only coarse RIGHT relation, an unavailable RIGHT relation) records an
/// INCONCLUSIVE note instead of a block. The member's edges are fetched at most once per
/// member (cached), and — crucially — the RIGHT relation is consulted ONLY after the LEFT
/// relation is known present, so a warehouse-free run never pays a needless API fetch.
pub fn assess_skew_targets(
    targets: &[SkewTarget],
    edges: &dyn MemberEdgeSource,
    apis: &dyn PublishedApiSource,
) -> SkewDispatch {
    let mut out = SkewDispatch::default();
    let mut edge_cache: BTreeMap<String, Option<Vec<CallEdgeRow>>> = BTreeMap::new();

    for t in targets {
        let member_edges = edge_cache
            .entry(t.member.clone())
            .or_insert_with(|| edges.member_call_edges(&t.member));
        let Some(member_edges) = member_edges.as_ref() else {
            // No call edges for the member ⇒ inconclusive, never a block. Since
            // Mímir v2 that is every caller that reads a warehouse: the `call_edges`
            // table has no writer, and the edges are nornir's code room's to supply.
            out.inconclusive.push(SkewInconclusive {
                member: t.member.clone(),
                dep: t.dep.clone(),
                reason: "no call edges for member — NOT PRODUCED: the `call_edges` table has no \
                         writer since Mímir v2 and the caller supplied none from the code room \
                         (skew leg inconclusive; verify build backstops)"
                    .to_string(),
            });
            continue;
        };

        match apis.published_api(&t.dep, &t.published_version) {
            // PREFERRED: glob-resolved symbol_facts — no blind spot, always enumerable.
            PublishedApi::Symbols(symbols) => {
                if let Some(skew) = assess_warehouse_skew(
                    &t.member,
                    &t.dep,
                    &t.published_version,
                    &t.dep_alias,
                    member_edges,
                    &symbols,
                ) {
                    out.findings.push(SkewFinding { repo: t.repo.clone(), skew });
                }
            }
            // FALLBACK: coarse ingest POD — honor enumerability (glob ⇒ inconclusive).
            PublishedApi::Ingested(pod) => {
                if !pod.api_enumerable {
                    out.inconclusive.push(SkewInconclusive {
                        member: t.member.clone(),
                        dep: t.dep.clone(),
                        reason: format!(
                            "published `{}` {} API is not enumerable (glob re-export); a \
                             missing-symbol verdict would be inconclusive — verify build backstops",
                            t.dep, t.published_version
                        ),
                    });
                    continue;
                }
                if let Some(skew) =
                    assess_skew_from_published_api(&t.member, &t.dep_alias, member_edges, &pod)
                {
                    out.findings.push(SkewFinding { repo: t.repo.clone(), skew });
                }
            }
            PublishedApi::Unavailable => out.inconclusive.push(SkewInconclusive {
                member: t.member.clone(),
                dep: t.dep.clone(),
                reason: format!(
                    "could not resolve published `{}` {} API (no warehouse rows, fetch/decompress \
                     failed); skew check skipped — verify build backstops",
                    t.dep, t.published_version
                ),
            }),
        }
    }
    out
}

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

    fn sym(name: &str, vis: &str) -> SymbolRow {
        SymbolRow {
            crate_name: "skade".into(),
            module_path: "skade".into(),
            item_kind: "fn".into(),
            item_name: name.into(),
            visibility: vis.into(),
            file: "src/lib.rs".into(),
            line: 1,
            doc_lines: 0,
            signature: None,
        }
    }

    fn edge(caller: &str, callee: &str, file: &str, line: u32) -> CallEdgeRow {
        CallEdgeRow {
            crate_name: "nornir".into(),
            caller_path: caller.into(),
            callee_ident: callee.into(),
            call_kind: "call".into(),
            file: file.into(),
            line,
        }
    }

    /// THE warehouse-native skade case, as a Mimir join over PODs: nornir's
    /// `call_edges` reference `skade::ingest_multi`, but the ingested published
    /// `skade 0.5.0` `symbol_facts` export only `ingest_with`. The anti-join must
    /// flag `ingest_multi` (and nothing else), and — the value the relational form
    /// adds — must carry the exact call-site provenance ("why").
    #[test]
    fn skade_ingest_multi_warehouse_join_with_provenance() {
        let member_edges = vec![
            edge("nornir::run_ingest", "skade::ingest_multi", "nornir/src/ingest.rs", 42),
            edge("nornir::run_ingest", "skade::ingest_with", "nornir/src/ingest.rs", 50),
        ];
        let published = vec![
            sym("ingest_with", "pub"),
            sym("IceTable", "pub"),
            sym("hidden_helper", "pub(crate)"),
        ];
        let ws = assess_warehouse_skew(
            "nornir",
            "skade",
            "0.5.0",
            "skade",
            &member_edges,
            &published,
        )
        .expect("warehouse anti-join must detect the skew");

        let missing_only = ws.skew.missing == ["ingest_multi".to_string()].into_iter().collect();
        let why = ws.why("ingest_multi");
        let provenance_ok = why.len() == 1
            && why[0].file == "nornir/src/ingest.rs"
            && why[0].line == 42
            && why[0].caller_path == "nornir::run_ingest";
        let explain_ok = ws.explain_with_sites().contains("ingest.rs:42");
        let ok = missing_only && provenance_ok && explain_ok;

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "release-skew-warehouse",
            "skade_ingest_multi_warehouse_join_with_provenance",
            ok,
            &format!("missing={:?} why={why:?}", ws.skew.missing),
        );
        assert!(ok, "missing={:?} why={why:?}", ws.skew.missing);
    }

    /// No skew when the published `symbol_facts` export everything referenced.
    #[test]
    fn no_skew_when_published_has_the_api() {
        let edges = vec![edge("m::f", "skade::ingest_with", "m/src/lib.rs", 3)];
        let published = vec![sym("ingest_with", "pub"), sym("ingest_multi", "pub")];
        assert!(
            assess_warehouse_skew("m", "skade", "0.5.1", "skade", &edges, &published).is_none()
        );
    }

    /// THE GLOB WIN: a name that a coarse scan could only see behind `pub use inner::*`
    /// is present as a CONCRETE ingested `symbol_facts` row (rustdoc/SCIP already
    /// resolved the glob), so the join sees the true surface and does NOT flag it —
    /// no "inconclusive" escape hatch needed, unlike `api_skew::assess_pair`.
    #[test]
    fn glob_reexport_is_resolved_in_warehouse_no_false_block() {
        let edges = vec![edge("m::f", "dep::mystery", "m/src/lib.rs", 7)];
        // ingest enumerated the glob's members as concrete rows:
        let published = vec![sym("known", "pub"), sym("mystery", "pub")];
        assert!(
            assess_warehouse_skew("m", "dep", "1.0.0", "dep", &edges, &published).is_none(),
            "glob-resolved published surface must not raise a false block"
        );
    }

    /// The path-boundary semantics are exactly `api_skew`'s: `foo_skade::x` must not
    /// resolve into `skade`, and a chain captures the first segment under the root.
    #[test]
    fn resolution_reuses_api_skew_boundaries() {
        let edges = vec![
            edge("m::f", "foo_skade::nope", "m/src/lib.rs", 1),
            edge("m::f", "skade::write", "m/src/lib.rs", 2),
        ];
        let (refs, _) = references_with_provenance("skade", &edges);
        assert!(refs.contains("write"));
        assert!(!refs.contains("nope"), "must not match inside foo_skade");
    }

    /// END-TO-END PURE PIPELINE: distil a published dep's API straight from an
    /// in-memory `path -> bytes` tarball file-map via `published_ingest`
    /// (`public_api_from_file_map`), then feed the resulting POD as the RIGHT relation
    /// to the warehouse anti-join — NO live warehouse, NO source grep, NO network.
    /// nornir's `call_edges` reach `skade::ingest_multi`; the published `skade 0.5.0`
    /// tarball only exports `ingest_with`, so the join must flag `ingest_multi` (and
    /// nothing else) and carry the exact call-site provenance. RED-WHEN-BROKEN: if the
    /// POD adapter drops the ingest surface or the provenance, this changes.
    #[test]
    fn published_api_pod_drives_warehouse_join_end_to_end() {
        use crate::published_ingest;
        use std::collections::BTreeMap;

        // The exact shape source_drift returns for a fetched .crate tarball.
        let files: BTreeMap<String, Vec<u8>> = [
            (
                "src/lib.rs".to_string(),
                b"pub async fn ingest_with() {}\npub struct IceTable;\npub(crate) fn hidden() {}"
                    .to_vec(),
            ),
            ("Cargo.toml".to_string(), b"[package]\nname = \"skade\"\n".to_vec()),
        ]
        .into_iter()
        .collect();
        let pod = published_ingest::public_api_from_file_map("skade", "0.5.0", &files);
        assert!(pod.api_enumerable, "no glob ⇒ enumerable surface");

        let member_edges = vec![
            edge("nornir::run_ingest", "skade::ingest_multi", "nornir/src/ingest.rs", 42),
            edge("nornir::run_ingest", "skade::ingest_with", "nornir/src/ingest.rs", 50),
        ];
        let ws = assess_skew_from_published_api("nornir", "skade", &member_edges, &pod)
            .expect("POD-driven anti-join must detect the skew");

        let missing_only = ws.skew.missing == ["ingest_multi".to_string()].into_iter().collect();
        // dep/version are lifted from the self-describing POD.
        let attribution_ok = ws.skew.dep == "skade" && ws.skew.published_version == "0.5.0";
        let why = ws.why("ingest_multi");
        let provenance_ok = why.len() == 1
            && why[0].file == "nornir/src/ingest.rs"
            && why[0].line == 42
            && why[0].caller_path == "nornir::run_ingest";
        let ok = missing_only && attribution_ok && provenance_ok;

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "release-skew-warehouse",
            "published_api_pod_drives_warehouse_join_end_to_end",
            ok,
            &format!("missing={:?} dep={} ver={} why={why:?}", ws.skew.missing, ws.skew.dep, ws.skew.published_version),
        );
        assert!(ok, "missing={:?} dep={} ver={} why={why:?}", ws.skew.missing, ws.skew.dep, ws.skew.published_version);
    }

    /// No skew when the ingested POD exports everything referenced.
    #[test]
    fn no_skew_when_pod_has_the_api() {
        let pod = PublishedCrateApi {
            krate: "skade".into(),
            version: "0.5.1".into(),
            symbols: vec!["ingest_multi".into(), "ingest_with".into()],
            api_enumerable: true,
            source_files: 1,
        };
        let edges = vec![edge("m::f", "skade::ingest_with", "m/src/lib.rs", 3)];
        assert!(assess_skew_from_published_api("m", "skade", &edges, &pod).is_none());
    }

    /// COARSE-INGEST BLIND SPOT: a non-enumerable POD (published crate glob-re-exports,
    /// so `public_api_from_file_map` set `api_enumerable = false`) makes any "missing"
    /// verdict INCONCLUSIVE. The POD adapter must return `None` (never a false block) —
    /// the coarse-ingest analogue of `api_skew::assess_pair` bailing on a glob. This is
    /// the one place the POD path differs from the glob-resolved `symbol_facts` path.
    #[test]
    fn non_enumerable_pod_is_inconclusive_never_a_false_block() {
        let pod = PublishedCrateApi {
            krate: "dep".into(),
            version: "1.0.0".into(),
            symbols: vec!["known".into()], // `mystery` hides behind the unresolved glob
            api_enumerable: false,
            source_files: 1,
        };
        let edges = vec![edge("m::f", "dep::mystery", "m/src/lib.rs", 7)];
        assert!(
            assess_skew_from_published_api("m", "dep", &edges, &pod).is_none(),
            "non-enumerable ingest surface => inconclusive, must not block"
        );
    }

    /// `pub(crate)`/`pub(super)`/`pub(in ...)`/private are NOT exported; `pub`,
    /// `public`, and blank (ingested/SCIP) ARE — inclusive so no true-public name is
    /// dropped into a false block.
    #[test]
    fn visibility_public_filter() {
        assert!(is_published_public("pub"));
        assert!(is_published_public("public"));
        assert!(is_published_public(""));
        assert!(!is_published_public("pub(crate)"));
        assert!(!is_published_public("pub(super)"));
        assert!(!is_published_public("pub(in crate::x)"));
        assert!(!is_published_public("priv"));

        let published = vec![sym("shown", "pub"), sym("hidden", "pub(crate)"), sym("ingested", "")];
        let api = published_api_from_symbols(&published);
        assert!(api.contains("shown") && api.contains("ingested") && !api.contains("hidden"));
    }

    // ── dispatcher stubs ────────────────────────────────────────────────────
    struct StubEdges(BTreeMap<String, Vec<CallEdgeRow>>);
    impl MemberEdgeSource for StubEdges {
        fn member_call_edges(&self, member: &str) -> Option<Vec<CallEdgeRow>> {
            self.0.get(member).cloned()
        }
    }
    struct NoEdges;
    impl MemberEdgeSource for NoEdges {
        fn member_call_edges(&self, _member: &str) -> Option<Vec<CallEdgeRow>> {
            None
        }
    }
    struct StubApi(PublishedApi);
    impl PublishedApiSource for StubApi {
        fn published_api(&self, _dep: &str, _version: &str) -> PublishedApi {
            match &self.0 {
                PublishedApi::Symbols(s) => PublishedApi::Symbols(s.clone()),
                PublishedApi::Ingested(p) => PublishedApi::Ingested(p.clone()),
                PublishedApi::Unavailable => PublishedApi::Unavailable,
            }
        }
    }

    fn target() -> SkewTarget {
        SkewTarget {
            repo: "nornir".into(),
            member: "nornir".into(),
            dep: "skade".into(),
            dep_alias: "skade".into(),
            published_version: "0.5.0".into(),
        }
    }

    /// RED-WHEN-BROKEN: the dispatcher, given a member-edge referencing `skade::ingest_multi`
    /// and a stub published API (warehouse `symbol_facts`) that lacks it, surfaces EXACTLY one
    /// skew finding via the PREFERRED glob-resolved path — with repo attribution + provenance.
    #[test]
    fn dispatcher_symbols_path_surfaces_exactly_one_skew() {
        let edges: BTreeMap<String, Vec<CallEdgeRow>> = [(
            "nornir".to_string(),
            vec![
                edge("nornir::run_ingest", "skade::ingest_multi", "nornir/src/ingest.rs", 42),
                edge("nornir::run_ingest", "skade::ingest_with", "nornir/src/ingest.rs", 50),
            ],
        )]
        .into_iter()
        .collect();
        let apis = StubApi(PublishedApi::Symbols(vec![sym("ingest_with", "pub")]));
        let d = assess_skew_targets(&[target()], &StubEdges(edges), &apis);

        let one = d.findings.len() == 1;
        let attributed = one && d.findings[0].repo == "nornir";
        let missing_only = one
            && d.findings[0].skew.skew.missing == ["ingest_multi".to_string()].into_iter().collect();
        let sited = one && d.findings[0].skew.explain_with_sites().contains("ingest.rs:42");
        let ok = one && attributed && missing_only && sited && d.inconclusive.is_empty();

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "release-skew-warehouse",
            "dispatcher_symbols_path_surfaces_exactly_one_skew",
            ok,
            &format!("findings={} inconclusive={}", d.findings.len(), d.inconclusive.len()),
        );
        assert!(ok, "findings={:?} inconclusive={:?}", d.findings, d.inconclusive);
    }

    /// The FALLBACK path: when the RIGHT relation is a coarse ingest POD (dep@version not in
    /// the warehouse), the dispatcher still evaluates the anti-join and surfaces the skew.
    #[test]
    fn dispatcher_falls_back_to_ingest_pod() {
        let edges: BTreeMap<String, Vec<CallEdgeRow>> = [(
            "nornir".to_string(),
            vec![edge("nornir::run_ingest", "skade::ingest_multi", "nornir/src/ingest.rs", 42)],
        )]
        .into_iter()
        .collect();
        let pod = PublishedCrateApi {
            krate: "skade".into(),
            version: "0.5.0".into(),
            symbols: vec!["ingest_with".into()],
            api_enumerable: true,
            source_files: 1,
        };
        let d = assess_skew_targets(
            &[target()],
            &StubEdges(edges),
            &StubApi(PublishedApi::Ingested(pod)),
        );
        assert_eq!(d.findings.len(), 1);
        assert!(d.inconclusive.is_empty());
        assert_eq!(
            d.findings[0].skew.skew.missing,
            ["ingest_multi".to_string()].into_iter().collect()
        );
    }

    /// No LIVE warehouse call_edges ⇒ the target is INCONCLUSIVE (a note), never a block —
    /// and the RIGHT relation is never even consulted (LEFT-first, no needless fetch).
    #[test]
    fn dispatcher_no_live_edges_is_inconclusive_not_a_block() {
        let d = assess_skew_targets(
            &[target()],
            &NoEdges,
            &StubApi(PublishedApi::Symbols(vec![sym("ingest_with", "pub")])),
        );
        assert!(d.findings.is_empty(), "no live edges must never block");
        assert_eq!(d.inconclusive.len(), 1);
    }

    /// A non-enumerable (glob) coarse POD ⇒ INCONCLUSIVE, never a false block — the coarse
    /// analogue of `assess_pair` bailing on a glob.
    #[test]
    fn dispatcher_non_enumerable_pod_is_inconclusive_not_a_block() {
        let edges: BTreeMap<String, Vec<CallEdgeRow>> = [(
            "nornir".to_string(),
            vec![edge("nornir::run_ingest", "skade::mystery", "nornir/src/ingest.rs", 42)],
        )]
        .into_iter()
        .collect();
        let pod = PublishedCrateApi {
            krate: "skade".into(),
            version: "0.5.0".into(),
            symbols: vec!["known".into()],
            api_enumerable: false,
            source_files: 1,
        };
        let d = assess_skew_targets(
            &[target()],
            &StubEdges(edges),
            &StubApi(PublishedApi::Ingested(pod)),
        );
        assert!(d.findings.is_empty(), "non-enumerable ingest must not block");
        assert_eq!(d.inconclusive.len(), 1);
    }

    /// An unavailable RIGHT relation ⇒ INCONCLUSIVE, never a block.
    #[test]
    fn dispatcher_unavailable_api_is_inconclusive() {
        let edges: BTreeMap<String, Vec<CallEdgeRow>> = [(
            "nornir".to_string(),
            vec![edge("nornir::run_ingest", "skade::mystery", "nornir/src/ingest.rs", 42)],
        )]
        .into_iter()
        .collect();
        let d = assess_skew_targets(
            &[target()],
            &StubEdges(edges),
            &StubApi(PublishedApi::Unavailable),
        );
        assert!(d.findings.is_empty());
        assert_eq!(d.inconclusive.len(), 1);
    }
}