| 145 | } |
| 146 | |
| 147 | fn lift_ac(d: &Directive, id_base: &str, n: usize) -> Result<AcceptanceCriterion> { |
| 148 | // Child ids are namespaced under the intent's stable id base (its ULID for |
| 149 | // intents created via `atomic intent new`) so they are globally unique and |
| 150 | // do not inherit the human key's `::`/`-` separators. |
| 151 | let local = |
| 152 | d.id.clone() |
| 153 | .unwrap_or_else(|| format!("{}-ac-{n}", slug(id_base))); |
| 154 | // Required verification kinds: a comma-separated `requiredKinds` attr |
| 155 | // (whitespace-tolerant, empty entries dropped). Absent ⇒ empty, so an AC |
| 156 | // that declares none is unchanged. |
| 157 | let required_kinds = d |
| 158 | .attr("requiredKinds") |
| 159 | .map(|v| { |
| 160 | v.split(',') |
| 161 | .map(str::trim) |
| 162 | .filter(|s| !s.is_empty()) |
| 163 | .map(str::to_string) |
| 164 | .collect::<Vec<_>>() |
| 165 | }) |
| 166 | .unwrap_or_default(); |
| 167 | // Nested `::verification{...}` leaf children become typed records. Absent ⇒ |
| 168 | // empty, so an AC without verifications serializes exactly as before. |
| 169 | let verifications = d |
| 170 | .children |
| 171 | .iter() |
| 172 | .filter(|c| c.name == "verification") |
| 173 | .map(lift_verification) |
| 174 | .collect(); |
| 175 | Ok(AcceptanceCriterion { |
| 176 | type_: NodeType::AcceptanceCriterion.as_str().to_string(), |
| 177 | id: as_urn("ac", &local), |
| 178 | text: d.body.clone(), |
| 179 | ac_status: d.attr("status").unwrap_or("open").to_string(), |
| 180 | verified_by: d.attr("verifiedBy").map(str::to_string), |
| 181 | evidence: d.attr("evidence").map(str::to_string), |
| 182 | verifications, |
| 183 | required_kinds, |
| 184 | }) |
| 185 | } |
| 186 | |
| 187 | /// Lift a nested `::verification{kind= outcome= scope= observedAtMerkle= ref= observation=}` |
| 188 | /// leaf into a typed [`VerificationRecord`]. Attributes map straight across; |