use std::{
collections::{BTreeMap, BTreeSet, VecDeque},
sync::Mutex,
time::Instant,
};
use serde::Serialize;
use sha2::Digest as _;
use crate::{
error::{AppError, Result},
pack::{dependency_scope, pack_digest_sha256, Pack, ScopeDepth},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum CandidateAuthority {
SelectOnly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum ScopeDisposition {
Candidates,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub struct ScopeRequirement {
pub capability: Option<String>,
pub semantic_type: Option<String>,
}
impl ScopeRequirement {
#[must_use]
pub fn capability(value: impl Into<String>) -> Self {
Self {
capability: Some(value.into()),
semantic_type: None,
}
}
#[must_use]
pub fn semantic_type(value: impl Into<String>) -> Self {
Self {
capability: None,
semantic_type: Some(value.into()),
}
}
#[must_use]
pub fn typed_capability(
semantic_type: impl Into<String>, capability: impl Into<String>,
) -> Self {
Self {
capability: Some(capability.into()),
semantic_type: Some(semantic_type.into()),
}
}
fn validate(&self) -> Result<()> {
let capability_valid = self
.capability
.as_deref()
.is_some_and(|value| !value.trim().is_empty());
let type_valid = self
.semantic_type
.as_deref()
.is_some_and(|value| !value.trim().is_empty());
if capability_valid || type_valid {
return Ok(());
}
Err(AppError::fm_pack(
19,
"scope requirement must contain a non-empty capability and/or semantic type. \
Remediation: provide the semantic boundary being resolved.",
))
}
fn matches(&self, pack: &Pack) -> bool {
let capability_matches = self
.capability
.as_ref()
.is_none_or(|capability| pack.provides.contains(capability));
let type_matches = self
.semantic_type
.as_ref()
.is_none_or(|semantic_type| pack.semantic_types.contains(semantic_type));
capability_matches && type_matches
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ScopeResolution {
pub disposition: ScopeDisposition,
pub depth: Option<ScopeDepth>,
pub candidate_names: Vec<String>,
pub searched_depths: Vec<ScopeDepth>,
pub cache_fingerprint: String,
pub cache_hit: bool,
pub authority: CandidateAuthority,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct ScopeCacheKey {
fingerprint: String,
subject: String,
requirement: ScopeRequirement,
}
pub struct DependencyScopeResolver<'a> {
packs: &'a [Pack],
registry_revision: String,
cache: Mutex<BTreeMap<ScopeCacheKey, ScopeResolution>>,
}
impl<'a> DependencyScopeResolver<'a> {
#[must_use]
pub fn new(packs: &'a [Pack], registry_revision: impl Into<String>) -> Self {
Self {
packs,
registry_revision: registry_revision.into(),
cache: Mutex::new(BTreeMap::new()),
}
}
pub fn set_registry_revision(&mut self, registry_revision: impl Into<String>) {
self.registry_revision = registry_revision.into();
if let Ok(mut cache) = self.cache.lock() {
cache.clear();
}
}
pub fn fingerprint(&self) -> Result<String> {
let mut packs: Vec<&Pack> = self.packs.iter().collect();
packs.sort_by(|a, b| a.name.cmp(&b.name));
let mut hasher = sha2::Sha256::new();
hasher.update(b"ggen-dfcm-scope-v1\0");
hash_field(&mut hasher, self.registry_revision.as_bytes());
for pack in packs {
hash_field(&mut hasher, pack.name.as_bytes());
hash_field(&mut hasher, pack.version.as_bytes());
hash_field(&mut hasher, &pack_digest_sha256(pack)?);
for (dependency, requirement) in &pack.dependencies {
hash_field(&mut hasher, b"dependency");
hash_field(&mut hasher, dependency.as_bytes());
hash_field(&mut hasher, requirement.as_bytes());
}
for semantic_type in &pack.semantic_types {
hash_field(&mut hasher, b"type");
hash_field(&mut hasher, semantic_type.as_bytes());
}
for capability in &pack.provides {
hash_field(&mut hasher, b"provides");
hash_field(&mut hasher, capability.as_bytes());
}
for capability in &pack.requires {
hash_field(&mut hasher, b"requires");
hash_field(&mut hasher, capability.as_bytes());
}
}
Ok(format!("sha256:{}", hex::encode(hasher.finalize())))
}
pub fn resolve(
&self, subject: &str, requirement: &ScopeRequirement,
) -> Result<ScopeResolution> {
requirement.validate()?;
let fingerprint = self.fingerprint()?;
let key = ScopeCacheKey {
fingerprint: fingerprint.clone(),
subject: subject.to_string(),
requirement: requirement.clone(),
};
if let Ok(cache) = self.cache.lock() {
if let Some(hit) = cache.get(&key) {
let mut replay = hit.clone();
replay.cache_hit = true;
return Ok(replay);
}
}
let mut searched_depths = Vec::new();
for depth in [
ScopeDepth::Local,
ScopeDepth::Direct,
ScopeDepth::TwoLevel,
ScopeDepth::Global,
] {
searched_depths.push(depth);
let candidates = dependency_scope(self.packs, subject, depth)?;
let candidate_names: Vec<String> = candidates
.into_iter()
.filter(|pack| requirement.matches(pack))
.map(|pack| pack.name.clone())
.collect();
if !candidate_names.is_empty() {
let result = ScopeResolution {
disposition: ScopeDisposition::Candidates,
depth: Some(depth),
candidate_names,
searched_depths,
cache_fingerprint: fingerprint,
cache_hit: false,
authority: CandidateAuthority::SelectOnly,
};
if let Ok(mut cache) = self.cache.lock() {
cache.insert(key, result.clone());
}
return Ok(result);
}
}
let result = ScopeResolution {
disposition: ScopeDisposition::Unknown,
depth: None,
candidate_names: Vec::new(),
searched_depths,
cache_fingerprint: fingerprint,
cache_hit: false,
authority: CandidateAuthority::SelectOnly,
};
if let Ok(mut cache) = self.cache.lock() {
cache.insert(key, result.clone());
}
Ok(result)
}
}
fn hash_field(hasher: &mut sha2::Sha256, bytes: &[u8]) {
hasher.update((bytes.len() as u64).to_be_bytes());
hasher.update(bytes);
}
pub fn reverse_dependency_closure(packs: &[Pack], changed: &str) -> Result<Vec<String>> {
if !packs.iter().any(|pack| pack.name == changed) {
return Err(AppError::fm_pack(
20,
format!(
"incremental invalidation subject '{changed}' is not among the resolved packs. \
Remediation: re-resolve the project before computing reverse closure."
),
));
}
let mut reverse: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
for pack in packs {
reverse.entry(pack.name.as_str()).or_default();
for dependency in pack.dependencies.keys() {
reverse
.entry(dependency.as_str())
.or_default()
.insert(pack.name.as_str());
}
}
let mut seen = BTreeSet::new();
let mut queue = VecDeque::from([changed]);
let mut result = Vec::new();
while let Some(name) = queue.pop_front() {
if !seen.insert(name) {
continue;
}
result.push(name.to_string());
if let Some(dependents) = reverse.get(name) {
for dependent in dependents {
queue.push_back(dependent);
}
}
}
Ok(result)
}
#[must_use]
pub fn topology_turtle(packs: &[Pack]) -> String {
if !packs.iter().any(|pack| {
!pack.dependencies.is_empty()
|| !pack.semantic_types.is_empty()
|| !pack.provides.is_empty()
|| !pack.requires.is_empty()
}) {
return String::new();
}
let mut triples = BTreeSet::new();
for pack in packs {
let pack_iri = resource_iri("pack", &pack.name);
triples.insert(format!(
"<{pack_iri}> a <https://schema.org/SoftwareSourceCode> ."
));
triples.insert(format!(
"<{pack_iri}> <https://schema.org/name> {} .",
ttl_literal(&pack.name)
));
triples.insert(format!(
"<{pack_iri}> <https://schema.org/softwareVersion> {} .",
ttl_literal(&pack.version)
));
for dependency in pack.dependencies.keys() {
let dependency_iri = resource_iri("pack", dependency);
triples.insert(format!(
"<{pack_iri}> <http://purl.org/dc/terms/requires> <{dependency_iri}> ."
));
}
for semantic_type in &pack.semantic_types {
let type_iri = resource_iri("semantic-type", semantic_type);
triples.insert(format!("<{type_iri}> a <https://schema.org/DefinedTerm> ."));
triples.insert(format!(
"<{type_iri}> <https://schema.org/name> {} .",
ttl_literal(semantic_type)
));
triples.insert(format!(
"<{pack_iri}> <https://schema.org/additionalType> <{type_iri}> ."
));
}
for capability in &pack.provides {
let capability_iri = resource_iri("capability", capability);
triples.insert(format!(
"<{capability_iri}> a <https://schema.org/Action> ."
));
triples.insert(format!(
"<{capability_iri}> <https://schema.org/name> {} .",
ttl_literal(capability)
));
triples.insert(format!(
"<{pack_iri}> <https://schema.org/potentialAction> <{capability_iri}> ."
));
}
for capability in &pack.requires {
let capability_iri = resource_iri("capability", capability);
triples.insert(format!(
"<{capability_iri}> a <https://schema.org/Action> ."
));
triples.insert(format!(
"<{capability_iri}> <https://schema.org/name> {} .",
ttl_literal(capability)
));
triples.insert(format!(
"<{pack_iri}> <http://purl.org/dc/terms/requires> <{capability_iri}> ."
));
}
}
triples.into_iter().collect::<Vec<_>>().join("\n") + "\n"
}
fn resource_iri(kind: &str, value: &str) -> String {
if oxigraph::model::NamedNode::new(value).is_ok() {
return value.to_string();
}
let digest = sha2::Sha256::digest(value.as_bytes());
format!("urn:ggen:{kind}:sha256:{}", hex::encode(digest))
}
fn ttl_literal(value: &str) -> String {
let mut escaped = String::with_capacity(value.len() + 2);
escaped.push('"');
for ch in value.chars() {
match ch {
'\\' => escaped.push_str("\\\\"),
'"' => escaped.push_str("\\\""),
'\n' => escaped.push_str("\\n"),
'\r' => escaped.push_str("\\r"),
'\t' => escaped.push_str("\\t"),
other => escaped.push(other),
}
}
escaped.push('"');
escaped
}
#[derive(Debug, Clone, Serialize)]
pub struct ScopeBenchmarkRecord {
pub depth: ScopeDepth,
pub latency_ns: u128,
pub candidate_count: usize,
pub reciprocal_rank: f64,
pub top_k_hit: bool,
pub unknown: bool,
pub semantic_capacity: usize,
pub constraint_coupling_width: usize,
pub llm_tokens: u64,
pub admission_events: u64,
pub artifact_count: u64,
pub receipt_count: u64,
pub admission_success: Option<bool>,
pub artifact_success: Option<bool>,
pub receipt_success: Option<bool>,
pub authority: CandidateAuthority,
}
pub fn benchmark_dfcm_scopes(
packs: &[Pack], subject: &str, relevant: &BTreeSet<String>, top_k: usize,
) -> Result<Vec<ScopeBenchmarkRecord>> {
[
ScopeDepth::Local,
ScopeDepth::Direct,
ScopeDepth::TwoLevel,
ScopeDepth::Global,
]
.into_iter()
.map(|depth| benchmark_one_scope(packs, subject, depth, relevant, top_k))
.collect()
}
fn benchmark_one_scope(
packs: &[Pack], subject: &str, depth: ScopeDepth, relevant: &BTreeSet<String>, top_k: usize,
) -> Result<ScopeBenchmarkRecord> {
let started = Instant::now();
let candidates = dependency_scope(packs, subject, depth)?;
let latency_ns = started.elapsed().as_nanos();
let first_rank = candidates
.iter()
.position(|pack| relevant.contains(&pack.name))
.map(|index| index + 1);
let reciprocal_rank = first_rank.map_or(0.0, |rank| 1.0 / count_as_f64(rank));
let top_k_hit = first_rank.is_some_and(|rank| rank <= top_k);
let mut capabilities = BTreeSet::new();
let scope_names: BTreeSet<&str> = candidates.iter().map(|pack| pack.name.as_str()).collect();
let mut coupling_width = 0usize;
for pack in &candidates {
capabilities.extend(pack.provides.iter().cloned());
coupling_width += pack
.dependencies
.keys()
.filter(|dependency| scope_names.contains(dependency.as_str()))
.count();
coupling_width += pack.requires.len();
}
Ok(ScopeBenchmarkRecord {
depth,
latency_ns,
candidate_count: candidates.len(),
reciprocal_rank,
top_k_hit,
unknown: first_rank.is_none(),
semantic_capacity: capabilities.len(),
constraint_coupling_width: coupling_width,
llm_tokens: 0,
admission_events: 0,
artifact_count: 0,
receipt_count: 0,
admission_success: None,
artifact_success: None,
receipt_success: None,
authority: CandidateAuthority::SelectOnly,
})
}
#[must_use]
fn count_as_f64(count: usize) -> f64 {
f64::from(u32::try_from(count).unwrap_or(u32::MAX))
}
#[must_use]
pub fn unknown_rate(records: &[ScopeBenchmarkRecord]) -> f64 {
if records.is_empty() {
return 0.0;
}
count_as_f64(records.iter().filter(|record| record.unknown).count())
/ count_as_f64(records.len())
}
#[must_use]
pub fn admission_success_rate(records: &[ScopeBenchmarkRecord]) -> Option<f64> {
observed_success_rate(records.iter().filter_map(|record| record.admission_success))
}
#[must_use]
pub fn artifact_success_rate(records: &[ScopeBenchmarkRecord]) -> Option<f64> {
observed_success_rate(records.iter().filter_map(|record| record.artifact_success))
}
#[must_use]
pub fn receipt_success_rate(records: &[ScopeBenchmarkRecord]) -> Option<f64> {
observed_success_rate(records.iter().filter_map(|record| record.receipt_success))
}
fn observed_success_rate(outcomes: impl Iterator<Item = bool>) -> Option<f64> {
let values: Vec<bool> = outcomes.collect();
if values.is_empty() {
return None;
}
Some(count_as_f64(values.iter().filter(|&&value| value).count()) / count_as_f64(values.len()))
}
#[must_use]
pub fn mean_reciprocal_rank(records: &[ScopeBenchmarkRecord]) -> f64 {
if records.is_empty() {
return 0.0;
}
records
.iter()
.map(|record| record.reciprocal_rank)
.sum::<f64>()
/ count_as_f64(records.len())
}