Skip to main content

etdl_parser/
spanned.rs

1//! Source-position tracking for ETDL documents.
2//!
3//! The typed AST produced by [`crate::parse_document`] carries no source
4//! positions. This module parses the same document a second time with `saphyr`
5//! (a position-aware YAML 1.2 parser) and builds a [`SpanIndex`] that records
6//! the location of every semantic element — sections, tree/node/gate/basic-event
7//! definitions, fields, and identifier reference value tokens.
8//!
9//! The index is keyed by the *serde output JSON path* (e.g.
10//! `event_trees.OrderFulfillment.nodes.InventoryCheckBarrier.branches[0].next`)
11//! so it can be injected directly into the AST serialization produced by
12//! [`crate::parse_document`].
13//!
14//! All line/column numbers are **0-based** (LSP convention). `start`/`end` are
15//! **character offsets** into the original document (not UTF-16 code units).
16
17use saphyr::LoadableYamlNode;
18use saphyr::MarkedYaml;
19use serde::Serialize;
20use serde_json::Value;
21
22use crate::ast::EtlDocument;
23
24/// A half-open `[start, end)` span into the source document.
25///
26/// Offsets are 0-based character offsets; `line`/`column`/`end_line`/`end_column`
27/// are 0-based line/column numbers (LSP convention).
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
29pub struct Span {
30    pub start: u32,
31    pub end: u32,
32    pub line: u32,
33    pub column: u32,
34    pub end_line: u32,
35    pub end_column: u32,
36}
37
38/// The kind of a recorded element, matching the `kind` field of `find_span`.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
40#[serde(rename_all = "lowercase")]
41pub enum ElementKind {
42    Section,
43    Definition,
44    Field,
45    Reference,
46}
47
48impl ElementKind {
49    fn rank(self) -> u8 {
50        match self {
51            ElementKind::Reference => 4,
52            ElementKind::Field => 3,
53            ElementKind::Definition => 2,
54            ElementKind::Section => 1,
55        }
56    }
57}
58
59/// A component of an index path: either a map key or a sequence index.
60#[derive(Debug, Clone, PartialEq, Eq, Hash)]
61pub enum PathPart {
62    Key(String),
63    Index(usize),
64}
65
66/// A path into the serde output JSON (see module docs).
67pub type PathKey = Vec<PathPart>;
68
69/// One recorded element in the [`SpanIndex`].
70#[derive(Debug, Clone, Serialize)]
71pub struct IndexedElement {
72    #[serde(rename = "kind")]
73    pub kind: ElementKind,
74    pub name: String,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub field: Option<String>,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub tree: Option<String>,
79    pub span: Span,
80    /// For definitions: the span of the name token itself (used to anchor
81    /// go-to-definition and to hit-test the identifier).
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub key_span: Option<Span>,
84    /// Structural depth in the document (used for find_span tie-breaking).
85    #[serde(skip)]
86    pub depth: usize,
87    /// The index path of this element (not serialized).
88    #[serde(skip)]
89    pub path: PathKey,
90}
91
92/// A structured locator for a semantic element, used by the validator to attach
93/// positions to diagnostics. Field names use the *serde output* naming
94/// (e.g. `on_failure`, `root_cause`).
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum SpanKey {
97    Section(&'static str),
98    Tree { tree: String },
99    FaultTree { tree: String },
100    InitiatingEvent { tree: String, field: &'static str },
101    TopEvent { tree: String, field: &'static str },
102    Node { tree: String, id: String },
103    NodeField { tree: String, id: String, field: &'static str },
104    BranchField { tree: String, id: String, branch: usize, field: &'static str },
105    Gate { tree: String, id: String },
106    GateField { tree: String, id: String, field: &'static str },
107    GateInput { tree: String, id: String, idx: usize },
108    BasicEvent { tree: String, id: String },
109    BasicEventField { tree: String, id: String, field: &'static str },
110    Transfer { tree: String, id: String, field: &'static str },
111    ImportAlias { alias: String },
112}
113
114impl SpanKey {
115    fn path(&self) -> PathKey {
116        let key = |s: &str| PathPart::Key(s.to_string());
117        match self {
118            SpanKey::Section(s) => vec![key(s)],
119            SpanKey::Tree { tree } => vec![key("event_trees"), key(tree)],
120            SpanKey::FaultTree { tree } => vec![key("fault_trees"), key(tree)],
121            SpanKey::InitiatingEvent { tree, field } => vec![
122                key("event_trees"),
123                key(tree),
124                key("initiating_event"),
125                key(field),
126            ],
127            SpanKey::TopEvent { tree, field } => vec![
128                key("fault_trees"),
129                key(tree),
130                key("top_event"),
131                key(field),
132            ],
133            SpanKey::Node { tree, id } => {
134                vec![key("event_trees"), key(tree), key("nodes"), key(id)]
135            }
136            SpanKey::NodeField { tree, id, field } => {
137                let mut p = SpanKey::Node {
138                    tree: tree.clone(),
139                    id: id.clone(),
140                }
141                .path();
142                p.push(key(field));
143                p
144            }
145            SpanKey::BranchField {
146                tree,
147                id,
148                branch,
149                field,
150            } => {
151                let mut p = SpanKey::Node {
152                    tree: tree.clone(),
153                    id: id.clone(),
154                }
155                .path();
156                p.push(key("branches"));
157                p.push(PathPart::Index(*branch));
158                p.push(key(field));
159                p
160            }
161            SpanKey::Gate { tree, id } => {
162                vec![key("fault_trees"), key(tree), key("gates"), key(id)]
163            }
164            SpanKey::GateField { tree, id, field } => {
165                let mut p = SpanKey::Gate {
166                    tree: tree.clone(),
167                    id: id.clone(),
168                }
169                .path();
170                p.push(key(field));
171                p
172            }
173            SpanKey::GateInput { tree, id, idx } => {
174                let mut p = SpanKey::Gate {
175                    tree: tree.clone(),
176                    id: id.clone(),
177                }
178                .path();
179                p.push(key("inputs"));
180                p.push(PathPart::Index(*idx));
181                p
182            }
183            SpanKey::BasicEvent { tree, id } => vec![
184                key("fault_trees"),
185                key(tree),
186                key("basic_events"),
187                key(id),
188            ],
189            SpanKey::BasicEventField { tree, id, field } => {
190                let mut p = SpanKey::BasicEvent {
191                    tree: tree.clone(),
192                    id: id.clone(),
193                }
194                .path();
195                p.push(key(field));
196                p
197            }
198            SpanKey::Transfer { tree, id, field } => vec![
199                key("fault_trees"),
200                key(tree),
201                key("transfers"),
202                key(id),
203                key(field),
204            ],
205            SpanKey::ImportAlias { alias } => vec![key("asyncapi_imports"), key(alias)],
206        }
207    }
208}
209
210/// A source-position index over an ETDL document.
211#[derive(Debug, Default, Clone)]
212pub struct SpanIndex {
213    pub elements: Vec<IndexedElement>,
214    by_path: std::collections::HashMap<PathKey, usize>,
215    /// `(tree, id)` -> indices of all definition + reference elements sharing
216    /// that identity (used for go-to-definition / find-references).
217    by_identity: std::collections::HashMap<(String, String), Vec<usize>>,
218}
219
220impl SpanIndex {
221    /// Resolve a [`SpanKey`] to the recorded element.
222    pub fn resolve(&self, key: &SpanKey) -> Option<&IndexedElement> {
223        self.by_path.get(&key.path()).map(|&i| &self.elements[i])
224    }
225
226    /// Return the deepest element whose span (or name-token span) contains the
227    /// given 0-based character offset. "Deepest" = the smallest containing span,
228    /// preferring reference > field > definition > section on ties.
229    pub fn find_deepest(&self, offset: u32) -> Option<&IndexedElement> {
230        let mut best: Option<&IndexedElement> = None;
231        let mut best_size: u64 = u64::MAX;
232        let mut best_rank: u8 = 0;
233        let mut best_depth: usize = 0;
234        for el in &self.elements {
235            let mut size: Option<u64> = None;
236            if el.span.start <= offset && offset < el.span.end {
237                size = Some((el.span.end - el.span.start) as u64);
238            }
239            if let Some(ks) = &el.key_span {
240                if ks.start <= offset && offset < ks.end {
241                    let s = (ks.end - ks.start) as u64;
242                    if size.is_none_or(|cur| s < cur) {
243                        size = Some(s);
244                    }
245                }
246            }
247            if let Some(size) = size {
248                let better = size < best_size
249                    || (size == best_size
250                        && (el.kind.rank() > best_rank
251                            || (el.kind.rank() == best_rank && el.depth > best_depth)));
252                if better {
253                    best = Some(el);
254                    best_size = size;
255                    best_rank = el.kind.rank();
256                    best_depth = el.depth;
257                }
258            }
259        }
260        best
261    }
262
263    /// All elements sharing the given identity `(tree, id)`.
264    pub fn by_identity(&self, tree: &str, id: &str) -> Vec<&IndexedElement> {
265        self.by_identity
266            .get(&(tree.to_string(), id.to_string()))
267            .map(|v| v.iter().map(|&i| &self.elements[i]).collect())
268            .unwrap_or_default()
269    }
270
271    /// The single definition element for `(tree, id)`, if any.
272    pub fn definition(&self, tree: &str, id: &str) -> Option<&IndexedElement> {
273        self.by_identity
274            .get(&(tree.to_string(), id.to_string()))
275            .and_then(|v| {
276                v.iter()
277                    .find(|&&i| self.elements[i].kind == ElementKind::Definition)
278                    .map(|&i| &self.elements[i])
279            })
280    }
281}
282
283/// A detected duplicate identifier under a `nodes`/`gates`/`basicEvents` map.
284#[derive(Debug, Clone)]
285pub struct DuplicateId {
286    pub tree: String,
287    pub kind: String,
288    pub id: String,
289    pub span: Span,
290}
291
292/// Parse the document with `serde_yaml` (producing the typed AST) and build a
293/// [`SpanIndex`] over the same content.
294pub fn parse_document_with_spans(content: &str) -> Result<(EtlDocument, SpanIndex), String> {
295    let doc = crate::parse_document(content)?;
296    let index = build_span_index(content)?;
297    Ok((doc, index))
298}
299
300/// Build a [`SpanIndex`] over an ETDL document.
301pub fn build_span_index(content: &str) -> Result<SpanIndex, String> {
302    let docs = MarkedYaml::load_from_str(content).map_err(|e| e.to_string())?;
303    let root = docs.first().ok_or("empty ETDL document")?;
304    let mut builder = Builder::new(content);
305    builder.walk_root(root);
306    Ok(builder.index)
307}
308
309/// Inject `span` objects into a serialized AST, wrapping scalar leaves that have
310/// spans as `{ "value": ..., "span": ... }`.
311pub fn inject_spans(value: &mut Value, index: &SpanIndex) {
312    let mut path: PathKey = Vec::new();
313    walk_inject(value, index, &mut path);
314}
315
316fn walk_inject(value: &mut Value, index: &SpanIndex, path: &mut PathKey) {
317    if let Some(&idx) = index.by_path.get(path) {
318        let el = &index.elements[idx];
319        let span = serde_json::to_value(el.span).unwrap_or_default();
320        match value {
321            Value::Object(map) => {
322                map.insert("span".to_string(), span);
323            }
324            Value::Array(_) => {
325                // Collection spans attach to a wrapping object only.
326            }
327            _ => {
328                let inner = std::mem::replace(value, Value::Null);
329                let mut map = serde_json::Map::new();
330                map.insert("value".to_string(), inner);
331                map.insert("span".to_string(), span);
332                *value = Value::Object(map);
333            }
334        }
335    }
336    match value {
337        Value::Object(map) => {
338            for (k, v) in map.iter_mut() {
339                path.push(PathPart::Key(k.clone()));
340                walk_inject(v, index, path);
341                path.pop();
342            }
343        }
344        Value::Array(arr) => {
345            for (i, v) in arr.iter_mut().enumerate() {
346                path.push(PathPart::Index(i));
347                walk_inject(v, index, path);
348                path.pop();
349            }
350        }
351        _ => {}
352    }
353}
354
355/// Detect duplicate ids under `nodes`/`gates`/`basicEvents` maps using saphyr's
356/// low-level event stream (duplicate YAML keys are collapsed before the typed
357/// AST is built, so they must be caught here).
358pub fn detect_duplicate_ids(content: &str) -> Result<Vec<DuplicateId>, String> {
359    use saphyr_parser::{Event, Parser};
360
361    #[derive(Clone, Copy, PartialEq)]
362    enum Container {
363        Mapping,
364        Sequence,
365    }
366
367    #[derive(Default)]
368    struct MapCtx {
369        expect_key: bool,
370        seen: std::collections::BTreeMap<String, usize>,
371        path: Vec<String>,
372        kind: Option<String>,
373        tree: Option<String>,
374    }
375
376    let mut parser = Parser::new_from_str(content);
377    let mut duplicates = Vec::new();
378    let line_map = LineMap::new(content);
379    let mut maps: Vec<MapCtx> = Vec::new();
380    let mut containers: Vec<Container> = Vec::new();
381
382    while let Some(res) = parser.next_event() {
383        let (ev, span) = res.map_err(|e| e.to_string())?;
384        match ev {
385            Event::MappingStart(..) => {
386                if let Some(parent) = maps.last_mut() {
387                    // A mapping value consumes the key it follows.
388                    parent.expect_key = true;
389                }
390                let parent_path = maps.last().map(|c| c.path.clone()).unwrap_or_default();
391                let mut ctx = MapCtx {
392                    expect_key: true,
393                    path: parent_path,
394                    ..Default::default()
395                };
396                if let Some(last) = ctx.path.last() {
397                    let section_idx = ctx
398                        .path
399                        .iter()
400                        .position(|p| p == "eventTrees" || p == "faultTrees");
401                    if let Some(section_idx) = section_idx {
402                        let section = ctx.path[section_idx].as_str();
403                        let tree = ctx.path.get(section_idx + 1).cloned();
404                        match (section, last.as_str()) {
405                            ("eventTrees", "nodes") => {
406                                ctx.kind = Some("node".to_string());
407                                ctx.tree = tree;
408                            }
409                            ("faultTrees", "gates") => {
410                                ctx.kind = Some("gate".to_string());
411                                ctx.tree = tree;
412                            }
413                            ("faultTrees", "basicEvents") => {
414                                ctx.kind = Some("basicEvent".to_string());
415                                ctx.tree = tree;
416                            }
417                            _ => {}
418                        }
419                    }
420                }
421                maps.push(ctx);
422                containers.push(Container::Mapping);
423            }
424            Event::MappingEnd => {
425                maps.pop();
426                containers.pop();
427            }
428            Event::SequenceStart(..) => {
429                if let Some(parent) = maps.last_mut() {
430                    parent.expect_key = true;
431                }
432                containers.push(Container::Sequence);
433            }
434            Event::SequenceEnd => {
435                containers.pop();
436            }
437            Event::Scalar(v, ..) => {
438                if containers.last() != Some(&Container::Mapping) {
439                    continue;
440                }
441                let Some(ctx) = maps.last_mut() else { continue };
442                if ctx.expect_key {
443                    let key = v.to_string();
444                    if let (Some(kind), Some(tree)) = (ctx.kind.clone(), ctx.tree.clone()) {
445                        if ctx.seen.contains_key(&key) {
446                            duplicates.push(DuplicateId {
447                                tree,
448                                kind,
449                                id: key.clone(),
450                                span: line_map.span_of(span.start.index(), span.end.index()),
451                            });
452                        } else {
453                            ctx.seen.insert(key.clone(), span.start.index());
454                        }
455                    }
456                    ctx.path.push(key);
457                    ctx.expect_key = false;
458                } else {
459                    ctx.path.pop();
460                    ctx.expect_key = true;
461                }
462            }
463            _ => {}
464        }
465    }
466
467    Ok(duplicates)
468}
469
470// ---------------------------------------------------------------------------
471// Index builder
472// ---------------------------------------------------------------------------
473
474struct Builder<'a> {
475    content: &'a str,
476    index: SpanIndex,
477    line_map: LineMap<'a>,
478}
479
480impl<'a> Builder<'a> {
481    fn new(content: &'a str) -> Self {
482        Builder {
483            content,
484            index: SpanIndex::default(),
485            line_map: LineMap::new(content),
486        }
487    }
488
489    fn span(&self, start: usize, end: usize) -> Span {
490        self.line_map.span_of(start, end)
491    }
492
493    /// Span covering the token of a scalar value (strips surrounding quotes).
494    fn value_span(&self, node: &MarkedYaml) -> Span {
495        let (s, e) = byte_range(node);
496        let (ts, te) = token_span(self.content, s, e, &node_str(node));
497        self.span(ts, te)
498    }
499
500    fn add(&mut self, el: IndexedElement) {
501        let identity = match el.kind {
502            ElementKind::Definition | ElementKind::Reference => {
503                Some((el.tree.clone().unwrap_or_default(), el.name.clone()))
504            }
505            _ => None,
506        };
507        let idx = if let Some(&existing) = self.index.by_path.get(&el.path) {
508            self.index.elements[existing] = el;
509            existing
510        } else {
511            let idx = self.index.elements.len();
512            self.index.by_path.insert(el.path.clone(), idx);
513            self.index.elements.push(el);
514            idx
515        };
516        if let Some(id) = identity {
517            self.index.by_identity.entry(id).or_default().push(idx);
518        }
519    }
520}
521
522// --- span / token helpers -------------------------------------------------
523
524fn node_str(n: &MarkedYaml) -> String {
525    n.data.as_str().map(|s| s.to_string()).unwrap_or_default()
526}
527
528fn byte_range(n: &MarkedYaml) -> (usize, usize) {
529    (n.span.start.index(), n.span.end.index())
530}
531
532/// Compute the exact token span for a scalar value, stripping surrounding
533/// quotes/whitespace by locating the decoded value inside the reported region.
534fn token_span(content: &str, start: usize, end: usize, value: &str) -> (usize, usize) {
535    let lo = start.min(content.len());
536    let hi = end.min(content.len());
537    let hay = &content[lo..hi];
538    if !value.is_empty() {
539        if let Some(rel) = hay.find(value) {
540            return (lo + rel, lo + rel + value.len());
541        }
542    }
543    (lo, hi)
544}
545
546/// Translate an input key to its serde output name (kept unchanged when unknown).
547fn out_name(key: &str) -> String {
548    match key {
549        "eventTrees" => "event_trees",
550        "faultTrees" => "fault_trees",
551        "basicEvents" => "basic_events",
552        "initiatingEvent" => "initiating_event",
553        "topEvent" => "top_event",
554        "onFailure" => "on_failure",
555        "onFailureProbabilitySource" => "on_failure_probability_source",
556        "probabilityOfSuccess" => "probability_of_success",
557        "probabilityOfFailure" => "probability_of_failure",
558        "probabilitySource" => "probability_source",
559        "rootCause" => "root_cause",
560        "inhibitCondition" => "inhibit_condition",
561        "retryPolicy" => "retry_policy",
562        "timeoutMs" => "timeout_ms",
563        "maxAttempts" => "max_attempts",
564        "backoffMs" => "backoff_ms",
565        "backoffStrategy" => "backoff_strategy",
566        "failureRate" => "failure_rate",
567        "missionTime" => "mission_time",
568        "eventType" => "event_type",
569        other => other,
570    }
571    .to_string()
572}
573
574// --- schema walker ---------------------------------------------------------
575
576impl<'a> Builder<'a> {
577    fn walk_root(&mut self, root: &MarkedYaml) {
578        let Some(map) = root.data.as_mapping() else { return };
579        for (k, v) in map {
580            let key = node_str(k);
581            let out = out_name(&key);
582            let (ks, _ke) = byte_range(k);
583            let (_vs, ve) = byte_range(v);
584            let path = vec![PathPart::Key(out.clone())];
585            self.add(IndexedElement {
586                kind: ElementKind::Section,
587                name: out.clone(),
588                field: None,
589                tree: None,
590                span: self.span(ks, ve),
591                key_span: Some(self.span(ks, ks + key.len())),
592                path: path.clone(),
593                depth: 1,
594            });
595            match key.as_str() {
596                "info" => self.walk_info(v, &out),
597                "asyncapi_imports" => self.walk_imports(v, &out),
598                "components" => self.walk_components(v, &out),
599                "eventTrees" => self.walk_event_trees(v, &out),
600                "faultTrees" => self.walk_fault_trees(v, &out),
601                _ => {}
602            }
603        }
604    }
605
606    fn walk_info(&mut self, node: &MarkedYaml, base: &str) {
607        let Some(map) = node.data.as_mapping() else { return };
608        for (k, v) in map {
609            let key = node_str(k);
610            let out = out_name(&key);
611            let (ks, _ke) = byte_range(k);
612            let path = vec![
613                PathPart::Key(base.to_string()),
614                PathPart::Key(out.clone()),
615            ];
616            self.add(IndexedElement {
617                kind: ElementKind::Field,
618                name: node_str(v),
619                field: Some(out.clone()),
620                tree: None,
621                span: self.span(ks, byte_range(v).1),
622                key_span: Some(self.span(ks, ks + key.len())),
623                path,
624                depth: 2,
625            });
626        }
627    }
628
629    fn walk_imports(&mut self, node: &MarkedYaml, base: &str) {
630        let Some(map) = node.data.as_mapping() else { return };
631        for (k, v) in map {
632            let alias = node_str(k);
633            let (ks, _ke) = byte_range(k);
634            let path = vec![
635                PathPart::Key(base.to_string()),
636                PathPart::Key(alias.clone()),
637            ];
638            self.add(IndexedElement {
639                kind: ElementKind::Field,
640                name: alias.clone(),
641                field: Some(alias.clone()),
642                tree: None,
643                span: self.span(ks, byte_range(v).1),
644                key_span: Some(self.span(ks, ks + alias.len())),
645                path,
646                depth: 2,
647            });
648        }
649    }
650
651    fn walk_event_trees(&mut self, node: &MarkedYaml, base: &str) {
652        let Some(map) = node.data.as_mapping() else { return };
653        for (tk, tv) in map {
654            let tree = node_str(tk);
655            let (ks, _ke) = byte_range(tk);
656            let (_, ve) = byte_range(tv);
657            let path = vec![
658                PathPart::Key(base.to_string()),
659                PathPart::Key(tree.clone()),
660            ];
661            self.add(IndexedElement {
662                kind: ElementKind::Definition,
663                name: tree.clone(),
664                field: None,
665                tree: Some(tree.clone()),
666                span: self.span(ks, ve),
667                key_span: Some(self.span(ks, ks + tree.len())),
668                path: path.clone(),
669                depth: 2,
670            });
671            self.walk_event_tree_fields(tv, base, &tree, &path);
672        }
673    }
674
675    fn walk_event_tree_fields(
676        &mut self,
677        node: &MarkedYaml,
678        base: &str,
679        tree: &str,
680        tree_path: &PathKey,
681    ) {
682        let Some(map) = node.data.as_mapping() else { return };
683        for (k, v) in map {
684            let key = node_str(k);
685            let (ks, _ke) = byte_range(k);
686            let (_, ve) = byte_range(v);
687            let mut path = tree_path.clone();
688            match key.as_str() {
689                "initiatingEvent" => {
690                    path.push(PathPart::Key("initiating_event".to_string()));
691                    self.add(IndexedElement {
692                        kind: ElementKind::Field,
693                        name: "initiatingEvent".to_string(),
694                        field: Some("initiating_event".to_string()),
695                        tree: Some(tree.to_string()),
696                        span: self.span(ks, ve),
697                        key_span: Some(self.span(ks, ks + key.len())),
698                        path: path.clone(),
699                        depth: 3,
700                    });
701                    self.walk_initiating_event(v, tree, &path);
702                }
703                "nodes" => {
704                    path.push(PathPart::Key("nodes".to_string()));
705                    self.add(IndexedElement {
706                        kind: ElementKind::Field,
707                        name: "nodes".to_string(),
708                        field: Some("nodes".to_string()),
709                        tree: Some(tree.to_string()),
710                        span: self.span(ks, ve),
711                        key_span: Some(self.span(ks, ks + key.len())),
712                        path: path.clone(),
713                        depth: 3,
714                    });
715                    self.walk_nodes(v, base, tree, &path);
716                }
717                "description" => {
718                    path.push(PathPart::Key("description".to_string()));
719                    self.add(IndexedElement {
720                        kind: ElementKind::Field,
721                        name: node_str(v),
722                        field: Some("description".to_string()),
723                        tree: Some(tree.to_string()),
724                        span: self.span(ks, ve),
725                        key_span: Some(self.span(ks, ks + key.len())),
726                        path,
727                        depth: 3,
728                    });
729                }
730                _ => {}
731            }
732        }
733    }
734
735    fn walk_initiating_event(&mut self, node: &MarkedYaml, tree: &str, base_path: &PathKey) {
736        self.walk_scalar_map(
737            node,
738            tree,
739            base_path,
740            &[
741                ("id", None),
742                ("message", Some(true)),
743                ("next", Some(true)),
744            ],
745        );
746    }
747
748    fn walk_nodes(&mut self, node: &MarkedYaml, base: &str, tree: &str, nodes_path: &PathKey) {
749        let Some(map) = node.data.as_mapping() else { return };
750        for (nk, nv) in map {
751            let nid = node_str(nk);
752            let (ks, _ke) = byte_range(nk);
753            let (_, ve) = byte_range(nv);
754            let mut path = nodes_path.clone();
755            path.push(PathPart::Key(nid.clone()));
756            self.add(IndexedElement {
757                kind: ElementKind::Definition,
758                name: nid.clone(),
759                field: None,
760                tree: Some(tree.to_string()),
761                span: self.span(ks, ve),
762                key_span: Some(self.span(ks, ks + nid.len())),
763                path: path.clone(),
764                depth: 4,
765            });
766            let Some(fields) = nv.data.as_mapping() else { continue };
767            for (k, v) in fields {
768                let key = node_str(k);
769                let (fks, _fke) = byte_range(k);
770                let mut fpath = path.clone();
771                let field = out_name(&key);
772                fpath.push(PathPart::Key(field.clone()));
773                let is_ref = matches!(
774                    key.as_str(),
775                    "next" | "onFailure" | "onFailureProbabilitySource"
776                        | "emits" | "channel" | "message"
777                );
778                if is_ref {
779                    self.add(IndexedElement {
780                        kind: ElementKind::Reference,
781                        name: node_str(v),
782                        field: Some(field.clone()),
783                        tree: Some(tree.to_string()),
784                        span: self.value_span(v),
785                        key_span: None,
786                        path: fpath,
787                        depth: 5,
788                    });
789                } else if key == "branches" {
790                    self.add(IndexedElement {
791                        kind: ElementKind::Field,
792                        name: "branches".to_string(),
793                        field: Some("branches".to_string()),
794                        tree: Some(tree.to_string()),
795                        span: self.span(fks, byte_range(v).1),
796                        key_span: Some(self.span(fks, fks + key.len())),
797                        path: fpath.clone(),
798                        depth: 5,
799                    });
800                    self.walk_branches(v, tree, &fpath);
801                } else {
802                    self.add(IndexedElement {
803                        kind: ElementKind::Field,
804                        name: node_str(v),
805                        field: Some(field.clone()),
806                        tree: Some(tree.to_string()),
807                        span: self.span(fks, byte_range(v).1),
808                        key_span: Some(self.span(fks, fks + key.len())),
809                        path: fpath,
810                        depth: 5,
811                    });
812                }
813            }
814            let _ = base;
815        }
816    }
817
818    fn walk_branches(&mut self, node: &MarkedYaml, tree: &str, branches_path: &PathKey) {
819        let Some(seq) = node.data.as_vec() else { return };
820        for (i, bv) in seq.iter().enumerate() {
821            let (bs, be) = byte_range(bv);
822            let mut bpath = branches_path.clone();
823            bpath.push(PathPart::Index(i));
824            self.add(IndexedElement {
825                kind: ElementKind::Field,
826                name: format!("branches[{}]", i),
827                field: Some("branches".to_string()),
828                tree: Some(tree.to_string()),
829                span: self.span(bs, be),
830                key_span: None,
831                path: bpath.clone(),
832                depth: 6,
833            });
834            let Some(map) = bv.data.as_mapping() else { continue };
835            for (k, v) in map {
836                let key = node_str(k);
837                let (ks, _ke) = byte_range(k);
838                let mut fpath = bpath.clone();
839                let field = out_name(&key);
840                fpath.push(PathPart::Key(field.clone()));
841                let is_ref = matches!(key.as_str(), "next" | "probabilitySource");
842                let kind = if is_ref {
843                    ElementKind::Reference
844                } else {
845                    ElementKind::Field
846                };
847                self.add(IndexedElement {
848                    kind,
849                    name: node_str(v),
850                    field: Some(field.clone()),
851                    tree: Some(tree.to_string()),
852                    span: if is_ref {
853                        self.value_span(v)
854                    } else {
855                        self.span(ks, byte_range(v).1)
856                    },
857                    key_span: None,
858                    path: fpath,
859                    depth: 7,
860                });
861            }
862        }
863    }
864
865    /// Generic walker for a mapping whose values are scalars, with a list of
866    /// fields and whether each is a reference / definition.
867    fn walk_scalar_map(
868        &mut self,
869        node: &MarkedYaml,
870        tree: &str,
871        base_path: &PathKey,
872        spec: &[(&str, Option<bool>)],
873    ) {
874        let Some(map) = node.data.as_mapping() else { return };
875        for (k, v) in map {
876            let key = node_str(k);
877            let field = out_name(&key);
878            if let Some((_, is_ref)) = spec.iter().find(|(f, _)| *f == key.as_str()) {
879                let (ks, _ke) = byte_range(k);
880                let mut path = base_path.clone();
881                path.push(PathPart::Key(field.clone()));
882                let kind = if is_ref.unwrap_or(false) {
883                    ElementKind::Reference
884                } else {
885                    ElementKind::Definition
886                };
887                self.add(IndexedElement {
888                    kind,
889                    name: node_str(v),
890                    field: Some(field.clone()),
891                    tree: Some(tree.to_string()),
892                    span: if kind == ElementKind::Reference {
893                        self.value_span(v)
894                    } else {
895                        self.span(ks, byte_range(v).1)
896                    },
897                    key_span: None,
898                    path,
899                    depth: 4,
900                });
901            }
902        }
903    }
904
905    fn walk_fault_trees(&mut self, node: &MarkedYaml, base: &str) {
906        let Some(map) = node.data.as_mapping() else { return };
907        for (tk, tv) in map {
908            let tree = node_str(tk);
909            let (ks, _ke) = byte_range(tk);
910            let (_, ve) = byte_range(tv);
911            let path = vec![
912                PathPart::Key(base.to_string()),
913                PathPart::Key(tree.clone()),
914            ];
915            self.add(IndexedElement {
916                kind: ElementKind::Definition,
917                name: tree.clone(),
918                field: None,
919                tree: Some(tree.clone()),
920                span: self.span(ks, ve),
921                key_span: Some(self.span(ks, ks + tree.len())),
922                path: path.clone(),
923                depth: 2,
924            });
925            self.walk_fault_tree_fields(tv, base, &tree, &path);
926        }
927    }
928
929    fn walk_fault_tree_fields(
930        &mut self,
931        node: &MarkedYaml,
932        base: &str,
933        tree: &str,
934        tree_path: &PathKey,
935    ) {
936        let Some(map) = node.data.as_mapping() else { return };
937        for (k, v) in map {
938            let key = node_str(k);
939            let (ks, _ke) = byte_range(k);
940            let (_, ve) = byte_range(v);
941            let mut path = tree_path.clone();
942            match key.as_str() {
943                "topEvent" => {
944                    path.push(PathPart::Key("top_event".to_string()));
945                    self.add(IndexedElement {
946                        kind: ElementKind::Field,
947                        name: "topEvent".to_string(),
948                        field: Some("top_event".to_string()),
949                        tree: Some(tree.to_string()),
950                        span: self.span(ks, ve),
951                        key_span: Some(self.span(ks, ks + key.len())),
952                        path: path.clone(),
953                        depth: 3,
954                    });
955                    self.walk_top_event(v, tree, &path);
956                }
957                "gates" => {
958                    path.push(PathPart::Key("gates".to_string()));
959                    self.add(IndexedElement {
960                        kind: ElementKind::Field,
961                        name: "gates".to_string(),
962                        field: Some("gates".to_string()),
963                        tree: Some(tree.to_string()),
964                        span: self.span(ks, ve),
965                        key_span: Some(self.span(ks, ks + key.len())),
966                        path: path.clone(),
967                        depth: 3,
968                    });
969                    self.walk_gates(v, base, tree, &path);
970                }
971                "basicEvents" => {
972                    let field = "basic_events";
973                    path.push(PathPart::Key(field.to_string()));
974                    self.add(IndexedElement {
975                        kind: ElementKind::Field,
976                        name: "basicEvents".to_string(),
977                        field: Some(field.to_string()),
978                        tree: Some(tree.to_string()),
979                        span: self.span(ks, ve),
980                        key_span: Some(self.span(ks, ks + key.len())),
981                        path: path.clone(),
982                        depth: 3,
983                    });
984                    self.walk_basic_events(v, base, tree, &path);
985                }
986                "transfers" => {
987                    path.push(PathPart::Key("transfers".to_string()));
988                    self.add(IndexedElement {
989                        kind: ElementKind::Field,
990                        name: "transfers".to_string(),
991                        field: Some("transfers".to_string()),
992                        tree: Some(tree.to_string()),
993                        span: self.span(ks, ve),
994                        key_span: Some(self.span(ks, ks + key.len())),
995                        path: path.clone(),
996                        depth: 3,
997                    });
998                    self.walk_transfers(v, base, tree, &path);
999                }
1000                "description" => {
1001                    path.push(PathPart::Key("description".to_string()));
1002                    self.add(IndexedElement {
1003                        kind: ElementKind::Field,
1004                        name: node_str(v),
1005                        field: Some("description".to_string()),
1006                        tree: Some(tree.to_string()),
1007                        span: self.span(ks, ve),
1008                        key_span: Some(self.span(ks, ks + key.len())),
1009                        path,
1010                        depth: 3,
1011                    });
1012                }
1013                _ => {}
1014            }
1015        }
1016    }
1017
1018    fn walk_top_event(&mut self, node: &MarkedYaml, tree: &str, base_path: &PathKey) {
1019        let Some(map) = node.data.as_mapping() else { return };
1020        for (k, v) in map {
1021            let key = node_str(k);
1022            let (ks, _ke) = byte_range(k);
1023            let field = out_name(&key);
1024            let mut path = base_path.clone();
1025            path.push(PathPart::Key(field.clone()));
1026            let is_ref = matches!(key.as_str(), "message" | "rootCause");
1027            let kind = if is_ref {
1028                ElementKind::Reference
1029            } else {
1030                ElementKind::Field
1031            };
1032            self.add(IndexedElement {
1033                kind,
1034                name: node_str(v),
1035                field: Some(field.clone()),
1036                tree: Some(tree.to_string()),
1037                span: if is_ref {
1038                    self.value_span(v)
1039                } else {
1040                    self.span(ks, byte_range(v).1)
1041                },
1042                key_span: None,
1043                path,
1044                depth: 4,
1045            });
1046        }
1047    }
1048
1049    fn walk_gates(&mut self, node: &MarkedYaml, base: &str, tree: &str, gates_path: &PathKey) {
1050        let Some(map) = node.data.as_mapping() else { return };
1051        for (gk, gv) in map {
1052            let gid = node_str(gk);
1053            let (ks, _ke) = byte_range(gk);
1054            let (_, ve) = byte_range(gv);
1055            let mut path = gates_path.clone();
1056            path.push(PathPart::Key(gid.clone()));
1057            self.add(IndexedElement {
1058                kind: ElementKind::Definition,
1059                name: gid.clone(),
1060                field: None,
1061                tree: Some(tree.to_string()),
1062                span: self.span(ks, ve),
1063                key_span: Some(self.span(ks, ks + gid.len())),
1064                path: path.clone(),
1065                depth: 4,
1066            });
1067            let Some(fields) = gv.data.as_mapping() else { continue };
1068            for (k, v) in fields {
1069                let key = node_str(k);
1070                let (fks, _fke) = byte_range(k);
1071                let field = out_name(&key);
1072                let mut fpath = path.clone();
1073                fpath.push(PathPart::Key(field.clone()));
1074                match key.as_str() {
1075                    "inputs" => {
1076                        self.add(IndexedElement {
1077                            kind: ElementKind::Field,
1078                            name: node_str(v),
1079                            field: Some("inputs".to_string()),
1080                            tree: Some(tree.to_string()),
1081                            span: self.span(fks, byte_range(v).1),
1082                            key_span: Some(self.span(fks, fks + key.len())),
1083                            path: fpath.clone(),
1084                            depth: 5,
1085                        });
1086                        if let Some(seq) = v.data.as_vec() {
1087                            for (i, item) in seq.iter().enumerate() {
1088                                let mut ipath = fpath.clone();
1089                                ipath.push(PathPart::Index(i));
1090                                self.add(IndexedElement {
1091                                    kind: ElementKind::Reference,
1092                                    name: node_str(item),
1093                                    field: Some("inputs".to_string()),
1094                                    tree: Some(tree.to_string()),
1095                                    span: self.value_span(item),
1096                                    key_span: None,
1097                                    path: ipath,
1098                                    depth: 6,
1099                                });
1100                            }
1101                        }
1102                    }
1103                    _ => {
1104                        self.add(IndexedElement {
1105                            kind: ElementKind::Field,
1106                            name: node_str(v),
1107                            field: Some(field.clone()),
1108                            tree: Some(tree.to_string()),
1109                            span: self.span(fks, byte_range(v).1),
1110                            key_span: Some(self.span(fks, fks + key.len())),
1111                            path: fpath,
1112                            depth: 5,
1113                        });
1114                    }
1115                }
1116            }
1117            let _ = base;
1118        }
1119    }
1120
1121    fn walk_basic_events(
1122        &mut self,
1123        node: &MarkedYaml,
1124        base: &str,
1125        tree: &str,
1126        events_path: &PathKey,
1127    ) {
1128        let Some(map) = node.data.as_mapping() else { return };
1129        for (ek, ev) in map {
1130            let eid = node_str(ek);
1131            let (ks, _ke) = byte_range(ek);
1132            let (_, ve) = byte_range(ev);
1133            let mut path = events_path.clone();
1134            path.push(PathPart::Key(eid.clone()));
1135            self.add(IndexedElement {
1136                kind: ElementKind::Definition,
1137                name: eid.clone(),
1138                field: None,
1139                tree: Some(tree.to_string()),
1140                span: self.span(ks, ve),
1141                key_span: Some(self.span(ks, ks + eid.len())),
1142                path: path.clone(),
1143                depth: 4,
1144            });
1145            let Some(fields) = ev.data.as_mapping() else { continue };
1146            for (k, v) in fields {
1147                let key = node_str(k);
1148                let (fks, _fke) = byte_range(k);
1149                let field = out_name(&key);
1150                let mut fpath = path.clone();
1151                fpath.push(PathPart::Key(field.clone()));
1152                let is_ref = key == "message";
1153                let kind = if is_ref {
1154                    ElementKind::Reference
1155                } else {
1156                    ElementKind::Field
1157                };
1158                self.add(IndexedElement {
1159                    kind,
1160                    name: node_str(v),
1161                    field: Some(field.clone()),
1162                    tree: Some(tree.to_string()),
1163                    span: if is_ref {
1164                        self.value_span(v)
1165                    } else {
1166                        self.span(fks, byte_range(v).1)
1167                    },
1168                    key_span: None,
1169                    path: fpath,
1170                    depth: 5,
1171                });
1172            }
1173            let _ = base;
1174        }
1175    }
1176
1177    fn walk_transfers(
1178        &mut self,
1179        node: &MarkedYaml,
1180        base: &str,
1181        tree: &str,
1182        transfers_path: &PathKey,
1183    ) {
1184        let Some(map) = node.data.as_mapping() else { return };
1185        for (tk, tv) in map {
1186            let tid = node_str(tk);
1187            let (ks, _ke) = byte_range(tk);
1188            let (_, ve) = byte_range(tv);
1189            let mut path = transfers_path.clone();
1190            path.push(PathPart::Key(tid.clone()));
1191            self.add(IndexedElement {
1192                kind: ElementKind::Definition,
1193                name: tid.clone(),
1194                field: None,
1195                tree: Some(tree.to_string()),
1196                span: self.span(ks, ve),
1197                key_span: Some(self.span(ks, ks + tid.len())),
1198                path: path.clone(),
1199                depth: 4,
1200            });
1201            let Some(fields) = tv.data.as_mapping() else { continue };
1202            for (k, v) in fields {
1203                let key = node_str(k);
1204                let (fks, _fke) = byte_range(k);
1205                let field = out_name(&key);
1206                let mut fpath = path.clone();
1207                fpath.push(PathPart::Key(field.clone()));
1208                self.add(IndexedElement {
1209                    kind: ElementKind::Field,
1210                    name: node_str(v),
1211                    field: Some(field.clone()),
1212                    tree: Some(tree.to_string()),
1213                    span: self.span(fks, byte_range(v).1),
1214                    key_span: None,
1215                    path: fpath,
1216                    depth: 5,
1217                });
1218            }
1219            let _ = base;
1220        }
1221    }
1222
1223    fn walk_components(&mut self, node: &MarkedYaml, base: &str) {
1224        let Some(map) = node.data.as_mapping() else { return };
1225        for (k, v) in map {
1226            let key = node_str(k);
1227            let (ks, _ke) = byte_range(k);
1228            let field = out_name(&key);
1229            let path = vec![
1230                PathPart::Key(base.to_string()),
1231                PathPart::Key(field.clone()),
1232            ];
1233            self.add(IndexedElement {
1234                kind: ElementKind::Field,
1235                name: key.clone(),
1236                field: Some(field.clone()),
1237                tree: None,
1238                span: self.span(ks, byte_range(v).1),
1239                key_span: Some(self.span(ks, ks + key.len())),
1240                path: path.clone(),
1241                depth: 2,
1242            });
1243            let Some(items) = v.data.as_mapping() else { continue };
1244            for (ik, iv) in items {
1245                let iid = node_str(ik);
1246                let (iks, _ike) = byte_range(ik);
1247                let mut ipath = path.clone();
1248                ipath.push(PathPart::Key(iid.clone()));
1249                self.add(IndexedElement {
1250                    kind: ElementKind::Definition,
1251                    name: iid.clone(),
1252                    field: None,
1253                    tree: Some(iid.clone()),
1254                    span: self.span(iks, byte_range(iv).1),
1255                    key_span: Some(self.span(iks, iks + iid.len())),
1256                    path: ipath.clone(),
1257                    depth: 3,
1258                });
1259                let Some(fields) = iv.data.as_mapping() else { continue };
1260                for (fk, fv) in fields {
1261                    let fkey = node_str(fk);
1262                    let (fks, _fke) = byte_range(fk);
1263                    let fname = out_name(&fkey);
1264                    let mut fpath = ipath.clone();
1265                    fpath.push(PathPart::Key(fname.clone()));
1266                    let is_ref = matches!(
1267                        fkey.as_str(),
1268                        "next" | "onFailure" | "message" | "channel" | "emits" | "inputs"
1269                    );
1270                    let kind = if is_ref {
1271                        ElementKind::Reference
1272                    } else {
1273                        ElementKind::Field
1274                    };
1275                    self.add(IndexedElement {
1276                        kind,
1277                        name: node_str(fv),
1278                        field: Some(fname.clone()),
1279                        tree: Some(iid.clone()),
1280                        span: if is_ref {
1281                            self.value_span(fv)
1282                        } else {
1283                            self.span(fks, byte_range(fv).1)
1284                        },
1285                        key_span: None,
1286                        path: fpath,
1287                        depth: 4,
1288                    });
1289                }
1290            }
1291        }
1292    }
1293}
1294
1295// ---------------------------------------------------------------------------
1296// Position helpers
1297// ---------------------------------------------------------------------------
1298
1299struct LineMap<'a> {
1300    content: &'a str,
1301    line_starts: Vec<usize>,
1302}
1303
1304impl<'a> LineMap<'a> {
1305    fn new(content: &'a str) -> Self {
1306        let mut line_starts = vec![0];
1307        for (i, b) in content.bytes().enumerate() {
1308            if b == b'\n' {
1309                line_starts.push(i + 1);
1310            }
1311        }
1312        LineMap {
1313            content,
1314            line_starts,
1315        }
1316    }
1317
1318    fn line_of(&self, byte: usize) -> usize {
1319        match self.line_starts.binary_search(&byte) {
1320            Ok(i) => i,
1321            Err(i) => i.saturating_sub(1),
1322        }
1323    }
1324
1325    fn char_offset(&self, byte: usize) -> usize {
1326        let byte = byte.min(self.content.len());
1327        self.content[..byte].chars().count()
1328    }
1329
1330    fn span_of(&self, start: usize, end: usize) -> Span {
1331        let start = start.min(self.content.len());
1332        let end = end.min(self.content.len());
1333        let line = self.line_of(start);
1334        let line_start = self.line_starts[line];
1335        let column = self.content[line_start..start].chars().count();
1336        let end_line = self.line_of(end);
1337        let end_line_start = self.line_starts[end_line];
1338        let end_column = self.content[end_line_start..end].chars().count();
1339        Span {
1340            start: self.char_offset(start) as u32,
1341            end: self.char_offset(end) as u32,
1342            line: line as u32,
1343            column: column as u32,
1344            end_line: end_line as u32,
1345            end_column: end_column as u32,
1346        }
1347    }
1348}
1349
1350#[cfg(test)]
1351mod tests {
1352    use super::*;
1353    use serde_json::json;
1354
1355    const FIXTURE: &str = include_str!("../tests/fixtures/order-fulfillment.etdl");
1356
1357    #[test]
1358    fn index_covers_sections_and_definitions() {
1359        let index = build_span_index(FIXTURE).unwrap();
1360        let event_trees = index
1361            .resolve(&SpanKey::Section("event_trees"))
1362            .expect("event_trees section");
1363        assert_eq!(event_trees.kind, ElementKind::Section);
1364        assert_eq!(event_trees.span.line, 11); // "eventTrees:" is 0-based line 11
1365
1366        let tree = index
1367            .resolve(&SpanKey::Tree {
1368                tree: "OrderFulfillment".to_string(),
1369            })
1370            .expect("tree definition");
1371        assert_eq!(tree.kind, ElementKind::Definition);
1372        assert_eq!(tree.span.line, 12);
1373
1374        let node = index
1375            .resolve(&SpanKey::Node {
1376                tree: "OrderFulfillment".to_string(),
1377                id: "InventoryCheckBarrier".to_string(),
1378            })
1379            .expect("node definition");
1380        assert_eq!(node.span.line, 20);
1381        let key_span = node.key_span.expect("key span");
1382        assert_eq!(key_span.line, 20);
1383        assert_eq!(key_span.column, 6); // 0-based column of the node name
1384    }
1385
1386    #[test]
1387    fn index_covers_references() {
1388        let index = build_span_index(FIXTURE).unwrap();
1389        let next = index
1390            .resolve(&SpanKey::NodeField {
1391                tree: "OrderFulfillment".to_string(),
1392                id: "ProcessPaymentOperation".to_string(),
1393                field: "next",
1394            })
1395            .expect("next reference");
1396        assert_eq!(next.kind, ElementKind::Reference);
1397        assert_eq!(next.name, "FulfillmentConsequence");
1398        assert_eq!(next.span.line, 39);
1399
1400        let message = index
1401            .resolve(&SpanKey::InitiatingEvent {
1402                tree: "OrderFulfillment".to_string(),
1403                field: "message",
1404            })
1405            .expect("initiatingEvent.message reference");
1406        assert_eq!(message.kind, ElementKind::Reference);
1407        assert_eq!(message.name, "orders_api#/components/messages/OrderPlaced");
1408
1409        let gate_input = index
1410            .resolve(&SpanKey::GateInput {
1411                tree: "PaymentGatewayFailure".to_string(),
1412                id: "GatewayUnavailableOrRejected".to_string(),
1413                idx: 1,
1414            })
1415            .expect("gate input reference");
1416        assert_eq!(gate_input.name, "ChargeRejected");
1417
1418        let root_cause = index
1419            .resolve(&SpanKey::TopEvent {
1420                tree: "PaymentGatewayFailure".to_string(),
1421                field: "root_cause",
1422            })
1423            .expect("rootCause reference");
1424        assert_eq!(root_cause.name, "GatewayUnavailableOrRejected");
1425    }
1426
1427    #[test]
1428    fn find_deepest_resolves_reference_tokens() {
1429        let index = build_span_index(FIXTURE).unwrap();
1430        // Line "        next: FulfillmentConsequence" is 0-based line 39.
1431        let line = FIXTURE.lines().nth(39).unwrap();
1432        let byte_offset = FIXTURE
1433            .lines()
1434            .take(39)
1435            .map(|l| l.len() + 1)
1436            .sum::<usize>()
1437            + line.find("FulfillmentConsequence").unwrap();
1438        let char_offset = FIXTURE[..byte_offset].chars().count() as u32;
1439
1440        let el = index.find_deepest(char_offset).expect("found");
1441        assert_eq!(el.kind, ElementKind::Reference);
1442        assert_eq!(el.name, "FulfillmentConsequence");
1443        assert_eq!(el.field.as_deref(), Some("next"));
1444        assert_eq!(el.tree.as_deref(), Some("OrderFulfillment"));
1445        assert!(el.span.start <= char_offset && char_offset < el.span.end);
1446    }
1447
1448    #[test]
1449    fn inject_spans_wraps_scalars() {
1450        let (doc, index) = parse_document_with_spans(FIXTURE).unwrap();
1451        let mut value = serde_json::to_value(&doc).unwrap();
1452        inject_spans(&mut value, &index);
1453
1454        let event_trees = value.get("event_trees").expect("event_trees");
1455        assert!(event_trees.get("span").is_some(), "section span attached");
1456        let tree = &event_trees["OrderFulfillment"];
1457        assert!(tree.get("span").is_some());
1458        let node = &tree["nodes"]["InventoryCheckBarrier"];
1459        assert!(node.get("span").is_some(), "node block span attached");
1460
1461        // Scalar reference wrapped as { value, span }.
1462        let op = &tree["nodes"]["ProcessPaymentOperation"];
1463        let next = &op["next"];
1464        assert!(next.is_object());
1465        assert_eq!(next["value"], "FulfillmentConsequence");
1466        assert!(next.get("span").is_some());
1467
1468        // A scalar without a recorded span stays plain (retryPolicy internals).
1469        assert_eq!(op["retry_policy"]["max_attempts"], 3);
1470        assert_eq!(op["action"]["value"], "execute");
1471        assert!(op["action"].get("span").is_some());
1472    }
1473
1474    #[test]
1475    fn duplicate_ids_are_detected() {
1476        let yaml = r#"
1477etdl: "1.0.0"
1478info:
1479  title: "T"
1480  version: "1.0.0"
1481  domain: "D"
1482asyncapi_imports: {}
1483eventTrees:
1484  T:
1485    initiatingEvent:
1486      id: I
1487      message: "a#/m"
1488      next: N
1489    nodes:
1490      N:
1491        type: barrier
1492        branches:
1493          - outcome: ok
1494            condition: "default"
1495            next: M
1496      M:
1497        type: consequence
1498        operation: terminate
1499      N:
1500        type: consequence
1501        operation: terminate
1502"#;
1503        let dups = detect_duplicate_ids(yaml).unwrap();
1504        assert_eq!(dups.len(), 1, "expected one duplicate node id");
1505        assert_eq!(dups[0].kind, "node");
1506        assert_eq!(dups[0].id, "N");
1507        assert_eq!(dups[0].tree, "T");
1508    }
1509
1510    #[test]
1511    fn span_key_resolves_to_path() {
1512        let index = build_span_index(FIXTURE).unwrap();
1513        let branch = index
1514            .resolve(&SpanKey::BranchField {
1515                tree: "OrderFulfillment".to_string(),
1516                id: "InventoryCheckBarrier".to_string(),
1517                branch: 1,
1518                field: "next",
1519            })
1520            .expect("branch next reference");
1521        assert_eq!(branch.name, "OutOfStockConsequence");
1522        let _ = json!({});
1523    }
1524}