specfold_core/corpus/
reader.rs1use super::cache::ScanCache;
4use super::types::{Corpus, LoadOptions, SpecRoot};
5use super::{aliases, assemble, roots, trace, walk};
6use crate::config::Config;
7use crate::markdown::body;
8use crate::workspace::Owners;
9
10impl Corpus {
11 pub fn load(cfg: Config, opts: &LoadOptions) -> Self {
12 let t = std::time::Instant::now();
13 let owners = Owners::discover(&cfg);
14 let t = trace::phase("owners", t);
15 let mut strays = vec![];
16 let mut all_roots: Vec<SpecRoot> = roots::committed(&cfg, &owners);
17 if opts.include_local {
18 all_roots.extend(roots::local(&cfg, &owners, &mut strays));
19 }
20 let mut pending = vec![];
21 for root in &all_roots {
22 let files = walk::files(&cfg.root, &root.dir, root.tier);
23 pending.extend(assemble::group(&cfg, root, &files, &mut strays));
24 }
25 let t = trace::phase("walk", t);
26 let to_scan: Vec<String> =
27 pending.iter().flat_map(|p| std::iter::once(p.main.clone()).chain(p.chapters.iter().cloned())).collect();
28 let mut cache = ScanCache::open(&cfg);
29 let t = trace::phase("cache-open", t);
30 let mut scans = cache.scan_all(&cfg.root, &to_scan, &body::path_cite_re(cfg.spec_dir()));
31 let t = trace::phase("scan", t);
32 cache.save();
33 let t = trace::phase("cache-save", t);
34 let mut docs: Vec<_> = pending.into_iter().filter_map(|p| assemble::build(p, &mut scans)).collect();
35 docs.sort_by(|a, b| a.path.cmp(&b.path));
36 strays.sort_by(|a, b| a.path.cmp(&b.path));
37 let aliases = aliases::load(&cfg);
38 trace::phase("assemble", t);
39 Corpus { cfg, owners, roots: all_roots, docs, strays, aliases }
40 }
41}
42
43#[cfg(test)]
44mod tests {
45 use super::*;
46 use crate::config::{OwnerKind, RawConfig};
47
48 pub fn write(root: &std::path::Path, rel: &str, text: &str) {
49 let p = root.join(rel);
50 std::fs::create_dir_all(p.parent().unwrap()).unwrap();
51 std::fs::write(p, text).unwrap();
52 }
53
54 #[test]
55 fn loads_owners_docs_and_strays() {
56 let tmp = tempfile::tempdir().unwrap();
57 let r = tmp.path();
58 write(
59 r,
60 ".spec/adr/20260101000000-pick-db.adr.md",
61 "---\nid: adr:ws/20260101000000\nstatus: active\n---\n# Pick the database\nSee [[sot:shop/cart]].\n",
62 );
63 write(r, "services/shop/.spec/sot/cart.sot.md", "---\nstatus: active\n---\n# Cart\n## Totals {#CO-1}\n");
64 write(
65 r,
66 "services/shop/.spec/design/20260102000000-cart/index.design.md",
67 "---\nstatus: draft\n---\n# Cart boards\n",
68 );
69 write(r, "services/shop/.spec/design/20260102000000-cart/assets/a.webp", "x");
70 write(r, "services/shop/.spec/plans/old.md", "legacy");
71 write(r, ".spec.local/shop/research/20260103000000-draft.research.md", "# Draft\n");
72 let mut raw = RawConfig::default();
73 raw.owners.root = "ws".into();
74 raw.owners.kinds = vec![OwnerKind { pattern: "services/*".into(), id: "{1}".into(), manifest: false }];
75 let cfg = Config::from_raw(r.to_path_buf(), raw, false);
76 let c = Corpus::load(cfg, &LoadOptions { include_local: true });
77 let ids: Vec<String> = c.docs.iter().map(|d| d.id.to_string()).collect();
78 assert_eq!(
79 ids,
80 ["research:shop/20260103000000", "adr:ws/20260101000000", "design:shop/20260102000000", "sot:shop/cart"]
81 );
82 assert_eq!(c.strays.len(), 1);
83 assert_eq!(c.strays[0].path, "services/shop/.spec/plans/old.md");
84 let design = c.docs.iter().find(|d| d.id.ty == "design").unwrap();
85 assert_eq!(design.bundle_files, ["services/shop/.spec/design/20260102000000-cart/assets/a.webp"]);
86 let adr = c.docs.iter().find(|d| d.id.ty == "adr").unwrap();
87 assert_eq!(adr.links[0].item, "sot:shop/cart");
88 assert_eq!(adr.title.as_deref(), Some("Pick the database"));
89 }
90}