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);
}
}