use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::error::{ContractError, Severity, Violation};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BindingRegistry {
pub version: String,
pub target_crate: String,
#[serde(default)]
pub critical_path: Vec<String>,
#[serde(default)]
pub bindings: Vec<KernelBinding>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KernelBinding {
pub contract: String,
pub equation: String,
#[serde(default)]
pub module_path: Option<String>,
#[serde(default)]
pub function: Option<String>,
#[serde(default)]
pub signature: Option<String>,
pub status: ImplStatus,
#[serde(default)]
pub notes: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ImplStatus {
Implemented,
Partial,
NotImplemented,
Pending,
}
impl std::fmt::Display for ImplStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Implemented => "implemented",
Self::Partial => "partial",
Self::NotImplemented => "not_implemented",
Self::Pending => "pending",
};
write!(f, "{s}")
}
}
pub fn parse_binding(path: &Path) -> Result<BindingRegistry, ContractError> {
let content = std::fs::read_to_string(path)?;
parse_binding_str(&content)
}
pub fn parse_binding_str(yaml: &str) -> Result<BindingRegistry, ContractError> {
let registry: BindingRegistry = serde_yaml::from_str(yaml)?;
Ok(registry)
}
pub fn normalize_contract_id(id: &str) -> &str {
id.strip_suffix(".yaml")
.or_else(|| id.strip_suffix(".yml"))
.unwrap_or(id)
}
impl BindingRegistry {
pub fn bindings_for(&self, contract_id: &str) -> Vec<&KernelBinding> {
let needle = normalize_contract_id(contract_id);
self.bindings
.iter()
.filter(|b| normalize_contract_id(&b.contract) == needle)
.collect()
}
pub fn find_binding(&self, contract_id: &str, equation: &str) -> Option<&KernelBinding> {
let needle = normalize_contract_id(contract_id);
self.bindings
.iter()
.find(|b| normalize_contract_id(&b.contract) == needle && b.equation == equation)
}
#[must_use]
pub fn verified(&self, source_root: &Path) -> BindingRegistry {
let fn_names = collect_fn_names(source_root);
let bindings = self
.bindings
.iter()
.map(|b| {
let mut b = b.clone();
if b.status == ImplStatus::Implemented && !b.function_defined_in(&fn_names) {
b.status = ImplStatus::NotImplemented;
}
b
})
.collect();
BindingRegistry {
version: self.version.clone(),
target_crate: self.target_crate.clone(),
critical_path: self.critical_path.clone(),
bindings,
}
}
}
impl KernelBinding {
#[must_use]
pub fn function_defined_in(&self, fn_names: &std::collections::HashSet<String>) -> bool {
self.function
.as_deref()
.is_some_and(|f| fn_names.contains(f))
}
}
#[must_use]
pub fn collect_fn_names(root: &Path) -> std::collections::HashSet<String> {
let mut names = std::collections::HashSet::new();
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let skip = matches!(
path.file_name().and_then(|n| n.to_str()),
Some("target" | ".git" | ".lake" | "node_modules")
);
if !skip {
stack.push(path);
}
} else if path.extension().is_some_and(|e| e == "rs") {
if let Ok(content) = std::fs::read_to_string(&path) {
extract_fn_names(&content, &mut names);
}
}
}
}
names
}
fn extract_fn_names(content: &str, names: &mut std::collections::HashSet<String>) {
for line in content.lines() {
let mut rest = line;
while let Some(pos) = rest.find("fn ") {
let ok_boundary = pos == 0
|| rest[..pos]
.chars()
.next_back()
.is_some_and(|c| !c.is_alphanumeric() && c != '_');
let after = &rest[pos + 3..];
if ok_boundary {
let name: String = after
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if !name.is_empty() {
names.insert(name);
}
}
rest = after;
}
}
}
#[must_use]
pub fn validate_binding_registry(registry: &BindingRegistry) -> Vec<Violation> {
let mut violations = Vec::new();
let err = |rule: &str, message: String, location: String| Violation {
severity: Severity::Error,
rule: rule.to_string(),
message,
location: Some(location),
};
if registry.version.trim().is_empty() {
violations.push(err(
"BINDING-001",
"binding registry has no `version:` — every consumer of this file records \
which version of the mapping it audited against"
.to_string(),
"version".to_string(),
));
}
if registry.target_crate.trim().is_empty() {
violations.push(err(
"BINDING-002",
"binding registry has no `target_crate:` — a mapping from equations to \
functions is meaningless without saying which crate those functions live in"
.to_string(),
"target_crate".to_string(),
));
}
if registry.bindings.is_empty() {
violations.push(err(
"BINDING-003",
"binding registry declares no `bindings:` — `pv audit --binding` would \
report 0/0 coverage, which reads as clean"
.to_string(),
"bindings".to_string(),
));
}
let mut seen: std::collections::HashSet<(String, String)> = std::collections::HashSet::new();
for (i, binding) in registry.bindings.iter().enumerate() {
validate_one_binding(i, binding, &mut seen, &mut violations);
}
violations
}
fn validate_one_binding(
index: usize,
binding: &KernelBinding,
seen: &mut std::collections::HashSet<(String, String)>,
violations: &mut Vec<Violation>,
) {
let at = |field: &str| format!("bindings[{index}].{field}");
let err = |rule: &str, message: String, location: String| Violation {
severity: Severity::Error,
rule: rule.to_string(),
message,
location: Some(location),
};
if binding.contract.trim().is_empty() {
violations.push(err(
"BINDING-004",
format!(
"bindings[{index}] names no `contract:` — the entry binds an equation to nothing"
),
at("contract"),
));
}
if binding.equation.trim().is_empty() {
violations.push(err(
"BINDING-004",
format!(
"bindings[{index}] names no `equation:` — `bindings_for()` matches on the \
contract/equation pair, so an entry without one can never be found"
),
at("equation"),
));
}
let claims_implementation = matches!(
binding.status,
ImplStatus::Implemented | ImplStatus::Partial
);
let names_rust = binding.module_path.is_some() && binding.function.is_some();
let names_evidence = binding
.notes
.as_deref()
.is_some_and(|n| !n.trim().is_empty());
if claims_implementation && !names_rust && !names_evidence {
violations.push(err(
"BINDING-005",
format!(
"bindings[{index}] ({}::{}) is `status: {}` but names neither a \
`module_path:`+`function:` nor any `notes:` saying what discharges it — \
an implementation claim nobody can go and look at",
binding.contract, binding.equation, binding.status
),
at("status"),
));
}
let key = (
normalize_contract_id(&binding.contract).to_string(),
binding.equation.clone(),
);
if !seen.insert(key) {
violations.push(err(
"BINDING-006",
format!(
"duplicate binding for {}::{} — `find_binding()` returns the first match, \
so the second entry is audited by nothing and can drift unnoticed",
binding.contract, binding.equation
),
at("equation"),
));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_minimal_binding() {
let yaml = r#"
version: "1.0.0"
target_crate: aprender
bindings: []
"#;
let reg = parse_binding_str(yaml).unwrap();
assert_eq!(reg.version, "1.0.0");
assert_eq!(reg.target_crate, "aprender");
assert!(reg.bindings.is_empty());
}
#[test]
fn parse_binding_with_entries() {
let yaml = r#"
version: "1.0.0"
target_crate: aprender
bindings:
- contract: softmax-kernel-v1.yaml
equation: softmax
module_path: "aprender::nn::functional::softmax"
function: softmax
signature: "fn softmax(x: &Tensor, dim: i32) -> Tensor"
status: implemented
- contract: activation-kernel-v1.yaml
equation: silu
status: not_implemented
notes: "Not yet available"
"#;
let reg = parse_binding_str(yaml).unwrap();
assert_eq!(reg.bindings.len(), 2);
assert_eq!(reg.bindings[0].equation, "softmax");
assert_eq!(reg.bindings[0].status, ImplStatus::Implemented);
assert!(reg.bindings[0].module_path.is_some());
assert_eq!(reg.bindings[1].equation, "silu");
assert_eq!(reg.bindings[1].status, ImplStatus::NotImplemented);
assert!(reg.bindings[1].module_path.is_none());
}
#[test]
fn parse_partial_status() {
let yaml = r#"
version: "1.0.0"
target_crate: test
bindings:
- contract: test.yaml
equation: f
module_path: "test::f"
function: f
status: partial
notes: "Only scalar path"
"#;
let reg = parse_binding_str(yaml).unwrap();
assert_eq!(reg.bindings[0].status, ImplStatus::Partial);
}
#[test]
fn impl_status_display() {
assert_eq!(ImplStatus::Implemented.to_string(), "implemented");
assert_eq!(ImplStatus::Partial.to_string(), "partial");
assert_eq!(ImplStatus::NotImplemented.to_string(), "not_implemented");
assert_eq!(ImplStatus::Pending.to_string(), "pending");
}
#[test]
fn parse_invalid_binding_yaml() {
let result = parse_binding_str("not: [valid: {{");
assert!(result.is_err());
}
#[test]
fn parse_binding_from_file() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../contracts/aprender/binding.yaml");
let reg = parse_binding(&path).unwrap();
assert_eq!(reg.target_crate, "aprender");
assert!(!reg.bindings.is_empty());
}
#[test]
fn parse_binding_nonexistent_file() {
let result = parse_binding(std::path::Path::new("/nonexistent/binding.yaml"));
assert!(result.is_err());
}
#[test]
fn extract_fn_names_finds_definitions() {
let mut names = std::collections::HashSet::new();
extract_fn_names(
"pub fn to_anthropic(m: &Message) -> Value {\n async fn helper() {}\n",
&mut names,
);
assert!(names.contains("to_anthropic"));
assert!(names.contains("helper"));
}
#[test]
fn extract_fn_names_respects_word_boundary() {
let mut names = std::collections::HashSet::new();
extract_fn_names("let my_fn foo = 1;", &mut names);
assert!(!names.contains("foo"));
}
#[test]
fn function_defined_in_checks_membership() {
let names: std::collections::HashSet<String> =
["to_anthropic".to_string()].into_iter().collect();
let bound = KernelBinding {
contract: "c-v1.yaml".into(),
equation: "e".into(),
module_path: None,
function: Some("to_anthropic".into()),
signature: None,
status: ImplStatus::Implemented,
notes: None,
};
assert!(bound.function_defined_in(&names));
let missing = KernelBinding {
function: Some("does_not_exist".into()),
..bound.clone()
};
assert!(!missing.function_defined_in(&names));
let no_fn = KernelBinding {
function: None,
..bound
};
assert!(!no_fn.function_defined_in(&names));
}
#[test]
fn verified_downgrades_phantom_implemented_bindings() {
let dir = std::env::temp_dir().join(format!("bindver_{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
std::fs::write(dir.join("lib.rs"), "pub fn real_one() {}\n").unwrap();
let reg = BindingRegistry {
version: "1.0.0".into(),
target_crate: "t".into(),
critical_path: vec![],
bindings: vec![
KernelBinding {
contract: "c-v1.yaml".into(),
equation: "a".into(),
module_path: None,
function: Some("real_one".into()),
signature: None,
status: ImplStatus::Implemented,
notes: None,
},
KernelBinding {
contract: "c-v1.yaml".into(),
equation: "b".into(),
module_path: None,
function: Some("phantom".into()),
signature: None,
status: ImplStatus::Implemented,
notes: None,
},
],
};
let v = reg.verified(&dir);
assert_eq!(v.bindings[0].status, ImplStatus::Implemented);
assert_eq!(v.bindings[1].status, ImplStatus::NotImplemented);
let _ = std::fs::remove_dir_all(&dir);
}
}