use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use crate::MANIFEST_FILE;
use crate::map::parse_manifest_file;
use crate::model::*;
use crate::paths::{manifest_source_paths, parse_inline_list, resolve_path};
use crate::validate::{gate_intent, validate_manifest};
pub fn resolve_package(request: PackageRequest) -> PackageConfig {
let mut diagnostics = Vec::new();
let manifest_path = match request.manifest.clone() {
Some(path) if path.is_file() => Some(path),
Some(path) => {
diagnostics.push(EkfDiagnostic {
severity: DiagnosticSeverity::Error,
kind: "manifest_missing".into(),
message: format!("EKF manifest does not exist: {}", path.display()),
source: Some(path.display().to_string()),
});
None
}
None => find_manifest(&request),
};
let (manifest, package_root) = match manifest_path.as_deref() {
Some(path) => {
let root = manifest_package_root(path);
match parse_manifest_file(path) {
Ok((manifest, mut parse_diagnostics)) => {
diagnostics.append(&mut parse_diagnostics);
(Some(manifest), root)
}
Err(message) => {
diagnostics.push(EkfDiagnostic {
severity: DiagnosticSeverity::Error,
kind: "manifest_unreadable".into(),
message,
source: Some(path.display().to_string()),
});
(None, root)
}
}
}
None => (None, None),
};
if let Some(manifest) = &manifest {
validate_manifest(
manifest,
manifest_path.as_deref(),
package_root.as_deref(),
&mut diagnostics,
);
}
let resolved = resolve_sources(&request, manifest.as_ref(), package_root.as_deref());
let trust = manifest
.as_ref()
.map(|m| m.trust.clone())
.unwrap_or_default();
let trust_policy = resolve_trust_policy(&trust, &mut diagnostics);
let relation_profiles = resolve_relation_profiles(
manifest.as_ref().map(|manifest| &manifest.relations),
&mut diagnostics,
);
let partitions = resolve_partitions(manifest.as_ref(), package_root.as_deref());
let gates = manifest
.as_ref()
.map(|m| m.gates.clone())
.unwrap_or_default();
let gate_intents = resolve_gate_intents(&gates);
PackageConfig {
report: PackageReport {
manifest_found: manifest_path.is_some() && manifest.is_some(),
manifest_path,
package_root,
name: manifest.as_ref().and_then(|m| m.name.clone()),
resolved,
partitions,
gates,
gate_intents,
trust,
trust_policy,
relation_profiles,
diagnostics,
},
manifest,
}
}
fn manifest_package_root(path: &Path) -> Option<PathBuf> {
path.parent().map(|parent| {
if parent.as_os_str().is_empty() {
PathBuf::from(".")
} else {
parent.to_path_buf()
}
})
}
fn resolve_gate_intents(
gates: &BTreeMap<String, BTreeMap<String, String>>,
) -> BTreeMap<String, BTreeMap<String, GateIntent>> {
gates
.iter()
.filter_map(|(section, metrics)| {
let intents = metrics
.keys()
.filter_map(|metric| {
gate_intent(section, metric).map(|intent| (metric.clone(), intent))
})
.collect::<BTreeMap<_, _>>();
(!intents.is_empty()).then(|| (section.clone(), intents))
})
.collect()
}
pub(crate) fn resolve_trust_policy(
trust: &BTreeMap<String, String>,
diagnostics: &mut Vec<EkfDiagnostic>,
) -> TrustPolicy {
let mut policy = TrustPolicy::default();
if let Some(value) = trust.get("gold_ground_threshold") {
match parse_positive_u32(value) {
Some(threshold) => policy.gold_ground_threshold = threshold,
None => diagnostics.push(EkfDiagnostic {
severity: DiagnosticSeverity::Warning,
kind: "invalid_trust_policy".into(),
message: format!(
"trust.gold_ground_threshold must be a positive integer, got '{value}'"
),
source: Some("trust.gold_ground_threshold".into()),
}),
}
}
policy.stale_after_days = parse_optional_days(trust, "stale_after_days", diagnostics);
policy.agent_accepted_ttl_days =
parse_optional_days(trust, "agent_accepted_ttl_days", diagnostics);
policy
}
pub(crate) fn resolve_relation_profiles(
manifest_relations: Option<&BTreeMap<String, BTreeMap<String, String>>>,
diagnostics: &mut Vec<EkfDiagnostic>,
) -> BTreeMap<String, RelationPolicy> {
let mut profiles = default_relation_profiles();
let Some(manifest_relations) = manifest_relations else {
return profiles;
};
for (name, fields) in manifest_relations {
if name.trim().is_empty() {
diagnostics.push(EkfDiagnostic {
severity: DiagnosticSeverity::Warning,
kind: "invalid_relation_profile".into(),
message: "relations entry has an empty relation name".into(),
source: Some("relations".into()),
});
continue;
}
let mut profile = profiles.get(name).cloned().unwrap_or_else(|| {
RelationPolicy::new(
name.replace('_', " "),
format!("{} by", name.replace('_', " ")),
4,
)
});
for (field, value) in fields {
match field.as_str() {
"forward" | "forward_phrase" => profile.forward_phrase = value.clone(),
"reverse" | "reverse_phrase" => profile.reverse_phrase = value.clone(),
"context_score" => match value.parse::<i64>() {
Ok(score) if score >= 0 => profile.context_score = score,
_ => diagnostics.push(EkfDiagnostic {
severity: DiagnosticSeverity::Warning,
kind: "invalid_relation_profile".into(),
message: format!(
"relations.{name}.context_score must be a non-negative integer, got '{value}'"
),
source: Some(format!("relations.{name}.context_score")),
}),
},
"searchable" => match parse_bool(value) {
Some(searchable) => profile.searchable = searchable,
None => diagnostics.push(EkfDiagnostic {
severity: DiagnosticSeverity::Warning,
kind: "invalid_relation_profile".into(),
message: format!(
"relations.{name}.searchable must be true or false, got '{value}'"
),
source: Some(format!("relations.{name}.searchable")),
}),
},
"traversable" => match parse_bool(value) {
Some(traversable) => profile.traversable = traversable,
None => diagnostics.push(EkfDiagnostic {
severity: DiagnosticSeverity::Warning,
kind: "invalid_relation_profile".into(),
message: format!(
"relations.{name}.traversable must be true or false, got '{value}'"
),
source: Some(format!("relations.{name}.traversable")),
}),
},
"domain" | "domain_kind" | "domain_kinds" => {
profile.domain_kinds = parse_kind_list(name, field, value, diagnostics);
}
"range" | "range_kind" | "range_kinds" => {
profile.range_kinds = parse_kind_list(name, field, value, diagnostics);
}
other => diagnostics.push(EkfDiagnostic {
severity: DiagnosticSeverity::Warning,
kind: "unknown_relation_field".into(),
message: format!("unknown relation profile field '{other}' for relation '{name}'"),
source: Some(format!("relations.{name}.{other}")),
}),
}
}
profiles.insert(name.clone(), profile);
}
profiles
}
pub(crate) fn default_relation_profiles() -> BTreeMap<String, RelationPolicy> {
[
(
"depends_on",
RelationPolicy::new("depends on", "required by", 30),
),
(
"produces",
RelationPolicy::new("produces", "produced by", 30),
),
(
"consumes",
RelationPolicy::new("consumes", "consumed by", 28),
),
(
"supports",
RelationPolicy::new("supports", "supported by", 34),
),
("asserts", RelationPolicy::new("asserts", "asserted by", 34)),
(
"qualifies",
RelationPolicy::new("qualifies", "qualified by", 24),
),
(
"requires_contract",
RelationPolicy::new("requires contract", "required contract for", 22),
),
("uses_tool", RelationPolicy::new("uses tool", "used by", 22)),
(
"dispatches",
RelationPolicy::new("dispatches", "dispatched by", 22),
),
(
"related_to",
RelationPolicy::new("related to", "related from", 18),
),
]
.into_iter()
.map(|(name, profile)| (name.to_string(), profile))
.collect()
}
pub(crate) fn parse_bool(value: &str) -> Option<bool> {
match value.trim().to_ascii_lowercase().as_str() {
"true" | "yes" | "1" => Some(true),
"false" | "no" | "0" => Some(false),
_ => None,
}
}
pub(crate) fn parse_kind_list(
relation: &str,
field: &str,
value: &str,
diagnostics: &mut Vec<EkfDiagnostic>,
) -> Vec<String> {
let mut kinds = Vec::new();
for raw in parse_inline_list(value) {
let kind = raw.trim().to_ascii_lowercase();
if kind.is_empty() {
continue;
}
if is_supported_kind_label(&kind) {
if !kinds.contains(&kind) {
kinds.push(kind);
}
} else {
diagnostics.push(EkfDiagnostic {
severity: DiagnosticSeverity::Warning,
kind: "invalid_relation_profile".into(),
message: format!("relations.{relation}.{field} contains unsupported kind '{raw}'"),
source: Some(format!("relations.{relation}.{field}")),
});
}
}
kinds
}
pub(crate) fn is_supported_kind_label(kind: &str) -> bool {
matches!(
kind,
"doc"
| "skill"
| "agent"
| "section"
| "function"
| "type"
| "trait"
| "module"
| "unknown"
)
}
pub(crate) fn parse_optional_days(
trust: &BTreeMap<String, String>,
key: &str,
diagnostics: &mut Vec<EkfDiagnostic>,
) -> Option<u64> {
let value = trust.get(key)?;
match value.parse::<u64>() {
Ok(days) if days > 0 => Some(days),
_ => {
diagnostics.push(EkfDiagnostic {
severity: DiagnosticSeverity::Warning,
kind: "invalid_trust_policy".into(),
message: format!("trust.{key} must be a positive integer day count, got '{value}'"),
source: Some(format!("trust.{key}")),
});
None
}
}
}
pub(crate) fn parse_positive_u32(value: &str) -> Option<u32> {
match value.parse::<u32>() {
Ok(value) if value > 0 => Some(value),
_ => None,
}
}
pub(crate) fn resolve_sources(
request: &PackageRequest,
manifest: Option<&Manifest>,
root: Option<&Path>,
) -> ResolvedSources {
let inferred_root = default_project_root(request, manifest, root);
let vault = request
.vault
.clone()
.or_else(|| root.map(Path::to_path_buf))
.or_else(|| inferred_root.clone());
let docs = request
.docs
.clone()
.or_else(|| {
manifest
.and_then(|m| m.sources.docs.first())
.and_then(|p| root.map(|root| resolve_path(root, Path::new(p))))
})
.or_else(|| {
manifest
.and_then(first_partition_root("aun"))
.and_then(|p| root.map(|root| resolve_path(root, Path::new(p))))
})
.or_else(|| inferred_root.as_deref().and_then(default_docs_root));
let code = request
.code
.clone()
.or_else(|| {
manifest
.and_then(|m| {
m.sources
.code
.iter()
.find(|code| !code.path.trim().is_empty())
})
.and_then(|code| root.map(|root| resolve_path(root, Path::new(&code.path))))
})
.or_else(|| {
manifest
.and_then(first_partition_root("glyph"))
.and_then(|p| root.map(|root| resolve_path(root, Path::new(p))))
})
.or_else(|| inferred_root.clone());
let source_roots = resolved_source_roots(request, manifest, root, &vault, &docs, &code);
ResolvedSources {
vault,
docs,
code,
source_roots,
}
}
pub(crate) fn default_project_root(
request: &PackageRequest,
manifest: Option<&Manifest>,
root: Option<&Path>,
) -> Option<PathBuf> {
if manifest.is_some()
|| root.is_some()
|| request.vault.is_some()
|| request.docs.is_some()
|| request.code.is_some()
{
return None;
}
request
.start_dir
.clone()
.or_else(|| std::env::current_dir().ok())
.map(|path| {
if path.is_file() {
path.parent().map(Path::to_path_buf).unwrap_or(path)
} else {
path
}
})
}
pub(crate) fn default_docs_root(root: &Path) -> Option<PathBuf> {
let docs = root.join("docs");
docs.is_dir().then_some(docs)
}
pub(crate) fn resolved_source_roots(
request: &PackageRequest,
manifest: Option<&Manifest>,
root: Option<&Path>,
vault: &Option<PathBuf>,
docs: &Option<PathBuf>,
code: &Option<PathBuf>,
) -> Vec<ResolvedSourceRoot> {
let mut roots = Vec::new();
if let Some(path) = vault {
push_resolved_root(
&mut roots,
"vault",
path.clone(),
None,
"compiled",
Vec::new(),
);
}
if let Some(path) = docs {
push_resolved_root(
&mut roots,
"docs",
path.clone(),
None,
"compiled",
Vec::new(),
);
}
if let Some(path) = code {
push_resolved_root(
&mut roots,
"code",
path.clone(),
None,
"compiled",
Vec::new(),
);
}
let Some((manifest, root)) = manifest.zip(root) else {
return roots;
};
for (kind, path, languages) in manifest_source_paths(manifest) {
if request_overrides_kind(request, &kind) {
continue;
}
push_resolved_root(
&mut roots,
kind.clone(),
resolve_path(root, Path::new(&path)),
None,
source_root_status(&kind),
languages,
);
}
for partition in &manifest.partitions {
let kind = partition_source_root_kind(partition);
if request_overrides_kind(request, &kind) {
continue;
}
for path in &partition.roots {
push_resolved_root(
&mut roots,
kind.clone(),
resolve_path(root, Path::new(path)),
Some(partition.id.clone()),
partition_status(partition),
partition.languages.clone(),
);
}
}
roots
}
pub(crate) fn first_partition_root(source: &str) -> impl FnOnce(&Manifest) -> Option<&String> + '_ {
move |manifest| {
manifest
.partitions
.iter()
.find(|partition| partition.source == source)
.and_then(|partition| partition.roots.first())
}
}
pub(crate) fn resolve_partitions(
manifest: Option<&Manifest>,
root: Option<&Path>,
) -> Vec<ResolvedPartition> {
let Some(manifest) = manifest else {
return Vec::new();
};
manifest
.partitions
.iter()
.map(|partition| ResolvedPartition {
id: partition.id.clone(),
title: partition.title.clone(),
source: partition.source.clone(),
role: partition.role.clone(),
roots: match root {
Some(root) => partition
.roots
.iter()
.map(|path| resolve_path(root, Path::new(path)))
.collect(),
None => partition.roots.iter().map(PathBuf::from).collect(),
},
status: partition_status(partition).to_string(),
languages: partition.languages.clone(),
tags: partition.tags.clone(),
})
.collect()
}
pub(crate) fn push_resolved_root(
roots: &mut Vec<ResolvedSourceRoot>,
kind: impl Into<String>,
path: PathBuf,
partition: Option<String>,
status: impl Into<String>,
languages: Vec<String>,
) {
let kind = kind.into();
let status = status.into();
if let Some(existing) = roots
.iter_mut()
.find(|root| root.kind == kind && root.path == path)
{
if existing.partition.is_none() && partition.is_some() {
existing.partition = partition;
}
if existing.languages.is_empty() && !languages.is_empty() {
existing.languages = languages;
}
return;
}
roots.push(ResolvedSourceRoot {
kind,
path,
partition,
status,
languages,
});
}
pub(crate) fn request_overrides_kind(request: &PackageRequest, kind: &str) -> bool {
match kind {
"docs" => request.docs.is_some(),
"code" => request.code.is_some(),
_ => false,
}
}
pub(crate) fn source_root_status(kind: &str) -> &'static str {
match kind {
"docs" | "code" | "skills" | "agents" | "capabilities" => "compiled",
"brief_profiles" | "workflows" => "planning",
"evals" => "eval",
_ => "declared",
}
}
pub(crate) fn partition_source_root_kind(partition: &PartitionManifest) -> String {
match partition.source.as_str() {
"glyph" => "code".into(),
"aether" => match partition.role.as_deref() {
Some("agents") | Some("agent") => "agents".into(),
Some("capabilities") | Some("capability") => "capabilities".into(),
_ => "vault".into(),
},
"eval" => "evals".into(),
"brief_profile" => "brief_profiles".into(),
"aun" => match partition.role.as_deref() {
Some("skills") | Some("skill") => "skills".into(),
_ => "docs".into(),
},
_ => "declared".into(),
}
}
pub(crate) fn partition_status(partition: &PartitionManifest) -> &'static str {
match partition.source.as_str() {
"eval" => "eval",
"brief_profile" => "planning",
"aun" | "glyph" | "aether" => "compiled",
_ => "declared",
}
}
pub(crate) fn find_manifest(request: &PackageRequest) -> Option<PathBuf> {
let mut starts = Vec::new();
starts.extend(request.vault.clone());
starts.extend(request.docs.clone());
starts.extend(request.code.clone());
if starts.is_empty()
&& let Some(start) = request.start_dir.clone()
{
starts.push(start);
} else if starts.is_empty()
&& let Ok(cwd) = std::env::current_dir()
{
starts.push(cwd);
}
let mut seen = BTreeSet::new();
for start in starts {
let start = if start.is_file() {
start.parent().map(Path::to_path_buf).unwrap_or(start)
} else {
start
};
if !seen.insert(start.clone()) {
continue;
}
let mut cur = Some(start.as_path());
while let Some(dir) = cur {
let candidate = dir.join(MANIFEST_FILE);
if candidate.is_file() {
return Some(candidate);
}
cur = dir.parent();
}
}
None
}