1use std::collections::{BTreeMap, BTreeSet, HashSet};
7use std::fmt;
8use std::io::Write;
9
10use anyhow::{bail, Context, Result};
11use serde::{Deserialize, Serialize};
12
13pub const SCHEMA: &str = "candle-graph/runtime/1";
15pub const SCHEMA_V2: &str = "candle-graph/runtime/2";
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct RunMetadata {
20 pub entrypoint: String,
22 pub profile: String,
24 #[serde(default)]
26 pub cargo_features: Vec<String>,
27 #[serde(default)]
29 pub cfg: Vec<String>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub analysis_id: Option<String>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub build_id: Option<String>,
38}
39
40#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
45pub struct ExpectedIdentity {
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub analysis_id: Option<String>,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub build_id: Option<String>,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case")]
55pub enum IdentityField {
56 AnalysisId,
57 BuildId,
58}
59
60impl fmt::Display for IdentityField {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 match self {
63 Self::AnalysisId => write!(f, "analysis_id"),
64 Self::BuildId => write!(f, "build_id"),
65 }
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71pub struct IdentityMismatch {
72 pub field: IdentityField,
73 pub expected: String,
74 pub observed: String,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
81#[serde(rename_all = "snake_case")]
82pub enum ObservationConfidence {
83 Proven,
85 Unknown,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub struct TensorObservation {
92 pub event_id: String,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub static_id: Option<String>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub source: Option<String>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub step: Option<u64>,
103 pub shape: Vec<usize>,
104 pub dtype: String,
105 pub device: String,
106 pub contiguous: bool,
108 pub requires_grad: bool,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub storage_id: Option<String>,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116pub struct OperationObservation {
117 pub event_id: String,
118 pub op: String,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub static_id: Option<String>,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub source: Option<String>,
126 #[serde(default)]
128 pub inputs: Vec<String>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub output: Option<String>,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
136pub struct ParamIdentity {
137 pub root: String,
139 pub key: String,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
145#[serde(rename_all = "snake_case")]
146pub enum GradientState {
147 Present,
148 Missing,
149 Zero,
150 NonFinite,
151}
152
153impl fmt::Display for GradientState {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 match self {
156 Self::Present => write!(f, "present"),
157 Self::Missing => write!(f, "missing"),
158 Self::Zero => write!(f, "zero"),
159 Self::NonFinite => write!(f, "non_finite"),
160 }
161 }
162}
163
164#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166pub struct GradientFact {
167 pub event_id: String,
168 pub root: String,
169 pub key: String,
170 pub state: GradientState,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub step: Option<u64>,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub norm: Option<f64>,
177}
178
179impl GradientFact {
180 pub fn identity(&self) -> ParamIdentity {
181 ParamIdentity {
182 root: self.root.clone(),
183 key: self.key.clone(),
184 }
185 }
186
187 pub fn validate(&self) -> Result<()> {
189 match self.state {
190 GradientState::Missing => {
191 if self.norm.is_some() {
192 bail!(
193 "invalid gradient fact {}:{}: missing state must not carry a norm",
194 self.root,
195 self.key
196 );
197 }
198 }
199 GradientState::Zero => {
200 if let Some(n) = self.norm {
201 if n != 0.0 || !n.is_finite() {
202 bail!(
203 "invalid gradient fact {}:{}: zero state requires norm 0.0 when set, got {n}",
204 self.root,
205 self.key
206 );
207 }
208 }
209 }
210 GradientState::Present => {
211 if let Some(n) = self.norm {
212 if !n.is_finite() {
213 bail!(
214 "invalid gradient fact {}:{}: present state cannot have non-finite norm",
215 self.root,
216 self.key
217 );
218 }
219 if n == 0.0 {
220 bail!(
221 "invalid gradient fact {}:{}: present state cannot have zero norm (use zero)",
222 self.root,
223 self.key
224 );
225 }
226 }
227 }
228 GradientState::NonFinite => {
229 if let Some(n) = self.norm {
230 if n.is_finite() {
231 bail!(
232 "invalid gradient fact {}:{}: non_finite state cannot have finite norm {n}",
233 self.root,
234 self.key
235 );
236 }
237 }
238 }
239 }
240 Ok(())
241 }
242}
243
244#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
246pub struct ValueObservation {
247 pub event_id: String,
248 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub static_id: Option<String>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub source: Option<String>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
253 pub step: Option<u64>,
254 pub min: f64,
255 pub max: f64,
256 pub abs_max: f64,
257 #[serde(default)]
258 pub nonfinite_count: u64,
259 #[serde(default)]
260 pub saturated_count: u64,
261}
262
263#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
265pub struct RuntimeTrace {
266 pub schema: String,
267 pub run: RunMetadata,
268 #[serde(default)]
269 pub tensors: Vec<TensorObservation>,
270 #[serde(default)]
271 pub operations: Vec<OperationObservation>,
272 #[serde(default)]
273 pub gradients: Vec<GradientFact>,
274 #[serde(default)]
275 pub values: Vec<ValueObservation>,
276}
277
278#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
280#[serde(tag = "kind", rename_all = "snake_case")]
281pub enum RuntimeEvent {
282 Meta {
284 schema: String,
285 #[serde(flatten)]
286 run: RunMetadata,
287 },
288 Tensor(TensorObservation),
289 Operation(OperationObservation),
290 Gradient(GradientFact),
291 Value(ValueObservation),
292}
293
294pub struct RuntimeTraceWriter<W: Write> {
300 writer: W,
301 seen_event_ids: HashSet<String>,
302}
303
304impl<W: Write> RuntimeTraceWriter<W> {
305 pub fn new(writer: W, run: RunMetadata) -> Result<Self> {
307 let mut output = Self {
308 writer,
309 seen_event_ids: HashSet::new(),
310 };
311 output.write_event(&RuntimeEvent::Meta {
312 schema: SCHEMA.to_string(),
313 run,
314 })?;
315 Ok(output)
316 }
317
318 pub fn tensor(&mut self, observation: TensorObservation) -> Result<()> {
320 self.reserve_event_id(&observation.event_id)?;
321 self.write_event(&RuntimeEvent::Tensor(observation))
322 }
323
324 pub fn operation(&mut self, observation: OperationObservation) -> Result<()> {
326 self.reserve_event_id(&observation.event_id)?;
327 self.write_event(&RuntimeEvent::Operation(observation))
328 }
329
330 pub fn gradient(&mut self, fact: GradientFact) -> Result<()> {
332 fact.validate()?;
333 self.reserve_event_id(&fact.event_id)?;
334 self.write_event(&RuntimeEvent::Gradient(fact))
335 }
336
337 pub fn value(&mut self, observation: ValueObservation) -> Result<()> {
339 self.reserve_event_id(&observation.event_id)?;
340 self.write_event(&RuntimeEvent::Value(observation))
341 }
342
343 pub fn finish(mut self) -> Result<W> {
345 self.writer
346 .flush()
347 .context("flushing runtime JSONL trace")?;
348 Ok(self.writer)
349 }
350
351 fn reserve_event_id(&mut self, event_id: &str) -> Result<()> {
352 if event_id.is_empty() {
353 bail!("empty event_id is not allowed");
354 }
355 if !self.seen_event_ids.insert(event_id.to_string()) {
356 bail!("duplicate event_id `{event_id}`");
357 }
358 Ok(())
359 }
360
361 fn write_event(&mut self, event: &RuntimeEvent) -> Result<()> {
362 let mut line = serde_json::to_vec(event).context("serializing runtime JSONL event")?;
363 line.push(b'\n');
364 self.writer
365 .write_all(&line)
366 .context("writing runtime JSONL event")
367 }
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
372#[serde(rename_all = "snake_case")]
373pub enum TensorConflictKind {
374 Dtype,
375 Device,
376 Shape,
377 Layout,
378 RequiresGrad,
379}
380
381fn unknown_confidence() -> ObservationConfidence {
382 ObservationConfidence::Unknown
383}
384
385#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
387pub struct TensorConflict {
388 pub static_id: String,
389 pub kind: TensorConflictKind,
390 pub values: Vec<String>,
392 #[serde(default = "unknown_confidence")]
394 pub confidence: ObservationConfidence,
395}
396
397#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
399pub struct GradientConflict {
400 pub identity: ParamIdentity,
401 pub states: Vec<String>,
403 pub event_ids: Vec<String>,
405 #[serde(default = "unknown_confidence")]
407 pub confidence: ObservationConfidence,
408}
409
410#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
412pub struct RuntimeAudit {
413 pub missing_gradients: Vec<ParamIdentity>,
414 pub non_finite_gradients: Vec<ParamIdentity>,
415 pub zero_gradients: Vec<ParamIdentity>,
416 #[serde(default)]
417 pub tensor_conflicts: Vec<TensorConflict>,
418 #[serde(default)]
419 pub gradient_conflicts: Vec<GradientConflict>,
420 #[serde(default)]
421 pub identity_mismatches: Vec<IdentityMismatch>,
422 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub first_non_finite_step: Option<u64>,
425 #[serde(default)]
427 pub saturating_activations: Vec<ParamIdentity>,
428}
429
430impl RuntimeAudit {
431 pub fn is_clean(&self) -> bool {
432 self.missing_gradients.is_empty()
433 && self.non_finite_gradients.is_empty()
434 && self.zero_gradients.is_empty()
435 && self.tensor_conflicts.is_empty()
436 && self.gradient_conflicts.is_empty()
437 && self.identity_mismatches.is_empty()
438 }
439
440 pub fn has_unknown_evidence(&self) -> bool {
442 !self.tensor_conflicts.is_empty()
443 || !self.gradient_conflicts.is_empty()
444 || !self.identity_mismatches.is_empty()
445 }
446}
447
448impl RuntimeTrace {
449 pub fn normalize(&mut self) {
451 self.run.cargo_features.sort();
452 self.run.cargo_features.dedup();
453 self.run.cfg.sort();
454 self.run.cfg.dedup();
455
456 self.tensors.sort_by(|a, b| {
457 a.event_id
458 .cmp(&b.event_id)
459 .then_with(|| a.static_id.cmp(&b.static_id))
460 .then_with(|| a.source.cmp(&b.source))
461 });
462 self.operations.sort_by(|a, b| {
463 a.event_id
464 .cmp(&b.event_id)
465 .then_with(|| a.op.cmp(&b.op))
466 .then_with(|| a.static_id.cmp(&b.static_id))
467 });
468 self.gradients.sort_by(|a, b| {
469 a.root
470 .cmp(&b.root)
471 .then_with(|| a.key.cmp(&b.key))
472 .then_with(|| a.step.cmp(&b.step))
473 .then_with(|| a.event_id.cmp(&b.event_id))
474 });
475 self.values.sort_by(|a, b| {
476 a.event_id
477 .cmp(&b.event_id)
478 .then_with(|| a.step.cmp(&b.step))
479 .then_with(|| a.source.cmp(&b.source))
480 });
481 }
482
483 pub fn validate(&self) -> Result<()> {
488 if self.schema != SCHEMA && self.schema != SCHEMA_V2 {
489 bail!(
490 "unsupported runtime schema {:?}; expected {:?} or {:?}",
491 self.schema,
492 SCHEMA,
493 SCHEMA_V2
494 );
495 }
496
497 let mut seen = HashSet::new();
498 let mut push_id = |id: &str| -> Result<()> {
499 if id.is_empty() {
500 bail!("empty event_id is not allowed");
501 }
502 if !seen.insert(id.to_string()) {
503 bail!("duplicate event_id `{id}`");
504 }
505 Ok(())
506 };
507
508 for t in &self.tensors {
509 push_id(&t.event_id)?;
510 }
511 for op in &self.operations {
512 push_id(&op.event_id)?;
513 }
514 for g in &self.gradients {
515 push_id(&g.event_id)?;
516 g.validate()?;
517 }
518 for v in &self.values {
519 push_id(&v.event_id)?;
520 }
521 Ok(())
522 }
523
524 pub fn finalize(mut self) -> Result<Self> {
526 self.normalize();
527 self.validate()?;
528 Ok(self)
529 }
530
531 pub fn check_identity(&self, expected: &ExpectedIdentity) -> Vec<IdentityMismatch> {
536 let mut out = Vec::new();
537 if let (Some(expected_id), Some(observed)) = (
538 expected.analysis_id.as_deref(),
539 self.run.analysis_id.as_deref(),
540 ) {
541 if expected_id != observed {
542 out.push(IdentityMismatch {
543 field: IdentityField::AnalysisId,
544 expected: expected_id.to_string(),
545 observed: observed.to_string(),
546 });
547 }
548 }
549 if let (Some(expected_id), Some(observed)) =
550 (expected.build_id.as_deref(), self.run.build_id.as_deref())
551 {
552 if expected_id != observed {
553 out.push(IdentityMismatch {
554 field: IdentityField::BuildId,
555 expected: expected_id.to_string(),
556 observed: observed.to_string(),
557 });
558 }
559 }
560 out
561 }
562
563 pub fn require_identity(&self, expected: &ExpectedIdentity) -> Result<()> {
567 let mismatches = self.check_identity(expected);
568 if let Some(first) = mismatches.first() {
569 bail!(
570 "runtime {} mismatch: expected {:?}, observed {:?}",
571 first.field,
572 first.expected,
573 first.observed
574 );
575 }
576 Ok(())
577 }
578
579 pub fn tensors_by_static_id(&self, static_id: &str) -> Vec<&TensorObservation> {
581 self.tensors
582 .iter()
583 .filter(|t| t.static_id.as_deref() == Some(static_id))
584 .collect()
585 }
586
587 pub fn agreed_tensor(&self, static_id: &str) -> Option<&TensorObservation> {
592 let obs = self.tensors_by_static_id(static_id);
593 if obs.is_empty() {
594 return None;
595 }
596 let first = obs[0];
597 for other in &obs[1..] {
598 if other.shape != first.shape
599 || other.dtype != first.dtype
600 || other.device != first.device
601 || other.contiguous != first.contiguous
602 || other.requires_grad != first.requires_grad
603 {
604 return None;
605 }
606 }
607 Some(first)
608 }
609
610 pub fn tensor_confidence(&self, static_id: &str) -> ObservationConfidence {
612 match self.agreed_tensor(static_id) {
613 Some(_) => ObservationConfidence::Proven,
614 None => ObservationConfidence::Unknown,
615 }
616 }
617
618 pub fn gradients_for(&self, root: &str, key: &str) -> Vec<&GradientFact> {
620 self.gradients
621 .iter()
622 .filter(|g| g.root == root && g.key == key)
623 .collect()
624 }
625
626 pub fn gradient(&self, root: &str, key: &str) -> Option<&GradientFact> {
631 let facts = self.gradients_for(root, key);
632 agreed_gradient(&facts)
633 }
634
635 pub fn gradient_confidence(&self, root: &str, key: &str) -> ObservationConfidence {
637 match self.gradient(root, key) {
638 Some(_) => ObservationConfidence::Proven,
639 None => ObservationConfidence::Unknown,
640 }
641 }
642
643 pub fn audit(&self) -> RuntimeAudit {
645 self.audit_with_identity(None)
646 }
647
648 pub fn audit_with_identity(&self, expected: Option<&ExpectedIdentity>) -> RuntimeAudit {
650 let gradient_conflicts = self.collect_gradient_conflicts();
651 let conflicted: HashSet<ParamIdentity> = gradient_conflicts
652 .iter()
653 .map(|c| c.identity.clone())
654 .collect();
655
656 let mut missing = BTreeSet::new();
657 let mut non_finite = BTreeSet::new();
658 let mut zero = BTreeSet::new();
659
660 let mut by_param: BTreeMap<ParamIdentity, Vec<&GradientFact>> = BTreeMap::new();
662 for g in &self.gradients {
663 by_param.entry(g.identity()).or_default().push(g);
664 }
665 for (id, facts) in by_param {
666 if conflicted.contains(&id) {
667 continue;
668 }
669 let Some(g) = latest_agreed_gradient(&facts).or_else(|| agreed_gradient(&facts)) else {
670 continue;
671 };
672 match g.state {
673 GradientState::Missing => {
674 missing.insert(id);
675 }
676 GradientState::NonFinite => {
677 non_finite.insert(id);
678 }
679 GradientState::Zero => {
680 zero.insert(id);
681 }
682 GradientState::Present => {}
683 }
684 }
685
686 let mut by_static: BTreeMap<&str, Vec<&TensorObservation>> = BTreeMap::new();
687 for t in &self.tensors {
688 if let Some(sid) = t.static_id.as_deref() {
689 by_static.entry(sid).or_default().push(t);
690 }
691 }
692
693 let mut conflicts = Vec::new();
694 for (static_id, obs) in by_static {
695 if obs.len() < 2 {
696 continue;
697 }
698 push_conflict(
699 &mut conflicts,
700 static_id,
701 TensorConflictKind::Dtype,
702 |t| t.dtype.clone(),
703 &obs,
704 );
705 push_conflict(
706 &mut conflicts,
707 static_id,
708 TensorConflictKind::Device,
709 |t| t.device.clone(),
710 &obs,
711 );
712 push_conflict(
713 &mut conflicts,
714 static_id,
715 TensorConflictKind::Shape,
716 |t| format_shape(&t.shape),
717 &obs,
718 );
719 push_conflict(
720 &mut conflicts,
721 static_id,
722 TensorConflictKind::Layout,
723 |t| t.contiguous.to_string(),
724 &obs,
725 );
726 push_conflict(
727 &mut conflicts,
728 static_id,
729 TensorConflictKind::RequiresGrad,
730 |t| t.requires_grad.to_string(),
731 &obs,
732 );
733 }
734 conflicts.sort_by(|a, b| {
735 a.static_id
736 .cmp(&b.static_id)
737 .then_with(|| a.kind.cmp(&b.kind))
738 });
739
740 let identity_mismatches = expected
741 .map(|expected| self.check_identity(expected))
742 .unwrap_or_default();
743
744 let first_non_finite_step = self
745 .gradients
746 .iter()
747 .filter(|g| matches!(g.state, GradientState::NonFinite))
748 .filter_map(|g| g.step)
749 .min();
750
751 let mut saturating = BTreeSet::new();
752 for value in &self.values {
753 if value.saturated_count == 0 {
754 continue;
755 }
756 if let Some(source) = &value.source {
757 saturating.insert(ParamIdentity {
758 root: "value".into(),
759 key: source.clone(),
760 });
761 }
762 }
763
764 RuntimeAudit {
765 missing_gradients: missing.into_iter().collect(),
766 non_finite_gradients: non_finite.into_iter().collect(),
767 zero_gradients: zero.into_iter().collect(),
768 tensor_conflicts: conflicts,
769 gradient_conflicts,
770 identity_mismatches,
771 first_non_finite_step,
772 saturating_activations: saturating.into_iter().collect(),
773 }
774 }
775
776 fn collect_gradient_conflicts(&self) -> Vec<GradientConflict> {
777 let mut by_param: BTreeMap<ParamIdentity, Vec<&GradientFact>> = BTreeMap::new();
778 for g in &self.gradients {
779 by_param.entry(g.identity()).or_default().push(g);
780 }
781 let mut out = Vec::new();
782 for (identity, facts) in by_param {
783 if facts.len() < 2 {
784 continue;
785 }
786 if facts.iter().all(|g| g.step.is_some()) {
788 let steps: BTreeSet<_> = facts.iter().filter_map(|g| g.step).collect();
789 if steps.len() == facts.len() {
790 continue;
791 }
792 }
793 if latest_agreed_gradient(&facts).is_some() || agreed_gradient(&facts).is_some() {
794 continue;
795 }
796 let mut states: BTreeSet<String> = BTreeSet::new();
797 let mut event_ids: BTreeSet<String> = BTreeSet::new();
798 for g in &facts {
799 states.insert(g.state.to_string());
800 event_ids.insert(g.event_id.clone());
801 }
802 let mut norms: BTreeSet<String> = BTreeSet::new();
804 for g in &facts {
805 norms.insert(match g.norm {
806 Some(n) => format!("{n}"),
807 None => "none".to_string(),
808 });
809 }
810 if states.len() <= 1 && norms.len() <= 1 {
811 continue;
812 }
813 out.push(GradientConflict {
814 identity,
815 states: states.into_iter().collect(),
816 event_ids: event_ids.into_iter().collect(),
817 confidence: ObservationConfidence::Unknown,
818 });
819 }
820 out.sort_by(|a, b| a.identity.cmp(&b.identity));
821 out
822 }
823}
824
825fn agreed_gradient<'a>(facts: &[&'a GradientFact]) -> Option<&'a GradientFact> {
827 let first = facts.first().copied()?;
828 for other in facts.iter().skip(1) {
829 if other.state != first.state {
830 return None;
831 }
832 match (first.norm, other.norm) {
833 (None, None) => {}
834 (Some(a), Some(b)) if float_eq(a, b) => {}
835 (None, Some(_)) | (Some(_), None) => return None,
836 (Some(_), Some(_)) => return None,
837 }
838 }
839 Some(first)
840}
841
842fn latest_agreed_gradient<'a>(facts: &[&'a GradientFact]) -> Option<&'a GradientFact> {
844 if facts.is_empty() || !facts.iter().all(|g| g.step.is_some()) {
845 return None;
846 }
847 facts
848 .iter()
849 .copied()
850 .max_by_key(|g| g.step.unwrap_or(0))
851}
852
853fn float_eq(a: f64, b: f64) -> bool {
854 if a.is_nan() && b.is_nan() {
855 return true;
856 }
857 a == b
858}
859
860fn format_shape(shape: &[usize]) -> String {
861 format!(
862 "[{}]",
863 shape
864 .iter()
865 .map(|d| d.to_string())
866 .collect::<Vec<_>>()
867 .join(", ")
868 )
869}
870
871fn push_conflict(
872 out: &mut Vec<TensorConflict>,
873 static_id: &str,
874 kind: TensorConflictKind,
875 project: impl Fn(&TensorObservation) -> String,
876 obs: &[&TensorObservation],
877) {
878 let mut values: BTreeSet<String> = BTreeSet::new();
879 for t in obs {
880 values.insert(project(t));
881 }
882 if values.len() > 1 {
883 out.push(TensorConflict {
884 static_id: static_id.to_string(),
885 kind,
886 values: values.into_iter().collect(),
887 confidence: ObservationConfidence::Unknown,
888 });
889 }
890}
891
892pub fn parse_json(input: &str) -> Result<RuntimeTrace> {
894 let trace: RuntimeTrace =
895 serde_json::from_str(input).context("failed to parse runtime JSON document")?;
896 trace.finalize()
897}
898
899pub fn parse_jsonl(input: &str) -> Result<RuntimeTrace> {
901 let mut schema: Option<String> = None;
902 let mut run: Option<RunMetadata> = None;
903 let mut tensors = Vec::new();
904 let mut operations = Vec::new();
905 let mut gradients = Vec::new();
906 let mut values = Vec::new();
907
908 for (line_no, line) in input.lines().enumerate() {
909 let line = line.trim();
910 if line.is_empty() {
911 continue;
912 }
913 let event: RuntimeEvent = serde_json::from_str(line)
914 .with_context(|| format!("failed to parse runtime JSONL line {}", line_no + 1))?;
915 match event {
916 RuntimeEvent::Meta {
917 schema: s,
918 run: meta,
919 } => {
920 if schema.is_some() || run.is_some() {
921 bail!(
922 "duplicate meta event on JSONL line {}; only one meta record is allowed",
923 line_no + 1
924 );
925 }
926 schema = Some(s);
927 run = Some(meta);
928 }
929 RuntimeEvent::Tensor(t) => tensors.push(t),
930 RuntimeEvent::Operation(op) => operations.push(op),
931 RuntimeEvent::Gradient(g) => gradients.push(g),
932 RuntimeEvent::Value(v) => values.push(v),
933 }
934 }
935
936 let schema = schema.unwrap_or_else(|| SCHEMA.to_string());
937 let run = run.context("JSONL stream is missing a meta event with run metadata")?;
938
939 RuntimeTrace {
940 schema,
941 run,
942 tensors,
943 operations,
944 gradients,
945 values,
946 }
947 .finalize()
948}
949
950pub fn parse(input: &str) -> Result<RuntimeTrace> {
952 let trimmed = input.trim();
953 if trimmed.is_empty() {
954 bail!("empty runtime evidence input");
955 }
956 if trimmed.starts_with('{') && !trimmed.contains('\n') {
958 return parse_json(trimmed);
959 }
960 if trimmed.starts_with('{') {
961 if let Ok(value) = serde_json::from_str::<serde_json::Value>(trimmed) {
963 if value.is_object() && value.get("schema").is_some() && value.get("run").is_some() {
964 let trace: RuntimeTrace = serde_json::from_value(value)
965 .context("failed to parse runtime JSON document")?;
966 return trace.finalize();
967 }
968 }
969 }
970 parse_jsonl(input)
971}