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 | // TODO(T5b): FreshnessShape / CodeFreshnessShape (substance-drift and |
| 75 | // code-drift → STALE) are NOT enforced here. They need inputs this pure, |
| 76 | // node-only gate does not have — the current intentSubstanceHash vs a pinned |
| 77 | // hash, and the pinned candidate change-set — so they belong to the consumer |
| 78 | // / write chokepoint, not this validator. |
| 79 | |
| 80 | // status ∈ closed set, exactly one (it is a single field, so cardinality holds). |
| 81 | if !vocab::is_known_intent_status(&node.status) { |
| 82 | out.push(Violation { |
| 83 | focus_node: focus.clone(), |
| 84 | shape: "IntentShape".into(), |
| 85 | path: Some("status".into()), |
| 86 | message: format!( |
| 87 | "status '{}' is not one of {:?}", |
| 88 | node.status, |
| 89 | vocab::INTENT_STATUS |
| 90 | ), |
| 91 | }); |
| 92 | } |
| 93 | |
| 94 | // kind ∈ closed set (the classification discriminator; `feature` by default). |
| 95 | if !vocab::is_known_intent_kind(&node.kind) { |
| 96 | out.push(Violation { |
| 97 | focus_node: focus.clone(), |
| 98 | shape: "IntentShape".into(), |
| 99 | path: Some("kind".into()), |
| 100 | message: format!( |
| 101 | "kind '{}' is not one of {:?}", |
| 102 | node.kind, |
| 103 | vocab::INTENT_KIND |
| 104 | ), |
| 105 | }); |
| 106 | } |
| 107 | |
| 108 | // ReviewShape: the `review` kind and the `reviews` edge are coupled. A |
| 109 | // review intent must declare what it reviews, and only a review intent may |
| 110 | // carry a `reviews` edge (kind and edge cannot contradict each other). |
| 111 | let has_reviews_edge = node.depends_on.iter().any(|r| r.edge == "reviews"); |
| 112 | if node.kind == "review" && !has_reviews_edge { |
| 113 | out.push(Violation { |
| 114 | focus_node: focus.clone(), |
| 115 | shape: "ReviewShape".into(), |
| 116 | path: Some("reviews".into()), |
| 117 | message: "a review intent (kind='review') must declare at least one 'reviews' ref \ |
| 118 | naming what it reviews" |
| 119 | .into(), |
| 120 | }); |
| 121 | } |
| 122 | if node.kind != "review" && has_reviews_edge { |
| 123 | out.push(Violation { |
| 124 | focus_node: focus.clone(), |
| 125 | shape: "ReviewShape".into(), |
| 126 | path: Some("reviews".into()), |
| 127 | message: format!( |