use crate::agent_mode::{self, CallEdge, OrchestrationFacts, ParserDecision, SizeTiming};
use crate::package_manifest::{
self, PackageDiagnostic, PackageManifest, PackageSemanticBinding, PackageSemanticImport,
PackageSourceIdentity, PackageSymbolIndexEntry,
};
use crate::parser_registry::ParserCli;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct GraphReportSelection {
pub imports: bool,
pub capabilities: bool,
pub symbols: bool,
pub calls: bool,
}
impl GraphReportSelection {
#[must_use]
pub const fn all() -> Self {
Self {
imports: true,
capabilities: true,
symbols: true,
calls: true,
}
}
fn has_flags(self) -> bool {
self.imports || self.capabilities || self.symbols || self.calls
}
fn include_imports(self) -> bool {
!self.has_flags() || self.imports
}
fn include_capabilities(self) -> bool {
!self.has_flags() || self.capabilities
}
fn include_symbols(self) -> bool {
!self.has_flags() || self.symbols
}
fn include_calls(self) -> bool {
!self.has_flags() || self.calls
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GraphSymbol {
pub kind: String,
pub name: String,
pub detail: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GraphReport {
pub schema_version: u32,
pub parser_decision: ParserDecision,
pub package_identity: Option<PackageSourceIdentity>,
pub semantic_imports: Vec<PackageSemanticImport>,
pub semantic_bindings: Vec<PackageSemanticBinding>,
pub package_symbol_index: Vec<PackageSymbolIndexEntry>,
pub package_diagnostics: Vec<PackageDiagnostic>,
pub imports: Vec<String>,
pub effects: Vec<String>,
pub capabilities: Vec<String>,
pub symbols: Vec<GraphSymbol>,
pub call_edges: Vec<CallEdge>,
pub entry_function: Option<String>,
pub orchestration: OrchestrationFacts,
pub timing: SizeTiming,
}
pub fn build_graph_report(
cwd: &Path,
path: &str,
module_id: &str,
parser: ParserCli,
selection: GraphReportSelection,
) -> GraphReport {
let report = agent_mode::analyze_path(cwd, path, module_id, parser);
let source_path = resolve_report_path(cwd, path);
let package_manifest = load_manifest_for_graph_source(&source_path);
let package_identity =
package_identity_for_graph_source(&source_path, package_manifest.as_ref());
let semantic_imports =
semantic_imports_for_graph_source(&source_path, package_manifest.as_ref());
let semantic_bindings = semantic_bindings_for_graph_source(&source_path, &semantic_imports);
let mut package_symbol_index =
package_manifest::symbol_index_for_semantic_imports(&semantic_imports);
package_symbol_index.extend(package_manifest::symbol_index_for_semantic_bindings(
&semantic_bindings,
));
let package_diagnostics = package_manifest::diagnostics_for_semantic_imports(&semantic_imports);
let mut imports = Vec::new();
let mut effects = Vec::new();
if selection.include_imports() {
imports = report
.effects
.iter()
.filter_map(|effect| effect.strip_prefix("import:").map(str::to_string))
.collect();
imports.sort();
effects = report.effects.clone();
effects.sort();
}
let mut capabilities = Vec::new();
if selection.include_capabilities() {
capabilities = report.capabilities.clone();
capabilities.sort();
}
let mut symbols = Vec::new();
if selection.include_symbols() {
if let Some(summary) = &report.core_ir_summary {
symbols.extend(summary.structs.iter().map(|item| {
GraphSymbol {
kind: "struct".to_string(),
name: item.name.clone(),
detail: item
.fields
.iter()
.map(|field| format!("{}: {}", field.name, field.typ))
.collect::<Vec<_>>()
.join(", "),
}
}));
symbols.extend(summary.functions.iter().map(|item| GraphSymbol {
kind: "function".to_string(),
name: item.name.clone(),
detail: format!(
"fn({}) -> {}",
item.params
.iter()
.map(|param| param.typ.clone())
.collect::<Vec<_>>()
.join(", "),
item.return_type
),
}));
}
symbols.extend(package_symbol_index.iter().map(|item| GraphSymbol {
kind: item.kind.clone(),
name: item.name.clone(),
detail: item.source_import.clone(),
}));
symbols.sort_by(|left, right| {
left.kind
.cmp(&right.kind)
.then_with(|| left.name.cmp(&right.name))
.then_with(|| left.detail.cmp(&right.detail))
});
}
let (mut call_edges, entry_function) = if let Some(graph) = report.graph_facts {
let call_edges = if selection.include_calls() {
graph.call_edges
} else {
Vec::new()
};
(call_edges, graph.entry_function)
} else {
(Vec::new(), None)
};
call_edges.sort_by(|left, right| {
left.caller
.cmp(&right.caller)
.then_with(|| left.callee.cmp(&right.callee))
});
GraphReport {
schema_version: 1,
parser_decision: report.parser_decision,
package_identity,
semantic_imports,
semantic_bindings,
package_symbol_index,
package_diagnostics,
imports,
effects,
capabilities,
symbols,
call_edges,
entry_function,
orchestration: report.orchestration,
timing: report.size_timing,
}
}
fn resolve_report_path(cwd: &Path, path: &str) -> std::path::PathBuf {
let path = Path::new(path);
if path.is_absolute() {
path.to_path_buf()
} else {
cwd.join(path)
}
}
fn load_manifest_for_graph_source(source_path: &Path) -> Option<PackageManifest> {
if source_path.extension().and_then(|ext| ext.to_str()) != Some("in") {
return None;
}
package_manifest::load_package_manifest_from_source(source_path)
.ok()
.map(|(_, manifest)| manifest)
}
fn package_identity_for_graph_source(
source_path: &Path,
manifest: Option<&PackageManifest>,
) -> Option<PackageSourceIdentity> {
if source_path.extension().and_then(|ext| ext.to_str()) != Some("in") {
return None;
}
Some(package_manifest::source_identity_for_path(
source_path,
manifest.map(|manifest| manifest.name.as_str()),
))
}
fn semantic_imports_for_graph_source(
source_path: &Path,
manifest: Option<&PackageManifest>,
) -> Vec<PackageSemanticImport> {
package_manifest::semantic_imports_for_source_path(source_path, manifest).unwrap_or_default()
}
fn semantic_bindings_for_graph_source(
source_path: &Path,
imports: &[PackageSemanticImport],
) -> Vec<PackageSemanticBinding> {
if source_path.extension().and_then(|ext| ext.to_str()) != Some("in") {
return Vec::new();
}
let Ok(source) = std::fs::read_to_string(source_path) else {
return Vec::new();
};
let Ok(surface) = crate::in_lang_parse::parse_in_surface_info(&source) else {
return Vec::new();
};
package_manifest::resolve_semantic_bindings(&surface.semantic_bindings, imports)
}
pub fn graph_report_to_json(report: &GraphReport) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(report)
}
pub fn graph_report_text(report: &GraphReport, selection: GraphReportSelection) -> String {
let mut lines = Vec::new();
lines.push(format!(
"parser: {} via {}",
report
.parser_decision
.parser_id
.as_deref()
.unwrap_or("swift"),
report.parser_decision.route
));
lines.push(format!(
"entry: {}",
report.entry_function.as_deref().unwrap_or("none")
));
if let Some(identity) = &report.package_identity {
lines.push(format!(
"package_identity: {} ({})",
identity.status, identity.reason
));
}
if !report.semantic_imports.is_empty() {
lines.push(format!(
"semantic_imports: {}",
report
.semantic_imports
.iter()
.map(|import| format!("{} {} ({})", import.import, import.status, import.reason))
.collect::<Vec<_>>()
.join(", ")
));
}
if !report.package_symbol_index.is_empty() {
lines.push(format!(
"package_symbol_index: {}",
report
.package_symbol_index
.iter()
.map(|symbol| format!("{} {}", symbol.id, symbol.source_import))
.collect::<Vec<_>>()
.join(", ")
));
}
if !report.package_diagnostics.is_empty() {
lines.push(format!(
"package_diagnostics: {}",
report
.package_diagnostics
.iter()
.map(|diagnostic| format!(
"{} {} ({})",
diagnostic.code, diagnostic.import, diagnostic.reason
))
.collect::<Vec<_>>()
.join(", ")
));
}
if selection.include_imports() {
lines.push(format!("imports: {}", join_or_none(&report.imports)));
lines.push(format!("effects: {}", join_or_none(&report.effects)));
}
if selection.include_capabilities() {
lines.push(format!(
"capabilities: {}",
join_or_none(&report.capabilities)
));
}
if selection.include_symbols() {
lines.push(format!(
"symbols: {}",
if report.symbols.is_empty() {
"none".to_string()
} else {
report
.symbols
.iter()
.map(|symbol| format!("{} {} {}", symbol.kind, symbol.name, symbol.detail))
.collect::<Vec<_>>()
.join("; ")
}
));
}
if selection.include_calls() {
lines.push(format!(
"calls: {}",
if report.call_edges.is_empty() {
"none".to_string()
} else {
report
.call_edges
.iter()
.map(|edge| format!("{} -> {}", edge.caller, edge.callee))
.collect::<Vec<_>>()
.join(", ")
}
));
}
lines.push(format!(
"timing: parse={}us lower={}us graph={}us total={}us",
report.timing.parse_micros,
report.timing.lower_micros,
report.timing.graph_micros,
report.timing.total_micros
));
lines.join("\n")
}
fn join_or_none(values: &[String]) -> String {
if values.is_empty() {
"none".to_string()
} else {
values.join(", ")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser_registry::ParserCli;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
struct TempFile {
path: PathBuf,
}
struct TempDir {
path: PathBuf,
}
impl Drop for TempFile {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}
fn temp_dir(name: &str) -> TempDir {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before UNIX_EPOCH")
.as_nanos();
let path = std::env::temp_dir().join(format!(
"inauguration-graph-report-{}-{unique}-{name}",
std::process::id()
));
fs::create_dir_all(&path).expect("create temp dir");
TempDir { path }
}
fn temp_source(name: &str, source: &str) -> TempFile {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before UNIX_EPOCH")
.as_nanos();
let path = std::env::temp_dir().join(format!(
"inauguration-graph-report-{}-{unique}-{name}.in",
std::process::id()
));
fs::write(&path, source).expect("write temp source");
TempFile { path }
}
#[test]
fn report_includes_imports_capabilities_symbols_and_calls_by_default() {
let temp = temp_source(
"default",
r#"
import std.io;
capability process.stdout;
fn ready() -> void { return; }
fn main() -> void { print("ok"); ready(); return; }
"#,
);
let path = temp.path.to_string_lossy().to_string();
let report = build_graph_report(
Path::new("."),
&path,
"App",
ParserCli::Auto,
GraphReportSelection::default(),
);
assert_eq!(report.parser_decision.parser_id.as_deref(), Some("in"));
assert!(report.imports.contains(&"std.io".to_string()));
assert!(report.effects.contains(&"import:std.io".to_string()));
assert!(
report
.effects
.contains(&"extern:std:print:requires=process.stdout".to_string())
);
assert!(report.capabilities.contains(&"process.stdout".to_string()));
assert!(report.symbols.iter().any(|symbol| symbol.name == "main"));
assert!(report.call_edges.contains(&CallEdge {
caller: "main".to_string(),
callee: "ready".to_string()
}));
assert!(report.call_edges.contains(&CallEdge {
caller: "main".to_string(),
callee: "print".to_string()
}));
assert_eq!(report.entry_function.as_deref(), Some("main"));
}
#[test]
fn report_includes_package_identity_status() {
let temp = temp_dir("package-identity");
fs::write(
temp.path.join("inauguration.package"),
"name: agents.sample\nversion: 0.1.0\n",
)
.expect("write manifest");
let source_path = temp.path.join("main.in");
fs::write(
&source_path,
"package agents.sample;\nmodule agents.sample.main;\nfn main() -> void { return; }\n",
)
.expect("write source");
let path = source_path.to_string_lossy().to_string();
let report = build_graph_report(
Path::new("."),
&path,
"App",
ParserCli::Auto,
GraphReportSelection::default(),
);
let identity = report.package_identity.expect("package identity");
assert_eq!(identity.package.as_deref(), Some("agents.sample"));
assert_eq!(identity.module.as_deref(), Some("agents.sample.main"));
assert_eq!(identity.manifest_name.as_deref(), Some("agents.sample"));
assert_eq!(identity.status, "match");
assert_eq!(identity.reason, "package-module-match");
}
#[test]
fn report_resolves_semantic_imports_through_package_graph() {
let temp = temp_dir("semantic-imports");
fs::write(
temp.path.join("inauguration.package"),
r#"name: hyperchat
version: 0.1.0
dependencies:
postgres:
version: ^1.0.0
"#,
)
.expect("write manifest");
let source_path = temp.path.join("main.in");
fs::write(
&source_path,
"package hyperchat;\nuse database.postgres;\nbind database.postgres as postgres;\nfn main() -> void { return; }\n",
)
.expect("write source");
let path = source_path.to_string_lossy().to_string();
let report = build_graph_report(
Path::new("."),
&path,
"App",
ParserCli::Auto,
GraphReportSelection::default(),
);
assert_eq!(report.semantic_imports.len(), 1);
assert_eq!(report.semantic_imports[0].import, "database.postgres");
assert_eq!(
report.semantic_imports[0].dependency.as_deref(),
Some("postgres")
);
assert_eq!(report.semantic_imports[0].status, "resolved");
assert_eq!(report.semantic_bindings.len(), 1);
assert_eq!(report.semantic_bindings[0].alias, "postgres");
assert_eq!(report.semantic_bindings[0].status, "resolved");
assert_eq!(report.package_symbol_index.len(), 2);
assert_eq!(
report.package_symbol_index[0].id,
"symbol:dependency:postgres"
);
assert_eq!(
report.package_symbol_index[0].source_import,
"database.postgres"
);
assert!(
report
.symbols
.iter()
.any(|symbol| symbol.kind == "dependency"
&& symbol.name == "postgres"
&& symbol.detail == "database.postgres")
);
assert!(report.symbols.iter().any(|symbol| symbol.kind == "binding"
&& symbol.name == "postgres"
&& symbol.detail == "database.postgres"));
assert!(report.package_diagnostics.is_empty());
}
#[test]
fn report_warns_for_unresolved_semantic_imports() {
let temp = temp_dir("unresolved-semantic-imports");
fs::write(
temp.path.join("inauguration.package"),
"name: hyperchat\nversion: 0.1.0\n",
)
.expect("write manifest");
let source_path = temp.path.join("main.in");
fs::write(
&source_path,
"package hyperchat;\nuse database.postgres;\nfn main() -> void { return; }\n",
)
.expect("write source");
let path = source_path.to_string_lossy().to_string();
let report = build_graph_report(
Path::new("."),
&path,
"App",
ParserCli::Auto,
GraphReportSelection::default(),
);
assert!(report.package_symbol_index.is_empty());
assert_eq!(report.package_diagnostics.len(), 1);
assert_eq!(report.package_diagnostics[0].code, "INPKG001");
assert_eq!(report.package_diagnostics[0].severity, "warning");
assert_eq!(
report.package_diagnostics[0].reason,
"dependency-not-declared"
);
}
#[test]
fn report_selection_limits_sections() {
let temp = temp_source(
"selection",
r#"
import std.io;
capability process.stdout;
fn main() -> void { print("ok"); return; }
"#,
);
let path = temp.path.to_string_lossy().to_string();
let report = build_graph_report(
Path::new("."),
&path,
"App",
ParserCli::Auto,
GraphReportSelection {
imports: false,
capabilities: true,
symbols: false,
calls: false,
},
);
assert!(report.imports.is_empty());
assert!(report.effects.is_empty());
assert_eq!(report.capabilities, vec!["process.stdout".to_string()]);
assert!(report.symbols.is_empty());
assert!(report.call_edges.is_empty());
}
#[test]
fn json_and_text_helpers_are_stable_enough_for_parent_cli() {
let temp = temp_source(
"helpers",
r#"
import std.io;
capability process.stdout;
fn main() -> void { print("ok"); return; }
"#,
);
let path = temp.path.to_string_lossy().to_string();
let report = build_graph_report(
Path::new("."),
&path,
"App",
ParserCli::Auto,
GraphReportSelection::default(),
);
let json = graph_report_to_json(&report).expect("json report");
assert!(json.contains("\"parser_decision\""));
assert!(json.contains("\"imports\""));
assert!(json.contains("\"capabilities\""));
assert!(json.contains("\"call_edges\""));
assert!(json.contains("\"orchestration\""));
assert!(json.contains("\"timing\""));
let text = graph_report_text(&report, GraphReportSelection::default());
assert!(text.contains("parser: in via core_ir"));
assert!(text.contains("imports: std.io"));
assert!(text.contains("capabilities: process.stdout"));
assert!(text.contains("calls: main -> print"));
}
#[test]
fn text_reports_selected_empty_sections() {
let temp = temp_source("empty-calls", "fn main() -> void { return; }\n");
let path = temp.path.to_string_lossy().to_string();
let selection = GraphReportSelection {
imports: false,
capabilities: false,
symbols: false,
calls: true,
};
let report = build_graph_report(Path::new("."), &path, "App", ParserCli::Auto, selection);
let text = graph_report_text(&report, selection);
assert!(text.contains("calls: none"));
assert!(!text.contains("imports:"));
assert!(!text.contains("capabilities:"));
assert!(!text.contains("symbols:"));
}
#[test]
fn graph_json_carries_orchestration_status_facts() {
let temp = temp_source(
"orchestration",
r#"
enable distributed-workers;
@gpu
distributed fn process_video(video: Video) -> void { return; }
parallel { process_video(video()); }
struct Video { Int id }
fn main() -> void { return; }
"#,
);
let path = temp.path.to_string_lossy().to_string();
let report = build_graph_report(
Path::new("."),
&path,
"App",
ParserCli::Auto,
GraphReportSelection::default(),
);
assert_eq!(
report.orchestration.enabled_extensions,
vec!["distributed-workers"]
);
assert_eq!(
report.orchestration.distributed_functions,
vec!["process_video"]
);
assert_eq!(report.orchestration.parallel_regions, 1);
assert!(
report
.orchestration
.local_plan
.iter()
.any(|step| step.kind == "parallel_task" && step.name == "process_video")
);
assert_eq!(
report.orchestration.distributed_jobs[0].function,
"process_video"
);
assert!(report.orchestration.runtime_status.iter().any(
|status| status.implemented && status.reason_code == "local-distributed-simulator"
));
}
}