Skip to content

Commit ab43d7b

Browse files
author
trae
committed
Milestone 10: whitepaper and research paper publishing
1 parent f33b120 commit ab43d7b

File tree

7 files changed

+280
-6
lines changed

7 files changed

+280
-6
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "consensusmind"
3-
version = "0.9.0"
3+
version = "0.10.0"
44
edition = "2021"
55
authors = ["Distributed Systems Labs, LLC"]
66
license = "Apache-2.0"

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ ConsensusMind is an autonomous research agent that conducts end-to-end research
88

99
## Status
1010

11-
**Current Version:** 0.9.0 - Milestone 9 Complete
11+
**Current Version:** 0.10.0 - Milestone 10 Complete
1212

1313
### Completed Milestones
1414

@@ -54,9 +54,12 @@ ConsensusMind is an autonomous research agent that conducts end-to-end research
5454
- Unified CLI entrypoints (help/version and command usage)
5555
- Repo structure improvements for outputs and data directories
5656

57+
#### Milestone 10: Whitepaper & Research Paper
58+
- Whitepaper (Markdown) and research paper (LaTeX) generation from hypotheses and experiment results
59+
5760
## Features
5861

59-
### Current (v0.9.0)
62+
### Current (v0.10.0)
6063
- Configuration management from TOML files
6164
- Environment variable overrides for sensitive data
6265
- Structured logging to file and console
@@ -73,6 +76,7 @@ ConsensusMind is an autonomous research agent that conducts end-to-end research
7376
- Consensus simulation and experimentation
7477
- LaTeX paper generation from experiment outputs
7578
- CLI help/version and stable command interface
79+
- Whitepaper + research paper publishing commands
7680

7781
### Planned
7882
- Automated LaTeX paper generation
@@ -136,13 +140,15 @@ consensusmind run "<query>"
136140
consensusmind hypothesize "<query>"
137141
consensusmind experiment <hypothesis-id> [--seeds N] [--ticks T] [--nodes N]
138142
consensusmind paper <hypothesis-id>
143+
consensusmind whitepaper <hypothesis-id>
144+
consensusmind publish <hypothesis-id>
139145
consensusmind index
140146
consensusmind semantic-search "<query>" [top_k]
141147
consensusmind simulate [rounds] [leader_failure_prob] [seed]
142148
consensusmind raft-simulate [nodes] [ticks] [seed]
143149
```
144150

145-
Currently initializes the system, validates configuration, and provides arXiv search and PDF download capabilities.
151+
Supports end-to-end research runs, hypothesis generation, experiments, and paper/whitepaper generation.
146152

147153
## Development
148154
```bash
@@ -166,7 +172,7 @@ CI runs `cargo fmt --check`, `cargo clippy -- -D warnings`, and `cargo test` on
166172
- [x] Milestone 7: Automated Experimentation
167173
- [x] Milestone 8: Paper Generation
168174
- [x] Milestone 9: Integration & Polish
169-
- [ ] Milestone 10: Whitepaper & Research Paper
175+
- [x] Milestone 10: Whitepaper & Research Paper
170176

171177
## License
172178

src/agent/mod.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,37 @@ impl Agent {
239239
crate::output::paper::write_paper_tex(&self.config.paths.output, &hypothesis, &run, &store)
240240
}
241241

242+
pub fn whitepaper(
243+
&self,
244+
hypothesis_id: &str,
245+
) -> Result<crate::output::whitepaper::SavedWhitepaper> {
246+
let hypothesis =
247+
experiment::find_hypothesis(&self.config.knowledge.hypotheses_file, hypothesis_id)?;
248+
let run = crate::output::whitepaper::load_experiment_results(
249+
&self.config.paths.experiments,
250+
hypothesis_id,
251+
)?;
252+
let store = MetadataStore::new(self.config.knowledge.metadata_file.clone())?;
253+
crate::output::whitepaper::write_whitepaper_md(
254+
&self.config.paths.output,
255+
&hypothesis,
256+
&run,
257+
&store,
258+
)
259+
}
260+
261+
pub fn publish(
262+
&self,
263+
hypothesis_id: &str,
264+
) -> Result<(
265+
crate::output::paper::SavedPaper,
266+
crate::output::whitepaper::SavedWhitepaper,
267+
)> {
268+
let paper = self.paper(hypothesis_id)?;
269+
let whitepaper = self.whitepaper(hypothesis_id)?;
270+
Ok((paper, whitepaper))
271+
}
272+
242273
async fn hypothesize_inner(&mut self, query: &str) -> Result<SavedReport> {
243274
let max_results = (self.config.agent.max_iterations as usize).clamp(1, 10);
244275
let download_limit = self.config.agent.download_limit;

src/main.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ fn print_usage() {
1919
println!(" consensusmind hypothesize \"<query>\"");
2020
println!(" consensusmind experiment <hypothesis-id> [--seeds N] [--ticks T] [--nodes N]");
2121
println!(" consensusmind paper <hypothesis-id>");
22+
println!(" consensusmind whitepaper <hypothesis-id>");
23+
println!(" consensusmind publish <hypothesis-id>");
2224
println!(" consensusmind index");
2325
println!(" consensusmind semantic-search \"<query>\" [top_k]");
2426
println!(" consensusmind simulate [rounds] [leader_failure_prob] [seed]");
@@ -185,6 +187,29 @@ async fn main() -> Result<()> {
185187
println!("Paper TeX saved to {}", paper.tex_path.display());
186188
return Ok(());
187189
}
190+
Some("whitepaper") => {
191+
let hypothesis_id = args.get(2).map(|s| s.as_str()).unwrap_or("");
192+
if hypothesis_id.trim().is_empty() {
193+
println!("Usage: consensusmind whitepaper <hypothesis-id>");
194+
return Ok(());
195+
}
196+
let agent = Agent::new(config)?;
197+
let whitepaper = agent.whitepaper(hypothesis_id)?;
198+
println!("Whitepaper saved to {}", whitepaper.md_path.display());
199+
return Ok(());
200+
}
201+
Some("publish") => {
202+
let hypothesis_id = args.get(2).map(|s| s.as_str()).unwrap_or("");
203+
if hypothesis_id.trim().is_empty() {
204+
println!("Usage: consensusmind publish <hypothesis-id>");
205+
return Ok(());
206+
}
207+
let agent = Agent::new(config)?;
208+
let (paper, whitepaper) = agent.publish(hypothesis_id)?;
209+
println!("Paper TeX saved to {}", paper.tex_path.display());
210+
println!("Whitepaper saved to {}", whitepaper.md_path.display());
211+
return Ok(());
212+
}
188213
Some("simulate") => {
189214
let rounds = args
190215
.get(2)

src/output/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::fs;
55
use std::path::{Path, PathBuf};
66

77
pub mod paper;
8+
pub mod whitepaper;
89

910
#[derive(Debug, Clone, Serialize, Deserialize)]
1011
pub struct AgentRunReport {

src/output/whitepaper.rs

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
use crate::agent::experiment::ExperimentRun;
2+
use crate::agent::hypothesis::Hypothesis;
3+
use crate::knowledge::database::MetadataStore;
4+
use anyhow::{Context, Result};
5+
use chrono::Utc;
6+
use serde::{Deserialize, Serialize};
7+
use std::cmp::Ordering;
8+
use std::fs;
9+
use std::path::{Path, PathBuf};
10+
11+
#[derive(Debug, Clone, Serialize, Deserialize)]
12+
pub struct SavedWhitepaper {
13+
pub md_path: PathBuf,
14+
}
15+
16+
pub fn write_whitepaper_md(
17+
output_root: &Path,
18+
hypothesis: &Hypothesis,
19+
experiment_run: &ExperimentRun,
20+
metadata_store: &MetadataStore,
21+
) -> Result<SavedWhitepaper> {
22+
let papers_dir = output_root.join("papers");
23+
fs::create_dir_all(&papers_dir)?;
24+
25+
let ts = Utc::now().format("%Y%m%dT%H%M%SZ").to_string();
26+
let filename = format!("whitepaper-{}-{}.md", sanitize_filename(&hypothesis.id), ts);
27+
let md_path = papers_dir.join(filename);
28+
29+
let md = render_md(hypothesis, experiment_run, metadata_store)?;
30+
crate::utils::fs::atomic_write(&md_path, md.as_bytes())?;
31+
32+
Ok(SavedWhitepaper { md_path })
33+
}
34+
35+
pub fn load_experiment_results(
36+
experiments_root: &Path,
37+
hypothesis_id: &str,
38+
) -> Result<ExperimentRun> {
39+
let path = experiments_root.join(hypothesis_id).join("results.json");
40+
let contents = fs::read_to_string(&path)
41+
.with_context(|| format!("Failed to read experiment results: {}", path.display()))?;
42+
Ok(serde_json::from_str(&contents)?)
43+
}
44+
45+
fn render_md(
46+
hypothesis: &Hypothesis,
47+
experiment_run: &ExperimentRun,
48+
metadata_store: &MetadataStore,
49+
) -> Result<String> {
50+
let mut s = String::new();
51+
s.push_str("# ");
52+
s.push_str(&hypothesis.title);
53+
s.push('\n');
54+
s.push('\n');
55+
56+
s.push_str("## Abstract\n");
57+
s.push_str(&hypothesis.description);
58+
s.push('\n');
59+
s.push('\n');
60+
61+
s.push_str("## Hypothesis\n");
62+
s.push_str("**Mechanism.** ");
63+
s.push_str(&hypothesis.mechanism);
64+
s.push('\n');
65+
s.push('\n');
66+
67+
if !hypothesis.evaluation_plan.is_empty() {
68+
s.push_str("## Evaluation Plan\n");
69+
for (i, step) in hypothesis.evaluation_plan.iter().enumerate() {
70+
s.push_str(&format!("{}. {}\n", i + 1, step));
71+
}
72+
s.push('\n');
73+
}
74+
75+
s.push_str("## Experiments\n");
76+
s.push_str("Experiments are executed as parameter sweeps over simulation configurations with multiple random seeds. This report summarizes aggregate metrics.\n\n");
77+
78+
if !experiment_run.raft.is_empty() {
79+
let mut raft_ranked = experiment_run.raft.clone();
80+
raft_ranked.sort_by(|a, b| {
81+
b.aggregate
82+
.mean_commit_rate_per_tick
83+
.partial_cmp(&a.aggregate.mean_commit_rate_per_tick)
84+
.unwrap_or(Ordering::Equal)
85+
});
86+
87+
s.push_str("### Raft-style Simulator (Top Cases)\n");
88+
for case in raft_ranked.iter().take(5) {
89+
s.push_str(&format!(
90+
"- nodes={} ticks={} p_req={:.2} mean_commit_rate_per_tick={:.6} mean_elections={:.2} mean_leader_changes={:.2}\n",
91+
case.params.nodes,
92+
case.params.ticks,
93+
case.params.client_request_prob,
94+
case.aggregate.mean_commit_rate_per_tick,
95+
case.aggregate.mean_elections,
96+
case.aggregate.mean_leader_changes
97+
));
98+
}
99+
s.push('\n');
100+
}
101+
102+
if !experiment_run.leader.is_empty() {
103+
s.push_str("### Leader-failure Baseline\n");
104+
for case in experiment_run.leader.iter() {
105+
s.push_str(&format!(
106+
"- p_fail={:.2} mean_commit_rate={:.4}\n",
107+
case.params.leader_failure_prob, case.aggregate.mean_commit_rate
108+
));
109+
}
110+
s.push('\n');
111+
}
112+
113+
s.push_str("## Related Work\n");
114+
if hypothesis.related_paper_ids.is_empty() {
115+
s.push_str("No related papers were explicitly linked to this hypothesis.\n\n");
116+
} else {
117+
for paper_id in hypothesis.related_paper_ids.iter() {
118+
let title = metadata_store
119+
.get_paper(paper_id)
120+
.map(|p| p.title.clone())
121+
.unwrap_or_else(|| paper_id.to_string());
122+
s.push_str(&format!(
123+
"- {} (arXiv: {}) https://arxiv.org/abs/{}\n",
124+
title, paper_id, paper_id
125+
));
126+
}
127+
s.push('\n');
128+
}
129+
130+
s.push_str("## Reproducibility\n");
131+
s.push_str("- Results file: ");
132+
s.push_str(&format!(
133+
"`data/experiments/{}/results.json`\n",
134+
hypothesis.id
135+
));
136+
s.push_str("- Generated at: ");
137+
s.push_str(&Utc::now().to_rfc3339());
138+
s.push('\n');
139+
140+
Ok(s)
141+
}
142+
143+
fn sanitize_filename(s: &str) -> String {
144+
s.chars()
145+
.map(|c| {
146+
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
147+
c
148+
} else {
149+
'_'
150+
}
151+
})
152+
.collect()
153+
}
154+
155+
#[cfg(test)]
156+
mod tests {
157+
use super::*;
158+
use crate::agent::experiment::{run_experiments, ExperimentOverrides};
159+
160+
fn temp_path(name: &str) -> PathBuf {
161+
let pid = std::process::id();
162+
let ts = Utc::now().timestamp_nanos_opt().unwrap_or(0);
163+
std::env::temp_dir().join(format!("consensusmind-whitepaper-{}-{}-{}", name, pid, ts))
164+
}
165+
166+
#[test]
167+
fn write_whitepaper_creates_md_file() {
168+
let root = temp_path("write");
169+
fs::create_dir_all(&root).unwrap();
170+
171+
let hypotheses_path = root.join("hypotheses.json");
172+
let metadata_path = root.join("metadata.json");
173+
let output_root = root.join("out");
174+
fs::create_dir_all(&output_root).unwrap();
175+
176+
let h = Hypothesis {
177+
id: "h1".to_string(),
178+
title: "Whitepaper Title".to_string(),
179+
description: "desc".to_string(),
180+
mechanism: "mech".to_string(),
181+
evaluation_plan: vec!["plan".to_string()],
182+
related_paper_ids: vec!["1234.56789".to_string()],
183+
novelty_score: 0.2,
184+
feasibility_score: 0.8,
185+
created_at: Utc::now().to_rfc3339(),
186+
};
187+
let h_contents = serde_json::to_string_pretty(&vec![h.clone()]).unwrap();
188+
crate::utils::fs::atomic_write(&hypotheses_path, h_contents.as_bytes()).unwrap();
189+
190+
crate::utils::fs::atomic_write(&metadata_path, "{}".as_bytes()).unwrap();
191+
let store = MetadataStore::new(metadata_path).unwrap();
192+
193+
let (_hyp, run, _results_path) = run_experiments(
194+
&root,
195+
&hypotheses_path,
196+
"h1",
197+
ExperimentOverrides {
198+
seeds: Some(2),
199+
ticks: Some(200),
200+
nodes: Some(3),
201+
},
202+
)
203+
.unwrap();
204+
205+
let saved = write_whitepaper_md(&output_root, &h, &run, &store).unwrap();
206+
assert!(saved.md_path.exists());
207+
let contents = fs::read_to_string(saved.md_path).unwrap();
208+
assert!(contents.contains("# Whitepaper Title"));
209+
assert!(contents.contains("## Experiments"));
210+
}
211+
}

0 commit comments

Comments
 (0)