pub mod resolve;
pub mod stale;
use std::collections::{BTreeMap, HashMap, VecDeque};
use crate::diagnostic::DiagnosticCollector;
use crate::lock::ConfigEntryRecord;
use crate::sync::AppliedState;
use crate::types::{MarsContext, SourceName};
pub(crate) fn compile_config_entries(
ctx: &MarsContext,
applied: &AppliedState,
dry_run: bool,
diag: &mut DiagnosticCollector,
) -> BTreeMap<String, BTreeMap<String, ConfigEntryRecord>> {
use crate::compiler::config_entries::resolve::{
resolve_hook_collisions_for_target, resolve_mcp_collisions_for_target,
};
use crate::compiler::hooks::{discover_hook_items, order_hooks, translate_hooks_for_target};
use crate::compiler::mcp::{TargetMcpEntry, check_env_refs, discover_mcp_items};
use crate::target::{ConfigEntry, HookEntry, McpServerEntry, TargetRegistry};
let graph = &applied.planned.targeted.resolved.graph;
let effective = &applied.planned.targeted.resolved.loaded.effective;
let target_roots: Vec<String> = effective.settings.managed_targets();
let depths = compute_depths(graph);
let decl_orders = compute_decl_orders(graph, &effective.dependencies);
let mut all_mcp: Vec<crate::compiler::mcp::ParsedMcpItem> = Vec::new();
let mut all_hooks: Vec<crate::compiler::hooks::ParsedHookItem> = Vec::new();
let local_mcp = match discover_mcp_items(&ctx.project_root, "_self", 0) {
Ok(items) => items,
Err(e) => {
diag.warn(
"mcp-discover",
format!("failed to scan local MCP items: {e}"),
);
Vec::new()
}
};
all_mcp.extend(local_mcp);
let local_hooks = match discover_hook_items(&ctx.project_root, "_self", 0, 0) {
Ok(items) => items,
Err(e) => {
diag.warn(
"hook-discover",
format!("failed to scan local hook items: {e}"),
);
Vec::new()
}
};
all_hooks.extend(local_hooks);
for source_name in &graph.order {
let Some(node) = graph.nodes.get(source_name) else {
continue;
};
let package_root = &node.rooted_ref.package_root;
let decl_order = decl_orders
.get(source_name)
.copied()
.unwrap_or(effective.dependencies.len() + graph.order.len() + 1);
match discover_mcp_items(package_root, source_name.as_str(), decl_order) {
Ok(items) => all_mcp.extend(items),
Err(e) => {
diag.warn(
"mcp-discover",
format!("failed to scan MCP items in `{source_name}`: {e}"),
);
}
}
let depth = depths.get(source_name).copied().unwrap_or(1);
match discover_hook_items(package_root, source_name.as_str(), depth, decl_order) {
Ok(items) => all_hooks.extend(items),
Err(e) => {
diag.warn(
"hook-discover",
format!("failed to scan hook items in `{source_name}`: {e}"),
);
}
}
}
{
use crate::compiler::visibility::{can_cross_package_boundary, resolve_visibility};
use crate::lock::ItemKind;
all_mcp.retain(|item| {
if item.source_name == "_self" {
return true;
}
let explicit = match item.def.visibility.as_str() {
"exported" => Some(true),
"local" => Some(false),
_ => None, };
let vis = resolve_visibility(ItemKind::McpServer, &item.name, explicit);
if !can_cross_package_boundary(&vis) {
return false;
}
true
});
all_hooks.retain(|item| {
if item.source_name == "_self" {
return true;
}
let explicit = match item.def.visibility.as_str() {
"exported" => Some(true),
"local" => Some(false),
_ => None,
};
let vis = resolve_visibility(ItemKind::Hook, &item.def.name, explicit);
can_cross_package_boundary(&vis)
});
}
if let Err(e) = check_env_refs(&all_mcp, false, diag) {
diag.warn("mcp-env", format!("MCP env check failed: {e}"));
}
let registry = TargetRegistry::new();
let mut current_records: BTreeMap<String, BTreeMap<String, ConfigEntryRecord>> =
BTreeMap::new();
for target_root in &target_roots {
let target_dir = ctx.project_root.join(target_root);
let mut entries_with_source: Vec<(ConfigEntry, String)> = Vec::new();
for parsed in resolve_mcp_collisions_for_target(&all_mcp, target_root, diag) {
let source = parsed.source_name.clone();
let e = TargetMcpEntry::from_parsed(parsed);
entries_with_source.push((
ConfigEntry::McpServer(McpServerEntry {
name: e.name,
command: e.command,
args: e.args,
env: e.env.into_iter().collect(),
}),
source,
));
}
let target_hooks: Vec<_> =
resolve_hook_collisions_for_target(&all_hooks, target_root, diag)
.into_iter()
.cloned()
.collect();
let translated_hooks = translate_hooks_for_target(order_hooks(target_hooks), target_root);
for th in &translated_hooks {
match th.lossiness {
crate::compiler::hooks::LossinessKind::Dropped => {
diag.warn(
"hook-dropped",
format!(
"hook `{}` (event `{}`) dropped for target `{target_root}` — \
no native hook support",
th.hook.item.def.name, th.hook.item.def.event
),
);
}
crate::compiler::hooks::LossinessKind::Approximate => {
diag.info(
"hook-approximate",
format!(
"hook `{}` (event `{}`) approximately mapped for target \
`{target_root}` — semantics may differ slightly",
th.hook.item.def.name, th.hook.item.def.event
),
);
}
crate::compiler::hooks::LossinessKind::Exact => {}
}
}
let hook_entries: Vec<(ConfigEntry, String)> = translated_hooks
.into_iter()
.filter_map(|th| {
let native_event = th.native_event?;
let source = th.hook.item.source_name.clone();
let script_path = match &th.hook.item.def.action {
crate::compiler::hooks::HookAction::Script { path } => {
let resolved = th.hook
.item
.package_root
.join("hooks")
.join(&th.hook.item.def.name)
.join(path);
if resolved.strip_prefix(&th.hook.item.package_root).is_err() {
diag.warn(
"hook-path-escape",
format!(
"hook `{}`: script path `{path}` escapes package root — skipped",
th.hook.item.def.name
),
);
return None;
}
resolved.to_string_lossy().to_string()
}
};
Some((
ConfigEntry::Hook(HookEntry {
name: th.hook.item.def.name.clone(),
event: th.hook.item.def.event.to_string(),
native_event,
script_path,
order: th.hook.item.def.order,
}),
source,
))
})
.collect();
entries_with_source.extend(hook_entries);
if entries_with_source.is_empty() {
continue;
}
let Some(adapter) = registry.get(target_root) else {
continue;
};
let entries: Vec<ConfigEntry> = entries_with_source
.iter()
.map(|(entry, _)| entry.clone())
.collect();
let mut target_records = BTreeMap::new();
for (entry, source) in &entries_with_source {
target_records.insert(
entry.key(),
ConfigEntryRecord {
source: source.clone(),
},
);
}
adapter.emit_pre_write_diagnostics(&entries, diag);
if dry_run {
current_records.insert(target_root.clone(), target_records);
} else {
match adapter.write_config_entries(&entries, &target_dir) {
Ok(_) => {
current_records.insert(target_root.clone(), target_records);
}
Err(e) => {
diag.warn(
"config-entry-write",
format!("failed to write config entries to `{target_root}`: {e}"),
);
}
}
}
}
let previous_records = &applied
.planned
.targeted
.resolved
.loaded
.old_lock
.config_entries;
let stale_entries = stale::find_stale_entries(previous_records, ¤t_records);
for (target_root, keys) in stale_entries {
if dry_run {
diag.warn(
"stale-config-entry",
format!(
"target `{target_root}` has stale config entries: {}",
keys.join(", ")
),
);
continue;
}
let Some(adapter) = registry.get(&target_root) else {
continue;
};
let target_dir = ctx.project_root.join(&target_root);
if let Err(e) = adapter.remove_config_entries(&keys, &target_dir) {
diag.warn(
"config-entry-remove",
format!("failed to remove stale config entries from `{target_root}`: {e}"),
);
if let Some(previous_target_records) = previous_records.get(&target_root) {
let target_records = current_records.entry(target_root.clone()).or_default();
for key in &keys {
if let Some(record) = previous_target_records.get(key) {
target_records.insert(key.clone(), record.clone());
}
}
}
} else {
diag.info(
"stale-config-entry",
format!(
"removed stale config entries from `{target_root}`: {}",
keys.join(", ")
),
);
}
}
current_records
}
fn compute_depths(graph: &crate::resolve::ResolvedGraph) -> HashMap<SourceName, usize> {
let mut in_degree: HashMap<SourceName, usize> = HashMap::new();
for name in graph.nodes.keys() {
in_degree.insert(name.clone(), 0);
}
for node in graph.nodes.values() {
for dep in &node.deps {
if graph.nodes.contains_key(dep) {
*in_degree.entry(dep.clone()).or_insert(0) += 1;
}
}
}
let mut depths: HashMap<SourceName, usize> = HashMap::new();
let mut queue: VecDeque<SourceName> = VecDeque::new();
for (name, degree) in &in_degree {
if *degree == 0 {
depths.insert(name.clone(), 1);
queue.push_back(name.clone());
}
}
while let Some(current) = queue.pop_front() {
let current_depth = depths[¤t];
if let Some(node) = graph.nodes.get(¤t) {
for dep in &node.deps {
if graph.nodes.contains_key(dep) {
depths
.entry(dep.clone())
.and_modify(|d| *d = (*d).max(current_depth + 1))
.or_insert_with(|| {
queue.push_back(dep.clone());
current_depth + 1
});
}
}
}
}
depths
}
fn compute_decl_orders(
graph: &crate::resolve::ResolvedGraph,
dependencies: &indexmap::IndexMap<SourceName, crate::config::EffectiveDependency>,
) -> HashMap<SourceName, usize> {
let mut orders: HashMap<SourceName, usize> = HashMap::new();
let mut queue: VecDeque<SourceName> = VecDeque::new();
for (idx, source_name) in dependencies.keys().enumerate() {
if graph.nodes.contains_key(source_name) {
orders.insert(source_name.clone(), idx + 1);
queue.push_back(source_name.clone());
}
}
while let Some(current) = queue.pop_front() {
let current_order = orders[¤t];
let Some(node) = graph.nodes.get(¤t) else {
continue;
};
for dep in &node.deps {
if !graph.nodes.contains_key(dep) {
continue;
}
match orders.get_mut(dep) {
Some(existing) if current_order < *existing => {
*existing = current_order;
queue.push_back(dep.clone());
}
Some(_) => {}
None => {
orders.insert(dep.clone(), current_order);
queue.push_back(dep.clone());
}
}
}
}
orders
}