Skip to main content

specfold_core/corpus/
reader.rs

1//! Loads the corpus: owners, spec roots, documents, strays and aliases.
2
3use 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
91    #[test]
92    fn the_alias_table_and_check_baseline_are_not_strays() {
93        let tmp = tempfile::tempdir().unwrap();
94        let r = tmp.path();
95        write(r, ".spec/_aliases.tsv", "# old\tnew\n");
96        write(r, ".spec/_baseline.json", "{}");
97        write(r, ".spec/notes.txt", "x");
98        let mut raw = RawConfig::default();
99        raw.aliases.file = Some(".spec/_aliases.tsv".into());
100        raw.check.baseline = Some(".spec/_baseline.json".into());
101        let c = Corpus::load(Config::from_raw(r.to_path_buf(), raw, false), &LoadOptions::default());
102        let strays: Vec<&str> = c.strays.iter().map(|s| s.path.as_str()).collect();
103        assert_eq!(strays, [".spec/notes.txt"]);
104    }
105}