1use std::path::{Path, PathBuf};
9use std::sync::atomic::{AtomicU64, Ordering};
10
11use crate::capability::{builtin, CapabilityRegistry, Context};
12use crate::discovery::{FileSystem, Sources};
13use crate::git::GitState;
14use crate::index::Index;
15use crate::metadata::KindSchema;
16use crate::repository::Repository;
17use crate::share::Share;
18
19const MANIFEST: &str = "schema: ai-repository/v1\nrepo:\n path: repo\nlocal:\n path: local\n tracked: false\n implicit_context: false\nsections:\n policy: repo/policy.yaml\n profiles: repo/profiles\n rules: repo/rules\n prompts: repo/prompts\n skills: repo/skills\n workflows: repo/workflows\n knowledge: repo/knowledge\n adrs: repo/adrs\n project: repo/project\n";
20const SOURCES: &str = "version: 1\nsources:\n - id: policy\n kind: policy\n discovery: vcs\n pathspec: ':(glob).ai/repo/policy.yaml'\n required: true\n - id: profile\n kind: profile\n discovery: vcs\n pathspec: ':(glob).ai/repo/profiles/*.yaml'\n required: true\n - id: rule\n kind: rule\n discovery: vcs\n pathspec: ':(glob).ai/repo/rules/**/*.md'\n required: true\n - id: prompt\n kind: prompt\n discovery: vcs\n pathspec: ':(glob).ai/repo/prompts/*.md'\n required: false\n - id: document\n kind: document\n discovery: vcs\n pathspec: ':(glob)docs/*.md'\n required: false\n";
21
22static SEQ: AtomicU64 = AtomicU64::new(0);
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct Shape {
27 pub rules: usize,
29 pub prompts: usize,
31 pub documents: usize,
33 pub body_lines: usize,
35}
36
37impl Default for Shape {
38 fn default() -> Self {
39 Shape {
40 rules: 10,
41 prompts: 2,
42 documents: 5,
43 body_lines: 8,
44 }
45 }
46}
47
48#[derive(Debug)]
50pub struct SyntheticRepository {
51 root: PathBuf,
52 pub shape: Shape,
54}
55
56impl SyntheticRepository {
57 pub fn new(shape: Shape) -> std::io::Result<Self> {
59 let seq = SEQ.fetch_add(1, Ordering::SeqCst);
60 let root =
61 std::env::temp_dir().join(format!("majordomus-synthetic-{}-{seq}", std::process::id()));
62 std::fs::create_dir_all(&root)?;
63 let repo = SyntheticRepository { root, shape };
64 repo.write(".ai/manifest.yaml", MANIFEST)?;
65 repo.write(
66 ".ai/repo/policy.yaml",
67 "version: 1\ncontext:\n always_loaded_budget_lines: 150\n",
68 )?;
69 repo.write(".ai/repo/profiles/implementation.yaml", "name: implementation\ndescription: d\ncapability: standard\neffort: medium\nverbosity: concise\npresentation: engineering\ncheckpoint_interval: 15m\n")?;
70 repo.write(".ai/repo/knowledge/sources.yaml", SOURCES)?;
71 for i in 0..shape.rules {
72 repo.write(
73 &format!(".ai/repo/rules/project/rule-{i}.v1.md"),
74 &rule(i, shape.body_lines),
75 )?;
76 }
77 for i in 0..shape.prompts {
78 repo.write(
79 &format!(".ai/repo/prompts/prompt-{i}.md"),
80 &format!("---\nname: prompt-{i}\ndescription: Prompt number {i}.\n---\n\n# Prompt {i}\n\n{{{{CONTEXT}}}}\n"),
81 )?;
82 }
83 for i in 0..shape.documents {
84 let body: String = (0..shape.body_lines)
85 .map(|l| {
86 format!("Line {l} of document {i}: the quick brown fox names scope and rule.\n")
87 })
88 .collect();
89 repo.write(
90 &format!("docs/DOC_{i}.md"),
91 &format!("# Document {i}\n\n{body}"),
92 )?;
93 }
94 Ok(repo)
95 }
96
97 pub fn small() -> std::io::Result<Self> {
99 Self::new(Shape::default())
100 }
101
102 pub fn root(&self) -> &Path {
104 &self.root
105 }
106
107 fn write(&self, rel: &str, content: &str) -> std::io::Result<()> {
108 let p = self.root.join(rel);
109 if let Some(dir) = p.parent() {
110 std::fs::create_dir_all(dir)?;
111 }
112 std::fs::write(p, content)
113 }
114
115 pub fn touch_rule(&self, i: usize, suffix: &str) -> std::io::Result<()> {
117 let rel = format!(".ai/repo/rules/project/rule-{i}.v1.md");
118 let text = std::fs::read_to_string(self.root.join(&rel))?;
119 self.write(&rel, &format!("{text}\n{suffix}\n"))
120 }
121
122 pub fn index(&self) -> crate::error::Result<Index> {
125 let repo = Repository::discover(&self.root)?;
126 let sources = Sources::load(&repo)?;
127 let share = Share::locate(Some(&crate_share()), &self.root)?;
128 let schema = KindSchema::load(&share, &repo)?;
129 let fs = FileSystem {
130 excluded: vec![".git".into(), repo.local_path()],
131 };
132 let scope = crate::scope::Scope::load(&share, &repo)?;
133 Index::build(
134 &repo,
135 &sources,
136 &schema,
137 &fs,
138 GitState::Unavailable {
139 reason: "synthetic repository".into(),
140 },
141 scope,
142 )
143 }
144
145 pub fn context(&self) -> crate::error::Result<std::sync::Arc<Context>> {
147 let index = self.index()?;
148 let registry = CapabilityRegistry::builder()
149 .with_modules(builtin::modules())
150 .with_index(&index)
151 .build()
152 .map_err(|errors| crate::error::Error::Registry { errors })?;
153 Ok(std::sync::Arc::new(Context::new(
154 std::sync::Arc::new(index),
155 std::sync::Arc::new(registry),
156 )))
157 }
158}
159
160#[cfg(test)]
162const PLAN_SOURCES: &str = "
163 - id: milestone
164 kind: milestone
165 discovery: vcs
166 pathspec: ':(glob).ai/repo/project/milestones/*.yaml'
167 required: false
168 - id: issue
169 kind: issue
170 discovery: vcs
171 pathspec: ':(glob).ai/repo/project/issues/*.yaml'
172 required: false
173";
174
175#[cfg(test)]
176fn plan_milestone_record(id: &str, order: u32, depends_on: &[&str]) -> String {
177 format!(
178 "id: {id}\ntitle: The milestone {id}\nslug: {id}\norder: {order}\npriority: p1\n\
179 problem: \"A problem.\"\noutcome: \"The outcome of {id}.\"\n\
180 depends_on: [{}]\nacceptance_criteria:\n - It is reached\n\
181 validation:\n - \"true\"\nevidence_required:\n - proof\n",
182 depends_on.join(", ")
183 )
184}
185
186#[cfg(test)]
187fn plan_issue_record(id: &str, milestone: &str, scope: &str, depends_on: &[&str]) -> String {
188 format!(
189 "id: {id}\nmilestone: {milestone}\ntitle: The work of {id}\nslug: work-{id}\n\
190 priority: p1\nprofile: implementation\nobjective: \"Do {id}.\"\n\
191 scope:\n - {scope}\ndepends_on: [{}]\nacceptance_criteria:\n - {id} is done\n\
192 validation:\n - \"true\"\nevidence_required:\n - proof\n",
193 depends_on.join(", ")
194 )
195}
196
197#[cfg(test)]
198impl SyntheticRepository {
199 pub(crate) fn planned() -> SyntheticRepository {
203 let repo = SyntheticRepository::small().expect("a synthetic repository");
204 assert!(std::process::Command::new("git")
207 .arg("-C")
208 .arg(repo.root())
209 .args(["init", "-q"])
210 .status()
211 .expect("git")
212 .success());
213 let write = |rel: &str, body: &str| {
214 let path = repo.root().join(rel);
215 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
216 std::fs::write(path, body).unwrap();
217 };
218 let sources = repo.root().join(".ai/repo/knowledge/sources.yaml");
219 let mut text = std::fs::read_to_string(&sources).unwrap();
220 text.push_str(PLAN_SOURCES);
221 std::fs::write(&sources, text).unwrap();
222 write(
223 ".ai/repo/project/project.yaml",
224 "schema_version: 1\nname: Synthetic\nrepository: example/synthetic\ndefault_branch: master\n",
225 );
226 for (id, order, deps) in [
227 ("m-first", 0, vec![]),
228 ("m-second", 1, vec!["m-first"]),
229 ("m-loop", 2, vec![]),
230 ] {
231 write(
232 &format!(".ai/repo/project/milestones/{id}.yaml"),
233 &plan_milestone_record(id, order, &deps),
234 );
235 }
236 for (id, milestone, scope, deps) in [
237 ("I0001", "m-first", "lib", vec![]),
238 ("I0002", "m-first", "docs", vec!["I0001"]),
239 ("I0003", "m-first", "lib", vec![]),
240 ("I0010", "m-loop", "lib", vec!["I0011"]),
241 ("I0011", "m-loop", "lib", vec!["I0010"]),
242 ] {
243 write(
244 &format!(".ai/repo/project/issues/{id}.yaml"),
245 &plan_issue_record(id, milestone, scope, &deps),
246 );
247 }
248 write(
250 ".ai/repo/project/issues/I0021.yaml",
251 "id: I0021\nmilestone: m-loop\ntitle: The evidenced work\nslug: work-I0021\n\
252 priority: p2\nprofile: implementation\nobjective: \"Prove it.\"\n\
253 scope:\n - share\nnon_scope:\n - share/elsewhere\n\
254 acceptance_criteria:\n - It is proven\nvalidation:\n - \"true\"\n\
255 evidence_required:\n - proof\nevidence:\n - covers: proof\n type: manual\n\
256 \x20 command: \"true\"\n result: \"it held\"\n",
257 );
258 repo
259 }
260}
261
262impl Drop for SyntheticRepository {
263 fn drop(&mut self) {
264 let _ = std::fs::remove_dir_all(&self.root);
265 }
266}
267
268pub fn crate_share() -> PathBuf {
270 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../share")
271}
272
273fn rule(i: usize, body_lines: usize) -> String {
274 let body: String = (0..body_lines)
275 .map(|l| format!("Rationale line {l} of rule {i}: keep the scope and check the state.\n"))
276 .collect();
277 format!("---\nid: project.rule-{i}\nversion: 1\nkind: rule\ntitle: Rule {i}\ndescription: Rule number {i}, in one sentence.\nstatement: Do the thing number {i}.\nstatus: active\nclass: advisory\ndepends_on: []\ntags: [synthetic]\n---\n\n# Rationale\n\n{body}")
278}