spec_driven_docs/gates/
instance_manifest.rs1use crate::domain::finding::Finding;
9use crate::domain::manifest::{MANIFEST_PATH, Manifest};
10use crate::domain::rule_id::RuleId;
11use crate::gates::{GateCtx, GateResult, Violation};
12
13pub const CITES: &[RuleId] = &[RuleId::TheManifestStaysReadable];
15
16pub fn run(ctx: &GateCtx, _files: &[String]) -> GateResult {
22 let violation = |detail: &str| {
23 vec![Violation::Finding(Finding::on_file(
24 RuleId::TheManifestStaysReadable,
25 MANIFEST_PATH,
26 detail,
27 ))]
28 };
29 if ctx.retained([MANIFEST_PATH]).is_empty() {
32 return Ok(vec![]);
33 }
34 let Ok(text) = std::fs::read_to_string(ctx.path(MANIFEST_PATH)) else {
35 return Ok(violation("invalid manifest shape"));
36 };
37 match Manifest::parse(&text) {
38 Ok(_) => Ok(vec![]),
39 Err(error) => Ok(violation(&format!("invalid manifest shape: {error}"))),
40 }
41}
42
43#[cfg(test)]
44mod tests {
45 use super::*;
46
47 fn run_with_manifest(json: Option<&str>) -> Vec<String> {
48 let dir = tempfile::tempdir().unwrap();
49 if let Some(json) = json {
50 let instance = dir.path().join(".spec-driven-docs");
51 std::fs::create_dir_all(&instance).unwrap();
52 std::fs::write(instance.join("manifest.json"), json).unwrap();
53 }
54 let ctx = GateCtx::new(dir.path().to_str().unwrap());
55 run(&ctx, &[])
56 .unwrap()
57 .iter()
58 .map(ToString::to_string)
59 .collect()
60 }
61
62 const VALID: &str = r#"{
63 "schema_version": 3,
64 "canon_version": "0.2.0",
65 "canon_source": "https://github.com/gubasso/spec-driven-docs",
66 "profile": "knowledge-base",
67 "docs_root": "_docs",
68 "installed_at": "2026-08-25T00:00:00Z",
69 "managed_files": [
70 {"source": ".markdownlint/spec.markdownlint-cli2.jsonc",
71 "destination": ".spec-driven-docs/markdownlint/spec.markdownlint-cli2.jsonc",
72 "sha256": "dc17d596ae2c196cc01b439c291416f91198cc274e2376fd01a4d614c1ff60ad"}
73 ],
74 "adopted_files": [],
75 "integration_blocks": []
76 }"#;
77
78 #[test]
79 fn accepts_a_current_schema_manifest() {
80 assert!(run_with_manifest(Some(VALID)).is_empty());
81 }
82
83 #[test]
84 fn rejects_a_missing_or_older_manifest() {
85 let missing = run_with_manifest(None);
86 assert_eq!(missing.len(), 1);
87 assert!(missing[0].contains("instance:the-manifest-stays-readable"));
88
89 let older = VALID.replace("\"schema_version\": 3", "\"schema_version\": 1");
90 let out = run_with_manifest(Some(&older));
91 assert_eq!(out.len(), 1);
92 assert!(out[0].contains("invalid manifest shape: manifest schema_version 1"));
93 }
94}