Validate a canonical Intent node against `IntentShape` (+ sub-shapes).
(node: &CanonicalNode)
| 68 | |
| 69 | /// Validate a canonical Intent node against `IntentShape` (+ sub-shapes). |
| 70 | pub fn validate_intent(node: &CanonicalNode) -> ValidationReport { |
| 71 | let mut out = Vec::new(); |
| 72 | let focus = &node.id; |
| 73 | |
| 74 | // status ∈ closed set, exactly one (it is a single field, so cardinality holds). |
| 75 | if !vocab::is_known_intent_status(&node.status) { |
| 76 | out.push(Violation { |
| 77 | focus_node: focus.clone(), |
| 78 | shape: "IntentShape".into(), |
| 79 | path: Some("status".into()), |
| 80 | message: format!( |
| 81 | "status '{}' is not one of {:?}", |
| 82 | node.status, |
| 83 | vocab::INTENT_STATUS |
| 84 | ), |
| 85 | }); |
| 86 | } |
| 87 | |
| 88 | // attributedTo must be present (sh:class prov:Agent deferred — see M0 notes). |
| 89 | if node.attributed_to.as_deref().unwrap_or("").is_empty() { |
| 90 | out.push(Violation { |
| 91 | focus_node: focus.clone(), |
| 92 | shape: "IntentShape".into(), |
| 93 | path: Some("attributedTo".into()), |
| 94 | message: "author (attributedTo) must be present as a DID".into(), |
| 95 | }); |
| 96 | } |
| 97 | |
| 98 | // proof must be present. |
| 99 | if node.proof.is_none() { |
| 100 | out.push(Violation { |
| 101 | focus_node: focus.clone(), |
| 102 | shape: "IntentShape".into(), |
| 103 | path: Some("proof".into()), |
| 104 | message: "intent must carry a Data Integrity proof".into(), |
| 105 | }); |
| 106 | } |
| 107 | |
| 108 | // presence-enforced: a reason must exist (content left honest). |
| 109 | match &node.why { |
| 110 | Some(w) if !w.trim().is_empty() => {} |
| 111 | _ => out.push(Violation { |
| 112 | focus_node: focus.clone(), |
| 113 | shape: "IntentShape".into(), |
| 114 | path: Some("why".into()), |
| 115 | message: "a reason (why) must be present — its content is not graded".into(), |
| 116 | }), |
| 117 | } |
| 118 | |
| 119 | // Scope-out presence: if scope is being declared (scope-in present), the |
| 120 | // intent must also state what is out of scope — the boundaries the agent |
| 121 | // must respect. This is presence-enforced (we never read the prose). An |
| 122 | // intent with no scope section at all is not forced to declare scope-out. |
| 123 | if !node.has_scope_in.is_empty() && node.has_scope_out.is_empty() { |
| 124 | out.push(Violation { |
| 125 | focus_node: focus.clone(), |
| 126 | shape: "IntentShape".into(), |
| 127 | path: Some("hasScopeOut".into()), |