lean_ctx_server/tools/
ctx_proof.rs1use std::path::Path;
3
4use lean_ctx::core::context_ir::{ContextIrV1, write_project_context_ir};
5use lean_ctx::core::context_proof::{
6 ProofOptions, ProofSources, collect_v1, proof_destination, write_project_proof,
7};
8
9pub fn handle_export(
10 project_root: &str,
11 format: Option<&str>,
12 write: bool,
13 filename: Option<&str>,
14 max_evidence: Option<usize>,
15 max_ledger_files: Option<usize>,
16 sources: ProofSources,
17) -> Result<String, String> {
18 let opts = ProofOptions {
19 max_evidence: max_evidence.unwrap_or(50),
20 max_ledger_files: max_ledger_files.unwrap_or(10),
21 };
22 let proof = collect_v1(sources, opts);
23 let ir = ContextIrV1::load();
24
25 let mut out = String::new();
26 let mut written: Option<String> = None;
27 let mut written_ir: Option<String> = None;
28 let mut written_architecture_json: Option<String> = None;
29 let mut written_architecture_html: Option<String> = None;
30 if write {
31 let root = Path::new(project_root);
32 let ts = chrono::Utc::now().format("%Y-%m-%d_%H%M%S").to_string();
33 let default_proof_name = format!("context-proof-v1_{ts}.json");
34 let ir_name = format!("context-ir-v1_{ts}.json");
35 let architecture_json_name = format!("architecture-overview-v1_{ts}.json");
36 let architecture_html_name = format!("architecture-overview-v1_{ts}.html");
37 let proof_name = filename.unwrap_or(default_proof_name.as_str());
38
39 let path = write_project_proof(root, &proof, Some(proof_name))?;
40 written = Some(path.to_string_lossy().to_string());
41
42 let ir_path = write_project_context_ir(root, &ir, Some(&ir_name))?;
43 written_ir = Some(ir_path.to_string_lossy().to_string());
44
45 {
48 let status =
49 crate::tools::ctx_impact::handle("status", None, project_root, None, Some("json"));
50 let freshness = serde_json::from_str::<serde_json::Value>(&status)
51 .ok()
52 .and_then(|v| {
53 v.get("freshness")
54 .and_then(|f| f.as_str())
55 .map(std::string::ToString::to_string)
56 })
57 .unwrap_or_else(|| "unknown".to_string());
58 if freshness != "fresh" {
59 let _ = crate::tools::ctx_impact::handle("build", None, project_root, None, None);
60 }
61
62 let arch_json = crate::tools::ctx_architecture::handle(
63 "overview",
64 None,
65 project_root,
66 Some("json"),
67 );
68
69 let json_redacted = lean_ctx::core::redaction::redact_text(&arch_json);
70 let json_path =
71 proof_destination(root, &architecture_json_name)?.write(&json_redacted)?;
72 written_architecture_json = Some(json_path.to_string_lossy().to_string());
73
74 let html = render_architecture_overview_html(&arch_json);
75 let html_redacted = lean_ctx::core::redaction::redact_text(&html);
76 let html_path =
77 proof_destination(root, &architecture_html_name)?.write(&html_redacted)?;
78 written_architecture_html = Some(html_path.to_string_lossy().to_string());
79 }
80 }
81
82 match format.unwrap_or("json") {
83 "summary" => {
84 out.push_str("ContextProofV1 exported\n");
85 if let Some(p) = written {
86 out.push_str(&format!("path: {p}\n"));
87 }
88 if let Some(p) = written_ir {
89 out.push_str(&format!("context_ir_path: {p}\n"));
90 }
91 if let Some(p) = written_architecture_json {
92 out.push_str(&format!("architecture_overview_json_path: {p}\n"));
93 }
94 if let Some(p) = written_architecture_html {
95 out.push_str(&format!("architecture_overview_html_path: {p}\n"));
96 }
97 out.push_str(&format!("schema_version: {}\n", proof.schema_version));
98 out.push_str(&format!(
99 "project_root_hash: {}\n",
100 proof.project.project_root_hash.clone().unwrap_or_default()
101 ));
102 out.push_str(&format!("role: {}\n", proof.role.name));
103 out.push_str(&format!("profile: {}\n", proof.profile.name));
104 out.push_str(&format!(
105 "context_ir_schema_version: {}\n",
106 ir.schema_version
107 ));
108 out.push_str(&format!("context_ir_items: {}\n", ir.items.len()));
109 }
110 "both" => {
111 if let Some(p) = &written {
112 out.push_str(&format!("[proof_path: {p}]\n\n"));
113 }
114 if let Some(p) = &written_ir {
115 out.push_str(&format!("[context_ir_path: {p}]\n\n"));
116 }
117 if let Some(p) = &written_architecture_json {
118 out.push_str(&format!("[architecture_overview_json_path: {p}]\n\n"));
119 }
120 if let Some(p) = &written_architecture_html {
121 out.push_str(&format!("[architecture_overview_html_path: {p}]\n\n"));
122 }
123 out.push_str(&serde_json::to_string_pretty(&proof).map_err(|e| e.to_string())?);
124 }
125 _ => {
126 if let Some(p) = &written {
127 out.push_str(&format!("[proof_path: {p}]\n\n"));
128 }
129 if let Some(p) = &written_ir {
130 out.push_str(&format!("[context_ir_path: {p}]\n\n"));
131 }
132 if let Some(p) = &written_architecture_json {
133 out.push_str(&format!("[architecture_overview_json_path: {p}]\n\n"));
134 }
135 if let Some(p) = &written_architecture_html {
136 out.push_str(&format!("[architecture_overview_html_path: {p}]\n\n"));
137 }
138 out.push_str(&serde_json::to_string_pretty(&proof).map_err(|e| e.to_string())?);
139 }
140 }
141
142 Ok(out)
143}
144
145fn escape_html(s: &str) -> String {
146 let mut out = String::with_capacity(s.len());
147 for ch in s.chars() {
148 match ch {
149 '&' => out.push_str("&"),
150 '<' => out.push_str("<"),
151 '>' => out.push_str(">"),
152 '"' => out.push_str("""),
153 '\'' => out.push_str("'"),
154 _ => out.push(ch),
155 }
156 }
157 out
158}
159
160fn render_architecture_overview_html(json_payload: &str) -> String {
161 let escaped = escape_html(json_payload);
162 format!(
163 r#"<!doctype html>
164<html lang="en">
165<head>
166 <meta charset="utf-8" />
167 <meta name="viewport" content="width=device-width, initial-scale=1" />
168 <title>LeanCTX — Architecture Overview</title>
169 <style>
170 :root {{ color-scheme: light dark; }}
171 body {{ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; margin: 24px; }}
172 h1 {{ margin: 0 0 12px 0; }}
173 p {{ margin: 0 0 18px 0; opacity: 0.8; }}
174 pre {{ padding: 16px; border-radius: 10px; overflow: auto; }}
175 </style>
176</head>
177<body>
178 <h1>Architecture Overview (JSON)</h1>
179 <p>Generated by LeanCTX proof export. Content is redacted-by-default for CI safety.</p>
180 <pre>{escaped}</pre>
181</body>
182</html>"#
183 )
184}