use std::{
collections::{BTreeMap, BTreeSet},
fs,
path::PathBuf,
};
use super::{
ArchError,
model::{Architecture, Finding, Package, Role},
};
fn finding(
package: &Package,
code: &'static str,
explanation: String,
guidance: &'static str,
) -> Finding {
Finding {
code,
manifest: package.manifest.clone(),
subject: package.name.clone(),
explanation,
guidance,
}
}
pub(super) fn validate(architecture: &mut Architecture) -> Result<(), ArchError> {
let template = include_str!("../../template/Cargo.toml.in");
let (_, lint_source) = template
.split_once("[workspace.lints.rust]")
.ok_or(ArchError::Capacity)?;
let template_lints: toml::Value =
toml::from_str(&format!("[workspace.lints.rust]{lint_source}"))
.map_err(|error| ArchError::Manifest(PathBuf::from("embedded starter lints"), error))?;
let required_lints = template_lints
.get("workspace")
.and_then(|workspace| workspace.get("lints"))
.ok_or(ArchError::Capacity)?;
let mut findings = Vec::new();
let mut sources = BTreeMap::new();
let mut adjacency: BTreeMap<PathBuf, Vec<PathBuf>> = BTreeMap::new();
for package in architecture.packages.values() {
let Some(declaration) = &package.declaration else {
findings.push(finding(package, "arch.role.missing", "Project crate has no architectural role.".to_owned(), "Use a canonical location or declare package.metadata.pipecircuit in its Cargo manifest."));
continue;
};
if declaration.role.pure() {
if let Some(path) = &package.library {
if !sources.contains_key(path) {
let text = fs::read_to_string(path)
.map_err(|error| ArchError::Read(path.clone(), error))?;
let syntax = syn::parse_file(&text)
.map_err(|error| ArchError::Rust(path.clone(), error))?;
let no_std = syntax.attrs.iter().any(|attribute| {
attribute.path().is_ident("no_std")
&& matches!(attribute.style, syn::AttrStyle::Inner(_))
});
sources.insert(path.clone(), no_std);
}
if sources.get(path) != Some(&true) {
findings.push(finding(package, "arch.purity.no-std", format!("Pure library {} has no unconditional #![no_std] declaration.", path.display()), "Keep semantic libraries no_std. Compile supported targets separately to verify feature and dependency compatibility."));
}
} else {
findings.push(finding(
package,
"arch.purity.library",
"A pure role requires a Rust library target.".to_owned(),
"Expose the semantic code as a no_std library.",
));
}
let local = package.document.get("lints");
let inherited = local
.and_then(|lints| lints.get("workspace"))
.and_then(toml::Value::as_bool)
== Some(true);
let lints = if inherited {
Some(package.workspace_lints.as_ref())
} else {
local
};
for (family, rules) in required_lints.as_table().into_iter().flatten() {
for (rule, expected) in rules.as_table().into_iter().flatten() {
let value = lints
.and_then(|lints| lints.get(family))
.and_then(|rules| rules.get(rule));
let expected_level = lint_level(expected);
let expected_label = expected_level.ok_or_else(|| {
ArchError::Configuration(
PathBuf::from("embedded starter lints"),
format!("Missing lint level for {family}.{rule}"),
)
})?;
let actual_level = value.and_then(lint_level);
let strong_enough = actual_level == expected_level
|| actual_level == Some("forbid")
|| (expected_level == Some("warn") && actual_level == Some("deny"));
let configuration_matches = expected.get("check-cfg").is_none_or(|required| {
value.and_then(|value| value.get("check-cfg")) == Some(required)
});
if !strong_enough || !configuration_matches {
findings.push(finding(package, "arch.purity.lint", format!("{family}.{rule} must retain the canonical {expected_label} level and check-cfg configuration."), "Inherit the canonical workspace lints or declare equivalent strictness."));
}
}
}
}
let edges = adjacency.entry(package.manifest.clone()).or_default();
for dependency in package
.dependencies
.iter()
.filter(|dependency| dependency.kind != "dev")
{
architecture.work.dependency_edges_checked = architecture
.work
.dependency_edges_checked
.checked_add(1)
.ok_or(ArchError::Capacity)?;
let target = dependency.path.as_ref().map(|path| path.join("Cargo.toml"));
if let Some(target) = target
.as_ref()
.and_then(|target| architecture.packages.get(target))
{
edges.push(target.manifest.clone());
if target
.declaration
.as_ref()
.is_some_and(|target| !declaration.allows(target))
{
findings.push(finding(package, "arch.dependency.outward", format!("{} → {} crosses the permitted role/application boundary (kind: {}, target: {}, optional: {}).", package.name, target.name, dependency.kind, dependency.target.as_deref().unwrap_or("all"), dependency.optional), "Move shared semantic types inward; keep physical implementations in wiring and routing in the owning application."));
}
} else if target
.as_ref()
.is_some_and(|target| target.starts_with(&architecture.root))
{
findings.push(finding(package, "arch.dependency.unclassified", format!("Local dependency {} is outside the inspected workspaces.", dependency.name), "List its workspace manifest in workspace.metadata.pipecircuit.workspaces; local project dependencies cannot silently escape analysis."));
} else if matches!(declaration.role, Role::Pipes | Role::Switchboard) {
let alias = dependency.alias.as_deref().unwrap_or(&dependency.name);
let forwarded = package
.document
.get("features")
.and_then(toml::Value::as_table)
.into_iter()
.flat_map(|features| features.values())
.filter_map(toml::Value::as_array)
.flatten()
.filter_map(toml::Value::as_str)
.filter_map(|feature| feature.split_once('/'))
.filter(|(name, _)| name.trim_end_matches('?') == alias)
.map(|(_, feature)| feature);
let permitted = forwarded.into_iter().all(semantic_feature)
&& dependency.name == "pipecircuit"
&& !dependency.default_features
&& dependency
.features
.iter()
.all(|feature| semantic_feature(feature));
if !permitted {
findings.push(finding(package, "arch.dependency.external", format!("Semantic boundary depends on {} with an unsupported external capability configuration.", dependency.name), "Keep application contracts and routing dependent on core and no_std Pipecircuit capabilities. Put reusable domain dependencies in core and physical capabilities in wiring."));
}
}
}
}
for package in architecture.packages.values() {
let Some(declaration) = &package.declaration else {
continue;
};
for forbidden in &declaration.forbid_dependencies {
let candidates: Vec<_> = architecture
.packages
.values()
.filter(|target| &target.name == forbidden)
.collect();
if candidates.len() != 1 {
findings.push(finding(package, "arch.isolation.target", format!("Dependency restriction target {forbidden} does not identify exactly one inspected package."), "Name an inspected package; stale or ambiguous isolation rules are errors."));
continue;
}
let mut pending = vec![&package.manifest];
let mut visited = BTreeSet::new();
while let Some(path) = pending.pop() {
if !visited.insert(path) {
continue;
}
if architecture
.packages
.get(path)
.is_some_and(|target| &target.name == forbidden)
{
findings.push(finding(package, "arch.isolation.reachable", format!("{} can reach forbidden execution dependency {forbidden}.", package.name), "Cross the worker boundary through its message contract instead of linking its execution implementation."));
break;
}
if let Some(edges) = adjacency.get(path) {
pending.extend(edges);
}
}
}
}
architecture.work.sources_read = sources.len();
architecture.findings.extend(findings);
architecture.findings.sort_by(|left, right| {
(&left.manifest, left.code, &left.explanation).cmp(&(
&right.manifest,
right.code,
&right.explanation,
))
});
Ok(())
}
fn lint_level(value: &toml::Value) -> Option<&str> {
value
.as_str()
.or_else(|| value.get("level").and_then(toml::Value::as_str))
}
fn semantic_feature(feature: &str) -> bool {
matches!(feature, "programs" | "secondary-indexes" | "serde")
}