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    /// Producer locations permitted by a gdp-ts proof rule.
72    #[serde(default, skip_serializing_if = "Vec::is_empty")]
73    pub allowed_files: Vec<String>,
74    /// Exact proof labels restricted by a gdp-ts proof rule; empty means all.
75    #[serde(default, skip_serializing_if = "Vec::is_empty")]
76    pub proof_kinds: Vec<String>,
77    /// Optional rule-authored remediation message.
78    pub message: Option<String>,
79    /// Effective severity for this rule at the target path.
80    pub severity: String,
81    /// Scoped suppression token for this specific policy rule.
82    pub suppress_token: String,
83}
84
85/// Effective guard-relevant rule severities for a target path.
86#[derive(Debug, Clone, serde::Serialize)]
87#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
88pub struct GuardSeverities {
89    /// Effective severity of boundary-violation findings.
90    pub boundary_violation: String,
91    /// Effective severity of policy-violation findings.
92    pub policy_violation: String,
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn guard_file_report_serializes_expected_wire_shape() {
101        let report = GuardReport {
102            files: vec![GuardFileReport {
103                path: "src/domain/user.ts".to_string(),
104                exists: false,
105                zone: None,
106                boundary: GuardBoundary {
107                    configured: true,
108                    unrestricted: true,
109                    allowed_zones: vec![],
110                    allowed_type_only_zones: vec![],
111                    forbidden_calls: vec!["child_process.*".to_string()],
112                    coverage_required: true,
113                },
114                policy_rules: vec![GuardPolicyRule {
115                    pack: "team-policy".to_string(),
116                    rule_id: "pure-domain".to_string(),
117                    kind: "banned-effect".to_string(),
118                    patterns: vec!["network".to_string()],
119                    allowed_files: vec![],
120                    proof_kinds: vec![],
121                    message: Some("Inject effects via ports.".to_string()),
122                    severity: "warn".to_string(),
123                    suppress_token: "policy-violation:team-policy/pure-domain".to_string(),
124                }],
125                severities: GuardSeverities {
126                    boundary_violation: "error".to_string(),
127                    policy_violation: "warn".to_string(),
128                },
129                notes: vec!["Files outside every zone are unrestricted.".to_string()],
130            }],
131        };
132
133        let json = serde_json::to_value(report).unwrap();
134        let file = &json["files"][0];
135        assert_eq!(file["path"], "src/domain/user.ts");
136        assert_eq!(file["exists"], false);
137        assert!(file["zone"].is_null());
138        assert_eq!(file["boundary"]["allowed_zones"], serde_json::json!([]));
139        assert_eq!(
140            file["boundary"]["allowed_type_only_zones"],
141            serde_json::json!([])
142        );
143        assert_eq!(file["boundary"]["coverage_required"], true);
144        assert_eq!(file["policy_rules"][0]["rule_id"], "pure-domain");
145        assert_eq!(
146            file["policy_rules"][0]["suppress_token"],
147            "policy-violation:team-policy/pure-domain"
148        );
149        assert_eq!(file["severities"]["boundary_violation"], "error");
150    }
151}