1use std::fmt;
10
11use camino::Utf8PathBuf;
12use clap::ValueEnum;
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum, Serialize, Deserialize)]
17#[value(rename_all = "kebab-case")]
18#[serde(rename_all = "kebab-case")]
19pub enum ProfileId {
20 Codebase,
22 KnowledgeBase,
24}
25
26impl ProfileId {
27 #[must_use]
29 pub const fn profile(self) -> &'static Profile {
30 match self {
31 Self::Codebase => &CODEBASE,
32 Self::KnowledgeBase => &KNOWLEDGE_BASE,
33 }
34 }
35
36 #[must_use]
38 pub const fn as_str(self) -> &'static str {
39 match self {
40 Self::Codebase => "codebase",
41 Self::KnowledgeBase => "knowledge-base",
42 }
43 }
44}
45
46impl fmt::Display for ProfileId {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 f.write_str(self.as_str())
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
54pub enum DocsRoot {
55 #[serde(rename = "docs")]
57 Docs,
58 #[serde(rename = "_docs")]
60 UnderscoreDocs,
61}
62
63impl DocsRoot {
64 #[must_use]
66 pub const fn as_str(self) -> &'static str {
67 match self {
68 Self::Docs => "docs",
69 Self::UnderscoreDocs => "_docs",
70 }
71 }
72}
73
74impl fmt::Display for DocsRoot {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 f.write_str(self.as_str())
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct Projection {
86 pub source: &'static str,
88 pub destination: &'static str,
90}
91
92const fn proj(source: &'static str, destination: &'static str) -> Projection {
93 Projection {
94 source,
95 destination,
96 }
97}
98
99#[must_use]
101#[allow(clippy::literal_string_with_formatting_args)]
103pub fn resolve_destination(destination: &str, docs_root: DocsRoot) -> Utf8PathBuf {
104 Utf8PathBuf::from(destination.replace("{docs_root}", docs_root.as_str()))
105}
106
107#[derive(Debug)]
109pub struct Profile {
110 pub id: ProfileId,
112 pub docs_root: DocsRoot,
114 pub managed: &'static [Projection],
116 pub adopted: &'static [Projection],
118}
119
120const MANAGED: &[Projection] = &[
127 proj(
128 ".markdownlint/adr.markdownlint-cli2.jsonc",
129 ".spec-driven-docs/markdownlint/adr.markdownlint-cli2.jsonc",
130 ),
131 proj(
132 ".markdownlint/spec.markdownlint-cli2.jsonc",
133 ".spec-driven-docs/markdownlint/spec.markdownlint-cli2.jsonc",
134 ),
135 proj(
136 ".markdownlint/relative-links.markdownlint-cli2.jsonc",
137 ".spec-driven-docs/markdownlint/relative-links.markdownlint-cli2.jsonc",
138 ),
139 proj(
144 "third-party/simpleenglish/LICENSE",
145 ".spec-driven-docs/upstreams/simpleenglish/LICENSE",
146 ),
147 proj(
148 "third-party/simpleenglish/prompts/system-prompt.md",
149 ".spec-driven-docs/upstreams/simpleenglish/prompts/system-prompt.md",
150 ),
151 proj(
152 "third-party/simpleenglish/skills/simple-english/SKILL.md",
153 ".spec-driven-docs/upstreams/simpleenglish/skills/simple-english/SKILL.md",
154 ),
155 proj(
156 "third-party/simpleenglish/skills/simple-english/references/checklist.md",
157 ".spec-driven-docs/upstreams/simpleenglish/skills/simple-english/references/checklist.md",
158 ),
159 proj(
160 "third-party/simpleenglish/skills/simple-english/references/strict-vocabulary.md",
161 ".spec-driven-docs/upstreams/simpleenglish/skills/simple-english/references/strict-vocabulary.md",
162 ),
163 proj(
164 "third-party/simpleenglish/skills/simple-english/references/use-cases.md",
165 ".spec-driven-docs/upstreams/simpleenglish/skills/simple-english/references/use-cases.md",
166 ),
167 proj(
168 "third-party/simpleenglish/skills/simple-english/references/word-swaps.md",
169 ".spec-driven-docs/upstreams/simpleenglish/skills/simple-english/references/word-swaps.md",
170 ),
171];
172
173pub const SIMPLE_ENGLISH_MANAGED: &[&str] = &[
179 "third-party/simpleenglish/LICENSE",
180 "third-party/simpleenglish/prompts/system-prompt.md",
181 "third-party/simpleenglish/skills/simple-english/SKILL.md",
182 "third-party/simpleenglish/skills/simple-english/references/checklist.md",
183 "third-party/simpleenglish/skills/simple-english/references/strict-vocabulary.md",
184 "third-party/simpleenglish/skills/simple-english/references/use-cases.md",
185 "third-party/simpleenglish/skills/simple-english/references/word-swaps.md",
186];
187
188pub const CANON_TEMPLATES: &[&str] = &[
196 "_docs/decisions/TEMPLATE-adr.md",
197 "_docs/reference/TEMPLATE-agents-digest.md",
198];
199
200const ADOPTED: &[Projection] = &[
201 proj(
202 "_docs/specs/SPEC-decision-records.md",
203 "{docs_root}/specs/SPEC-decision-records.md",
204 ),
205 proj(
206 "_docs/specs/SPEC-instance.md",
207 "{docs_root}/specs/SPEC-instance.md",
208 ),
209 proj(
210 "_docs/specs/SPEC-docs-format.md",
211 "{docs_root}/specs/SPEC-docs-format.md",
212 ),
213 proj(
214 "_docs/specs/SPEC-docs-foundations.md",
215 "{docs_root}/specs/SPEC-docs-foundations.md",
216 ),
217 proj(
218 "_docs/specs/SPEC-docs-specs.md",
219 "{docs_root}/specs/SPEC-docs-specs.md",
220 ),
221 proj(
222 "_docs/specs/SPEC-comparison-docs.md",
223 "{docs_root}/specs/SPEC-comparison-docs.md",
224 ),
225 proj(
226 "_docs/specs/SPEC-known-issues.md",
227 "{docs_root}/specs/SPEC-known-issues.md",
228 ),
229 proj(
230 "_docs/specs/SPEC-spec-to-code.md",
231 "{docs_root}/specs/SPEC-spec-to-code.md",
232 ),
233 proj(
234 "_docs/specs/SPEC-guides.md",
235 "{docs_root}/specs/SPEC-guides.md",
236 ),
237 proj(
238 "_docs/specs/SPEC-simple-english.md",
239 "{docs_root}/specs/SPEC-simple-english.md",
240 ),
241 proj(
242 "_docs/specs/SPEC-tracking.md",
243 "{docs_root}/specs/SPEC-tracking.md",
244 ),
245 proj(
246 "_docs/specs/SPEC-tracking/tracking.schema.json",
247 "{docs_root}/specs/SPEC-tracking/tracking.schema.json",
248 ),
249 proj(
250 "templates/TEMPLATE-tracking.yaml",
251 "{docs_root}/reference/tracking.yaml",
252 ),
253 proj(
254 "templates/TEMPLATE-spec.md",
255 "{docs_root}/specs/TEMPLATE-spec.md",
256 ),
257 proj(
258 "templates/TEMPLATE-adr.md",
259 "{docs_root}/decisions/TEMPLATE-adr.md",
260 ),
261 proj(
262 "templates/TEMPLATE-agents-digest.md",
263 "{docs_root}/reference/TEMPLATE-agents-digest.md",
264 ),
265 proj(
266 "templates/TEMPLATE-guide.md",
267 "{docs_root}/guides/TEMPLATE-guide.md",
268 ),
269 proj(
270 "templates/TEMPLATE-known-issue.md",
271 "{docs_root}/reference/TEMPLATE-known-issue.md",
272 ),
273];
274
275static CODEBASE: Profile = Profile {
276 id: ProfileId::Codebase,
277 docs_root: DocsRoot::Docs,
278 managed: MANAGED,
279 adopted: ADOPTED,
280};
281
282static KNOWLEDGE_BASE: Profile = Profile {
283 id: ProfileId::KnowledgeBase,
284 docs_root: DocsRoot::UnderscoreDocs,
285 managed: MANAGED,
286 adopted: ADOPTED,
287};
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn profiles_bind_their_roots() {
295 assert_eq!(ProfileId::Codebase.profile().docs_root, DocsRoot::Docs);
296 assert_eq!(
297 ProfileId::KnowledgeBase.profile().docs_root,
298 DocsRoot::UnderscoreDocs
299 );
300 }
301
302 #[test]
303 fn destinations_resolve_per_root() {
304 assert_eq!(
305 resolve_destination("{docs_root}/specs/SPEC-distribution.md", DocsRoot::Docs),
306 Utf8PathBuf::from("docs/specs/SPEC-distribution.md")
307 );
308 assert_eq!(
309 resolve_destination(
310 ".spec-driven-docs/markdownlint/x.jsonc",
311 DocsRoot::UnderscoreDocs
312 ),
313 Utf8PathBuf::from(".spec-driven-docs/markdownlint/x.jsonc")
314 );
315 }
316
317 #[test]
318 fn serde_uses_the_kebab_names() {
319 assert_eq!(
320 serde_json::to_string(&ProfileId::KnowledgeBase).unwrap(),
321 "\"knowledge-base\""
322 );
323 assert_eq!(
324 serde_json::to_string(&DocsRoot::UnderscoreDocs).unwrap(),
325 "\"_docs\""
326 );
327 }
328
329 #[test]
330 fn destination_templates_only_use_the_placeholder_in_adopted_paths() {
331 for entry in MANAGED {
332 assert!(
333 !entry.destination.contains('{'),
334 "{} is templated",
335 entry.destination
336 );
337 }
338 for entry in ADOPTED {
339 assert!(
340 entry.destination.starts_with("{docs_root}/"),
341 "{} is not rooted",
342 entry.destination
343 );
344 }
345 }
346}