Skip to main content

spec_driven_docs/gates/
instance_manifest.rs

1//! Gate: the instance manifest is present and fits the schema.
2//!
3//! The manifest is what every other ownership check reads, so a shape error
4//! in it disables them all at once. Only the shape is judged here; comparing
5//! the recorded hashes to the disk is `sdd verify`, which the managed block
6//! wires as its own hook.
7
8use crate::domain::finding::Finding;
9use crate::domain::manifest::{MANIFEST_PATH, Manifest};
10use crate::domain::rule_id::RuleId;
11use crate::gates::{GateCtx, GateResult, Violation};
12
13/// The rules this gate can cite.
14pub const CITES: &[RuleId] = &[RuleId::ManifestIdentifiesEveryOwnedFile];
15
16/// Judge the manifest's shape.
17///
18/// # Errors
19///
20/// None; an unreadable manifest is the violation itself.
21pub fn run(ctx: &GateCtx, _files: &[String]) -> GateResult {
22    let violation = |detail: &str| {
23        vec![Violation::Finding(Finding::on_file(
24            RuleId::ManifestIdentifiesEveryOwnedFile,
25            MANIFEST_PATH,
26            detail,
27        ))]
28    };
29    let Ok(text) = std::fs::read_to_string(ctx.path(MANIFEST_PATH)) else {
30        return Ok(violation("invalid manifest shape"));
31    };
32    match Manifest::parse(&text) {
33        Ok(_) => Ok(vec![]),
34        Err(error) => Ok(violation(&format!("invalid manifest shape: {error}"))),
35    }
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    fn run_with_manifest(json: Option<&str>) -> Vec<String> {
43        let dir = tempfile::tempdir().unwrap();
44        if let Some(json) = json {
45            let instance = dir.path().join(".spec-driven-docs");
46            std::fs::create_dir_all(&instance).unwrap();
47            std::fs::write(instance.join("manifest.json"), json).unwrap();
48        }
49        let ctx = GateCtx::new(dir.path().to_str().unwrap());
50        run(&ctx, &[])
51            .unwrap()
52            .iter()
53            .map(ToString::to_string)
54            .collect()
55    }
56
57    const VALID: &str = r#"{
58        "schema_version": 2,
59        "canon_version": "0.2.0",
60        "canon_source": "https://github.com/gubasso/spec-driven-docs",
61        "profile": "knowledge-base",
62        "docs_root": "_docs",
63        "installed_at": "2026-08-25T00:00:00Z",
64        "managed_files": [
65            {"source": ".markdownlint/spec.markdownlint-cli2.jsonc",
66             "destination": ".spec-driven-docs/markdownlint/spec.markdownlint-cli2.jsonc",
67             "sha256": "dc17d596ae2c196cc01b439c291416f91198cc274e2376fd01a4d614c1ff60ad"}
68        ],
69        "adopted_files": [],
70        "integration_blocks": []
71    }"#;
72
73    #[test]
74    fn accepts_a_schema_two_manifest() {
75        assert!(run_with_manifest(Some(VALID)).is_empty());
76    }
77
78    #[test]
79    fn rejects_a_missing_or_older_manifest() {
80        let missing = run_with_manifest(None);
81        assert_eq!(missing.len(), 1);
82        assert!(missing[0].contains("distribution:manifest-identifies-every-owned-file"));
83
84        let older = VALID.replace("\"schema_version\": 2", "\"schema_version\": 1");
85        let out = run_with_manifest(Some(&older));
86        assert_eq!(out.len(), 1);
87        assert!(out[0].contains("invalid manifest shape: manifest schema_version 1"));
88    }
89}