Skip to main content

fallow_types/
guard.rs

1//! Guard report contracts for pre-edit architecture guidance.
2
3/// Per-file guard report for one or more requested paths.
4#[derive(Debug, Clone, serde::Serialize)]
5#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
6pub struct GuardReport {
7    /// Reports in the same order as the requested files.
8    pub files: Vec<GuardFileReport>,
9}
10
11/// Guard information for a single project-root-relative path.
12#[derive(Debug, Clone, serde::Serialize)]
13#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
14pub struct GuardFileReport {
15    /// Project-root-relative path using forward slashes.
16    pub path: String,
17    /// Whether the path currently exists on disk.
18    pub exists: bool,
19    /// Boundary zone classification for this file, when any zone matches.
20    pub zone: Option<GuardZone>,
21    /// Boundary rules that apply to this file.
22    pub boundary: GuardBoundary,
23    /// Rule-pack policy rules in scope for this file.
24    pub policy_rules: Vec<GuardPolicyRule>,
25    /// Effective severities for rule families relevant to guard output.
26    pub severities: GuardSeverities,
27    /// Human-readable notes for unrestricted or degraded cases.
28    pub notes: Vec<String>,
29}
30
31/// Boundary zone matched by a guard target.
32#[derive(Debug, Clone, serde::Serialize)]
33#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
34pub struct GuardZone {
35    /// Zone name from the boundary configuration.
36    pub name: String,
37    /// Configured glob patterns that define the zone.
38    pub patterns: Vec<String>,
39}
40
41/// Boundary permissions and call restrictions for a guard target.
42#[derive(Debug, Clone, serde::Serialize)]
43#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
44pub struct GuardBoundary {
45    /// Whether boundary zones are configured at all.
46    pub configured: bool,
47    /// Whether boundary imports are unrestricted for this file.
48    pub unrestricted: bool,
49    /// Zones this file may import from.
50    pub allowed_zones: Vec<String>,
51    /// Zones this file may import from with type-only imports.
52    pub allowed_type_only_zones: Vec<String>,
53    /// Forbidden callee patterns for the file's zone.
54    pub forbidden_calls: Vec<String>,
55    /// Whether boundary coverage requires this file to belong to a zone.
56    pub coverage_required: bool,
57}
58
59/// Rule-pack policy rule that applies to a guard target.
60#[derive(Debug, Clone, serde::Serialize)]
61#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
62pub struct GuardPolicyRule {
63    /// Rule-pack name.
64    pub pack: String,
65    /// Rule id inside the pack.
66    pub rule_id: String,
67    /// Rule kind in kebab-case.
68    pub kind: String,
69    /// Matcher patterns for the rule, such as callees, import specifiers, or effects.
70    pub patterns: Vec<String>,
71    /// Optional rule-authored remediation message.
72    pub message: Option<String>,
73    /// Effective severity for this rule at the target path.
74    pub severity: String,
75    /// Scoped suppression token for this specific policy rule.
76    pub suppress_token: String,
77}
78
79/// Effective guard-relevant rule severities for a target path.
80#[derive(Debug, Clone, serde::Serialize)]
81#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
82pub struct GuardSeverities {
83    /// Effective severity of boundary-violation findings.
84    pub boundary_violation: String,
85    /// Effective severity of policy-violation findings.
86    pub policy_violation: String,
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn guard_file_report_serializes_expected_wire_shape() {
95        let report = GuardReport {
96            files: vec![GuardFileReport {
97                path: "src/domain/user.ts".to_string(),
98                exists: false,
99                zone: None,
100                boundary: GuardBoundary {
101                    configured: true,
102                    unrestricted: true,
103                    allowed_zones: vec![],
104                    allowed_type_only_zones: vec![],
105                    forbidden_calls: vec!["child_process.*".to_string()],
106                    coverage_required: true,
107                },
108                policy_rules: vec![GuardPolicyRule {
109                    pack: "team-policy".to_string(),
110                    rule_id: "pure-domain".to_string(),
111                    kind: "banned-effect".to_string(),
112                    patterns: vec!["network".to_string()],
113                    message: Some("Inject effects via ports.".to_string()),
114                    severity: "warn".to_string(),
115                    suppress_token: "policy-violation:team-policy/pure-domain".to_string(),
116                }],
117                severities: GuardSeverities {
118                    boundary_violation: "error".to_string(),
119                    policy_violation: "warn".to_string(),
120                },
121                notes: vec!["Files outside every zone are unrestricted.".to_string()],
122            }],
123        };
124
125        let json = serde_json::to_value(report).unwrap();
126        let file = &json["files"][0];
127        assert_eq!(file["path"], "src/domain/user.ts");
128        assert_eq!(file["exists"], false);
129        assert!(file["zone"].is_null());
130        assert_eq!(file["boundary"]["allowed_zones"], serde_json::json!([]));
131        assert_eq!(
132            file["boundary"]["allowed_type_only_zones"],
133            serde_json::json!([])
134        );
135        assert_eq!(file["boundary"]["coverage_required"], true);
136        assert_eq!(file["policy_rules"][0]["rule_id"], "pure-domain");
137        assert_eq!(
138            file["policy_rules"][0]["suppress_token"],
139            "policy-violation:team-policy/pure-domain"
140        );
141        assert_eq!(file["severities"]["boundary_violation"], "error");
142    }
143}