2026-07-14 08:12:37 +07:00
|
|
|
//! Guaranteed, deterministic documentation output for hive-mind runs.
|
|
|
|
|
//!
|
|
|
|
|
//! Because cycles/directives are entirely Core-Intelligence-authored (see
|
|
|
|
|
//! `app::workflow::hive_mind`), it could in principle never plan a "write
|
|
|
|
|
//! docs" node for a given task. Durable documentation can't depend on that
|
|
|
|
|
//! choice, so this step is plain Rust — not an LLM call, not a cycle the
|
|
|
|
|
//! Core Intelligence can omit or reshape — and always runs after any
|
|
|
|
|
//! hive-mind convergence completes.
|
|
|
|
|
use crate::app::workflow::hive_mind::NodeReport;
|
|
|
|
|
use crate::model::memory::Memory;
|
2026-07-16 07:42:03 +07:00
|
|
|
use std::fmt::Write as _;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
2026-07-14 08:12:37 +07:00
|
|
|
|
|
|
|
|
/// Write a markdown report of one hive-mind convergence to
|
|
|
|
|
/// `<workspace_root>/docs/runs/<timestamp>-<slug>.md`.
|
|
|
|
|
///
|
|
|
|
|
/// Flow: build a slug from the user request → format every `NodeReport`
|
|
|
|
|
/// (grouped by cycle) with its complete output (no truncation — this is
|
|
|
|
|
/// the durable record of what the hive actually decided and did) → append
|
|
|
|
|
/// the final reconciled `consensus` as its own section → create
|
|
|
|
|
/// `docs/runs/` if missing → write the file.
|
|
|
|
|
///
|
|
|
|
|
/// Return: the path written, so callers can log/reference it.
|
|
|
|
|
pub fn write_hive_mind_convergence(
|
|
|
|
|
workspace_root: &Path,
|
|
|
|
|
user_request: &str,
|
|
|
|
|
reports: &[NodeReport],
|
|
|
|
|
consensus: &str,
|
|
|
|
|
) -> anyhow::Result<PathBuf> {
|
|
|
|
|
let runs_dir = workspace_root.join("docs").join("runs");
|
|
|
|
|
std::fs::create_dir_all(&runs_dir)?;
|
|
|
|
|
|
|
|
|
|
let ts = chrono::Utc::now();
|
|
|
|
|
let slug = Memory::slugify(user_request).unwrap_or_else(|| "run".to_string());
|
|
|
|
|
let filename = format!("{}-{}.md", ts.format("%Y%m%d-%H%M%S"), slug);
|
|
|
|
|
let path = runs_dir.join(filename);
|
|
|
|
|
|
|
|
|
|
let content = render_report(user_request, ts.timestamp_millis(), reports, consensus);
|
|
|
|
|
std::fs::write(&path, content)?;
|
|
|
|
|
Ok(path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Render a hive-mind convergence as a markdown document.
|
2026-07-16 07:42:03 +07:00
|
|
|
fn render_report(
|
|
|
|
|
user_request: &str,
|
|
|
|
|
ts_millis: i64,
|
|
|
|
|
reports: &[NodeReport],
|
|
|
|
|
consensus: &str,
|
|
|
|
|
) -> String {
|
2026-07-14 08:12:37 +07:00
|
|
|
let mut out = String::new();
|
2026-07-16 07:42:03 +07:00
|
|
|
let _ = writeln!(out, "# The Hive converges: {user_request}");
|
|
|
|
|
let _ = writeln!(out, "\nTimestamp (ms): {ts_millis}\n");
|
2026-07-14 08:12:37 +07:00
|
|
|
|
2026-07-16 07:42:03 +07:00
|
|
|
let cycle_count = reports
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|r| r.cycle_index)
|
|
|
|
|
.max()
|
|
|
|
|
.map_or(0, |m| m + 1);
|
2026-07-14 08:12:37 +07:00
|
|
|
for cycle_index in 0..cycle_count {
|
2026-07-16 07:42:03 +07:00
|
|
|
let _ = writeln!(out, "## Cycle {cycle_index}\n");
|
2026-07-14 08:12:37 +07:00
|
|
|
for r in reports.iter().filter(|r| r.cycle_index == cycle_index) {
|
2026-07-16 07:42:03 +07:00
|
|
|
let _ = writeln!(out, "### {}\n", r.node_id);
|
|
|
|
|
let _ = writeln!(out, "{}\n", r.output);
|
2026-07-14 08:12:37 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 07:42:03 +07:00
|
|
|
let _ = writeln!(out, "## The Hive's Verdict\n");
|
|
|
|
|
let _ = writeln!(out, "{consensus}\n");
|
2026-07-14 08:12:37 +07:00
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn writes_run_file_under_docs_runs() {
|
|
|
|
|
let tmp = std::env::temp_dir().join(format!("zesdex-docs-test-{}", uuid::Uuid::new_v4()));
|
|
|
|
|
std::fs::create_dir_all(&tmp).unwrap();
|
|
|
|
|
|
2026-07-16 07:42:03 +07:00
|
|
|
let reports = vec![NodeReport {
|
|
|
|
|
node_id: "Node-0-0".to_string(),
|
|
|
|
|
cycle_index: 0,
|
|
|
|
|
output: "found the bug".to_string(),
|
|
|
|
|
}];
|
|
|
|
|
let path =
|
|
|
|
|
write_hive_mind_convergence(&tmp, "fix the bug", &reports, "the bug is a null check")
|
|
|
|
|
.unwrap();
|
2026-07-14 08:12:37 +07:00
|
|
|
|
|
|
|
|
assert!(path.starts_with(tmp.join("docs").join("runs")));
|
|
|
|
|
let content = std::fs::read_to_string(&path).unwrap();
|
|
|
|
|
assert!(content.contains("fix the bug"));
|
|
|
|
|
assert!(content.contains("Node-0-0"));
|
|
|
|
|
assert!(content.contains("found the bug"));
|
2026-07-15 00:20:41 +07:00
|
|
|
assert!(content.contains("The Hive's Verdict"));
|
2026-07-14 08:12:37 +07:00
|
|
|
assert!(content.contains("the bug is a null check"));
|
|
|
|
|
|
|
|
|
|
std::fs::remove_dir_all(&tmp).ok();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn falls_back_to_generic_slug_for_unslugifiable_request() {
|
|
|
|
|
let tmp = std::env::temp_dir().join(format!("zesdex-docs-test-{}", uuid::Uuid::new_v4()));
|
|
|
|
|
std::fs::create_dir_all(&tmp).unwrap();
|
|
|
|
|
|
|
|
|
|
let path = write_hive_mind_convergence(&tmp, "???", &[], "").unwrap();
|
|
|
|
|
assert!(path.file_name().unwrap().to_str().unwrap().contains("run"));
|
|
|
|
|
|
|
|
|
|
std::fs::remove_dir_all(&tmp).ok();
|
|
|
|
|
}
|
|
|
|
|
}
|