| 201 | } |
| 202 | |
| 203 | fn lift_task(d: &Directive, id_base: &str, n: usize) -> Task { |
| 204 | let local = |
| 205 | d.id.clone() |
| 206 | .unwrap_or_else(|| format!("{}-{n}", slug(id_base))); |
| 207 | let mut touches: Vec<String> = Vec::new(); |
| 208 | if let Some(f) = d.attr("touchesFile") { |
| 209 | touches.push(f.to_string()); |
| 210 | } |
| 211 | for child in &d.children { |
| 212 | if child.name == "file-ref" { |
| 213 | if let Some(path) = child.attr("path") { |
| 214 | touches.push(path.to_string()); |
| 215 | } |
| 216 | } |
| 217 | } |
| 218 | // A task may fulfill more than one acceptance criterion. Both `satisfies` |
| 219 | // and the `criteria` alias accept a comma-separated list; each entry becomes |
| 220 | // its own `urn:atomic:ac:*` edge rather than being collapsed into a single |
| 221 | // malformed URN. |
| 222 | let satisfies = d |
| 223 | .attr("satisfies") |
| 224 | .or_else(|| d.attr("criteria")) |
| 225 | .map(|v| { |
| 226 | v.split(',') |
| 227 | .map(str::trim) |
| 228 | .filter(|s| !s.is_empty()) |
| 229 | .map(|s| as_urn("ac", s)) |
| 230 | .collect::<Vec<_>>() |
| 231 | }) |
| 232 | .unwrap_or_default(); |
| 233 | Task { |
| 234 | type_: NodeType::Task.as_str().to_string(), |
| 235 | id: as_urn("task", &local), |
| 236 | text: d.body.clone(), |
| 237 | task_status: d.attr("status").unwrap_or("open").to_string(), |
| 238 | touches_file: touches, |
| 239 | // `.into()` yields `Satisfies::Many`, so freshly lifted tasks always |
| 240 | // serialize as a list. The scalar variant is reachable only by |
| 241 | // deserializing a pre-widening attestation; nothing here mints one. |
| 242 | satisfies: satisfies.into(), |
| 243 | } |
| 244 | } |
| 245 | |
| 246 | /// Lift a `:::scope-in` / `:::scope-out` container into a `ScopeItem`. |
| 247 | /// `kind` is "scope-in" or "scope-out" and drives the generated id slug. |