Skip to main content

omena_transform_passes/
model.rs

1//! Public transform planning, execution, provenance, and context contracts.
2//!
3//! These data models are the stable JSON-facing boundary for Omena CSS transform
4//! passes. Runtime modules own mutation execution, while this module keeps the
5//! pass registry, execution summaries, semantic-removal witnesses, fuzz reports,
6//! and cross-file transform context shapes serializable for `omena-query`,
7//! bindings, CLI runners, and release gates.
8
9use omena_abstract_value::{AbstractCssValueV0, FactPrecision};
10use omena_cascade::{
11    CascadeDeclaration, CascadeLevel, CascadeOriginV0, CascadeOutcome, CascadeProof,
12    ElementSignature, GuardedCascadeWinnerAuthorityV0, GuardedCascadeWinnerPlaneAnswerV0,
13    GuardedCascadeWinnerRootV0, SupportsTargetCapabilityV0,
14};
15use omena_cascade_proof::{
16    CanonicalSmtInputV0, DischargeLedgerLookupStatusV0, DischargeLedgerLookupV0,
17    DischargeLedgerVerdictV0,
18};
19use omena_evidence_graph::{
20    EvidenceDemandEdgeV0, EvidenceGraphBuildErrorV0, EvidenceGraphV0, EvidenceNodeKeyV0,
21    EvidenceNodeSeedV0, GuaranteeFamilyV0, GuaranteeKindV0, build_evidence_graph_from_edges_v0,
22};
23use omena_incremental::{IncrementalComputationPlanV0, IncrementalSnapshotV0};
24use omena_parser::ModuleInstanceKeyV0;
25use omena_transform_cst::{
26    StableNodeKeyV0, TransformBuildProfileV0, TransformDagEdgeV0, TransformPassContractV0,
27    TransformPassDescriptorV0, TransformPassKind, TransformStrictPolicyDescriptorV0,
28    strict_policy_descriptor_for_profile,
29};
30use serde::{Deserialize, Serialize};
31use serde_json::Value;
32
33const TRANSFORM_PASS_OUTCOME_EVIDENCE_QUERY_V0: &str =
34    "omena-transform-passes.transform-pass-execution-outcome";
35const TRANSFORM_PROVENANCE_NODE_EVIDENCE_QUERY_V0: &str =
36    "omena-transform-passes.provenance-derivation-node";
37const TRANSFORM_EVIDENCE_EDGE_KIND_V0: &str = "transform-evidence";
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
40#[serde(rename_all = "camelCase")]
41pub enum TransformPassExecutionStatus {
42    RegistryAndPlannerReady,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
46#[serde(rename_all = "camelCase")]
47pub enum TransformPassDispatchKindV0 {
48    TextLocalSliceRewrite,
49    StructuralIrTransaction,
50    ModuleEvaluationHandler,
51    EmissionBoundary,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
55#[serde(rename_all = "camelCase")]
56pub struct TransformPassRegistryEntryV0 {
57    pub contract: TransformPassContractV0,
58    pub descriptor: TransformPassDescriptorV0,
59    pub module_family: &'static str,
60    pub query_family: &'static str,
61    pub dispatch_kind: TransformPassDispatchKindV0,
62    pub execution_status: TransformPassExecutionStatus,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
66#[serde(rename_all = "camelCase")]
67pub struct TransformPassRegistryV0 {
68    pub schema_version: &'static str,
69    pub product: &'static str,
70    pub entries: Vec<TransformPassRegistryEntryV0>,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
74#[serde(rename_all = "camelCase")]
75pub struct TransformPassesBoundarySummaryV0 {
76    pub schema_version: &'static str,
77    pub product: &'static str,
78    pub registry_entries: Vec<TransformPassRegistryEntryV0>,
79    pub dag_edges: Vec<TransformDagEdgeV0>,
80    pub pass_count: usize,
81    pub full_catalog_registered: bool,
82    pub semantic_aware_pass_count: usize,
83    pub cascade_aware_pass_count: usize,
84    pub structural_pass_count: usize,
85    pub text_local_pass_count: usize,
86    pub module_evaluation_pass_count: usize,
87    pub planner_enforces_dag_edges: bool,
88    pub planner_uses_pass_descriptors: bool,
89    pub ordinal_has_execution_semantics: bool,
90    pub execution_runtime_ready: bool,
91    pub incremental_execution_runtime_ready: bool,
92    pub module_evaluation_native_output_marker: &'static str,
93    pub module_evaluation_requires_native_product_output: bool,
94    pub module_evaluation_requires_oracle_readiness: bool,
95    pub module_evaluation_legacy_output_is_oracle_only: bool,
96    pub module_evaluation_preserves_source_without_native_output: bool,
97    pub implemented_mutation_pass_ids: Vec<&'static str>,
98    pub next_surfaces: Vec<&'static str>,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
102#[serde(rename_all = "camelCase")]
103pub struct TransformPassPlanV0 {
104    pub schema_version: &'static str,
105    pub product: &'static str,
106    pub build_profile: TransformBuildProfileV0,
107    pub requested_pass_ids: Vec<&'static str>,
108    pub ordered_pass_ids: Vec<&'static str>,
109    pub satisfied_dag_edge_count: usize,
110    pub violated_dag_edge_count: usize,
111    pub all_requested_registered: bool,
112    pub conflicting_unordered_pass_pairs: Vec<TransformPlanPassConflictV0>,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
116#[serde(rename_all = "camelCase")]
117pub struct TransformPlanPassConflictV0 {
118    pub pass_a: &'static str,
119    pub pass_b: &'static str,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
123#[serde(rename_all = "camelCase")]
124pub struct TransformStructuralIrShadowFieldReportV0 {
125    pub field: &'static str,
126    pub string_path_values: Vec<String>,
127    pub ir_path_values: Vec<String>,
128    pub typed_path_values: Vec<String>,
129    pub matches: bool,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
133#[serde(rename_all = "camelCase")]
134pub struct TransformStructuralIrShadowFixtureReportV0 {
135    pub schema_version: &'static str,
136    pub product: &'static str,
137    pub fixture: String,
138    pub pass_id: &'static str,
139    pub dialect: &'static str,
140    pub string_path_mutation_count: Option<usize>,
141    pub ir_path_mutation_count: Option<usize>,
142    pub typed_path_mutation_count: Option<usize>,
143    pub ir_path_transaction_commit_count: Option<u64>,
144    pub typed_payload_projections_consumed: usize,
145    pub typed_payload_memo_hits: usize,
146    pub fields: Vec<TransformStructuralIrShadowFieldReportV0>,
147    pub all_fields_match: bool,
148    pub all_typed_path_fields_match: bool,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
152#[serde(rename_all = "camelCase")]
153pub struct TransformStructuralIrShadowEquivalenceReportV0 {
154    pub schema_version: &'static str,
155    pub product: &'static str,
156    pub fixture_count: usize,
157    pub compared_pass_ids: Vec<&'static str>,
158    pub compared_fields: Vec<&'static str>,
159    pub reports: Vec<TransformStructuralIrShadowFixtureReportV0>,
160    pub all_fields_match: bool,
161    pub all_typed_path_fields_match: bool,
162    pub typed_payload_projections_consumed: usize,
163    pub typed_payload_memo_hits: usize,
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
167#[serde(rename_all = "camelCase")]
168pub enum TransformPassRuntimeStatus {
169    Applied,
170    NoChange,
171    PlannedOnly,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
175#[serde(rename_all = "camelCase")]
176pub struct TransformPassExecutionOutcomeV0 {
177    pub pass_id: &'static str,
178    pub status: TransformPassRuntimeStatus,
179    pub input_byte_len: usize,
180    pub output_byte_len: usize,
181    pub mutation_count: usize,
182    pub provenance_preserved: bool,
183    pub detail: &'static str,
184}
185
186impl TransformPassExecutionOutcomeV0 {
187    pub fn evidence_node_key(&self) -> EvidenceNodeKeyV0 {
188        EvidenceNodeKeyV0::new(TRANSFORM_PASS_OUTCOME_EVIDENCE_QUERY_V0, self.pass_id)
189    }
190
191    pub fn evidence_node_seed(&self) -> EvidenceNodeSeedV0 {
192        EvidenceNodeSeedV0::new(
193            self.evidence_node_key(),
194            vec![
195                ["pass:", self.pass_id].concat(),
196                ["detail:", self.detail].concat(),
197                ["mutationCount:", self.mutation_count.to_string().as_str()].concat(),
198                [
199                    "provenancePreserved:",
200                    self.provenance_preserved.to_string().as_str(),
201                ]
202                .concat(),
203            ],
204            GuaranteeKindV0::for_label_less_family(),
205        )
206    }
207
208    pub fn evidence_demand_edge(&self) -> EvidenceDemandEdgeV0 {
209        EvidenceDemandEdgeV0::new(
210            TRANSFORM_PASS_OUTCOME_EVIDENCE_QUERY_V0,
211            self.evidence_node_key(),
212            TRANSFORM_EVIDENCE_EDGE_KIND_V0,
213        )
214    }
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
218#[serde(rename_all = "camelCase")]
219pub enum TransformEvaluationProfileV0 {
220    Scss,
221    Less,
222}
223
224#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
225#[serde(tag = "kind", rename_all = "camelCase")]
226pub enum TransformPreconditionV0 {
227    EvaluatorOutput {
228        profile: TransformEvaluationProfileV0,
229    },
230    ResolvedImportReplacements,
231    CssModulesComposesResolution,
232    DesignTokenRoutes,
233    SelectorIdentity,
234    ClosedStyleWorldBundle,
235    ClosedWorldBundle,
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
239#[serde(tag = "kind", rename_all = "camelCase")]
240pub enum TransformNoChangeReasonV0 {
241    NoMutation,
242    EmissionBoundary,
243    ProfileNotApplicable {
244        profile: TransformEvaluationProfileV0,
245    },
246    NoMatchingSelectorRewrite,
247    DialectNotApplicable,
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
251#[serde(
252    tag = "kind",
253    rename_all = "camelCase",
254    rename_all_fields = "camelCase"
255)]
256pub enum TransformBlockedReasonV0 {
257    MissingPrecondition {
258        precondition: TransformPreconditionV0,
259    },
260    PrecisionBelowFloor {
261        required: FactPrecision,
262        observed: FactPrecision,
263    },
264    DischargeMissing {
265        lookup_status: Option<DischargeLedgerLookupStatusV0>,
266        verdict: Option<DischargeLedgerVerdictV0>,
267    },
268    StrictVerification {
269        reasons: Vec<TransformStrictPolicyReasonV0>,
270    },
271    PassImplementation,
272    ClosedWorldAdmission {
273        reasons: Vec<TransformStrictPolicyReasonV0>,
274    },
275}
276
277#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
278#[serde(tag = "kind", rename_all = "camelCase")]
279pub enum TransformRejectionReasonV0 {
280    IrTransaction {
281        pass: TransformPassKind,
282    },
283    SemanticPreservation,
284    StrictVerification {
285        reasons: Vec<TransformStrictPolicyReasonV0>,
286    },
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
290#[serde(
291    tag = "kind",
292    rename_all = "camelCase",
293    rename_all_fields = "camelCase"
294)]
295pub enum TransformStructuralDecisionClassV0 {
296    FactConsuming { required_precision: FactPrecision },
297    StaticExact,
298    ObligationDischarge,
299    NonRemovalRewrite,
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
303#[serde(rename_all = "camelCase")]
304pub struct TransformStructuralDecisionPolicyV0 {
305    pub pass: TransformPassKind,
306    pub class: TransformStructuralDecisionClassV0,
307    pub reason: &'static str,
308}
309
310impl TransformStructuralDecisionPolicyV0 {
311    pub const fn new(
312        pass: TransformPassKind,
313        class: TransformStructuralDecisionClassV0,
314        reason: &'static str,
315    ) -> Self {
316        Self {
317            pass,
318            class,
319            reason,
320        }
321    }
322
323    pub const fn required_precision(self) -> Option<FactPrecision> {
324        match self.class {
325            TransformStructuralDecisionClassV0::FactConsuming { required_precision } => {
326                Some(required_precision)
327            }
328            TransformStructuralDecisionClassV0::StaticExact
329            | TransformStructuralDecisionClassV0::ObligationDischarge
330            | TransformStructuralDecisionClassV0::NonRemovalRewrite => None,
331        }
332    }
333}
334
335pub const TRANSFORM_STRUCTURAL_DECISION_POLICIES_V0: &[TransformStructuralDecisionPolicyV0] = &[
336    TransformStructuralDecisionPolicyV0::new(
337        TransformPassKind::ImportInline,
338        TransformStructuralDecisionClassV0::NonRemovalRewrite,
339        "materializes explicitly resolved imports without reachability pruning",
340    ),
341    TransformStructuralDecisionPolicyV0::new(
342        TransformPassKind::ResolveCssModulesComposes,
343        TransformStructuralDecisionClassV0::NonRemovalRewrite,
344        "materializes explicit CSS Modules composition resolution",
345    ),
346    TransformStructuralDecisionPolicyV0::new(
347        TransformPassKind::DesignTokenRouting,
348        TransformStructuralDecisionClassV0::NonRemovalRewrite,
349        "rewrites values through explicit design-token routes",
350    ),
351    TransformStructuralDecisionPolicyV0::new(
352        TransformPassKind::HashCssModuleClassNames,
353        TransformStructuralDecisionClassV0::NonRemovalRewrite,
354        "rewrites selectors through an explicit identity map",
355    ),
356    TransformStructuralDecisionPolicyV0::new(
357        TransformPassKind::RuleDeduplication,
358        TransformStructuralDecisionClassV0::StaticExact,
359        "removes only statically equivalent duplicate rules",
360    ),
361    TransformStructuralDecisionPolicyV0::new(
362        TransformPassKind::RuleMerging,
363        TransformStructuralDecisionClassV0::NonRemovalRewrite,
364        "combines adjacent declarations without reachability pruning",
365    ),
366    TransformStructuralDecisionPolicyV0::new(
367        TransformPassKind::SelectorMerging,
368        TransformStructuralDecisionClassV0::NonRemovalRewrite,
369        "combines equivalent selector blocks without reachability pruning",
370    ),
371    TransformStructuralDecisionPolicyV0::new(
372        TransformPassKind::NestingUnwrap,
373        TransformStructuralDecisionClassV0::NonRemovalRewrite,
374        "expands nested selectors without reachability pruning",
375    ),
376    TransformStructuralDecisionPolicyV0::new(
377        TransformPassKind::ScopeFlatten,
378        TransformStructuralDecisionClassV0::ObligationDischarge,
379        "requires accepted scope-flatten obligations",
380    ),
381    TransformStructuralDecisionPolicyV0::new(
382        TransformPassKind::LayerFlatten,
383        TransformStructuralDecisionClassV0::ObligationDischarge,
384        "requires accepted layer-flatten obligations",
385    ),
386    TransformStructuralDecisionPolicyV0::new(
387        TransformPassKind::SupportsStaticEval,
388        TransformStructuralDecisionClassV0::StaticExact,
389        "removes only statically decided supports branches",
390    ),
391    TransformStructuralDecisionPolicyV0::new(
392        TransformPassKind::MediaStaticEval,
393        TransformStructuralDecisionClassV0::StaticExact,
394        "removes only statically unsatisfiable media branches",
395    ),
396    TransformStructuralDecisionPolicyV0::new(
397        TransformPassKind::ContainerStaticEval,
398        TransformStructuralDecisionClassV0::StaticExact,
399        "removes only statically unsatisfiable container branches",
400    ),
401    TransformStructuralDecisionPolicyV0::new(
402        TransformPassKind::NativeCssStaticEval,
403        TransformStructuralDecisionClassV0::StaticExact,
404        "folds only statically evaluable native CSS expressions",
405    ),
406    TransformStructuralDecisionPolicyV0::new(
407        TransformPassKind::DeadMediaBranchRemoval,
408        TransformStructuralDecisionClassV0::StaticExact,
409        "removes only media branches selected by explicit static policy",
410    ),
411    TransformStructuralDecisionPolicyV0::new(
412        TransformPassKind::DeadSupportsBranchRemoval,
413        TransformStructuralDecisionClassV0::StaticExact,
414        "removes only statically decided supports branches",
415    ),
416    TransformStructuralDecisionPolicyV0::new(
417        TransformPassKind::TreeShakeClass,
418        TransformStructuralDecisionClassV0::FactConsuming {
419            required_precision: FactPrecision::Conservative,
420        },
421        "removes class rules only from a closed-world reachability over-approximation",
422    ),
423    TransformStructuralDecisionPolicyV0::new(
424        TransformPassKind::TreeShakeKeyframes,
425        TransformStructuralDecisionClassV0::FactConsuming {
426            required_precision: FactPrecision::Conservative,
427        },
428        "removes keyframes only from a closed-world reachability over-approximation",
429    ),
430    TransformStructuralDecisionPolicyV0::new(
431        TransformPassKind::TreeShakeValue,
432        TransformStructuralDecisionClassV0::FactConsuming {
433            required_precision: FactPrecision::Conservative,
434        },
435        "removes CSS Modules values only from a closed-world reachability over-approximation",
436    ),
437    TransformStructuralDecisionPolicyV0::new(
438        TransformPassKind::TreeShakeCustomProperty,
439        TransformStructuralDecisionClassV0::FactConsuming {
440            required_precision: FactPrecision::Conservative,
441        },
442        "removes custom properties only from a closed-world reachability over-approximation",
443    ),
444    TransformStructuralDecisionPolicyV0::new(
445        TransformPassKind::EmptyRuleRemoval,
446        TransformStructuralDecisionClassV0::StaticExact,
447        "removes only structurally empty rules",
448    ),
449];
450
451pub fn transform_structural_decision_policy(
452    pass: TransformPassKind,
453) -> Option<&'static TransformStructuralDecisionPolicyV0> {
454    TRANSFORM_STRUCTURAL_DECISION_POLICIES_V0
455        .iter()
456        .find(|policy| policy.pass == pass)
457}
458
459#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
460#[serde(tag = "kind", rename_all = "camelCase")]
461pub enum RollbackScopeV0 {
462    RejectPreservedInput,
463    InversePatch,
464    CommittedIrrecoverable,
465}
466
467#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
468#[serde(rename_all = "camelCase")]
469pub struct RollbackReceiptV0 {
470    pub pass_id: String,
471    pub attempted_mutation_count: Option<usize>,
472    pub input_content_signature: String,
473    #[serde(skip_serializing_if = "Option::is_none")]
474    pub output_preserved_content_signature: Option<String>,
475    pub restorable: RollbackScopeV0,
476}
477
478#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
479#[serde(rename_all = "camelCase")]
480pub struct TransformDischargeEvidenceV0 {
481    pub evidence_node_key: EvidenceNodeKeyV0,
482    pub guarantee_family: GuaranteeFamilyV0,
483    pub ledger_cell_key: String,
484    pub boundedness_kind: String,
485}
486
487/// Cascade dimensions covered by an observed winner-equality comparison.
488///
489/// Coverage is explicit so consumers do not mistake a partial observation for
490/// a guarantee over cascade dimensions that have no production driver yet.
491#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
492#[serde(rename_all = "camelCase")]
493pub enum TransformWinnerEqualityAxisV0 {
494    CascadeLevel,
495    LayerRank,
496    ScopeProximity,
497    Specificity,
498    SourceOrder,
499}
500
501/// Why a winner-equality observation could not cover one cascade dimension.
502#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
503#[serde(
504    tag = "kind",
505    rename_all = "camelCase",
506    rename_all_fields = "camelCase"
507)]
508pub enum TransformWinnerEqualityAbsenceReasonV0 {
509    DriverUnavailable {
510        level: Option<CascadeLevel>,
511    },
512    AffectedPairUnavailable,
513    SpecificityInexact,
514    WinnerNotDefinite,
515    WinnerChanged,
516    GuardedWinnerFunctionsDiffer {
517        input_root: GuardedCascadeWinnerRootV0,
518        output_root: GuardedCascadeWinnerRootV0,
519    },
520    GuardedWinnerPlaneDisagreement {
521        side: &'static str,
522        canonical_mtbdd: GuardedCascadeWinnerPlaneAnswerV0,
523        scenario_sweep: GuardedCascadeWinnerPlaneAnswerV0,
524    },
525}
526
527/// A typed precision boundary for a missing winner-equality observation.
528#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
529#[serde(rename_all = "camelCase")]
530pub struct TransformWinnerEqualityAbsenceV0 {
531    pub axis: TransformWinnerEqualityAxisV0,
532    pub reason: TransformWinnerEqualityAbsenceReasonV0,
533}
534
535/// The semantic location whose cascade winner is compared across a transform.
536#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
537#[serde(rename_all = "camelCase")]
538pub struct TransformWinnerEqualityAffectedPairV0 {
539    pub element_signature: ElementSignature,
540    pub property: String,
541}
542
543/// A definite winner and the proof emitted by the cascade authority.
544///
545/// Keeping the authority-owned types here prevents transform code from
546/// reconstructing winner order or proof fields independently.
547#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
548#[serde(rename_all = "camelCase")]
549pub struct TransformWinnerEqualityWitnessV0 {
550    pub winner: CascadeDeclaration,
551    pub proof: CascadeProof,
552}
553
554impl TransformWinnerEqualityWitnessV0 {
555    pub fn from_cascade_outcome(outcome: &CascadeOutcome) -> Option<Self> {
556        match outcome {
557            CascadeOutcome::Definite { winner, proof, .. } => Some(Self {
558                winner: winner.clone(),
559                proof: proof.as_ref().clone(),
560            }),
561            CascadeOutcome::RankedSet(_) | CascadeOutcome::Inherit | CascadeOutcome::Top => None,
562        }
563    }
564}
565
566/// Result of comparing authority-produced cascade witnesses for one affected pair.
567#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
568#[serde(
569    tag = "kind",
570    rename_all = "camelCase",
571    rename_all_fields = "camelCase"
572)]
573pub enum TransformWinnerEqualityObservationV0 {
574    ObservedEqual {
575        axes: Vec<TransformWinnerEqualityAxisV0>,
576        input: TransformWinnerEqualityWitnessV0,
577        output: TransformWinnerEqualityWitnessV0,
578    },
579    ObservedDifferent {
580        axes: Vec<TransformWinnerEqualityAxisV0>,
581        input: TransformWinnerEqualityWitnessV0,
582        output: TransformWinnerEqualityWitnessV0,
583    },
584    Absent {
585        reasons: Vec<TransformWinnerEqualityAbsenceV0>,
586    },
587    ObservedGuardedEqual {
588        axes: Vec<TransformWinnerEqualityAxisV0>,
589        input: TransformWinnerEqualityWitnessV0,
590        output: TransformWinnerEqualityWitnessV0,
591        authority: GuardedCascadeWinnerAuthorityV0,
592    },
593}
594
595/// A cascade-winner comparison requested for one admitted transform mutation.
596#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
597#[serde(rename_all = "camelCase")]
598pub struct TransformWinnerEqualityObligationV0 {
599    pub pass_id: &'static str,
600    pub affected_pair: TransformWinnerEqualityAffectedPairV0,
601    pub observation: TransformWinnerEqualityObservationV0,
602}
603
604#[derive(Debug, Clone, Default, PartialEq, Eq)]
605pub struct TransformExecutionPolicyV0 {
606    pub strict_policy: Option<TransformStrictPolicyDescriptorV0>,
607}
608
609impl TransformExecutionPolicyV0 {
610    pub fn for_profile(profile_id: &str) -> Option<Self> {
611        strict_policy_descriptor_for_profile(profile_id).map(|strict_policy| Self {
612            strict_policy: Some(strict_policy),
613        })
614    }
615}
616
617#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
618#[serde(
619    tag = "kind",
620    rename_all = "camelCase",
621    rename_all_fields = "camelCase"
622)]
623pub enum TransformStrictPolicyReasonV0 {
624    RequiredAxisUnavailable {
625        axis: TransformWinnerEqualityAxisV0,
626    },
627    CascadeEnvironmentUnavailable,
628    WinnerChanged {
629        axes: Vec<TransformWinnerEqualityAxisV0>,
630    },
631    ObservationUnavailable {
632        reasons: Vec<TransformWinnerEqualityAbsenceV0>,
633    },
634    UnknownPass,
635    ClosedWorldEvidenceUnavailable,
636    DecisionCoverageIncomplete,
637    ClosedWorldEvidenceIncomplete {
638        missing: Vec<String>,
639    },
640    LivenessNotClosed {
641        symbol: String,
642        from_module: ModuleInstanceKeyV0,
643        via_edge: &'static str,
644    },
645    EvidenceUnavailable,
646    OwnershipNotSeparable {
647        token: String,
648        module_paths: Vec<String>,
649    },
650}
651
652#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
653#[serde(rename_all = "camelCase")]
654#[non_exhaustive]
655pub enum CssModuleTokenCollisionPathScopeV0 {
656    BothPaths,
657    ImportInlineLegacyOnly,
658    LinkedOrderOnly,
659}
660
661impl CssModuleTokenCollisionPathScopeV0 {
662    pub const fn as_wire_label(self) -> &'static str {
663        match self {
664            Self::BothPaths => "bothPaths",
665            Self::ImportInlineLegacyOnly => "importInlineLegacyOnly",
666            Self::LinkedOrderOnly => "linkedOrderOnly",
667        }
668    }
669}
670
671#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
672#[serde(rename_all = "camelCase")]
673#[non_exhaustive]
674pub struct CssModuleTokenOwnershipV0 {
675    pub emitted_token: String,
676    pub module_instances: Vec<ModuleInstanceKeyV0>,
677    pub module_paths: Vec<String>,
678    pub original_names: Vec<String>,
679}
680
681impl CssModuleTokenOwnershipV0 {
682    pub fn new(
683        emitted_token: impl Into<String>,
684        module_instances: Vec<ModuleInstanceKeyV0>,
685        module_paths: Vec<String>,
686        original_names: Vec<String>,
687    ) -> Self {
688        Self {
689            emitted_token: emitted_token.into(),
690            module_instances,
691            module_paths,
692            original_names,
693        }
694    }
695}
696
697#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
698#[serde(rename_all = "camelCase")]
699#[non_exhaustive]
700pub struct CssModuleTokenCollisionV0 {
701    pub emitted_token: String,
702    pub module_instances: Vec<ModuleInstanceKeyV0>,
703    pub module_paths: Vec<String>,
704    pub original_names: Vec<String>,
705    pub observed_emission_paths: Vec<&'static str>,
706    pub path_scope: CssModuleTokenCollisionPathScopeV0,
707}
708
709impl CssModuleTokenCollisionV0 {
710    pub fn new(
711        ownership: CssModuleTokenOwnershipV0,
712        observed_emission_paths: Vec<&'static str>,
713        path_scope: CssModuleTokenCollisionPathScopeV0,
714    ) -> Self {
715        Self {
716            emitted_token: ownership.emitted_token,
717            module_instances: ownership.module_instances,
718            module_paths: ownership.module_paths,
719            original_names: ownership.original_names,
720            observed_emission_paths,
721            path_scope,
722        }
723    }
724}
725
726#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
727#[serde(rename_all = "camelCase")]
728#[non_exhaustive]
729pub struct CssModuleTokenInterfaceMismatchV0 {
730    pub module_instance: ModuleInstanceKeyV0,
731    pub module_path: String,
732    pub original_name: String,
733    pub promised_token: String,
734    pub emitted_token: String,
735}
736
737impl CssModuleTokenInterfaceMismatchV0 {
738    pub fn new(
739        module_instance: ModuleInstanceKeyV0,
740        module_path: impl Into<String>,
741        original_name: impl Into<String>,
742        promised_token: impl Into<String>,
743        emitted_token: impl Into<String>,
744    ) -> Self {
745        Self {
746            module_instance,
747            module_path: module_path.into(),
748            original_name: original_name.into(),
749            promised_token: promised_token.into(),
750            emitted_token: emitted_token.into(),
751        }
752    }
753}
754
755#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
756#[serde(rename_all = "camelCase")]
757#[non_exhaustive]
758pub struct CssModuleTokenOwnershipCensusV0 {
759    pub schema_version: &'static str,
760    pub product: &'static str,
761    pub scope: &'static str,
762    pub emission_path: &'static str,
763    pub complete: bool,
764    pub modeled_preimage_count: usize,
765    pub emitted_token_count: usize,
766    pub token_ownerships: Vec<CssModuleTokenOwnershipV0>,
767    pub module_token_collision_count: usize,
768    pub module_token_collisions: Vec<CssModuleTokenCollisionV0>,
769    pub unattributed_emitted_tokens: Vec<String>,
770    pub interface_mismatches: Vec<CssModuleTokenInterfaceMismatchV0>,
771    pub unavailable_reasons: Vec<String>,
772}
773
774impl CssModuleTokenOwnershipCensusV0 {
775    pub fn new(
776        emission_path: &'static str,
777        modeled_preimage_count: usize,
778        token_ownerships: Vec<CssModuleTokenOwnershipV0>,
779        module_token_collisions: Vec<CssModuleTokenCollisionV0>,
780        unattributed_emitted_tokens: Vec<String>,
781        interface_mismatches: Vec<CssModuleTokenInterfaceMismatchV0>,
782    ) -> Self {
783        let module_token_collision_count = module_token_collisions.len();
784        let emitted_token_count = token_ownerships.len() + unattributed_emitted_tokens.len();
785        Self {
786            schema_version: "0",
787            product: "omena-query.css-module-token-ownership-census",
788            scope: "bundleEmission",
789            emission_path,
790            complete: unattributed_emitted_tokens.is_empty(),
791            modeled_preimage_count,
792            emitted_token_count,
793            token_ownerships,
794            module_token_collision_count,
795            module_token_collisions,
796            unattributed_emitted_tokens,
797            interface_mismatches,
798            unavailable_reasons: Vec::new(),
799        }
800    }
801
802    pub fn unavailable(emission_path: &'static str, reason: impl Into<String>) -> Self {
803        Self {
804            schema_version: "0",
805            product: "omena-query.css-module-token-ownership-census",
806            scope: "bundleEmission",
807            emission_path,
808            complete: false,
809            modeled_preimage_count: 0,
810            emitted_token_count: 0,
811            token_ownerships: Vec::new(),
812            module_token_collision_count: 0,
813            module_token_collisions: Vec::new(),
814            unattributed_emitted_tokens: Vec::new(),
815            interface_mismatches: Vec::new(),
816            unavailable_reasons: vec![reason.into()],
817        }
818    }
819}
820
821#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
822#[serde(rename_all = "camelCase")]
823pub struct TransformStrictPolicyEventV0 {
824    pub pass_id: String,
825    pub reasons: Vec<TransformStrictPolicyReasonV0>,
826}
827
828#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
829#[serde(rename_all = "camelCase")]
830pub struct TransformStrictPolicySummaryV0 {
831    pub profile_id: Option<String>,
832    pub refused_count: usize,
833    pub rolled_back_count: usize,
834    pub refusal_reasons: Vec<TransformStrictPolicyEventV0>,
835    pub rollback_reasons: Vec<TransformStrictPolicyEventV0>,
836}
837
838impl TransformStrictPolicySummaryV0 {
839    pub fn for_profile(profile_id: &str) -> Self {
840        Self {
841            profile_id: Some(profile_id.to_string()),
842            ..Self::default()
843        }
844    }
845
846    pub fn record_refusal(
847        &mut self,
848        pass_id: impl Into<String>,
849        reasons: Vec<TransformStrictPolicyReasonV0>,
850    ) {
851        self.refusal_reasons.push(TransformStrictPolicyEventV0 {
852            pass_id: pass_id.into(),
853            reasons,
854        });
855        self.refused_count = self.refusal_reasons.len();
856    }
857
858    pub fn record_rollback(
859        &mut self,
860        pass_id: impl Into<String>,
861        reasons: Vec<TransformStrictPolicyReasonV0>,
862    ) {
863        self.rollback_reasons.push(TransformStrictPolicyEventV0 {
864            pass_id: pass_id.into(),
865            reasons,
866        });
867        self.rolled_back_count = self.rollback_reasons.len();
868    }
869}
870
871#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
872pub struct ClosedWorldAdmissionTierV0;
873
874#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
875#[serde(rename_all = "camelCase")]
876pub struct TransformClosedWorldAdmissionEventV0 {
877    pub pass_id: String,
878    pub module_instance: Option<ModuleInstanceKeyV0>,
879    pub reasons: Vec<TransformStrictPolicyReasonV0>,
880}
881
882#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
883#[serde(rename_all = "camelCase")]
884pub struct TransformClosedWorldAdmissionSummaryV0 {
885    pub refused_count: usize,
886    #[serde(skip_serializing_if = "Option::is_none")]
887    pub evidence_scope: Option<&'static str>,
888    pub refusal_reasons: Vec<TransformClosedWorldAdmissionEventV0>,
889}
890
891impl TransformClosedWorldAdmissionSummaryV0 {
892    pub fn record_refusal(
893        &mut self,
894        pass_id: impl Into<String>,
895        module_instance: Option<ModuleInstanceKeyV0>,
896        reasons: Vec<TransformStrictPolicyReasonV0>,
897    ) {
898        self.refusal_reasons
899            .push(TransformClosedWorldAdmissionEventV0 {
900                pass_id: pass_id.into(),
901                module_instance,
902                reasons,
903            });
904        self.refused_count = self.refusal_reasons.len();
905    }
906}
907
908/// Trust carried by an admitted transform decision.
909///
910/// This is descriptive evidence for default and other non-strict profiles. The
911/// enum itself never participates in admission. An opt-in strict profile may
912/// separately enforce the underlying typed obligations while leaving the base
913/// admission predicate unchanged.
914#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
915#[serde(
916    tag = "kind",
917    rename_all = "camelCase",
918    rename_all_fields = "camelCase"
919)]
920pub enum TransformSemanticGuaranteeTierV0 {
921    L0Observed,
922    WinnerEqualityObserved {
923        axes: Vec<TransformWinnerEqualityAxisV0>,
924    },
925    Absent {
926        reasons: Vec<TransformWinnerEqualityAbsenceV0>,
927    },
928}
929
930impl RollbackReceiptV0 {
931    pub fn preserves_rejected_input(&self) -> bool {
932        self.restorable == RollbackScopeV0::RejectPreservedInput
933            && self.output_preserved_content_signature.as_deref()
934                == Some(self.input_content_signature.as_str())
935    }
936
937    pub fn covers_inverse_patch(
938        &self,
939        inverse_patch_count: usize,
940        input_content_signature: &str,
941    ) -> bool {
942        self.restorable == RollbackScopeV0::InversePatch
943            && self.attempted_mutation_count == Some(inverse_patch_count)
944            && self.input_content_signature == input_content_signature
945            && self.output_preserved_content_signature.is_none()
946    }
947}
948
949#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
950#[serde(
951    tag = "kind",
952    rename_all = "camelCase",
953    rename_all_fields = "camelCase"
954)]
955pub enum TransformDecision {
956    Applied {
957        outcome: TransformPassExecutionOutcomeV0,
958        rollback_receipt: RollbackReceiptV0,
959        #[serde(skip_serializing_if = "Option::is_none")]
960        semantic_guarantee_tier: Option<TransformSemanticGuaranteeTierV0>,
961        #[serde(skip_serializing_if = "Vec::is_empty")]
962        discharge_evidence: Vec<TransformDischargeEvidenceV0>,
963    },
964    NoChange {
965        reason: TransformNoChangeReasonV0,
966        outcome: TransformPassExecutionOutcomeV0,
967    },
968    Blocked {
969        reason: TransformBlockedReasonV0,
970        outcome: TransformPassExecutionOutcomeV0,
971    },
972    Rejected {
973        reason: TransformRejectionReasonV0,
974        outcome: TransformPassExecutionOutcomeV0,
975        rollback_receipt: RollbackReceiptV0,
976    },
977}
978
979impl TransformDecision {
980    pub fn compatibility_outcome(&self) -> &TransformPassExecutionOutcomeV0 {
981        match self {
982            Self::Applied { outcome, .. }
983            | Self::NoChange { outcome, .. }
984            | Self::Blocked { outcome, .. }
985            | Self::Rejected { outcome, .. } => outcome,
986        }
987    }
988
989    pub fn into_compatibility_outcome(self) -> TransformPassExecutionOutcomeV0 {
990        match self {
991            Self::Applied { outcome, .. }
992            | Self::NoChange { outcome, .. }
993            | Self::Blocked { outcome, .. }
994            | Self::Rejected { outcome, .. } => outcome,
995        }
996    }
997
998    pub fn rollback_receipt(&self) -> Option<&RollbackReceiptV0> {
999        match self {
1000            Self::Applied {
1001                rollback_receipt, ..
1002            }
1003            | Self::Rejected {
1004                rollback_receipt, ..
1005            } => Some(rollback_receipt),
1006            Self::NoChange { .. } | Self::Blocked { .. } => None,
1007        }
1008    }
1009
1010    pub fn semantic_guarantee_tier(&self) -> Option<&TransformSemanticGuaranteeTierV0> {
1011        match self {
1012            Self::Applied {
1013                semantic_guarantee_tier,
1014                ..
1015            } => semantic_guarantee_tier.as_ref(),
1016            Self::NoChange { .. } | Self::Blocked { .. } | Self::Rejected { .. } => None,
1017        }
1018    }
1019}
1020
1021#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1022#[serde(rename_all = "camelCase")]
1023pub struct TransformProvenanceDerivationForestV0 {
1024    pub schema_version: &'static str,
1025    pub product: &'static str,
1026    pub root_count: usize,
1027    pub node_count: usize,
1028    pub nodes: Vec<TransformProvenanceDerivationNodeV0>,
1029}
1030
1031impl TransformProvenanceDerivationForestV0 {
1032    pub fn evidence_graph(&self) -> Result<EvidenceGraphV0, EvidenceGraphBuildErrorV0> {
1033        build_evidence_graph_from_edges_v0(
1034            self.nodes
1035                .iter()
1036                .map(TransformProvenanceDerivationNodeV0::evidence_node_seed),
1037            self.nodes
1038                .iter()
1039                .map(TransformProvenanceDerivationNodeV0::evidence_demand_edge),
1040        )
1041    }
1042}
1043
1044#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1045#[serde(rename_all = "camelCase")]
1046pub struct TransformProvenanceDerivationNodeV0 {
1047    pub node_index: usize,
1048    pub parent_index: Option<usize>,
1049    pub pass_id: &'static str,
1050    pub status: TransformPassRuntimeStatus,
1051    pub input_byte_len: usize,
1052    pub output_byte_len: usize,
1053    pub source_span_start: usize,
1054    pub source_span_end: usize,
1055    pub generated_span_start: usize,
1056    pub generated_span_end: usize,
1057    pub mutation_spans: Vec<TransformProvenanceMutationSpanV0>,
1058    pub mutation_count: usize,
1059    pub provenance_preserved: bool,
1060    pub detail: &'static str,
1061}
1062
1063impl TransformProvenanceDerivationNodeV0 {
1064    pub fn evidence_node_key(&self) -> EvidenceNodeKeyV0 {
1065        EvidenceNodeKeyV0::new(
1066            TRANSFORM_PROVENANCE_NODE_EVIDENCE_QUERY_V0,
1067            format!("{}#{}", self.pass_id, self.node_index),
1068        )
1069    }
1070
1071    pub fn evidence_node_seed(&self) -> EvidenceNodeSeedV0 {
1072        EvidenceNodeSeedV0::new(
1073            self.evidence_node_key(),
1074            vec![
1075                ["pass:", self.pass_id].concat(),
1076                ["detail:", self.detail].concat(),
1077                ["mutationCount:", self.mutation_count.to_string().as_str()].concat(),
1078                [
1079                    "provenancePreserved:",
1080                    self.provenance_preserved.to_string().as_str(),
1081                ]
1082                .concat(),
1083            ],
1084            GuaranteeKindV0::for_label_less_family(),
1085        )
1086    }
1087
1088    pub fn evidence_demand_edge(&self) -> EvidenceDemandEdgeV0 {
1089        EvidenceDemandEdgeV0::new(
1090            TRANSFORM_PROVENANCE_NODE_EVIDENCE_QUERY_V0,
1091            self.evidence_node_key(),
1092            TRANSFORM_EVIDENCE_EDGE_KIND_V0,
1093        )
1094    }
1095}
1096
1097#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1098#[serde(rename_all = "camelCase")]
1099pub struct TransformProvenanceMutationSpanV0 {
1100    pub source_span_start: usize,
1101    pub source_span_end: usize,
1102    pub generated_span_start: usize,
1103    pub generated_span_end: usize,
1104    #[serde(skip_serializing_if = "Option::is_none")]
1105    pub node_key: Option<StableNodeKeyV0>,
1106}
1107
1108/// Counts incremental lex-splice outcomes inside a transform execution.
1109///
1110/// A fallback is conservative: the cache declines to reuse token ranges and the
1111/// next consumer re-lexes the generated source normally.
1112#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
1113#[serde(rename_all = "camelCase")]
1114pub struct TransformLexCacheSpliceTelemetryV0 {
1115    /// Number of generated token streams inserted through bounded splicing.
1116    pub splice_hit_count: u64,
1117    /// Number of active-cache attempts that intentionally fell back to full re-lex.
1118    pub full_relex_fallback_count: u64,
1119    /// Fallbacks caused by invalid or non-projectable mutation windows.
1120    pub window_derivation_fallback_count: u64,
1121    /// Fallbacks where the safe restart window covers the full generated output.
1122    pub full_output_window_fallback_count: u64,
1123    /// Fallbacks caused by token offset arithmetic or projection failure.
1124    pub token_offset_fallback_count: u64,
1125}
1126
1127/// Counts structural IR transaction outcomes that matter for String-currency
1128/// retirement.
1129#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
1130#[serde(rename_all = "camelCase")]
1131#[non_exhaustive]
1132pub struct TransformStructuralIrTransactionTelemetryV0 {
1133    pub transaction_commit_count: u64,
1134    pub ir_metadata_refresh_count: u64,
1135    pub ir_transaction_commit_count: u64,
1136    pub ir_materialization_count: u64,
1137    pub ir_mutation_count: u64,
1138}
1139
1140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1141#[serde(rename_all = "camelCase")]
1142pub enum TransformSemanticObservationKeyAxisV0 {
1143    Selector,
1144    Property,
1145    Context,
1146}
1147
1148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1149#[serde(rename_all = "camelCase")]
1150pub enum TransformSemanticObservationValueAxisV0 {
1151    Value,
1152    Important,
1153}
1154
1155#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1156#[serde(rename_all = "camelCase")]
1157pub enum TransformSemanticObservationOrderingRuleV0 {
1158    SourceOrder,
1159    ImportantPrecedence,
1160}
1161
1162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1163#[serde(rename_all = "camelCase")]
1164pub enum TransformSemanticUnobservedAxisV0 {
1165    InterSelectorSpecificityCompetition,
1166    CascadeLayerOrder,
1167    Origin,
1168    ScopeProximity,
1169    DomDependentMatching,
1170    Inheritance,
1171    CustomPropertyEnvironment,
1172    AnimationAndTransition,
1173}
1174
1175#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1176#[serde(rename_all = "camelCase")]
1177pub enum TransformSemanticPreservationClaimScopeV0 {
1178    ObservedSurfaceOnly,
1179}
1180
1181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1182#[serde(rename_all = "camelCase")]
1183pub enum TransformSemanticPreservationVocabularyReviewV0 {
1184    DeferredUntilFullCascadeObservation,
1185}
1186
1187/// Declares exactly which semantic projection the transform guard compares.
1188#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1189#[serde(rename_all = "camelCase")]
1190pub struct TransformSemanticObservationSurfaceV0 {
1191    pub key_axes: Vec<TransformSemanticObservationKeyAxisV0>,
1192    pub value_axes: Vec<TransformSemanticObservationValueAxisV0>,
1193    pub ordering_rules: Vec<TransformSemanticObservationOrderingRuleV0>,
1194    pub unobserved_axes: Vec<TransformSemanticUnobservedAxisV0>,
1195    pub claim_scope: TransformSemanticPreservationClaimScopeV0,
1196    pub vocabulary_review: TransformSemanticPreservationVocabularyReviewV0,
1197}
1198
1199#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1200#[serde(rename_all = "camelCase")]
1201pub struct TransformSemanticPreservationTelemetryV0 {
1202    pub observed_pass_count: u64,
1203    pub preserved_pass_count: u64,
1204    pub blocked_pass_count: u64,
1205    pub observed_surface: TransformSemanticObservationSurfaceV0,
1206}
1207
1208#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
1209#[serde(rename_all = "camelCase")]
1210pub struct TransformDischargeLedgerTelemetryV0 {
1211    pub lookup_count: u64,
1212    pub matched_lookup_count: u64,
1213    pub accepted_stamp_count: u64,
1214    pub blocked_lookup_count: u64,
1215}
1216
1217#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1218#[serde(rename_all = "camelCase")]
1219pub struct TransformExecutionSummaryV0 {
1220    pub schema_version: &'static str,
1221    pub product: &'static str,
1222    pub input_byte_len: usize,
1223    pub output_byte_len: usize,
1224    pub requested_pass_ids: Vec<&'static str>,
1225    pub ordered_pass_ids: Vec<&'static str>,
1226    pub executed_pass_ids: Vec<&'static str>,
1227    pub planned_only_pass_ids: Vec<&'static str>,
1228    pub mutation_count: usize,
1229    pub provenance_preserved: bool,
1230    pub output_css: String,
1231    pub css_module_evaluation: Option<TransformModuleEvaluationV0>,
1232    pub css_import_inlines: Vec<TransformImportInlineV0>,
1233    pub css_module_composes_exports: Vec<TransformCssModuleComposesResolutionV0>,
1234    pub design_token_routes: Vec<TransformDesignTokenRouteV0>,
1235    pub semantic_removals: Vec<TransformSemanticRemovalV0>,
1236    #[serde(skip_serializing_if = "Option::is_none")]
1237    pub module_qualified_shake: Option<TransformModuleQualifiedShakeSummaryV0>,
1238    pub cascade_proof_obligations: TransformCascadeProofObligationReportV0,
1239    #[serde(skip_serializing_if = "Vec::is_empty")]
1240    pub winner_equality_obligations: Vec<TransformWinnerEqualityObligationV0>,
1241    pub provenance_derivation_forest: TransformProvenanceDerivationForestV0,
1242    pub structural_ir_transaction_telemetry: TransformStructuralIrTransactionTelemetryV0,
1243    pub semantic_preservation_telemetry: TransformSemanticPreservationTelemetryV0,
1244    pub discharge_ledger_telemetry: TransformDischargeLedgerTelemetryV0,
1245    pub strict_policy: TransformStrictPolicySummaryV0,
1246    pub closed_world_admission: TransformClosedWorldAdmissionSummaryV0,
1247    pub decisions: Vec<TransformDecision>,
1248    pub outcomes: Vec<TransformPassExecutionOutcomeV0>,
1249    pub pass_plan: TransformPassPlanV0,
1250}
1251
1252#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1253#[serde(rename_all = "camelCase")]
1254pub struct TransformModuleQualifiedShakeSummaryV0 {
1255    pub module_instance: ModuleInstanceKeyV0,
1256    pub removed_count: usize,
1257}
1258
1259#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1260#[serde(
1261    tag = "kind",
1262    rename_all = "camelCase",
1263    rename_all_fields = "camelCase"
1264)]
1265pub enum TransformModuleQualifiedExecutionErrorV0 {
1266    UnknownModuleInstance {
1267        module_instance: ModuleInstanceKeyV0,
1268    },
1269}
1270
1271#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1272#[serde(rename_all = "camelCase")]
1273pub struct TransformCascadeProofObligationReportV0 {
1274    pub schema_version: &'static str,
1275    pub product: &'static str,
1276    pub obligation_count: usize,
1277    pub accepted_count: usize,
1278    pub blocked_count: usize,
1279    pub checked_pass_ids: Vec<&'static str>,
1280    pub obligations: Vec<TransformCascadeProofObligationV0>,
1281}
1282
1283#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1284#[serde(rename_all = "camelCase")]
1285pub struct TransformCascadeProofObligationV0 {
1286    pub pass_id: &'static str,
1287    pub proof_product: &'static str,
1288    pub accepted: bool,
1289    pub blocked_reason: Option<String>,
1290    pub provenance_preserved: bool,
1291    pub cascade_safe_witness: String,
1292    pub source_span_start: Option<usize>,
1293    pub source_span_end: Option<usize>,
1294    pub checked_obligations: Vec<&'static str>,
1295    #[serde(skip_serializing_if = "Option::is_none")]
1296    pub canonical_smt_input: Option<CanonicalSmtInputV0>,
1297    #[serde(skip_serializing_if = "Option::is_none")]
1298    pub discharge_ledger_lookup: Option<DischargeLedgerLookupV0>,
1299    #[serde(skip_serializing_if = "Option::is_none")]
1300    pub discharge_evidence: Option<TransformDischargeEvidenceV0>,
1301    pub proof_payload: Value,
1302}
1303
1304#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1305#[serde(rename_all = "camelCase")]
1306pub struct TransformSemanticRemovalV0 {
1307    pub pass_id: &'static str,
1308    pub symbol_kind: &'static str,
1309    pub name: String,
1310    pub source_span_start: usize,
1311    pub source_span_end: usize,
1312    pub reason: &'static str,
1313    pub certainty: &'static str,
1314    pub derivation_steps: Vec<&'static str>,
1315}
1316
1317#[derive(Debug, Clone, PartialEq, Eq)]
1318pub(crate) struct TransformSemanticRemovalCandidate {
1319    pub(crate) symbol_kind: &'static str,
1320    pub(crate) name: String,
1321    pub(crate) source_span_start: usize,
1322    pub(crate) source_span_end: usize,
1323    pub(crate) reason: &'static str,
1324}
1325
1326impl TransformSemanticRemovalCandidate {
1327    pub(crate) fn into_public(self, pass_id: &'static str) -> TransformSemanticRemovalV0 {
1328        TransformSemanticRemovalV0 {
1329            pass_id,
1330            symbol_kind: self.symbol_kind,
1331            name: self.name,
1332            source_span_start: self.source_span_start,
1333            source_span_end: self.source_span_end,
1334            reason: self.reason,
1335            certainty: "high",
1336            derivation_steps: vec![
1337                "closedStyleWorld",
1338                "reachableRootSetComputed",
1339                "symbolNotMarkedReachable",
1340                "sourceRangeRemoved",
1341            ],
1342        }
1343    }
1344}
1345
1346#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1347#[serde(rename_all = "camelCase")]
1348pub struct TransformIncrementalExecutionSummaryV0 {
1349    pub schema_version: &'static str,
1350    pub product: &'static str,
1351    pub incremental_engine: &'static str,
1352    pub query_model: &'static str,
1353    pub reuse_policy: &'static str,
1354    pub reused_previous_execution: bool,
1355    pub incremental_plan: IncrementalComputationPlanV0,
1356    pub next_snapshot: IncrementalSnapshotV0,
1357    pub execution: TransformExecutionSummaryV0,
1358    pub ready_surfaces: Vec<&'static str>,
1359}
1360
1361#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1362#[serde(rename_all = "camelCase")]
1363pub struct TransformCascadeSafetyFuzzCaseV0 {
1364    pub seed: u64,
1365    pub pass_count: usize,
1366}
1367
1368#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1369#[serde(rename_all = "camelCase")]
1370pub struct TransformCascadeSafetyFuzzResultV0 {
1371    pub seed: u64,
1372    pub pass_count: usize,
1373    pub requested_pass_ids: Vec<&'static str>,
1374    pub executed_pass_ids: Vec<&'static str>,
1375    pub output_byte_len: usize,
1376    pub output_token_count: usize,
1377    pub output_error_count: usize,
1378    pub provenance_node_count: usize,
1379    pub passed: bool,
1380}
1381
1382#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1383#[serde(rename_all = "camelCase")]
1384pub struct TransformFuzzSeedReportV0 {
1385    pub schema_version: &'static str,
1386    pub product: &'static str,
1387    pub case_count: usize,
1388    pub passed_count: usize,
1389    pub failed_count: usize,
1390    pub results: Vec<TransformCascadeSafetyFuzzResultV0>,
1391}
1392
1393#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
1394#[serde(default, rename_all = "camelCase")]
1395pub struct TransformExecutionContextV0 {
1396    pub drop_dark_mode_media_queries: bool,
1397    pub supports_target_capability: Option<SupportsTargetCapabilityV0>,
1398    pub vendor_prefix_policy: Option<TransformVendorPrefixPolicyV0>,
1399    pub reachable_class_names: Vec<String>,
1400    pub reachable_keyframe_names: Vec<String>,
1401    pub reachable_value_names: Vec<String>,
1402    pub reachable_custom_property_names: Vec<String>,
1403    pub scss_module_evaluation: Option<TransformModuleEvaluationV0>,
1404    pub less_module_evaluation: Option<TransformModuleEvaluationV0>,
1405    pub import_inlines: Vec<TransformImportInlineV0>,
1406    pub class_name_rewrites: Vec<TransformClassNameRewriteV0>,
1407    pub css_module_composes_resolutions: Vec<TransformCssModuleComposesResolutionV0>,
1408    pub css_module_value_resolutions: Vec<TransformCssModuleValueResolutionV0>,
1409    pub design_token_routes: Vec<TransformDesignTokenRouteV0>,
1410    /// Complete declarations outside the transformed stylesheet that may
1411    /// participate in the cascade. Absence keeps winner trust fail-closed.
1412    #[serde(default, skip_serializing_if = "Option::is_none")]
1413    pub cascade_environment: Option<TransformCascadeEnvironmentV0>,
1414}
1415
1416#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
1417#[serde(default, rename_all = "camelCase")]
1418pub struct TransformCascadeEnvironmentV0 {
1419    /// Global source-order coordinate assigned to the first declaration in
1420    /// the transformed stylesheet.
1421    pub stylesheet_source_order_base: u32,
1422    pub declarations: Vec<TransformCascadeEnvironmentDeclarationV0>,
1423}
1424
1425#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1426#[serde(rename_all = "camelCase")]
1427pub struct TransformCascadeEnvironmentDeclarationV0 {
1428    pub declaration_id: String,
1429    pub selector: String,
1430    pub property: String,
1431    pub value: String,
1432    pub origin: CascadeOriginV0,
1433    pub important: bool,
1434    #[serde(default, skip_serializing_if = "Option::is_none")]
1435    pub layer_rank: Option<i32>,
1436    #[serde(default, skip_serializing_if = "Option::is_none")]
1437    pub scope_proximity: Option<u32>,
1438    pub source_order: u32,
1439}
1440
1441#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
1442#[serde(rename_all = "camelCase")]
1443pub struct TransformVendorPrefixPolicyV0 {
1444    pub webkit: bool,
1445    pub moz: bool,
1446    pub ms: bool,
1447}
1448
1449impl TransformVendorPrefixPolicyV0 {
1450    pub const fn none() -> Self {
1451        Self {
1452            webkit: false,
1453            moz: false,
1454            ms: false,
1455        }
1456    }
1457
1458    pub const fn conservative() -> Self {
1459        Self {
1460            webkit: true,
1461            moz: true,
1462            ms: true,
1463        }
1464    }
1465
1466    pub const fn is_empty(self) -> bool {
1467        !(self.webkit || self.moz || self.ms)
1468    }
1469
1470    pub fn allows_prefix(self, prefixed_name: &str) -> bool {
1471        if prefixed_name.starts_with("-webkit-") {
1472            return self.webkit;
1473        }
1474        if prefixed_name.starts_with("-moz-") {
1475            return self.moz;
1476        }
1477        if prefixed_name.starts_with("-ms-") {
1478            return self.ms;
1479        }
1480        true
1481    }
1482}
1483
1484#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1485#[serde(rename_all = "camelCase")]
1486pub struct TransformModuleEvaluationV0 {
1487    pub evaluator: String,
1488    #[serde(default, skip_serializing_if = "Option::is_none")]
1489    pub product_output_source: Option<String>,
1490    pub evaluated_css: String,
1491    #[serde(default, skip_serializing_if = "Option::is_none")]
1492    pub native_edit_output: Option<String>,
1493    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1494    pub native_replacements: Vec<TransformModuleEvaluationNativeReplacementV0>,
1495    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1496    pub native_edits: Vec<TransformModuleEvaluationNativeEditV0>,
1497    #[serde(default, skip_serializing_if = "Option::is_none")]
1498    pub oracle: Option<TransformModuleEvaluationOracleV0>,
1499}
1500
1501impl TransformModuleEvaluationV0 {
1502    pub fn declares_native_product_output(&self) -> bool {
1503        self.product_output_source
1504            .as_deref()
1505            .is_some_and(|source| source == "nativeEditOutput")
1506    }
1507
1508    // HONESTY NOTE: `divergence_count == 0` is a value-WELL-FORMEDNESS self-check on the
1509    // native-edit output (every native-emitted declaration value canonically round-trips), NOT a
1510    // differential against an external SCSS/Less compiler. So this gate means "native output is
1511    // self-consistent and value-preserving", NOT "native agrees with dart-sass/lessc". External
1512    // agreement is witnessed separately by the `externalDifferential` gate
1513    // (`scripts/check-rust-omena-diff-test-external-corpus-differential.ts`, pinned dart-sass/lessc) over
1514    // its covered fixture slices only; this self-check stays the cheap inner oracle for every
1515    // evaluated candidate, and the production rail remains a self-comparison.
1516    pub fn oracle_allows_native_product_output(&self) -> bool {
1517        self.oracle.as_ref().is_some_and(|oracle| {
1518            oracle.mode == "oracleOnly"
1519                && oracle.divergence_count == 0
1520                && oracle.all_legacy_declaration_values_preserved
1521        })
1522    }
1523
1524    pub fn may_consume_native_product_output(&self) -> bool {
1525        self.declares_native_product_output() && self.oracle_allows_native_product_output()
1526    }
1527
1528    // NOTE: the "retained oracle" here is the retained product-output string (`evaluated_css`),
1529    // which in the production rail is itself native-derived — so this is a byte-equality
1530    // self-consistency check between two native-derived strings, not a comparison to an
1531    // independent external evaluator.
1532    pub fn native_output_matches_retained_oracle(&self, native_output: &str) -> bool {
1533        self.oracle
1534            .as_ref()
1535            .is_some_and(|_| native_output == self.evaluated_css)
1536    }
1537}
1538
1539#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1540#[serde(rename_all = "camelCase")]
1541pub struct TransformModuleEvaluationNativeReplacementV0 {
1542    pub name: String,
1543    pub start: usize,
1544    pub end: usize,
1545    pub text: String,
1546    #[serde(default, skip_serializing_if = "Option::is_none")]
1547    pub rendered_value: Option<String>,
1548    pub abstract_value: AbstractCssValueV0,
1549    pub abstract_value_kind: String,
1550}
1551
1552#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1553#[serde(rename_all = "camelCase")]
1554pub struct TransformModuleEvaluationNativeEditV0 {
1555    pub start: usize,
1556    pub end: usize,
1557    pub replacement: String,
1558    pub edit_kind: String,
1559    #[serde(default, skip_serializing_if = "Option::is_none")]
1560    pub abstract_value: Option<AbstractCssValueV0>,
1561    #[serde(default, skip_serializing_if = "Option::is_none")]
1562    pub abstract_value_kind: Option<String>,
1563}
1564
1565#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
1566#[serde(default, rename_all = "camelCase")]
1567pub struct TransformModuleEvaluationOracleV0 {
1568    pub mode: String,
1569    pub product_output_source: String,
1570    pub legacy_declaration_value_count: usize,
1571    pub abstract_value_count: usize,
1572    pub exact_value_count: usize,
1573    pub raw_value_count: usize,
1574    pub bottom_value_count: usize,
1575    pub top_value_count: usize,
1576    pub divergence_count: usize,
1577    pub all_legacy_declaration_values_preserved: bool,
1578    pub native_replacement_count: usize,
1579    pub native_replacement_legacy_reflection_count: usize,
1580    pub native_replacement_legacy_unreflected_count: usize,
1581    pub native_value_reference_count: usize,
1582    pub native_resolved_value_count: usize,
1583    pub native_raw_value_count: usize,
1584    pub native_top_value_count: usize,
1585    pub native_cycle_count: usize,
1586    pub native_fuel_exhausted_count: usize,
1587    pub native_unresolved_reference_count: usize,
1588    pub native_unsupported_dynamic_count: usize,
1589}
1590
1591#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1592#[serde(rename_all = "camelCase")]
1593pub struct TransformImportInlineV0 {
1594    pub import_source: String,
1595    pub replacement_css: String,
1596}
1597
1598#[derive(Debug, Clone, PartialEq, Eq)]
1599pub struct TransformLessInlineLiteralPlaceholderV0 {
1600    pub placeholder: String,
1601    pub literal_css: String,
1602}
1603
1604#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1605#[serde(rename_all = "camelCase")]
1606pub struct TransformClassNameRewriteV0 {
1607    pub original_name: String,
1608    pub rewritten_name: String,
1609}
1610
1611/// Module-qualified CSS Modules rewrite input.
1612///
1613/// Consumers that need different rewrites for identical class spellings in
1614/// different modules use this carrier instead of flattening those rewrites
1615/// into [`TransformExecutionContextV0::class_name_rewrites`]. The module key is
1616/// compared before the canonical class-name key. Under an equal compound key,
1617/// the consumer-supplied record is the first witness and wins independently of
1618/// raw spelling or later presentation sorting.
1619#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1620#[serde(rename_all = "camelCase")]
1621#[non_exhaustive]
1622pub struct TransformModuleCssModuleContextV0 {
1623    pub module_instance: ModuleInstanceKeyV0,
1624    pub class_name_rewrites: Vec<TransformClassNameRewriteV0>,
1625    pub composes_resolutions: Vec<TransformCssModuleComposesResolutionV0>,
1626}
1627
1628impl TransformModuleCssModuleContextV0 {
1629    pub fn new(module_instance: ModuleInstanceKeyV0) -> Self {
1630        Self {
1631            module_instance,
1632            class_name_rewrites: Vec::new(),
1633            composes_resolutions: Vec::new(),
1634        }
1635    }
1636
1637    pub fn with_class_name_rewrites(
1638        mut self,
1639        class_name_rewrites: Vec<TransformClassNameRewriteV0>,
1640    ) -> Self {
1641        self.class_name_rewrites = class_name_rewrites;
1642        self
1643    }
1644
1645    pub fn with_composes_resolutions(
1646        mut self,
1647        composes_resolutions: Vec<TransformCssModuleComposesResolutionV0>,
1648    ) -> Self {
1649        self.composes_resolutions = composes_resolutions;
1650        self
1651    }
1652}
1653
1654#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1655#[serde(rename_all = "camelCase")]
1656pub struct TransformCssModuleComposesResolutionV0 {
1657    pub local_class_name: String,
1658    pub exported_class_names: Vec<String>,
1659}
1660
1661#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1662#[serde(rename_all = "camelCase")]
1663pub struct TransformCssModuleValueResolutionV0 {
1664    pub local_name: String,
1665    pub resolved_value: String,
1666}
1667
1668#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1669#[serde(rename_all = "camelCase")]
1670pub struct TransformDesignTokenRouteV0 {
1671    pub token_name: String,
1672    pub routed_value: String,
1673}
1674
1675#[cfg(test)]
1676mod evidence_graph_tests {
1677    use super::*;
1678    use omena_cascade::{
1679        CascadeKey, CascadeValue, LayerOrdinal, OpenWorldTieEvidence, Specificity,
1680        cascade_property, normalized_layer_rank,
1681    };
1682
1683    fn winner_equality_test_declaration(
1684        id: &str,
1685        value: &str,
1686        source_order: u32,
1687    ) -> CascadeDeclaration {
1688        CascadeDeclaration {
1689            id: id.to_string(),
1690            property: "color".to_string(),
1691            value: CascadeValue::Literal(value.to_string()),
1692            key: CascadeKey::new(
1693                CascadeLevel::AuthorNormal,
1694                normalized_layer_rank(false, LayerOrdinal::new(0)),
1695                0,
1696                Specificity::new(0, 1, 0),
1697                source_order,
1698            ),
1699            open_world_tie_evidence: OpenWorldTieEvidence::NONE,
1700            specificity_exactness: omena_cascade::SpecificityExactnessV0::Exact,
1701        }
1702    }
1703
1704    #[test]
1705    fn winner_equality_witness_consumes_the_cascade_authority_outcome() -> Result<(), String> {
1706        let outcome = cascade_property(
1707            [
1708                winner_equality_test_declaration("earlier", "red", 0),
1709                winner_equality_test_declaration("later", "blue", 1),
1710            ],
1711            "color",
1712        );
1713        let witness = TransformWinnerEqualityWitnessV0::from_cascade_outcome(&outcome)
1714            .ok_or_else(|| "the closed cascade should have a definite winner".to_string())?;
1715
1716        assert_eq!(witness.winner.id, "later");
1717        assert_eq!(
1718            witness.proof,
1719            CascadeProof::from_declaration(&witness.winner)
1720        );
1721        Ok(())
1722    }
1723
1724    #[test]
1725    fn winner_equality_witness_stays_absent_for_non_definite_outcomes() {
1726        assert!(
1727            TransformWinnerEqualityWitnessV0::from_cascade_outcome(&CascadeOutcome::Top).is_none()
1728        );
1729        assert!(
1730            TransformWinnerEqualityWitnessV0::from_cascade_outcome(&CascadeOutcome::Inherit)
1731                .is_none()
1732        );
1733    }
1734
1735    #[test]
1736    fn winner_equality_trust_records_name_covered_axes() -> Result<(), serde_json::Error> {
1737        let tier = TransformSemanticGuaranteeTierV0::WinnerEqualityObserved {
1738            axes: vec![
1739                TransformWinnerEqualityAxisV0::CascadeLevel,
1740                TransformWinnerEqualityAxisV0::LayerRank,
1741            ],
1742        };
1743
1744        assert_eq!(
1745            serde_json::to_value(tier)?,
1746            serde_json::json!({
1747                "kind": "winnerEqualityObserved",
1748                "axes": ["cascadeLevel", "layerRank"]
1749            })
1750        );
1751        Ok(())
1752    }
1753
1754    #[test]
1755    fn winner_equality_absence_names_the_undriven_level() -> Result<(), serde_json::Error> {
1756        let tier = TransformSemanticGuaranteeTierV0::Absent {
1757            reasons: vec![TransformWinnerEqualityAbsenceV0 {
1758                axis: TransformWinnerEqualityAxisV0::CascadeLevel,
1759                reason: TransformWinnerEqualityAbsenceReasonV0::DriverUnavailable {
1760                    level: Some(CascadeLevel::Animation),
1761                },
1762            }],
1763        };
1764
1765        assert_eq!(
1766            serde_json::to_value(tier)?,
1767            serde_json::json!({
1768                "kind": "absent",
1769                "reasons": [{
1770                    "axis": "cascadeLevel",
1771                    "reason": {
1772                        "kind": "driverUnavailable",
1773                        "level": "animation"
1774                    }
1775                }]
1776            })
1777        );
1778        Ok(())
1779    }
1780
1781    #[test]
1782    fn transform_outcome_evidence_graph_preserves_public_shape() -> Result<(), serde_json::Error> {
1783        let outcome = TransformPassExecutionOutcomeV0 {
1784            pass_id: "number-compression",
1785            status: TransformPassRuntimeStatus::Applied,
1786            input_byte_len: 32,
1787            output_byte_len: 28,
1788            mutation_count: 1,
1789            provenance_preserved: true,
1790            detail: "fixture pass",
1791        };
1792
1793        let before = serde_json::to_value(&outcome)?;
1794        let node = outcome.evidence_node_seed();
1795        let graph = build_evidence_graph_from_edges_v0([node], [outcome.evidence_demand_edge()])
1796            .map_err(|_| serde::ser::Error::custom("outcome edge must target its node"))?;
1797        let after = serde_json::to_value(&outcome)?;
1798
1799        assert_eq!(before, after);
1800        assert_eq!(graph.nodes.len(), 1);
1801        assert_eq!(graph.nodes[0].key.input_identity, "number-compression");
1802        assert_eq!(graph.nodes[0].guarantee, GuaranteeKindV0::Floor);
1803        assert!(
1804            graph.nodes[0]
1805                .provenance
1806                .iter()
1807                .any(|item| item == "mutationCount:1")
1808        );
1809        Ok(())
1810    }
1811
1812    #[test]
1813    fn transform_derivation_forest_evidence_graph_preserves_public_shape()
1814    -> Result<(), serde_json::Error> {
1815        let forest = TransformProvenanceDerivationForestV0 {
1816            schema_version: "0",
1817            product: "omena-transform-passes.provenance-derivation-forest",
1818            root_count: 1,
1819            node_count: 1,
1820            nodes: vec![TransformProvenanceDerivationNodeV0 {
1821                node_index: 0,
1822                parent_index: None,
1823                pass_id: "comment-strip",
1824                status: TransformPassRuntimeStatus::Applied,
1825                input_byte_len: 48,
1826                output_byte_len: 36,
1827                source_span_start: 0,
1828                source_span_end: 12,
1829                generated_span_start: 0,
1830                generated_span_end: 0,
1831                mutation_spans: Vec::new(),
1832                mutation_count: 1,
1833                provenance_preserved: true,
1834                detail: "fixture derivation",
1835            }],
1836        };
1837
1838        let before = serde_json::to_value(&forest)?;
1839        let graph = forest
1840            .evidence_graph()
1841            .map_err(|_| serde::ser::Error::custom("forest edge must target its node"))?;
1842        let after = serde_json::to_value(&forest)?;
1843
1844        assert_eq!(before, after);
1845        assert_eq!(graph.nodes.len(), 1);
1846        assert_eq!(graph.nodes[0].key.input_identity, "comment-strip#0");
1847        assert_eq!(graph.nodes[0].guarantee, GuaranteeKindV0::Floor);
1848        Ok(())
1849    }
1850}