use std::collections::BTreeMap;
use super::range::{ReqKind, Requirement};
use crate::release::discovered::{CargoMetadata, DiscoveredCrate};
use crate::release::error::Diagnostic;
use crate::release::graph::CrateGraph;
use crate::release::metadata::CrateMetadata;
pub(crate) fn validate_ranges(
document: &CargoMetadata,
crates: &[DiscoveredCrate],
graph: &CrateGraph,
) -> Vec<Diagnostic> {
let mut findings: Vec<Diagnostic> = Vec::new();
let unit_of: BTreeMap<&str, &str> = crates
.iter()
.filter_map(|c| match &c.metadata {
CrateMetadata::Valid(md) if md.publish => {
md.release_unit.as_deref().map(|u| (c.name.as_str(), u))
}
_ => None,
})
.collect();
let deps_by_pkg: BTreeMap<&str, &Vec<crate::release::discovered::Dependency>> = document
.packages
.iter()
.map(|p| (p.name.as_str(), &p.dependencies))
.collect();
for (crate_name, node) in &graph.nodes {
let crate_unit = unit_of.get(crate_name.as_str()).copied().unwrap_or("");
let deps = match deps_by_pkg.get(crate_name.as_str()) {
Some(d) => *d,
None => continue,
};
for dep in deps.iter() {
let dep_name = dep.rename.as_ref().unwrap_or(&dep.name);
if !node.dependencies.contains_key(dep_name) {
continue;
}
let dep_kind = &node.dependencies[dep_name];
let dep_unit = unit_of.get(dep_name.as_str()).copied().unwrap_or("");
let raw = match dep.req.as_deref() {
None | Some("*") => continue,
Some(r) => r,
};
let req = match Requirement::parse(raw) {
Ok(r) => r,
Err(e) => {
findings.push(Diagnostic {
crate_name: crate_name.clone(),
message: format!("dependency {dep_name}: {e}",),
});
continue;
}
};
if req.kind == ReqKind::Caret {
findings.push(Diagnostic {
crate_name: crate_name.clone(),
message: format!(
"dependency {dep_name} uses caret {raw:?}; \
caret admits a breaking generation within the year — \
use tilde \"~YEAR.BREAK.0\" for cross-unit or exact \
\"=V\" inside core",
),
});
continue;
}
let same_unit = crate_unit == dep_unit && crate_unit == "core";
if same_unit && req.kind != ReqKind::Exact {
findings.push(Diagnostic {
crate_name: crate_name.clone(),
message: format!(
"core-internal dependency {dep_name} uses {kind} {raw:?}; \
core members must use exact \"=V\" so a published \
facade resolves the matching proc-macro",
kind = kind_name(req.kind),
),
});
continue;
}
let cross_unit = crate_unit != dep_unit;
if cross_unit && req.kind == ReqKind::Exact {
findings.push(Diagnostic {
crate_name: crate_name.clone(),
message: format!(
"cross-unit dependency {dep_name} uses exact {raw:?}; \
exact couples independent units — use tilde \
\"~YEAR.BREAK.0\" for compatible cross-unit deps",
),
});
continue;
}
let _ = dep_kind; }
}
findings.sort_by(|a, b| {
a.crate_name
.cmp(&b.crate_name)
.then(a.message.cmp(&b.message))
});
findings
}
fn kind_name(kind: ReqKind) -> &'static str {
match kind {
ReqKind::Tilde => "tilde",
ReqKind::Exact => "exact",
ReqKind::Caret => "caret",
ReqKind::Wildcard => "wildcard",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::release::discovered::{Dependency, Package, Target};
use crate::release::graph::build_crate_graph;
use crate::release::metadata::ReleaseMetadata;
use crate::release::metadata::Role;
fn package(
name: &str,
metadata: Option<&str>,
deps: &[(&str, Option<&str>, &str, Option<&str>)],
) -> Package {
Package {
name: name.to_string(),
version: None,
manifest_path: format!("/repo/crates/{name}/Cargo.toml"),
publish: None,
metadata: metadata.map(|m| serde_json::from_str(m).unwrap()),
targets: vec![Target {
kind: vec!["lib".to_string()],
}],
dependencies: deps
.iter()
.map(|(n, kind, req, path)| Dependency {
name: (*n).to_string(),
kind: kind.map(|s| s.to_string()),
optional: Some(false),
req: Some((*req).to_string()),
path: path.map(|s| s.to_string()),
rename: None,
})
.collect(),
}
}
fn discovered(name: &str, unit: &str) -> DiscoveredCrate {
DiscoveredCrate {
name: name.to_string(),
manifest_path: std::path::PathBuf::from(format!("/repo/crates/{name}/Cargo.toml")),
publishable: true,
proc_macro: name == "arcature-dx",
metadata: CrateMetadata::Valid(ReleaseMetadata {
publish: true,
role: Some(if name == "arcature-dx" {
Role::ProcMacro
} else if unit == "core" {
Role::Facade
} else {
Role::Subsystem
}),
release_unit: Some(unit.to_string()),
}),
}
}
#[test]
fn tilde_cross_unit_is_valid() {
let doc = CargoMetadata {
packages: vec![
package(
"arcature-auth",
Some(
r#"{"arcature":{"publish":true,"role":"subsystem","release-unit":"arcature-auth"}}"#,
),
&[],
),
package(
"arcature",
Some(r#"{"arcature":{"publish":true,"role":"facade","release-unit":"core"}}"#),
&[("arcature-auth", None, "~2026.1.0", Some("../arcature-auth"))],
),
],
};
let crates = vec![
discovered("arcature-auth", "arcature-auth"),
discovered("arcature", "core"),
];
let graph = build_crate_graph(&doc, &crates);
let findings = validate_ranges(&doc, &crates, &graph);
assert!(findings.is_empty(), "{findings:?}");
}
#[test]
fn exact_core_internal_is_valid() {
let doc = CargoMetadata {
packages: vec![
package(
"arcature",
Some(r#"{"arcature":{"publish":true,"role":"facade","release-unit":"core"}}"#),
&[("arcature-dx", None, "=2026.1.0", Some("../arcature-dx"))],
),
package(
"arcature-dx",
Some(
r#"{"arcature":{"publish":true,"role":"proc-macro","release-unit":"core"}}"#,
),
&[],
),
],
};
let crates = vec![
discovered("arcature", "core"),
discovered("arcature-dx", "core"),
];
let graph = build_crate_graph(&doc, &crates);
let findings = validate_ranges(&doc, &crates, &graph);
assert!(findings.is_empty(), "{findings:?}");
}
#[test]
fn caret_is_forbidden() {
let doc = CargoMetadata {
packages: vec![
package(
"arcature-auth",
Some(
r#"{"arcature":{"publish":true,"role":"subsystem","release-unit":"arcature-auth"}}"#,
),
&[],
),
package(
"arcature-api",
Some(
r#"{"arcature":{"publish":true,"role":"subsystem","release-unit":"arcature-api"}}"#,
),
&[("arcature-auth", None, "^2026.1.0", None)],
),
],
};
let crates = vec![
discovered("arcature-auth", "arcature-auth"),
discovered("arcature-api", "arcature-api"),
];
let graph = build_crate_graph(&doc, &crates);
let findings = validate_ranges(&doc, &crates, &graph);
assert_eq!(findings.len(), 1);
assert!(findings[0].message.contains("caret"), "{:?}", findings[0]);
}
#[test]
fn bare_version_is_forbidden_caret() {
let doc = CargoMetadata {
packages: vec![
package(
"arcature-auth",
Some(
r#"{"arcature":{"publish":true,"role":"subsystem","release-unit":"arcature-auth"}}"#,
),
&[],
),
package(
"arcature-api",
Some(
r#"{"arcature":{"publish":true,"role":"subsystem","release-unit":"arcature-api"}}"#,
),
&[("arcature-auth", None, "2026.1.0", None)],
),
],
};
let crates = vec![
discovered("arcature-auth", "arcature-auth"),
discovered("arcature-api", "arcature-api"),
];
let graph = build_crate_graph(&doc, &crates);
let findings = validate_ranges(&doc, &crates, &graph);
assert_eq!(findings.len(), 1);
assert!(findings[0].message.contains("caret"));
}
#[test]
fn core_internal_tilde_is_error() {
let doc = CargoMetadata {
packages: vec![
package(
"arcature",
Some(r#"{"arcature":{"publish":true,"role":"facade","release-unit":"core"}}"#),
&[("arcature-dx", None, "~2026.1.0", Some("../arcature-dx"))],
),
package(
"arcature-dx",
Some(
r#"{"arcature":{"publish":true,"role":"proc-macro","release-unit":"core"}}"#,
),
&[],
),
],
};
let crates = vec![
discovered("arcature", "core"),
discovered("arcature-dx", "core"),
];
let graph = build_crate_graph(&doc, &crates);
let findings = validate_ranges(&doc, &crates, &graph);
assert_eq!(findings.len(), 1);
assert!(findings[0].message.contains("exact"), "{:?}", findings[0]);
}
#[test]
fn cross_unit_exact_is_error() {
let doc = CargoMetadata {
packages: vec![
package(
"arcature-auth",
Some(
r#"{"arcature":{"publish":true,"role":"subsystem","release-unit":"arcature-auth"}}"#,
),
&[],
),
package(
"arcature-api",
Some(
r#"{"arcature":{"publish":true,"role":"subsystem","release-unit":"arcature-api"}}"#,
),
&[("arcature-auth", None, "=2026.1.0", None)],
),
],
};
let crates = vec![
discovered("arcature-auth", "arcature-auth"),
discovered("arcature-api", "arcature-api"),
];
let graph = build_crate_graph(&doc, &crates);
let findings = validate_ranges(&doc, &crates, &graph);
assert_eq!(findings.len(), 1);
assert!(
findings[0].message.contains("cross-unit"),
"{:?}",
findings[0]
);
}
#[test]
fn wildcard_and_path_only_are_skipped() {
let doc = CargoMetadata {
packages: vec![
package(
"arcature-auth",
Some(
r#"{"arcature":{"publish":true,"role":"subsystem","release-unit":"arcature-auth"}}"#,
),
&[],
),
package(
"arcature-test",
Some(
r#"{"arcature":{"publish":true,"role":"subsystem","release-unit":"arcature-test"}}"#,
),
&[("arcature-auth", Some("dev"), "*", Some("../arcature-auth"))],
),
],
};
let crates = vec![
discovered("arcature-auth", "arcature-auth"),
discovered("arcature-test", "arcature-test"),
];
let graph = build_crate_graph(&doc, &crates);
let findings = validate_ranges(&doc, &crates, &graph);
assert!(findings.is_empty(), "{findings:?}");
}
#[test]
fn unparseable_requirement_is_diagnostic() {
let doc = CargoMetadata {
packages: vec![
package(
"arcature-auth",
Some(
r#"{"arcature":{"publish":true,"role":"subsystem","release-unit":"arcature-auth"}}"#,
),
&[],
),
package(
"arcature-api",
Some(
r#"{"arcature":{"publish":true,"role":"subsystem","release-unit":"arcature-api"}}"#,
),
&[("arcature-auth", None, "garbage", None)],
),
],
};
let crates = vec![
discovered("arcature-auth", "arcature-auth"),
discovered("arcature-api", "arcature-api"),
];
let graph = build_crate_graph(&doc, &crates);
let findings = validate_ranges(&doc, &crates, &graph);
assert_eq!(findings.len(), 1);
assert!(findings[0].message.contains("invalid requirement"));
}
#[test]
fn versioned_dev_dep_with_tilde_is_valid() {
let doc = CargoMetadata {
packages: vec![
package(
"arcature-auth",
Some(
r#"{"arcature":{"publish":true,"role":"subsystem","release-unit":"arcature-auth"}}"#,
),
&[],
),
package(
"arcature-test",
Some(
r#"{"arcature":{"publish":true,"role":"subsystem","release-unit":"arcature-test"}}"#,
),
&[(
"arcature-auth",
Some("dev"),
"~2026.1.0",
Some("../arcature-auth"),
)],
),
],
};
let crates = vec![
discovered("arcature-auth", "arcature-auth"),
discovered("arcature-test", "arcature-test"),
];
let graph = build_crate_graph(&doc, &crates);
let findings = validate_ranges(&doc, &crates, &graph);
assert!(findings.is_empty(), "{findings:?}");
}
}