use super::{
base_entity_rule_allows, endpoint_matches, resolved_pair, EdgeEndpointKind, EdgeEndpointRule,
EdgeRelation, EndpointKind, Resolved, RuntimeError, RuntimeResult, Uuid,
BASE_ENTITY_ENDPOINT_RULES,
};
fn pattern_endpoint_matches(
spec: &EndpointKind,
substrate: &str,
kind: &str,
entity_type: Option<&str>,
) -> bool {
match spec {
EndpointKind::EntityOfType {
kind: k,
entity_type: t,
} => substrate == "entity" && *k == kind && entity_type.is_none_or(|et| et == *t),
_ => endpoint_matches(spec, substrate, kind, entity_type),
}
}
pub fn accepted_pack_relations_for_entities(
rules: &[EdgeEndpointRule],
src_kind: &str,
src_entity_type: Option<&str>,
tgt_kind: &str,
tgt_entity_type: Option<&str>,
) -> Vec<EdgeRelation> {
let mut relations: Vec<EdgeRelation> = rules
.iter()
.filter(|r| {
endpoint_matches(&r.source, "entity", src_kind, src_entity_type)
&& endpoint_matches(&r.target, "entity", tgt_kind, tgt_entity_type)
})
.map(|r| r.relation)
.collect();
relations.sort_by_key(|r| r.as_str());
relations.dedup();
relations
}
pub fn accepted_entity_relations_for_entities(
rules: &[EdgeEndpointRule],
src_kind: &str,
src_entity_type: Option<&str>,
tgt_kind: &str,
tgt_entity_type: Option<&str>,
) -> Vec<EdgeRelation> {
let mut relations: Vec<EdgeRelation> = BASE_ENTITY_ENDPOINT_RULES
.iter()
.filter(|(src, _relation, tgt)| (*src == "*" || *src == src_kind) && *tgt == tgt_kind)
.map(|(_src, relation, _tgt)| *relation)
.collect();
relations.extend(
accepted_pack_relations_for_entities(
rules,
src_kind,
src_entity_type,
tgt_kind,
tgt_entity_type,
)
.into_iter()
.filter(|relation| {
*relation != EdgeRelation::Annotates && !crate::pack::is_special_relation(*relation)
}),
);
relations.sort_by_key(|relation| relation.as_str());
relations.dedup();
relations
}
pub(super) fn accepted_entity_relations_description(
rules: &[EdgeEndpointRule],
src_kind: &str,
src_entity_type: Option<&str>,
tgt_kind: &str,
tgt_entity_type: Option<&str>,
) -> String {
let relations = accepted_entity_relations_for_entities(
rules,
src_kind,
src_entity_type,
tgt_kind,
tgt_entity_type,
);
if relations.is_empty() {
"none".to_string()
} else {
relations
.iter()
.map(EdgeRelation::as_str)
.collect::<Vec<_>>()
.join(", ")
}
}
fn accepted_pack_relations_for_pattern_entities(
rules: &[EdgeEndpointRule],
src_kind: &str,
src_entity_type: Option<&str>,
tgt_kind: &str,
tgt_entity_type: Option<&str>,
) -> Vec<EdgeRelation> {
let mut relations: Vec<EdgeRelation> = rules
.iter()
.filter(|r| {
pattern_endpoint_matches(&r.source, "entity", src_kind, src_entity_type)
&& pattern_endpoint_matches(&r.target, "entity", tgt_kind, tgt_entity_type)
})
.map(|r| r.relation)
.collect();
relations.sort_by_key(|r| r.as_str());
relations.dedup();
relations
}
fn accepted_entity_kind_pairs_for_relation(
pack_rules: &[EdgeEndpointRule],
relation: EdgeRelation,
) -> Vec<(&'static str, &'static str)> {
let mut pairs = Vec::new();
for src in khive_types::EntityKind::ALL {
for tgt in khive_types::EntityKind::ALL {
let allowed = base_entity_rule_allows(src.name(), relation, tgt.name())
|| (!crate::pack::is_special_relation(relation)
&& accepted_pack_relations_for_pattern_entities(
pack_rules,
src.name(),
None,
tgt.name(),
None,
)
.contains(&relation));
if allowed {
pairs.push((src.name(), tgt.name()));
}
}
}
pairs
}
pub(super) fn static_impossible_edge_pattern_warnings(
language: khive_query::QueryLanguage,
pattern: &khive_query::ast::MatchPattern,
pack_rules: &[EdgeEndpointRule],
) -> Vec<String> {
use khive_query::ast::{EdgeDirection, PatternElement};
if language != khive_query::QueryLanguage::Gql {
return Vec::new();
}
let elements = &pattern.elements;
let mut warnings = Vec::new();
for (i, el) in elements.iter().enumerate() {
let PatternElement::Edge(edge) = el else {
continue;
};
if edge.relations.len() != 1 || edge.min_hops != 1 || edge.max_hops != 1 {
continue;
}
let (left, right) = match (elements.get(i.wrapping_sub(1)), elements.get(i + 1)) {
(Some(PatternElement::Node(l)), Some(PatternElement::Node(r))) => (l, r),
_ => continue,
};
let (src_node, tgt_node) = match edge.direction {
EdgeDirection::Out => (left, right),
EdgeDirection::In => (right, left),
EdgeDirection::Both => continue,
};
let (Some(src_raw), Some(tgt_raw)) = (src_node.kind.as_deref(), tgt_node.kind.as_deref())
else {
continue;
};
let (Ok(src_kind), Ok(tgt_kind)) = (
src_raw.parse::<khive_types::EntityKind>(),
tgt_raw.parse::<khive_types::EntityKind>(),
) else {
continue;
};
let Ok(relation) = edge.relations[0].parse::<EdgeRelation>() else {
continue;
};
let possible = base_entity_rule_allows(src_kind.name(), relation, tgt_kind.name())
|| (!crate::pack::is_special_relation(relation)
&& accepted_pack_relations_for_pattern_entities(
pack_rules,
src_kind.name(),
src_node.entity_type.as_deref(),
tgt_kind.name(),
tgt_node.entity_type.as_deref(),
)
.contains(&relation));
if possible {
continue;
}
let accepted = accepted_entity_kind_pairs_for_relation(pack_rules, relation);
let accepted_str = if accepted.is_empty() {
"none".to_string()
} else {
accepted
.iter()
.map(|(s, t)| format!("{s}->{t}"))
.collect::<Vec<_>>()
.join(", ")
};
warnings.push(format!(
"pattern ({src})-[:{relation}]->({tgt}) can never match: '{relation}' does not accept \
{src}->{tgt} endpoints; accepted source->target kinds for '{relation}': {accepted_str}",
src = src_kind.name(),
tgt = tgt_kind.name(),
));
}
warnings
}
pub(super) fn pack_rule_allows(
rules: &[EdgeEndpointRule],
relation: EdgeRelation,
src: Option<&Resolved>,
tgt: Option<&Resolved>,
) -> bool {
let Some((src_sub, src_kind, src_type)) = resolved_pair(src) else {
return false;
};
let Some((tgt_sub, tgt_kind, tgt_type)) = resolved_pair(tgt) else {
return false;
};
rules.iter().any(|r| {
r.relation == relation
&& endpoint_matches(&r.source, src_sub, src_kind, src_type)
&& endpoint_matches(&r.target, tgt_sub, tgt_kind, tgt_type)
})
}
pub(crate) fn canonical_edge_endpoints(
relation: EdgeRelation,
source_id: Uuid,
target_id: Uuid,
) -> (Uuid, Uuid) {
relation.canonical_endpoints(source_id, target_id)
}
pub(crate) fn canonical_edge_endpoint_kinds(
requested_source_id: Uuid,
canonical_source_id: Uuid,
source_kind: EdgeEndpointKind,
target_kind: EdgeEndpointKind,
) -> (EdgeEndpointKind, EdgeEndpointKind) {
if requested_source_id == canonical_source_id {
(source_kind, target_kind)
} else {
(target_kind, source_kind)
}
}
pub(crate) fn infer_dependency_kind(src_kind: &str, tgt_kind: &str) -> Option<&'static str> {
match (src_kind, tgt_kind) {
("project", "project") => Some("build"),
("service", "service") => Some("runtime"),
("service", "dataset") => Some("data"),
("service", "artifact") => Some("artifact"),
("artifact", "project") | ("artifact", "service") => Some("tooling"),
("document", "document") => Some("normative"),
_ => None,
}
}
pub(crate) fn merge_dependency_kind(
src_kind: &str,
tgt_kind: &str,
metadata: Option<serde_json::Value>,
) -> Option<serde_json::Value> {
let metadata = metadata.filter(|value| !value.is_null());
if let Some(ref m) = metadata {
if m.get("dependency_kind").is_some() {
return metadata;
}
}
let Some(inferred) = infer_dependency_kind(src_kind, tgt_kind) else {
return metadata;
};
let mut obj = metadata.unwrap_or_else(|| serde_json::json!({}));
if let Some(o) = obj.as_object_mut() {
o.insert("dependency_kind".to_string(), serde_json::json!(inferred));
}
Some(obj)
}
pub fn merge_entry_metadata(
metadata: Option<serde_json::Value>,
dependency_kind: Option<String>,
) -> RuntimeResult<Option<serde_json::Value>> {
validate_metadata_shape(metadata.as_ref())?;
let metadata = metadata.filter(|value| !value.is_null());
let Some(dk) = dependency_kind else {
return Ok(metadata);
};
let mut obj = metadata.unwrap_or_else(|| serde_json::json!({}));
let map = obj
.as_object_mut()
.ok_or_else(|| RuntimeError::InvalidInput("metadata must be a JSON object".into()))?;
map.entry("dependency_kind".to_string())
.or_insert_with(|| serde_json::json!(dk));
Ok(Some(obj))
}
const VALID_DEPENDENCY_KINDS: &[&str] = &[
"build",
"runtime",
"data",
"artifact",
"tooling",
"normative",
];
pub(crate) fn validate_edge_weight(weight: f64) -> RuntimeResult<()> {
if !khive_types::validate_edge_weight(weight) {
return Err(RuntimeError::InvalidInput(format!(
"edge weight must be finite and in [0.0, 1.0], got {weight}"
)));
}
Ok(())
}
fn validate_metadata_shape(metadata: Option<&serde_json::Value>) -> RuntimeResult<()> {
if metadata.is_some_and(|value| !value.is_null() && !value.is_object()) {
return Err(RuntimeError::InvalidInput(
"metadata must be a JSON object".into(),
));
}
Ok(())
}
pub(crate) fn validate_edge_metadata(
relation: EdgeRelation,
metadata: Option<&serde_json::Value>,
) -> RuntimeResult<()> {
validate_metadata_shape(metadata)?;
let Some(meta) = metadata.filter(|value| !value.is_null()) else {
return Ok(());
};
let object = meta.as_object().expect("validated metadata object");
if object
.get("optional")
.is_some_and(|value| !value.is_boolean())
{
return Err(RuntimeError::InvalidInput(
"metadata.optional must be a boolean".into(),
));
}
if let Some(dk) = meta.get("dependency_kind") {
if relation != EdgeRelation::DependsOn {
return Err(RuntimeError::InvalidInput(format!(
"dependency_kind is only valid on depends_on edges (got {})",
relation.as_str()
)));
}
let dk_str = dk
.as_str()
.ok_or_else(|| RuntimeError::InvalidInput("dependency_kind must be a string".into()))?;
if !VALID_DEPENDENCY_KINDS.contains(&dk_str) {
return Err(RuntimeError::InvalidInput(format!(
"unknown dependency_kind {dk_str:?}; valid: {}",
VALID_DEPENDENCY_KINDS.join(" | ")
)));
}
}
Ok(())
}