//! Tier propagation and the exact version-1 serialized contract.
//!
//! The tag vocabulary is stated here for every producer. It is the graph's own,
//! not a language's, so a second adapter reads the same lists rather than
//! writing its own copy of them.
use std::collections::BTreeSet;
use pedant_graph::{
GraphCertainty, GraphCollection, GraphDependencyKind, GraphEdgeKind, GraphReferenceKind,
build_rust_graph,
};
use pedant_types::{ResolutionGap, ResolutionReport, ResolutionTier};
use super::corpus::MINIMAL_CORPUS;
use super::fixture;
use super::promotion::promoted_resolution;
use super::render;
use super::surface::serialized_tags;
/// Every node category the graph owns.
pub const NODE_CATEGORIES: &[&str] = &["file", "container", "function", "type", "value"];
/// Every node location kind the graph owns.
pub const LOCATION_KINDS: &[&str] = &["file", "span"];
/// Every reference kind the graph owns.
pub const REFERENCE_KINDS: &[&str] = &["call", "import", "implementation", "reference"];
/// Every edge kind the graph owns.
pub const EDGE_KINDS: &[&str] = &[
"call",
"import",
"implementation",
"reference",
"depends_on",
];
/// Every certainty the graph owns.
pub const CERTAINTIES: &[&str] = &["resolved", "possible"];
/// Every edge origin kind the graph owns.
pub const ORIGIN_KINDS: &[&str] = &["reference", "dependency"];
/// Every written-down tag list, beside the declaration it must equal.
const DECLARED_TAGS: &[(&str, &str, &str, &[&str])] = &[
(
"node categories",
"src/node.rs",
"GraphNodeKind",
NODE_CATEGORIES,
),
(
"location kinds",
"src/node.rs",
"GraphNodeLocation",
LOCATION_KINDS,
),
(
"reference kinds",
"src/reference.rs",
"GraphReferenceKind",
REFERENCE_KINDS,
),
("edge kinds", "src/edge.rs", "GraphEdgeKind", EDGE_KINDS),
("certainties", "src/edge.rs", "GraphCertainty", CERTAINTIES),
(
"origin kinds",
"src/edge.rs",
"GraphEdgeOrigin",
ORIGIN_KINDS,
),
];
/// The exact compact serialization of the minimal corpus.
const MINIMAL_JSON: &str = r#"{"schema_version":1,"tier":"syntactic","nodes":[{"id":0,"language":"rust","name":"app","kind":{"category":"container","level":"library"},"location":null},{"id":1,"language":"rust","name":"src/lib.rs","kind":{"category":"file"},"location":{"kind":"file","path":"src/lib.rs"}},{"id":2,"language":"rust","name":"run","kind":{"category":"function","declaration":"function"},"location":{"kind":"span","file":1,"span":{"file":"src/lib.rs","start":{"line":0,"column":7},"end":{"line":0,"column":10}}}},{"id":3,"language":"rust","name":"work","kind":{"category":"function","declaration":"function"},"location":{"kind":"span","file":1,"span":{"file":"src/lib.rs","start":{"line":1,"column":7},"end":{"line":1,"column":11}}}}],"containment":[{"parent":0,"child":1},{"parent":0,"child":2},{"parent":0,"child":3}],"references":[{"id":0,"source":2,"language":"rust","kind":"call","text":"work","span":{"file":"src/lib.rs","start":{"line":0,"column":15},"end":{"line":0,"column":19}},"gaps":[],"edges":[0]}],"edges":[{"id":0,"source":2,"target":3,"kind":"call","certainty":"resolved","origin":{"kind":"reference","reference":0}}]}"#;
/// Every graph-owned object and tag the version-1 contract specifies.
const REQUIRED_FRAGMENTS: &[&str] = &[
r#"{"category":"file"}"#,
r#"{"category":"container","level":"library"}"#,
r#"{"category":"container","level":"module"}"#,
r#"{"category":"function","declaration":"function"}"#,
r#"{"category":"function","declaration":"method"}"#,
r#"{"category":"type","declaration":"struct"}"#,
r#"{"category":"type","declaration":"enum"}"#,
r#"{"category":"type","declaration":"union"}"#,
r#"{"category":"type","declaration":"trait"}"#,
r#"{"category":"type","declaration":"type_alias"}"#,
r#"{"category":"value","declaration":"constant"}"#,
r#"{"category":"value","declaration":"static"}"#,
r#""location":null"#,
r#""location":{"kind":"file","path":"src/lib.rs"}"#,
r#""location":{"kind":"span","file":"#,
// Every reference-kind token is required twice over, once per object that
// can carry it: a record states its `text` next, an edge its `certainty`.
// An unqualified `"kind":"<token>"` fragment is satisfied by either object
// and by the edge-origin tag, so neither enum would be pinned alone.
r#""kind":"call","text":"#,
r#""kind":"import","text":"#,
r#""kind":"implementation","text":"#,
r#""kind":"reference","text":"#,
r#""kind":"call","certainty":"#,
r#""kind":"import","certainty":"#,
r#""kind":"implementation","certainty":"#,
r#""kind":"reference","certainty":"#,
r#""kind":"depends_on","certainty":"#,
r#""certainty":"resolved""#,
r#""certainty":"possible""#,
r#""gaps":[]"#,
r#""gaps":["external_definition"]"#,
r#""edges":[]"#,
r#""origin":{"kind":"reference","reference":"#,
r#""origin":{"kind":"dependency","evidence":{"alias":"helper","kind":"normal","predicate":null}}"#,
r#""origin":{"kind":"dependency","evidence":{"alias":"gated","kind":"normal","predicate":"cfg(unix)"}}"#,
r#""origin":{"kind":"dependency","evidence":{"alias":"harness","kind":"development","predicate":"cfg(test)"}}"#,
r#""schema_version":1"#,
r#""tier":"syntactic""#,
];
/// Every shape only a promoted graph reaches.
const PROMOTED_FRAGMENTS: &[&str] = &[
r#""tier":"semantic""#,
r#""gaps":["conditional_compilation"]"#,
r#""gaps":["missing_definition"]"#,
];
/// Vocabulary a layout or renderer contract would introduce. None may appear.
const LAYOUT_VOCABULARY: &[&str] = &[
"\"x\"",
"\"y\"",
"\"width\"",
"\"height\"",
"\"color\"",
"\"rank\"",
"\"layout\"",
"\"position\"",
"\"style\"",
"\"shape\"",
"\"route\"",
"\"cluster\"",
];
/// Repeated serialization of equal inputs is byte-identical and exact.
pub fn assert_json_is_exact_and_deterministic() {
let (_fixture, resolved) = fixture::resolve_library(MINIMAL_CORPUS);
let graph = build_rust_graph(&resolved.snapshot, &resolved.resolution)
.expect("the minimal corpus projects");
let first = serde_json::to_string(&graph).expect("the graph serializes");
let second = serde_json::to_string(&graph).expect("the graph serializes again");
assert_eq!(first, second, "repeated serialization is byte-identical");
assert_eq!(first, MINIMAL_JSON, "the version-1 wire shape changed");
let rebuilt = build_rust_graph(&resolved.snapshot, &resolved.resolution)
.expect("a second build over equal inputs succeeds");
assert_eq!(
serde_json::to_string(&rebuilt).expect("the rebuilt graph serializes"),
first,
"equal inputs produce byte-identical output"
);
assert_layout_free(&first);
}
/// Every graph-owned variant, tag, explicit null, and empty array appears.
pub fn assert_json_covers_every_variant() {
let (_corpus, _resolved, graph) = fixture::project_corpus_library();
let text = serde_json::to_string(&graph).expect("the corpus graph serializes");
let (_build, _script, build_graph) = fixture::project_build_script();
let build_text =
serde_json::to_string(&build_graph).expect("the build-script graph serializes");
let missing: Vec<&str> = REQUIRED_FRAGMENTS
.iter()
.copied()
.filter(|fragment| !text.contains(fragment))
.collect();
assert!(
missing.is_empty(),
"version-1 shapes are missing: {missing:?}"
);
assert!(
build_text.contains(
r#""origin":{"kind":"dependency","evidence":{"alias":"builder","kind":"build","predicate":null}}"#
),
"the build dependency table has no serialized form"
);
assert!(
build_text.contains(r#"{"category":"container","level":"build_script"}"#),
"a unit container states the Cargo target kind it was built for"
);
let tags = [
(
serde_json::to_string(&GraphReferenceKind::Implementation),
"GraphReferenceKind::Implementation",
"\"implementation\"",
),
(
serde_json::to_string(&GraphEdgeKind::DependsOn),
"GraphEdgeKind::DependsOn",
"\"depends_on\"",
),
(
serde_json::to_string(&GraphCertainty::Possible),
"GraphCertainty::Possible",
"\"possible\"",
),
(
serde_json::to_string(&GraphDependencyKind::Development),
"GraphDependencyKind::Development",
"\"development\"",
),
(
serde_json::to_string(&GraphCollection::Node),
"GraphCollection::Node",
"\"node\"",
),
(
serde_json::to_string(&GraphCollection::Reference),
"GraphCollection::Reference",
"\"reference\"",
),
(
serde_json::to_string(&GraphCollection::Edge),
"GraphCollection::Edge",
"\"edge\"",
),
];
for (rendered, subject, expected) in tags {
assert_eq!(
rendered.unwrap_or_else(|error| panic!("{subject} does not serialize: {error}")),
expected,
"{subject} must serialize as the lower-snake-case token {expected}"
);
}
assert_every_tag_list_is_the_declared_set();
assert_layout_free(&text);
assert_layout_free(&build_text);
}
/// Every written-down tag list is exactly the set its declaration serializes.
///
/// The lists stay written down, because they are what a reader compares real
/// bytes against and a set derived from the declaration alone would agree with
/// whatever the crate happens to emit. Holding each list to its own declaration
/// is what turns an upper bound into a set: an entry that stopped being a
/// variant would otherwise sit in a list nothing ever selects, admitting a tag
/// the graph can no longer emit.
fn assert_every_tag_list_is_the_declared_set() {
for (subject, path, name, written) in DECLARED_TAGS {
assert_eq!(
serialized_tags(path, name),
written
.iter()
.map(|tag| (*tag).to_owned())
.collect::<BTreeSet<String>>(),
"the {subject} this crate writes down are exactly what {name} serializes"
);
}
}
fn assert_layout_free(text: &str) {
let found: Vec<&str> = LAYOUT_VOCABULARY
.iter()
.copied()
.filter(|key| text.contains(key))
.collect();
assert!(found.is_empty(), "the wire shape is layout-free: {found:?}");
}
/// Two tiers over one snapshot change only resolution evidence.
pub fn assert_tiers_change_only_evidence() {
let (_corpus, syntactic) = fixture::resolve_corpus_library();
let promoted = promoted_resolution(&syntactic.snapshot, syntactic.resolution.report());
let evidence_delta = stated_evidence_delta(syntactic.resolution.report(), promoted.report());
let first = build_rust_graph(&syntactic.snapshot, &syntactic.resolution)
.expect("the syntactic corpus projects");
let second =
build_rust_graph(&syntactic.snapshot, &promoted).expect("the promoted report projects");
assert_eq!(first.tier(), ResolutionTier::Syntactic);
assert_eq!(second.tier(), ResolutionTier::Semantic);
assert_structure_is_tier_free((&first, &second));
assert_evidence_follows_the_reports((&first, &second), &evidence_delta);
assert_promoted_wire_shape(&second);
}
/// Which records the promoted report restates, proved to be a deliberate,
/// non-empty set.
fn stated_evidence_delta(before: &ResolutionReport, after: &ResolutionReport) -> Vec<usize> {
assert_eq!(after.tier(), ResolutionTier::Semantic);
assert_eq!(
before.resolutions().len(),
after.resolutions().len(),
"the restated report answers exactly the records the first one did"
);
let evidence_delta: Vec<usize> = before
.resolutions()
.iter()
.zip(after.resolutions())
.enumerate()
.filter(|(_, (left, right))| {
left.candidates() != right.candidates() || left.gaps() != right.gaps()
})
.map(|(index, _)| index)
.collect();
assert_eq!(
evidence_delta,
[13, 14],
"the paired report must state a deliberate, non-empty evidence delta"
);
evidence_delta
}
/// Everything but resolution evidence is identical across the two tiers.
fn assert_structure_is_tier_free(graphs: (&pedant_graph::CodeGraph, &pedant_graph::CodeGraph)) {
let (first, second) = graphs;
assert_eq!(
render::nodes(first),
render::nodes(second),
"nodes and source associations are identical across tiers"
);
assert_eq!(
render::containment(first),
render::containment(second),
"containment is identical across tiers"
);
assert_eq!(
render::reference_sites(first),
render::reference_sites(second),
"every reference keeps its id, source, language, kind, text, and span"
);
}
/// Graph evidence changes exactly where the two reports differ, and each
/// restated record keeps the gap and the certainty its report states.
fn assert_evidence_follows_the_reports(
graphs: (&pedant_graph::CodeGraph, &pedant_graph::CodeGraph),
evidence_delta: &[usize],
) {
let (first, second) = graphs;
assert_eq!(
first.references().len(),
second.references().len(),
"both tiers keep one record per stated reference"
);
let changed: Vec<usize> = first
.references()
.iter()
.zip(second.references())
.enumerate()
.filter(|(_, (left, right))| {
left.gaps() != right.gaps() || left.edges().len() != right.edges().len()
})
.map(|(index, _)| index)
.collect();
assert_eq!(
changed, evidence_delta,
"graph evidence changes exactly where the reports differ"
);
assert_eq!(
second.references()[13].gaps(),
[ResolutionGap::ConditionalCompilation],
"the promoted record keeps its stated gap"
);
assert_eq!(
second
.edge(second.references()[13].edges()[0])
.map(pedant_graph::GraphEdge::certainty),
Some(GraphCertainty::Possible),
"a downgraded candidate keeps its exact certainty"
);
assert!(
second.references()[14].edges().is_empty()
&& second.references()[14].gaps() == [ResolutionGap::MissingDefinition],
"a withdrawn candidate leaves its record and its gap in place"
);
}
/// The promoted graph's own serialized form.
///
/// The tier token and both promoted gap tokens reach the wire only here: every
/// other case serializes a syntactic graph, so a serde change spelling the tier
/// or a gap another way would ship unread.
fn assert_promoted_wire_shape(promoted: &pedant_graph::CodeGraph) {
let text = serde_json::to_string(promoted).expect("the promoted graph serializes");
let missing: Vec<&str> = PROMOTED_FRAGMENTS
.iter()
.copied()
.filter(|fragment| !text.contains(fragment))
.collect();
assert!(
missing.is_empty(),
"the promoted wire shape is missing: {missing:?}"
);
assert!(
!text.contains(r#""tier":"syntactic""#),
"a promoted graph must not carry the syntactic tier token"
);
assert_layout_free(&text);
}