use std::{
collections::HashMap,
sync::{Arc, Mutex},
time::{SystemTime, UNIX_EPOCH},
};
use serde::{Deserialize, Serialize};
#[derive(Debug)]
pub enum DocsError {
LockPoisoned,
Io(String),
}
impl std::fmt::Display for DocsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DocsError::LockPoisoned => write!(f, "docs lock poisoned"),
DocsError::Io(msg) => write!(f, "I/O error: {msg}"),
}
}
}
impl std::error::Error for DocsError {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricDelta {
pub name: String,
pub before: f64,
pub after: f64,
pub delta: f64,
pub lower_is_better: bool,
}
impl MetricDelta {
pub fn new(name: impl Into<String>, before: f64, after: f64, lower_is_better: bool) -> Self {
let delta = after - before;
Self {
name: name.into(),
before,
after,
delta,
lower_is_better,
}
}
pub fn is_improvement(&self) -> bool {
if self.lower_is_better {
self.delta < 0.0
} else {
self.delta > 0.0
}
}
pub fn improvement_pct(&self) -> f64 {
if self.before == 0.0 {
return 0.0;
}
(self.delta / self.before.abs()) * 100.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangelogEntry {
pub modification_id: String,
pub title: String,
pub rationale: String,
pub files_changed: Vec<String>,
pub metric_deltas: Vec<MetricDelta>,
pub initiated_by: String,
pub timestamp_secs: u64,
pub kept: bool,
}
impl ChangelogEntry {
pub fn to_markdown(&self) -> String {
let ts = self.timestamp_secs;
let kept_str = if self.kept { "Deployed" } else { "Rolled back" };
let mut md = format!(
"## [{title}](#{id}) -- {kept}\n\n\
- **ID**: `{id}`\n\
- **When**: Unix timestamp `{ts}`\n\
- **By**: {by}\n\
- **Status**: {kept}\n\n\
### Why\n\n{rationale}\n\n\
### Files changed\n\n",
title = self.title,
id = self.modification_id,
kept = kept_str,
ts = ts,
by = self.initiated_by,
rationale = self.rationale,
);
for f in &self.files_changed {
md.push_str(&format!("- `{f}`\n"));
}
if !self.metric_deltas.is_empty() {
md.push_str("\n### Performance impact\n\n");
md.push_str("| Metric | Before | After | Delta | Result |\n");
md.push_str("|--------|--------|-------|-------|--------|\n");
for d in &self.metric_deltas {
let result = if d.is_improvement() {
"Improved"
} else {
"Degraded"
};
md.push_str(&format!(
"| {} | {:.3} | {:.3} | {:+.3} ({:+.1}%) | {} |\n",
d.name,
d.before,
d.after,
d.delta,
d.improvement_pct(),
result,
));
}
}
md.push('\n');
md
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchitectureDiagram {
pub mermaid_source: String,
pub generated_at_secs: u64,
pub version: String,
}
impl ArchitectureDiagram {
pub fn to_markdown(&self) -> String {
format!(
"```mermaid\n{}\n```\n\n_Generated at Unix `{}` (version `{}`)_\n",
self.mermaid_source, self.generated_at_secs, self.version
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyImpact {
pub changed_files: Vec<String>,
pub transitive_dependents: Vec<String>,
pub blast_radius: usize,
pub touches_public_api: bool,
}
impl DependencyImpact {
pub fn compute(
changed_files: Vec<String>,
dependency_map: &HashMap<String, Vec<String>>,
) -> Self {
let mut transitive: Vec<String> = Vec::new();
let mut visited = std::collections::HashSet::new();
for file in &changed_files {
collect_dependents(file, dependency_map, &mut transitive, &mut visited);
}
let blast_radius = transitive.len();
let touches_public_api = changed_files
.iter()
.any(|f| f.contains("lib.rs") || f.contains("mod.rs") || f.contains("pub "));
DependencyImpact {
changed_files,
transitive_dependents: transitive,
blast_radius,
touches_public_api,
}
}
pub fn to_markdown(&self) -> String {
let api_warning = if self.touches_public_api {
"**This change touches public API surface -- review carefully.**\n\n"
} else {
""
};
let mut md = format!(
"### Dependency impact\n\n{api_warning}\
- **Blast radius**: {blast} modules affected\n\
- **Changed files**: {count}\n\n",
api_warning = api_warning,
blast = self.blast_radius,
count = self.changed_files.len(),
);
if !self.transitive_dependents.is_empty() {
md.push_str("**Transitively affected modules:**\n\n");
for dep in &self.transitive_dependents {
md.push_str(&format!("- `{dep}`\n"));
}
}
md
}
}
fn collect_dependents(
module: &str,
map: &HashMap<String, Vec<String>>,
result: &mut Vec<String>,
visited: &mut std::collections::HashSet<String>,
) {
for (parent, deps) in map {
if deps.iter().any(|d| d == module) && !visited.contains(parent) {
visited.insert(parent.clone());
result.push(parent.clone());
collect_dependents(parent, map, result, visited);
}
}
}
struct DocsInner {
changelog: Vec<ChangelogEntry>,
diagrams: Vec<ArchitectureDiagram>,
output_dir: String,
max_changelog_entries: usize,
}
#[derive(Clone)]
pub struct SelfDocGenerator {
inner: Arc<Mutex<DocsInner>>,
}
impl SelfDocGenerator {
pub fn new(output_dir: impl Into<String>) -> Self {
Self {
inner: Arc::new(Mutex::new(DocsInner {
changelog: Vec::new(),
diagrams: Vec::new(),
output_dir: output_dir.into(),
max_changelog_entries: 1000,
})),
}
}
pub fn record_change(&self, entry: ChangelogEntry) -> Result<(), DocsError> {
let mut inner = self.inner.lock().map_err(|_| DocsError::LockPoisoned)?;
if inner.changelog.len() >= inner.max_changelog_entries {
inner.changelog.remove(0);
}
inner.changelog.push(entry);
Ok(())
}
pub fn record_diagram(&self, diagram: ArchitectureDiagram) -> Result<(), DocsError> {
let mut inner = self.inner.lock().map_err(|_| DocsError::LockPoisoned)?;
inner.diagrams.push(diagram);
Ok(())
}
pub fn changelog(&self) -> Vec<ChangelogEntry> {
self.inner
.lock()
.map(|inner| {
let mut v = inner.changelog.clone();
v.reverse();
v
})
.unwrap_or_default()
}
pub fn render_changelog_md(&self) -> String {
let entries = self.changelog();
let mut md = "# Self-Modification Changelog\n\n".to_string();
md.push_str(&format!("_Last updated: Unix `{}`_\n\n---\n\n", unix_now()));
if entries.is_empty() {
md.push_str("_No modifications recorded yet._\n");
} else {
for entry in &entries {
md.push_str(&entry.to_markdown());
md.push_str("---\n\n");
}
}
md
}
pub fn render_latest_diagram_md(&self) -> Option<String> {
self.inner
.lock()
.ok()
.and_then(|inner| inner.diagrams.last().cloned())
.map(|d| d.to_markdown())
}
pub fn generate_pipeline_diagram(&self, version: &str) -> ArchitectureDiagram {
let source = "flowchart LR\n\
A[TokenStream] --> B[Interceptor]\n\
B --> C[Modifier]\n\
C --> D[Writer]\n\
B -.-> E[(TelemetryBus)]\n\
subgraph self-tune\n\
E --> F[AnomalyDetector]\n\
E --> G[Controller]\n\
E --> H[Orchestrator]\n\
end\n\
subgraph self-modify\n\
H --> I[TaskGenerator]\n\
I --> J[ValidationGate]\n\
J --> K[Deployment]\n\
K --> L[AgentMemory]\n\
end"
.to_string();
ArchitectureDiagram {
mermaid_source: source,
generated_at_secs: unix_now(),
version: version.to_string(),
}
}
pub fn output_dir(&self) -> String {
self.inner
.lock()
.map(|inner| inner.output_dir.clone())
.unwrap_or_default()
}
pub fn entry_count(&self) -> usize {
self.inner
.lock()
.map(|inner| inner.changelog.len())
.unwrap_or(0)
}
}
fn unix_now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_entry(id: &str) -> ChangelogEntry {
ChangelogEntry {
modification_id: id.to_string(),
title: format!("Fix {id}"),
rationale: "Latency was too high".to_string(),
files_changed: vec!["src/interceptor.rs".to_string()],
metric_deltas: vec![MetricDelta::new("p95_latency_ms", 10.0, 7.0, true)],
initiated_by: "controller".to_string(),
timestamp_secs: 1_700_000_000,
kept: true,
}
}
#[test]
fn test_metric_delta_improvement_lower_is_better() {
let d = MetricDelta::new("latency", 10.0, 7.0, true);
assert!(d.is_improvement());
assert!(d.delta < 0.0);
}
#[test]
fn test_metric_delta_degradation_lower_is_better() {
let d = MetricDelta::new("latency", 7.0, 10.0, true);
assert!(!d.is_improvement());
}
#[test]
fn test_metric_delta_improvement_higher_is_better() {
let d = MetricDelta::new("throughput", 100.0, 120.0, false);
assert!(d.is_improvement());
}
#[test]
fn test_metric_delta_improvement_pct() {
let d = MetricDelta::new("latency", 100.0, 80.0, true);
assert!((d.improvement_pct() - (-20.0)).abs() < 0.01);
}
#[test]
fn test_metric_delta_zero_before() {
let d = MetricDelta::new("m", 0.0, 5.0, false);
assert_eq!(d.improvement_pct(), 0.0);
}
#[test]
fn test_changelog_entry_to_markdown_contains_title() {
let e = make_entry("mod-1");
let md = e.to_markdown();
assert!(md.contains("Fix mod-1"));
}
#[test]
fn test_changelog_entry_to_markdown_shows_metrics() {
let e = make_entry("mod-1");
let md = e.to_markdown();
assert!(md.contains("p95_latency_ms"));
}
#[test]
fn test_architecture_diagram_to_markdown() {
let diag = ArchitectureDiagram {
mermaid_source: "flowchart LR\n A --> B".to_string(),
generated_at_secs: 0,
version: "v1".to_string(),
};
let md = diag.to_markdown();
assert!(md.contains("```mermaid"));
assert!(md.contains("A --> B"));
}
#[test]
fn test_dependency_impact_blast_radius() {
let mut map: HashMap<String, Vec<String>> = HashMap::new();
map.insert("src/a.rs".into(), vec!["src/interceptor.rs".into()]);
map.insert("src/b.rs".into(), vec!["src/a.rs".into()]);
let impact = DependencyImpact::compute(vec!["src/interceptor.rs".into()], &map);
assert!(impact.blast_radius >= 1);
}
#[test]
fn test_dependency_impact_no_dependents() {
let map: HashMap<String, Vec<String>> = HashMap::new();
let impact = DependencyImpact::compute(vec!["src/isolated.rs".into()], &map);
assert_eq!(impact.blast_radius, 0);
}
#[test]
fn test_dependency_impact_to_markdown() {
let map: HashMap<String, Vec<String>> = HashMap::new();
let impact = DependencyImpact::compute(vec!["src/lib.rs".into()], &map);
let md = impact.to_markdown();
assert!(md.contains("Dependency impact"));
assert!(md.contains("public API"));
}
#[test]
fn test_generator_record_and_retrieve_changelog() {
let gen = SelfDocGenerator::new("docs/self_modifications");
gen.record_change(make_entry("m1")).unwrap();
assert_eq!(gen.entry_count(), 1);
}
#[test]
fn test_changelog_newest_first() {
let gen = SelfDocGenerator::new("docs");
gen.record_change(make_entry("a")).unwrap();
gen.record_change(make_entry("b")).unwrap();
let log = gen.changelog();
assert_eq!(log[0].modification_id, "b");
}
#[test]
fn test_render_changelog_md_empty() {
let gen = SelfDocGenerator::new("docs");
let md = gen.render_changelog_md();
assert!(md.contains("No modifications recorded yet"));
}
#[test]
fn test_render_changelog_md_with_entry() {
let gen = SelfDocGenerator::new("docs");
gen.record_change(make_entry("m1")).unwrap();
let md = gen.render_changelog_md();
assert!(md.contains("Fix m1"));
}
#[test]
fn test_generate_pipeline_diagram() {
let gen = SelfDocGenerator::new("docs");
let diag = gen.generate_pipeline_diagram("v1.2.3");
assert!(diag.mermaid_source.contains("flowchart"));
assert_eq!(diag.version, "v1.2.3");
}
#[test]
fn test_record_and_render_latest_diagram() {
let gen = SelfDocGenerator::new("docs");
let diag = gen.generate_pipeline_diagram("v1");
gen.record_diagram(diag).unwrap();
let md = gen.render_latest_diagram_md();
assert!(md.is_some());
assert!(md.unwrap().contains("mermaid"));
}
#[test]
fn test_output_dir_returned() {
let gen = SelfDocGenerator::new("/tmp/docs");
assert_eq!(gen.output_dir(), "/tmp/docs");
}
#[test]
fn test_generator_clone_shares_state() {
let gen = SelfDocGenerator::new("docs");
let gen2 = gen.clone();
gen.record_change(make_entry("x")).unwrap();
assert_eq!(gen2.entry_count(), 1);
}
#[test]
fn test_kept_false_shows_rolled_back() {
let mut entry = make_entry("m2");
entry.kept = false;
let md = entry.to_markdown();
assert!(md.contains("Rolled back"));
}
#[test]
fn test_max_entries_cap() {
let gen = SelfDocGenerator::new("docs");
{
let mut inner = gen.inner.lock().unwrap();
inner.max_changelog_entries = 3;
}
for i in 0..5 {
gen.record_change(make_entry(&format!("m{i}"))).unwrap();
}
assert_eq!(gen.entry_count(), 3);
}
#[test]
fn test_docs_error_display() {
let e = DocsError::LockPoisoned;
assert!(e.to_string().contains("lock poisoned"));
let e2 = DocsError::Io("disk full".to_string());
assert!(e2.to_string().contains("disk full"));
}
#[test]
fn test_metric_delta_serde_roundtrip() {
let d = MetricDelta::new("lat", 10.0, 7.0, true);
let json = serde_json::to_string(&d).unwrap();
let back: MetricDelta = serde_json::from_str(&json).unwrap();
assert_eq!(back.name, "lat");
assert!((back.delta - d.delta).abs() < f64::EPSILON);
}
#[test]
fn test_changelog_entry_serde_roundtrip() {
let e = make_entry("serde-test");
let json = serde_json::to_string(&e).unwrap();
let back: ChangelogEntry = serde_json::from_str(&json).unwrap();
assert_eq!(back.modification_id, "serde-test");
}
#[test]
fn test_architecture_diagram_serde_roundtrip() {
let d = ArchitectureDiagram {
mermaid_source: "flowchart LR".to_string(),
generated_at_secs: 123,
version: "v1".to_string(),
};
let json = serde_json::to_string(&d).unwrap();
let back: ArchitectureDiagram = serde_json::from_str(&json).unwrap();
assert_eq!(back.version, "v1");
}
#[test]
fn test_dependency_impact_serde_roundtrip() {
let impact = DependencyImpact {
changed_files: vec!["a.rs".into()],
transitive_dependents: vec!["b.rs".into()],
blast_radius: 1,
touches_public_api: false,
};
let json = serde_json::to_string(&impact).unwrap();
let back: DependencyImpact = serde_json::from_str(&json).unwrap();
assert_eq!(back.blast_radius, 1);
}
}