fallow_output/audit_brief.rs
1//! Audit brief output contracts.
2
3use crate::root_envelopes::{RootEnvelopeMode, attach_telemetry_meta, serialize_named_json_output};
4use fallow_types::envelope::{ElapsedMs, Meta, ToolVersion};
5use serde::Serialize;
6use serde_json::Value;
7
8/// Wire version for the `fallow audit --brief --format json` envelope.
9pub const REVIEW_BRIEF_SCHEMA_VERSION: u32 = 6;
10
11/// Independently-versioned wire-version newtype for the brief envelope.
12/// Serializes as the integer `REVIEW_BRIEF_SCHEMA_VERSION`.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
14#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
15pub struct ReviewBriefSchemaVersion(pub u32);
16
17impl Default for ReviewBriefSchemaVersion {
18 fn default() -> Self {
19 Self(REVIEW_BRIEF_SCHEMA_VERSION)
20 }
21}
22
23/// Coarse risk classification for a changeset, a pure function of the change
24/// size (file count plus, once threaded, net lines).
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
26#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
27#[serde(rename_all = "snake_case")]
28pub enum RiskClass {
29 /// Small, contained change.
30 Low,
31 /// Moderately sized change.
32 Medium,
33 /// Large change spanning many files or lines.
34 High,
35}
36
37/// Suggested reviewer effort, a pure function of [`RiskClass`].
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
39#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
40#[serde(rename_all = "snake_case")]
41pub enum ReviewEffort {
42 /// A quick scan is enough.
43 Glance,
44 /// A normal line-by-line review.
45 Review,
46 /// A careful, deep review is warranted.
47 DeepDive,
48}
49
50/// Stage 0 of the brief: triage facts derived purely from the diff size.
51///
52/// `hunks` and `net_lines` are populated when the caller supplies parsed diff
53/// evidence. They remain absent when no diff is available.
54#[derive(Debug, Clone, Serialize)]
55#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
56pub struct DiffTriage {
57 /// Number of changed files in the audit scope.
58 pub files: usize,
59 /// Number of diff hunks, or `None` when no diff evidence was supplied.
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub hunks: Option<usize>,
62 /// Net added-minus-removed lines, or `None` without diff evidence.
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub net_lines: Option<i64>,
65 /// Coarse risk class derived from the change size.
66 pub risk_class: RiskClass,
67 /// Suggested reviewer effort derived from `risk_class`.
68 pub review_effort: ReviewEffort,
69}
70
71/// Stage 1 of the brief: graph-derived orientation facts.
72///
73/// `boundaries_touched` is derived from the run's boundary-violation zones;
74/// `reachable_from` is populated by the impact closure (the affected-not-shown
75/// set: modules the changed code is reachable from / affects, none in the diff).
76/// `exports_added` and `api_width_delta` both report the exports-aware public API
77/// widening count. Removed exports are not represented in this widening-only
78/// signal.
79#[derive(Debug, Clone, Serialize)]
80#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
81pub struct GraphFacts {
82 /// Number of public API exports added by the changeset. Zero means the
83 /// changeset adds no public API exports.
84 pub exports_added: usize,
85 /// Widening-only public API delta, currently equal to `exports_added`.
86 /// Removed exports are not represented, so zero means no public API exports
87 /// were added.
88 pub api_width_delta: i64,
89 /// Root-relative paths of modules the changed code is reachable from / affects
90 /// (the impact closure's affected-but-not-in-diff set), deduped and sorted.
91 /// Empty when no graph was retained or nothing depends on the changed files.
92 pub reachable_from: Vec<String>,
93 /// Architecture boundary zones touched by the changeset, deduped and sorted.
94 /// Derived from the run's boundary-violation findings.
95 pub boundaries_touched: Vec<String>,
96}
97
98/// Stage 3 of the brief: the impact closure. The transitive
99/// affected-but-not-in-diff set plus the coordination gap. The differentiator a
100/// diff tool fundamentally cannot do, because it has no graph.
101///
102/// Honest scope (ADR-001, syntactic): the coordination gap is an attention
103/// pointer at the exact inter-module failure mode, NOT a correctness proof.
104#[derive(Debug, Clone, Default, Serialize)]
105#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
106pub struct ImpactClosureFacts {
107 /// Root-relative paths transitively affected by the changeset (reverse-deps +
108 /// re-export chains) that are NOT in the diff, deduped and sorted.
109 pub affected_not_shown: Vec<String>,
110 /// Coordination gaps: a changed file exports a contract consumed by a module
111 /// absent from the diff. One entry per (changed file, consumer) pair.
112 pub coordination_gap: Vec<CoordinationGapFact>,
113}
114
115/// One coordination-gap entry: a changed file exports symbols consumed by a
116/// `consumer_file` that is NOT in the diff. Deduped per (changed, consumer) pair
117/// (firing-precision rule R2).
118#[derive(Debug, Clone, Serialize)]
119#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
120pub struct CoordinationGapFact {
121 /// Root-relative path of the changed file whose contract is consumed elsewhere.
122 pub changed_file: String,
123 /// Root-relative path of the consumer module that is NOT in the diff.
124 pub consumer_file: String,
125 /// The exported symbol names the consumer references, sorted.
126 pub consumed_symbols: Vec<String>,
127 /// Honest scope note: this is a syntactic attention pointer, not a proof.
128 pub note: String,
129}
130
131/// Stage 2 of the brief: the partition + order. The changed files split into
132/// coherent BY-MODULE units (the only byte-identical-deterministic clustering
133/// definition straight from the graph), plus a dependency-sensible review ORDER
134/// over those units (definitions before consumers, mechanical/leaf units last,
135/// ties broken by the path sort). Stage 2 sits UNDER the decision surface as a
136/// drill-down; it is the backbone the directed-review loop hands the agent.
137///
138/// Feature-cluster and concern partitioning are deferred (they need scoring
139/// heuristics whose tie-breaks are a fresh nondeterminism surface).
140#[derive(Debug, Clone, Default, Serialize)]
141#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
142pub struct PartitionFacts {
143 /// The by-module units, sorted by module directory. Empty when no graph was
144 /// retained or no changed file maps to a known module.
145 pub units: Vec<ReviewUnitFact>,
146 /// The dependency-sensible review order: module-directory strings,
147 /// definitions before consumers, mechanical/leaf units last. A permutation of
148 /// the `units` module directories.
149 pub order: Vec<String>,
150}
151
152/// One review unit: a coherent by-module cluster of the changed set.
153#[derive(Debug, Clone, Serialize)]
154#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
155pub struct ReviewUnitFact {
156 /// The module directory the unit covers (root-relative, forward-slashed).
157 /// The empty string is the repository-root group.
158 pub module_dir: String,
159 /// The changed files in this unit, path-sorted.
160 pub files: Vec<String>,
161}
162
163/// Diff-aware deterministic deltas (6.A), framed new-vs-pre-existing against
164/// the audit base snapshot. Each entry is a brief summary/verdict line.
165///
166/// `public_api` is batch-consolidated to ONE decision per change (rule R1):
167/// the `added` list carries the introduced public-export keys as evidence, but a
168/// reviewer reads "the public surface widened by N", never one decision per
169/// symbol.
170#[derive(Debug, Clone, Default, Serialize)]
171#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
172pub struct ReviewDeltas {
173 /// Cross-zone boundary EDGES introduced vs base (R2 first-edge-only: one per
174 /// `<from_zone>-><to_zone>` pair, never per import). New-vs-pre-existing.
175 pub boundary_introduced: Vec<String>,
176 /// Circular dependencies introduced vs base (canonical file-set keys).
177 pub cycle_introduced: Vec<String>,
178 /// Exports-aware public-API surface delta: the public-export keys
179 /// (`<rel_path>::<name>`) added vs base, resolved through `package.json`
180 /// `exports` + re-export reachability. A symbol re-exported only through an
181 /// internal barrel NOT in `exports` is absent here (zero delta); one
182 /// reachable through an `exports` path is present (exactly one).
183 pub public_api_added: Vec<String>,
184}
185
186/// The full `fallow audit --brief --format json` envelope. Carries the
187/// informational verdict, the triage and graph-facts orientation stages, plus
188/// the reused "subtract" section (the same dead-code / duplication / complexity
189/// payload `fallow audit --format json` emits).
190#[derive(Debug, Clone, Serialize)]
191#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
192#[cfg_attr(
193 feature = "schema",
194 schemars(title = "fallow audit --brief --format json")
195)]
196pub struct ReviewBriefOutput<Focus, Weakening, Routing, Decisions> {
197 /// Independently-versioned brief schema version.
198 pub schema_version: ReviewBriefSchemaVersion,
199 /// Fallow CLI version that produced this output.
200 pub version: String,
201 /// Command discriminator singleton: always `"audit-brief"`.
202 pub command: String,
203 pub triage: DiffTriage,
204 /// Stage 1: graph orientation facts.
205 pub graph_facts: GraphFacts,
206 /// Stage 2: the partition + order (by-module units + dependency-sensible
207 /// review order). The backbone the directed-review loop hands the agent.
208 pub partition: PartitionFacts,
209 /// Stage 3: the impact closure (affected-not-shown + coordination gap).
210 pub impact_closure: ImpactClosureFacts,
211 /// Stage 4: the weighted focus map. A composite attention score per
212 /// changed-file unit (fan-in/out + security taint + risk zone + change shape),
213 /// with `review-here` / `not-prioritized` labels (NEVER `skip` in free mode),
214 /// a per-unit confidence flag, and the FULL `deprioritized` escape-hatch list
215 /// so every de-prioritized piece is reachable. Stage 4 sits UNDER the decision
216 /// surface as drill-down.
217 pub focus: Focus,
218 /// 6.A: diff-aware deterministic deltas (boundary/cycle introduced +
219 /// exports-aware public-API surface delta), new-vs-pre-existing.
220 pub deltas: ReviewDeltas,
221 /// 6.F, headline: reviewer-private weakening signals (tests
222 /// removed/skipped, thresholds lowered, suppressions added, security steps
223 /// removed). Advisory, never gates, never auto-posted.
224 pub weakening: Vec<Weakening>,
225 /// 6.D: ownership-aware reviewer routing (per-file expert + bus-factor).
226 pub routing: Routing,
227 /// 6.G, the APEX: the decision surface. The ranked, capped,
228 /// signal_id-anchored set of consequential structural decisions, each framed
229 /// as a judgment question with its routed expert. This is the only thing the
230 /// brief visibly leads with; the stages above are its drill-down derivation.
231 pub decisions: Decisions,
232}
233
234/// The standard audit brief payload shape used by the CLI, schema emitter,
235/// API, and agent-facing review surfaces.
236pub type StandardReviewBriefOutput = ReviewBriefOutput<
237 crate::audit_focus::FocusMap,
238 crate::audit_weakening::WeakeningSignal,
239 crate::audit_routing::RoutingFacts,
240 crate::audit_decision_surface::DecisionSurface,
241>;
242
243/// Informational audit metadata carried by the review brief wire envelope.
244#[derive(Debug, Clone, Serialize)]
245#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
246pub struct ReviewBriefHeader<Verdict, Summary, Attribution> {
247 /// Fallow CLI version that produced this output.
248 pub version: ToolVersion,
249 /// Audit verdict, informational only on the brief path.
250 pub verdict: Verdict,
251 /// Number of changed files in the audit scope.
252 pub changed_files_count: u32,
253 /// Base ref used to determine the changeset.
254 pub base_ref: String,
255 /// Human-readable description of the resolved base, when available.
256 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub base_description: Option<String>,
258 /// Head commit SHA, when the audit ran against a committed head.
259 #[serde(default, skip_serializing_if = "Option::is_none")]
260 pub head_sha: Option<String>,
261 /// Analysis duration in milliseconds.
262 pub elapsed_ms: ElapsedMs,
263 /// Whether base-snapshot analysis was skipped for this run.
264 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub base_snapshot_skipped: Option<bool>,
266 /// Per-category audit summary.
267 pub summary: Summary,
268 /// Introduced-versus-inherited issue attribution.
269 pub attribution: Attribution,
270}
271
272/// Complete `fallow audit --brief --format json` wire envelope.
273///
274/// This is distinct from [`ReviewBriefOutput`], which is the reusable review
275/// digest embedded in walkthrough output. The wire envelope also carries audit
276/// metadata, optional telemetry, and the subtract-style analysis subreports.
277#[derive(Debug, Clone, Serialize)]
278#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
279#[cfg_attr(
280 feature = "schema",
281 schemars(title = "fallow audit --brief --format json")
282)]
283pub struct ReviewBriefWireOutput<
284 Focus,
285 Weakening,
286 Routing,
287 Decisions,
288 Verdict,
289 Summary,
290 Attribution,
291 DeadCode,
292 Duplication,
293 Complexity,
294> {
295 /// Independently-versioned brief schema version.
296 pub schema_version: ReviewBriefSchemaVersion,
297 /// Fallow CLI version that produced this output.
298 pub version: ToolVersion,
299 /// Command discriminator singleton: always `"audit-brief"`.
300 pub command: String,
301 /// Audit verdict, informational only on the brief path.
302 pub verdict: Verdict,
303 /// Number of changed files in the audit scope.
304 pub changed_files_count: u32,
305 /// Base ref used to determine the changeset.
306 pub base_ref: String,
307 /// Human-readable description of the resolved base, when available.
308 #[serde(default, skip_serializing_if = "Option::is_none")]
309 pub base_description: Option<String>,
310 /// Head commit SHA, when available.
311 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub head_sha: Option<String>,
313 /// Analysis duration in milliseconds.
314 pub elapsed_ms: ElapsedMs,
315 /// Whether base-snapshot analysis was skipped for this run.
316 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub base_snapshot_skipped: Option<bool>,
318 /// Per-category audit summary.
319 pub summary: Summary,
320 /// Introduced-versus-inherited issue attribution.
321 pub attribution: Attribution,
322 /// Optional metric definitions and local telemetry correlation metadata.
323 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
324 pub meta: Option<Meta>,
325 /// Ranked, capped review decisions.
326 pub decisions: Decisions,
327 /// Diff-size triage facts.
328 pub triage: DiffTriage,
329 /// Graph-derived orientation facts.
330 pub graph_facts: GraphFacts,
331 /// Changed-file partition and review order.
332 pub partition: PartitionFacts,
333 /// Transitive impact closure outside the diff.
334 pub impact_closure: ImpactClosureFacts,
335 /// Weighted focus map for changed-file units.
336 pub focus: Focus,
337 /// Deterministic introduced deltas against the base snapshot.
338 pub deltas: ReviewDeltas,
339 /// Reviewer-private weakening signals.
340 pub weakening: Vec<Weakening>,
341 /// Ownership-aware reviewer routing.
342 pub routing: Routing,
343 /// Dead-code findings scoped to the audit changeset.
344 #[serde(default, skip_serializing_if = "Option::is_none")]
345 pub dead_code: Option<DeadCode>,
346 /// Duplication findings scoped to the audit changeset.
347 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub duplication: Option<Duplication>,
349 /// Complexity findings scoped to the audit changeset.
350 #[serde(default, skip_serializing_if = "Option::is_none")]
351 pub complexity: Option<Complexity>,
352}
353
354/// CLI-built audit subreports that are embedded in the audit brief envelope.
355///
356/// The brief envelope and field ordering belong to `fallow-output`; the
357/// underlying subreport payloads are still supplied by the CLI until their
358/// builders are fully command-neutral.
359#[derive(Debug, Clone, Default)]
360pub struct ReviewBriefSubtractSections<DeadCode = Value, Duplication = Value, Complexity = Value> {
361 pub dead_code: Option<DeadCode>,
362 pub duplication: Option<Duplication>,
363 pub complexity: Option<Complexity>,
364}
365
366/// Build the complete `fallow audit --brief --format json` value.
367///
368/// `header` carries informational audit scope fields such as verdict, base ref,
369/// summary, and attribution. The independent brief schema and command always
370/// come from the typed brief payload.
371pub fn build_review_brief_json_output<
372 Focus,
373 Weakening,
374 Routing,
375 Decisions,
376 Verdict,
377 Summary,
378 Attribution,
379 DeadCode,
380 Duplication,
381 Complexity,
382>(
383 brief: ReviewBriefOutput<Focus, Weakening, Routing, Decisions>,
384 header: ReviewBriefHeader<Verdict, Summary, Attribution>,
385 subtract: ReviewBriefSubtractSections<DeadCode, Duplication, Complexity>,
386) -> Result<Value, serde_json::Error>
387where
388 Focus: Serialize,
389 Weakening: Serialize,
390 Routing: Serialize,
391 Decisions: Serialize,
392 Verdict: Serialize,
393 Summary: Serialize,
394 Attribution: Serialize,
395 DeadCode: Serialize,
396 Duplication: Serialize,
397 Complexity: Serialize,
398{
399 serde_json::to_value(ReviewBriefWireOutput {
400 schema_version: brief.schema_version,
401 version: header.version,
402 command: brief.command,
403 verdict: header.verdict,
404 changed_files_count: header.changed_files_count,
405 base_ref: header.base_ref,
406 base_description: header.base_description,
407 head_sha: header.head_sha,
408 elapsed_ms: header.elapsed_ms,
409 base_snapshot_skipped: header.base_snapshot_skipped,
410 summary: header.summary,
411 attribution: header.attribution,
412 meta: None,
413 decisions: brief.decisions,
414 triage: brief.triage,
415 graph_facts: brief.graph_facts,
416 partition: brief.partition,
417 impact_closure: brief.impact_closure,
418 focus: brief.focus,
419 deltas: brief.deltas,
420 weakening: brief.weakening,
421 routing: brief.routing,
422 dead_code: subtract.dead_code,
423 duplication: subtract.duplication,
424 complexity: subtract.complexity,
425 })
426}
427
428fn serialize_agent_contract_json_output<T: Serialize>(
429 output: T,
430 kind: &'static str,
431 mode: RootEnvelopeMode,
432 analysis_run_id: Option<&str>,
433) -> Result<Value, serde_json::Error> {
434 let mut value = serialize_named_json_output(output, kind, mode)?;
435 attach_telemetry_meta(&mut value, analysis_run_id);
436 Ok(value)
437}
438
439/// Serialize the `fallow audit --brief --format json` envelope.
440///
441/// # Errors
442///
443/// Returns a serde error when the brief output cannot be converted to JSON.
444pub fn serialize_review_brief_json_output<T: Serialize>(
445 output: T,
446 mode: RootEnvelopeMode,
447 analysis_run_id: Option<&str>,
448) -> Result<Value, serde_json::Error> {
449 serialize_agent_contract_json_output(output, "audit-brief", mode, analysis_run_id)
450}
451
452/// Serialize the standalone decision-surface envelope.
453///
454/// # Errors
455///
456/// Returns a serde error when the decision-surface output cannot be converted
457/// to JSON.
458pub fn serialize_decision_surface_json_output<T: Serialize>(
459 output: T,
460 mode: RootEnvelopeMode,
461 analysis_run_id: Option<&str>,
462) -> Result<Value, serde_json::Error> {
463 serialize_agent_contract_json_output(output, "decision-surface", mode, analysis_run_id)
464}
465
466/// Serialize the review walkthrough guide envelope.
467///
468/// # Errors
469///
470/// Returns a serde error when the walkthrough guide cannot be converted to
471/// JSON.
472pub fn serialize_walkthrough_guide_json_output<T: Serialize>(
473 output: T,
474 mode: RootEnvelopeMode,
475 analysis_run_id: Option<&str>,
476) -> Result<Value, serde_json::Error> {
477 serialize_agent_contract_json_output(output, "review-walkthrough-guide", mode, analysis_run_id)
478}
479
480/// Serialize the review walkthrough validation envelope.
481///
482/// # Errors
483///
484/// Returns a serde error when the walkthrough validation cannot be converted
485/// to JSON.
486pub fn serialize_walkthrough_validation_json_output<T: Serialize>(
487 output: T,
488 mode: RootEnvelopeMode,
489 analysis_run_id: Option<&str>,
490) -> Result<Value, serde_json::Error> {
491 serialize_agent_contract_json_output(
492 output,
493 "review-walkthrough-validation",
494 mode,
495 analysis_run_id,
496 )
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502 use serde_json::json;
503
504 #[test]
505 fn review_brief_json_output_assembles_typed_wire_contract() {
506 let brief = ReviewBriefOutput {
507 schema_version: ReviewBriefSchemaVersion::default(),
508 version: "1.2.3".to_string(),
509 command: "audit-brief".to_string(),
510 triage: DiffTriage {
511 files: 1,
512 hunks: None,
513 net_lines: None,
514 risk_class: RiskClass::Low,
515 review_effort: ReviewEffort::Glance,
516 },
517 graph_facts: GraphFacts {
518 exports_added: 0,
519 api_width_delta: 0,
520 reachable_from: Vec::new(),
521 boundaries_touched: Vec::new(),
522 },
523 partition: PartitionFacts::default(),
524 impact_closure: ImpactClosureFacts::default(),
525 focus: json!({"units": []}),
526 deltas: ReviewDeltas::default(),
527 weakening: Vec::<Value>::new(),
528 routing: json!({"units": []}),
529 decisions: json!({"decisions": []}),
530 };
531 let header = ReviewBriefHeader {
532 version: ToolVersion("1.2.3".to_string()),
533 verdict: json!("fail"),
534 changed_files_count: 1,
535 base_ref: "main".to_string(),
536 base_description: Some("merge base".to_string()),
537 head_sha: Some("abc123".to_string()),
538 elapsed_ms: ElapsedMs(12),
539 base_snapshot_skipped: Some(false),
540 summary: json!({"dead_code_issues": 0}),
541 attribution: json!({"gate": "new_only"}),
542 };
543
544 let value = build_review_brief_json_output(
545 brief,
546 header,
547 ReviewBriefSubtractSections::<Value, Value, Value> {
548 dead_code: Some(json!({"issues": []})),
549 duplication: None,
550 complexity: None,
551 },
552 )
553 .expect("brief output should serialize");
554
555 assert_eq!(value["schema_version"], REVIEW_BRIEF_SCHEMA_VERSION);
556 assert_eq!(value["command"], "audit-brief");
557 assert_eq!(value["verdict"], "fail");
558 assert_eq!(value["base_ref"], "main");
559 assert_eq!(value["summary"]["dead_code_issues"], 0);
560 assert_eq!(value["attribution"]["gate"], "new_only");
561 assert_eq!(value["dead_code"]["issues"], json!([]));
562 }
563
564 #[test]
565 fn review_brief_serializer_owns_root_contract() {
566 let value = serialize_review_brief_json_output(
567 json!({"command": "audit-brief"}),
568 RootEnvelopeMode::Tagged,
569 Some("run-brief"),
570 )
571 .expect("brief output should serialize");
572
573 assert_eq!(value["kind"], "audit-brief");
574 assert_eq!(value["_meta"]["telemetry"]["analysis_run_id"], "run-brief");
575 }
576
577 #[test]
578 fn decision_surface_serializer_owns_root_contract() {
579 let value = serialize_decision_surface_json_output(
580 json!({"decisions": []}),
581 RootEnvelopeMode::Tagged,
582 Some("run-decision"),
583 )
584 .expect("decision surface should serialize");
585
586 assert_eq!(value["kind"], "decision-surface");
587 assert_eq!(
588 value["_meta"]["telemetry"]["analysis_run_id"],
589 "run-decision"
590 );
591 }
592}