1use std::collections::BTreeMap;
16
17use choir_identity::{ActorKey, Registry};
18use choir_merge::{MergeOutcome, Pipeline};
19use choir_oplog::{MemLog, OpEntry};
20use choir_sequencer::{Sequencer, Submission, SubmitPolicy};
21use choir_store::{get_blob, put_blob, ChunkerParams, MemStore};
22use choir_view::{Commit, OpKind, TreeEntry, View, ViewOp};
23
24struct ChoirPolicy {
27 registry: Registry,
28 view: View,
29}
30
31impl SubmitPolicy for ChoirPolicy {
32 fn check(&mut self, sub: &Submission) -> Result<(), String> {
33 let sig = sub.author_sig.as_ref().ok_or("unsigned submission")?;
34 self.registry
35 .verify_submission(&sub.channel, &sub.payload, sig)
36 .map_err(|e| format!("bad signature: {e:?}"))?;
37 let op = ViewOp::from_payload(&sub.payload).map_err(|e| format!("bad op: {e:?}"))?;
38 let mut trial = self.view.clone();
39 trial.apply(&op).map_err(|e| format!("stale head: {e:?}"))
40 }
41
42 fn accepted(&mut self, entry: &OpEntry, _hash: &choir_hash::ContentHash) {
43 let op = ViewOp::from_payload(&entry.payload).expect("checked");
44 self.view.apply(&op).expect("checked");
45 }
46}
47
48fn short(h: &choir_hash::ContentHash) -> String {
49 h.to_hex()[..11].to_string()
50}
51
52fn section(title: &str) {
53 println!("\n=== {title} ===");
54}
55
56fn main() {
57 let mut store = MemStore::new();
58
59 section("1. Identity (L8): one ed25519 key per agent");
60 let alice = ActorKey::generate();
61 let bob = ActorKey::generate();
62 let mallory = ActorKey::generate();
63 let mut registry = Registry::new();
64 registry.register(&alice.public_key_bytes()).unwrap();
65 registry.register(&bob.public_key_bytes()).unwrap();
66 println!(
67 " alice -> actor id {} (registered)",
68 short(&alice.actor_id())
69 );
70 println!(
71 " bob -> actor id {} (registered)",
72 short(&bob.actor_id())
73 );
74 println!(
75 " mallory -> actor id {} (NOT registered)",
76 short(&mallory.actor_id())
77 );
78
79 section("2. Content-addressed commits (L0/L1): BLAKE3 + FastCDC");
80 let base_src = "fn main() {\n println!(\"v1\");\n}\n";
81 let blob = put_blob(&mut store, base_src.as_bytes(), ChunkerParams::default()).unwrap();
82 let mut tree = BTreeMap::new();
83 tree.insert("main.rs".to_string(), TreeEntry::File { blob });
84 let c_base = Commit {
85 format_version: choir_view::FORMAT_VERSION,
86 parents: vec![],
87 tree,
88 author: short(&alice.actor_id()),
89 message: "base".into(),
90 resolves: None,
91 }
92 .put(&mut store)
93 .unwrap();
94 println!(" base commit {} (tree: main.rs)", short(&c_base));
95
96 section("3. Signed ops through the single-writer sequencer (L2)");
97 let sequencer = Sequencer::spawn_with_policy(
98 Box::new(MemLog::new()),
99 Box::new(ChoirPolicy {
100 registry,
101 view: View::default(),
102 }),
103 );
104 let handle = sequencer.handle();
105
106 let op = ViewOp::new(OpKind::SetRef {
107 name: "main".into(),
108 commit: c_base.clone(),
109 prev: None,
110 })
111 .to_payload();
112 let sig = alice.sign_submission("alice", &op);
113 let acc = handle.try_submit("alice", op, Some(sig)).unwrap();
114 println!(
115 " alice sets main -> {} ACCEPTED seq={} ({:?})",
116 short(&c_base),
117 acc.seq,
118 acc.decision_latency
119 );
120
121 let op = ViewOp::new(OpKind::SetWorkspaceHead {
122 workspace: "bob".into(),
123 commit: c_base.clone(),
124 prev: None,
125 })
126 .to_payload();
127 let sig = bob.sign_submission("bob", &op);
128 let acc = handle.try_submit("bob", op, Some(sig)).unwrap();
129 println!(" bob checks out base ACCEPTED seq={}", acc.seq);
130
131 let op = ViewOp::new(OpKind::SetRef {
132 name: "main".into(),
133 commit: c_base.clone(),
134 prev: None,
135 })
136 .to_payload();
137 let sig = mallory.sign_submission("mallory", &op);
138 let err = handle.try_submit("mallory", op, Some(sig)).unwrap_err();
139 println!(" mallory (unregistered key) REJECTED: {err}");
140
141 let op = ViewOp::new(OpKind::SetRef {
142 name: "main".into(),
143 commit: c_base.clone(),
144 prev: None, })
146 .to_payload();
147 let sig = bob.sign_submission("bob", &op);
148 let err = handle.try_submit("bob", op, Some(sig)).unwrap_err();
149 println!(
150 " bob with a stale CAS REJECTED: {}",
151 &err[..err.len().min(80)]
152 );
153
154 section("4. Merge pipeline (L4): disjoint edits fold, real conflicts stay first-class");
155 let left = "fn main() {\n println!(\"v1\");\n}\n// alice: added docs\n";
156 let right = "// bob: added header\nfn main() {\n println!(\"v1\");\n}\n";
157 let r = Pipeline::default_v1().merge(base_src, left, right);
158 match &r.outcome {
159 MergeOutcome::Resolved(text) => {
160 println!(" disjoint edits -> RESOLVED by `{}` strategy:", r.strategy);
161 for l in text.lines() {
162 println!(" | {l}");
163 }
164 }
165 _ => unreachable!(),
166 }
167
168 let l2 = "fn main() {\n println!(\"alice wins\");\n}\n";
169 let r2 = "fn main() {\n println!(\"bob wins\");\n}\n";
170 let r = Pipeline::default_v1().merge(base_src, l2, r2);
171 let annotated = match &r.outcome {
172 MergeOutcome::Conflict { annotated } => {
173 println!(" same-line edits -> CONFLICT (no silent pick). Stored as a commit:");
174 annotated.clone()
175 }
176 _ => unreachable!(),
177 };
178
179 let b = put_blob(&mut store, base_src.as_bytes(), ChunkerParams::default()).unwrap();
181 let l = put_blob(&mut store, l2.as_bytes(), ChunkerParams::default()).unwrap();
182 let rr = put_blob(&mut store, r2.as_bytes(), ChunkerParams::default()).unwrap();
183 let mut tree = BTreeMap::new();
184 tree.insert(
185 "main.rs".to_string(),
186 TreeEntry::Conflict {
187 base: Some(b),
188 left: l,
189 right: rr,
190 },
191 );
192 let c_conflict = Commit {
193 format_version: choir_view::FORMAT_VERSION,
194 parents: vec![c_base.clone()],
195 tree,
196 author: "merge".into(),
197 message: "conflicted merge (first-class)".into(),
198 resolves: None,
199 }
200 .put(&mut store)
201 .unwrap();
202 let op = ViewOp::new(OpKind::SetWorkspaceHead {
203 workspace: "bob".into(),
204 commit: c_conflict.clone(),
205 prev: Some(c_base),
206 })
207 .to_payload();
208 let sig = bob.sign_submission("bob", &op);
209 let acc = handle.try_submit("bob", op, Some(sig)).unwrap();
210 println!(
211 " bob's head -> conflicted commit {} ACCEPTED seq={} (work continues!)",
212 short(&c_conflict),
213 acc.seq
214 );
215 for l in annotated.lines().take(7) {
216 println!(" | {l}");
217 }
218
219 section("5. Time travel (L1): the op log replays to any point");
220 let log = sequencer.shutdown();
221 let now = View::materialize(log.as_ref()).unwrap();
222 let before = View::at(log.as_ref(), 2).unwrap();
223 println!(
224 " view NOW : bob @ {} (conflicted)",
225 short(now.workspaces.get("bob").unwrap())
226 );
227 println!(
228 " view at op 2 : bob @ {} (before the merge -- undo is just prefix replay)",
229 short(before.workspaces.get("bob").unwrap())
230 );
231 println!(
232 " log entries : {} total, every one signed + hash-chained",
233 log.len()
234 );
235
236 section("6. Round-trip a blob out of the store (verified on read)");
237 let head = Commit::get(&store, now.workspaces.get("bob").unwrap()).unwrap();
238 if let TreeEntry::Conflict { left, .. } = &head.tree["main.rs"] {
239 let bytes = get_blob(&store, left).unwrap();
240 println!(" left side of bob's conflict, straight from the chunk store:");
241 for l in String::from_utf8_lossy(&bytes).lines() {
242 println!(" | {l}");
243 }
244 }
245
246 section("7. The daemon (L3): a real `git` push through choir-node");
247 let work = std::env::temp_dir().join(format!("choir-demo-{}", std::process::id()));
248 std::fs::create_dir_all(&work).unwrap();
249 let node = choir_node::Node::bind(&work.join("repos"), 0).unwrap();
250 let port = node.port();
251 node.create_repo("demo/hello.git").unwrap();
252 let node = std::sync::Arc::new(node);
253 {
254 let node = node.clone();
255 std::thread::spawn(move || node.serve_forever());
256 }
257 let url = format!("http://127.0.0.1:{port}/demo/hello.git");
258 println!(" daemon up at {url}");
259 let clone_dir = work.join("clone");
260 let git = |dir: &std::path::Path, args: &[&str]| {
261 let out = std::process::Command::new("git")
262 .args([
263 "-c",
264 "commit.gpgsign=false",
265 "-c",
266 "init.defaultBranch=main",
267 ])
268 .args(args)
269 .current_dir(dir)
270 .env("GIT_TERMINAL_PROMPT", "0")
271 .env("GIT_AUTHOR_NAME", "demo")
272 .env("GIT_AUTHOR_EMAIL", "demo@choir")
273 .env("GIT_COMMITTER_NAME", "demo")
274 .env("GIT_COMMITTER_EMAIL", "demo@choir")
275 .output()
276 .expect("git runs");
277 assert!(
278 out.status.success(),
279 "git {args:?}: {}",
280 String::from_utf8_lossy(&out.stderr)
281 );
282 String::from_utf8_lossy(&out.stdout).into_owned()
283 };
284 git(&work, &["clone", "-q", &url, clone_dir.to_str().unwrap()]);
285 std::fs::write(clone_dir.join("README.md"), "# pushed through choir-node\n").unwrap();
286 git(&clone_dir, &["add", "."]);
287 git(&clone_dir, &["commit", "-q", "-m", "hello from the demo"]);
288 git(&clone_dir, &["push", "-q", "origin", "HEAD:main"]);
289 println!(" cloned, committed, pushed. Fresh clone sees:");
290 let verify_dir = work.join("verify");
291 git(&work, &["clone", "-q", &url, verify_dir.to_str().unwrap()]);
292 let log_out = git(&verify_dir, &["log", "--oneline"]);
293 for l in log_out.lines() {
294 println!(" | {l}");
295 }
296 node.unblock();
297 std::fs::remove_dir_all(&work).ok();
298
299 println!("\nAll seven layers exercised. This is the stack the plan calls L0-L8.");
300}