| 156 | } |
| 157 | |
| 158 | fn lift_task(d: &Directive, id_base: &str, n: usize) -> Task { |
| 159 | let local = |
| 160 | d.id.clone() |
| 161 | .unwrap_or_else(|| format!("{}-{n}", slug(id_base))); |
| 162 | let mut touches: Vec<String> = Vec::new(); |
| 163 | if let Some(f) = d.attr("touchesFile") { |
| 164 | touches.push(f.to_string()); |
| 165 | } |
| 166 | for child in &d.children { |
| 167 | if child.name == "file-ref" { |
| 168 | if let Some(path) = child.attr("path") { |
| 169 | touches.push(path.to_string()); |
| 170 | } |
| 171 | } |
| 172 | } |
| 173 | // A task may fulfill more than one acceptance criterion. Both `satisfies` |
| 174 | // and the `criteria` alias accept a comma-separated list; each entry becomes |
| 175 | // its own `urn:atomic:ac:*` edge rather than being collapsed into a single |
| 176 | // malformed URN. |
| 177 | let satisfies = d |
| 178 | .attr("satisfies") |
| 179 | .or_else(|| d.attr("criteria")) |
| 180 | .map(|v| { |
| 181 | v.split(',') |
| 182 | .map(str::trim) |
| 183 | .filter(|s| !s.is_empty()) |
| 184 | .map(|s| as_urn("ac", s)) |
| 185 | .collect::<Vec<_>>() |
| 186 | }) |
| 187 | .unwrap_or_default(); |
| 188 | Task { |
| 189 | type_: NodeType::Task.as_str().to_string(), |
| 190 | id: as_urn("task", &local), |
| 191 | text: d.body.clone(), |
| 192 | task_status: d.attr("status").unwrap_or("open").to_string(), |
| 193 | touches_file: touches, |
| 194 | // `.into()` yields `Satisfies::Many`, so freshly lifted tasks always |
| 195 | // serialize as a list. The scalar variant is reachable only by |
| 196 | // deserializing a pre-widening attestation; nothing here mints one. |
| 197 | satisfies: satisfies.into(), |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | /// Lift a `:::scope-in` / `:::scope-out` container into a `ScopeItem`. |
| 202 | /// `kind` is "scope-in" or "scope-out" and drives the generated id slug. |