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