spec_driven_docs/gates/
paths.rs1use camino::{Utf8Path, Utf8PathBuf};
11
12use crate::domain::manifest::MANIFEST_PATH;
13use crate::gates::GateCtx;
14
15#[must_use]
17pub fn docs_root(ctx: &GateCtx) -> Utf8PathBuf {
18 if let Ok(text) = std::fs::read_to_string(ctx.path(MANIFEST_PATH))
19 && let Ok(value) = serde_json::from_str::<serde_json::Value>(&text)
20 && let Some(root) = value.get("docs_root").and_then(serde_json::Value::as_str)
21 && !root.is_empty()
22 {
23 return Utf8PathBuf::from(root);
24 }
25 for candidate in ["_docs", "docs"] {
26 if ctx.path(candidate).join("specs").is_dir() {
27 return Utf8PathBuf::from(candidate);
28 }
29 }
30 Utf8PathBuf::from("_docs")
31}
32
33#[must_use]
37pub fn ki_record_roots(ctx: &GateCtx, args: &[String]) -> Vec<Utf8PathBuf> {
38 if !args.is_empty() {
39 return args.iter().map(Utf8PathBuf::from).collect();
40 }
41 if ctx.path(MANIFEST_PATH).is_file() {
42 return vec![docs_root(ctx).join("reference/known-issues")];
43 }
44 ["_docs", "docs"]
45 .into_iter()
46 .map(|candidate| Utf8Path::new(candidate).join("reference/known-issues"))
47 .filter(|root| ctx.path(root).is_dir())
48 .collect()
49}
50
51#[must_use]
53pub fn ki_records(ctx: &GateCtx, args: &[String]) -> Vec<Utf8PathBuf> {
54 let mut records = Vec::new();
55 for root in ki_record_roots(ctx, args) {
56 let Ok(entries) = ctx.path(&root).read_dir_utf8() else {
57 continue;
58 };
59 let mut names: Vec<String> = entries
60 .filter_map(Result::ok)
61 .filter(|entry| entry.file_type().is_ok_and(|kind| kind.is_file()))
62 .map(|entry| entry.file_name().to_string())
63 .filter(|name| {
64 name.strip_prefix("KI-")
65 .and_then(|rest| rest.strip_suffix(".md"))
66 .is_some_and(|slug| !slug.is_empty())
67 })
68 .collect();
69 names.sort();
70 records.extend(names.into_iter().map(|name| root.join(name)));
71 }
72 records
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 fn ctx(dir: &tempfile::TempDir) -> GateCtx {
80 GateCtx::new(dir.path().to_str().unwrap())
81 }
82
83 fn write(dir: &tempfile::TempDir, path: &str, text: &str) {
84 let path = dir.path().join(path);
85 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
86 std::fs::write(path, text).unwrap();
87 }
88
89 #[test]
90 fn manifest_root_wins() {
91 let dir = tempfile::tempdir().unwrap();
92 write(
93 &dir,
94 ".spec-driven-docs/manifest.json",
95 "{\n \"docs_root\": \"docs\"\n}\n",
96 );
97 assert_eq!(docs_root(&ctx(&dir)), "docs");
98 }
99
100 #[test]
101 fn roots_are_discovered_without_a_manifest() {
102 let dir = tempfile::tempdir().unwrap();
103 write(&dir, "docs/specs/SPEC-sample.md", "# S\n");
104 assert_eq!(docs_root(&ctx(&dir)), "docs");
105
106 let both = tempfile::tempdir().unwrap();
107 write(&both, "_docs/specs/SPEC-sample.md", "# S\n");
108 write(&both, "docs/specs/SPEC-sample.md", "# S\n");
109 assert_eq!(docs_root(&ctx(&both)), "_docs");
110
111 let neither = tempfile::tempdir().unwrap();
112 assert_eq!(docs_root(&ctx(&neither)), "_docs");
113 }
114
115 #[test]
116 fn record_arguments_win_over_discovery() {
117 let dir = tempfile::tempdir().unwrap();
118 write(&dir, "docs/reference/known-issues/KI-real.md", "# R\n");
119 let roots = ki_record_roots(&ctx(&dir), &["tests/fixtures".to_string()]);
120 assert_eq!(roots, vec![Utf8PathBuf::from("tests/fixtures")]);
121 }
122
123 #[test]
124 fn records_follow_the_manifest_root() {
125 let dir = tempfile::tempdir().unwrap();
126 write(
127 &dir,
128 ".spec-driven-docs/manifest.json",
129 "{\n \"docs_root\": \"docs\"\n}\n",
130 );
131 write(&dir, "docs/reference/known-issues/KI-vendor.md", "# V\n");
132 write(&dir, "docs/reference/known-issues/KI-.md", "# empty slug\n");
133 write(
134 &dir,
135 "docs/reference/known-issues/notes.md",
136 "# not a record\n",
137 );
138 assert_eq!(
139 ki_records(&ctx(&dir), &[]),
140 vec![Utf8PathBuf::from(
141 "docs/reference/known-issues/KI-vendor.md"
142 )]
143 );
144 }
145
146 #[test]
147 fn bare_consumer_roots_are_discovered() {
148 let dir = tempfile::tempdir().unwrap();
149 write(&dir, "docs/reference/known-issues/KI-a.md", "# A\n");
150 write(&dir, "docs/reference/known-issues/KI-b.md", "# B\n");
151 assert_eq!(
152 ki_records(&ctx(&dir), &[]),
153 vec![
154 Utf8PathBuf::from("docs/reference/known-issues/KI-a.md"),
155 Utf8PathBuf::from("docs/reference/known-issues/KI-b.md"),
156 ]
157 );
158 }
159}