Skip to main content

omena_transform_passes/runtime/
structural_shadow.rs

1//! Structural IR shadow equivalence fixtures for transform pass runtimes.
2
3use std::{
4    collections::{BTreeSet, hash_map::DefaultHasher},
5    hash::{Hash, Hasher},
6};
7
8use omena_cascade::StaticSupportsAssumptionV0;
9use omena_incremental::IncrementalRevisionV0;
10use omena_parser::{
11    ClosedWorldBundleV0, ClosedWorldComposesScanStateV0, ClosedWorldLinkedModuleV0,
12    ClosedWorldModuleMetadataV0, ClosedWorldModuleReachabilityEvidenceV0,
13    ClosedWorldSourcePrecisionSummaryV0, ConfigurationHashV0, ModuleIdV0, ModuleInstanceKeyV0,
14    StyleDialect, parse, summarize_omena_parser_parity_lite, summarize_omena_parser_style_facts,
15};
16use omena_syntax::SyntaxKind;
17use omena_transform_cst::{
18    IrNodeIdV0, IrNodeKindV0, TransformIrV0, TransformPassClassV0, TransformPassKind,
19    default_transform_pass_descriptors, lower_transform_ir_from_source, print_transform_ir_css,
20};
21
22use super::planner::{plan_transform_passes, transform_pass_kind_from_id};
23use super::provenance::derive_transform_mutation_spans;
24use super::semantic_preservation::{
25    SemanticObservationProjectionV0, SemanticObservationScopeV0,
26    compare_semantic_observation_for_pass_with_scopes, semantic_preservation_applies,
27};
28use crate::runtime::cascade_proof::collect_cascade_proof_obligations_for_pass_input;
29use crate::{
30    TransformProvenanceMutationSpanV0, TransformSemanticRemovalCandidate,
31    TransformStructuralIrShadowEquivalenceReportV0, TransformStructuralIrShadowFieldReportV0,
32    TransformStructuralIrShadowFixtureReportV0,
33    domains::{
34        cascade_flatten::{
35            collect_layer_flatten_proof_candidates_with_lexer,
36            collect_scope_flatten_proof_candidates_with_lexer, flatten_css_layers_with_lexer,
37            flatten_css_scopes_with_lexer,
38        },
39        css_modules_classes::{
40            local_css_module_composes_resolutions_with_lexer,
41            rewrite_css_module_class_names_with_lexer,
42            strip_resolved_css_module_composes_with_lexer, tree_shake_css_class_rules_with_lexer,
43        },
44        css_modules_values::tree_shake_css_modules_values_with_lexer,
45        custom_property::tree_shake_css_custom_properties_with_lexer,
46        design_token::route_design_token_values_with_lexer,
47        import_inline::inline_css_imports_with_lexer,
48        keyframes::tree_shake_css_keyframes_with_lexer,
49        nesting::unwrap_css_nesting_with_lexer,
50        rule_cleanup::{dedupe_exact_css_rules_with_lexer, remove_empty_css_rules_with_lexer},
51        rule_merge::{
52            merge_adjacent_same_block_css_selectors_with_lexer,
53            merge_adjacent_same_selector_css_rules_with_lexer,
54        },
55        static_eval::{
56            StaticMediaEvaluationOptions, evaluate_static_container_rules_with_lexer,
57            evaluate_static_media_rules_with_lexer, evaluate_static_supports_rules_with_lexer,
58        },
59    },
60    helpers::ir_transaction::{
61        reset_structural_ir_transaction_telemetry, structural_ir_transaction_telemetry_snapshot,
62    },
63    model::{
64        TransformClassNameRewriteV0, TransformCssModuleComposesResolutionV0,
65        TransformDesignTokenRouteV0, TransformExecutionContextV0, TransformExecutionSummaryV0,
66        TransformImportInlineV0, TransformSemanticRemovalV0,
67        TransformStructuralIrTransactionTelemetryV0,
68    },
69    registry::{evaluate_native_css_static_values_with_plan, unwrap_css_nesting_in_ir},
70    runtime::executor::{
71        execute_transform_passes_on_source_with_dialect_and_context,
72        execute_transform_passes_on_source_with_dialect_context_and_closed_world_bundle,
73    },
74};
75
76const COMPARED_FIELDS: [&str; 12] = [
77    "canonicalCssBytes",
78    "selectorSet",
79    "declarationSet",
80    "cascadeOutcome",
81    "mutationSpanRanges",
82    "mutationCount",
83    "semanticRemovals",
84    "cssImportInlines",
85    "cssModuleComposesExports",
86    "cssModuleEvaluation",
87    "designTokenRoutes",
88    "irTransactionCommitCount",
89];
90
91#[derive(Debug, Clone, Copy)]
92pub struct TransformStructuralIrShadowFixtureInputV0<'source> {
93    pub fixture: &'source str,
94    pub pass: TransformPassKind,
95    pub dialect: StyleDialect,
96    pub source: &'source str,
97    pub closed_bundle: bool,
98}
99
100#[derive(Debug, Clone, Copy)]
101pub struct TransformStructuralIrPipelineShadowFixtureInputV0<'source> {
102    pub fixture: &'source str,
103    pub dialect: StyleDialect,
104    pub source: &'source str,
105    pub closed_bundle: bool,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
109struct StructuralShadowPathSnapshotV0 {
110    output_css: String,
111    mutation_count: usize,
112    selector_values: Vec<String>,
113    declaration_values: Vec<String>,
114    cascade_values: Vec<String>,
115    mutation_span_values: Vec<String>,
116    semantic_removal_values: Vec<String>,
117    css_import_inline_values: Vec<String>,
118    css_module_composes_values: Vec<String>,
119    css_module_evaluation_values: Vec<String>,
120    design_token_route_values: Vec<String>,
121    ir_transaction_telemetry: TransformStructuralIrTransactionTelemetryV0,
122    typed_payload_telemetry: StructuralShadowTypedPayloadTelemetryV0,
123}
124
125#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
126struct StructuralShadowTypedPayloadTelemetryV0 {
127    projections_consumed: usize,
128    memo_hits: usize,
129}
130
131#[derive(Debug, Clone)]
132struct TypedPayloadProjectionV0 {
133    node_id: IrNodeIdV0,
134    revision: IncrementalRevisionV0,
135    content_signature: u64,
136    typed_node_count: usize,
137    style_rule_count: usize,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141struct TypedPayloadProjectionKeyV0 {
142    node_id: IrNodeIdV0,
143    revision: IncrementalRevisionV0,
144    content_signature: u64,
145}
146
147#[derive(Default)]
148struct TypedPayloadProjectionMemoV0 {
149    entries: Vec<(TypedPayloadProjectionKeyV0, TypedPayloadProjectionV0)>,
150    telemetry: StructuralShadowTypedPayloadTelemetryV0,
151}
152
153struct StructuralShadowReachabilityV0 {
154    class_names: Vec<String>,
155    keyframe_names: Vec<String>,
156    value_names: Vec<String>,
157    custom_property_names: Vec<String>,
158}
159
160#[derive(Debug, Clone, Default)]
161struct StructuralShadowModuleContextV0 {
162    import_inlines: Vec<TransformImportInlineV0>,
163    class_name_rewrites: Vec<TransformClassNameRewriteV0>,
164    css_module_composes_resolutions: Vec<TransformCssModuleComposesResolutionV0>,
165    design_token_routes: Vec<TransformDesignTokenRouteV0>,
166}
167
168#[derive(Debug, Clone, Default)]
169struct StructuralShadowModuleEgressValuesV0 {
170    css_import_inline_values: Vec<String>,
171    css_module_composes_values: Vec<String>,
172    css_module_evaluation_values: Vec<String>,
173    design_token_route_values: Vec<String>,
174}
175
176pub fn summarize_structural_ir_shadow_equivalence_v0()
177-> TransformStructuralIrShadowEquivalenceReportV0 {
178    let fixtures = structural_shadow_fixtures();
179    summarize_structural_ir_shadow_equivalence_for_fixtures_v0(fixtures.as_slice())
180}
181
182pub fn summarize_structural_ir_shadow_equivalence_for_fixtures_v0(
183    fixtures: &[TransformStructuralIrShadowFixtureInputV0<'_>],
184) -> TransformStructuralIrShadowEquivalenceReportV0 {
185    let reports = fixtures
186        .iter()
187        .copied()
188        .map(structural_shadow_report_for_fixture)
189        .collect::<Vec<_>>();
190    let all_fields_match = reports.iter().all(|report| report.all_fields_match);
191    let all_typed_path_fields_match = reports
192        .iter()
193        .all(|report| report.all_typed_path_fields_match);
194    let typed_payload_projections_consumed = reports
195        .iter()
196        .map(|report| report.typed_payload_projections_consumed)
197        .sum::<usize>();
198    let typed_payload_memo_hits = reports
199        .iter()
200        .map(|report| report.typed_payload_memo_hits)
201        .sum::<usize>();
202
203    TransformStructuralIrShadowEquivalenceReportV0 {
204        schema_version: "0",
205        product: "omena-transform-passes.structural-ir-shadow-equivalence",
206        fixture_count: reports.len(),
207        compared_pass_ids: compared_pass_ids(),
208        compared_fields: COMPARED_FIELDS.to_vec(),
209        reports,
210        all_fields_match,
211        all_typed_path_fields_match,
212        typed_payload_projections_consumed,
213        typed_payload_memo_hits,
214    }
215}
216
217pub fn summarize_structural_ir_pipeline_shadow_equivalence_for_fixtures_v0(
218    fixtures: &[TransformStructuralIrPipelineShadowFixtureInputV0<'_>],
219) -> TransformStructuralIrShadowEquivalenceReportV0 {
220    let reports = fixtures
221        .iter()
222        .copied()
223        .map(structural_pipeline_shadow_report_for_fixture)
224        .collect::<Vec<_>>();
225    let all_fields_match = reports.iter().all(|report| report.all_fields_match);
226    let all_typed_path_fields_match = reports
227        .iter()
228        .all(|report| report.all_typed_path_fields_match);
229    let typed_payload_projections_consumed = reports
230        .iter()
231        .map(|report| report.typed_payload_projections_consumed)
232        .sum::<usize>();
233    let typed_payload_memo_hits = reports
234        .iter()
235        .map(|report| report.typed_payload_memo_hits)
236        .sum::<usize>();
237
238    TransformStructuralIrShadowEquivalenceReportV0 {
239        schema_version: "0",
240        product: "omena-transform-passes.structural-ir-pipeline-shadow-equivalence",
241        fixture_count: reports.len(),
242        compared_pass_ids: compared_pass_ids(),
243        compared_fields: COMPARED_FIELDS.to_vec(),
244        reports,
245        all_fields_match,
246        all_typed_path_fields_match,
247        typed_payload_projections_consumed,
248        typed_payload_memo_hits,
249    }
250}
251
252fn structural_shadow_report_for_fixture(
253    fixture: TransformStructuralIrShadowFixtureInputV0<'_>,
254) -> TransformStructuralIrShadowFixtureReportV0 {
255    let string_snapshot = string_path_snapshot(fixture);
256    let ir_snapshot = ir_path_snapshot(fixture);
257    let typed_snapshot = typed_path_snapshot(fixture);
258    let expected_commit_flag = expected_ir_transaction_commit_flag(string_snapshot.mutation_count);
259    let (
260        ir_path_mutation_count,
261        typed_path_mutation_count,
262        ir_path_transaction_commit_count,
263        typed_payload_projections_consumed,
264        typed_payload_memo_hits,
265        fields,
266    ) = match (ir_snapshot, typed_snapshot) {
267        (Ok(ir_snapshot), Ok(typed_snapshot)) => {
268            let telemetry = ir_snapshot.ir_transaction_telemetry;
269            let typed_payload_telemetry = typed_snapshot.typed_payload_telemetry;
270            let actual_commit_flag = if telemetry.transaction_commit_count > 0 {
271                "1".to_string()
272            } else {
273                "0".to_string()
274            };
275            (
276                Some(ir_snapshot.mutation_count),
277                Some(typed_snapshot.mutation_count),
278                Some(telemetry.transaction_commit_count),
279                typed_payload_telemetry.projections_consumed,
280                typed_payload_telemetry.memo_hits,
281                vec![
282                    shadow_field_report_with_typed(
283                        "canonicalCssBytes",
284                        [string_snapshot.output_css.clone()],
285                        [ir_snapshot.output_css],
286                        [typed_snapshot.output_css],
287                    ),
288                    shadow_field_report_with_typed(
289                        "selectorSet",
290                        string_snapshot.selector_values,
291                        ir_snapshot.selector_values,
292                        typed_snapshot.selector_values,
293                    ),
294                    shadow_field_report_with_typed(
295                        "declarationSet",
296                        string_snapshot.declaration_values,
297                        ir_snapshot.declaration_values,
298                        typed_snapshot.declaration_values,
299                    ),
300                    shadow_field_report_with_typed(
301                        "cascadeOutcome",
302                        string_snapshot.cascade_values,
303                        ir_snapshot.cascade_values,
304                        typed_snapshot.cascade_values,
305                    ),
306                    shadow_field_report_with_typed(
307                        "mutationSpanRanges",
308                        string_snapshot.mutation_span_values,
309                        ir_snapshot.mutation_span_values,
310                        typed_snapshot.mutation_span_values,
311                    ),
312                    shadow_field_report_with_typed(
313                        "mutationCount",
314                        [string_snapshot.mutation_count.to_string()],
315                        [ir_snapshot.mutation_count.to_string()],
316                        [typed_snapshot.mutation_count.to_string()],
317                    ),
318                    shadow_field_report_with_typed(
319                        "semanticRemovals",
320                        string_snapshot.semantic_removal_values,
321                        ir_snapshot.semantic_removal_values,
322                        typed_snapshot.semantic_removal_values,
323                    ),
324                    shadow_field_report_with_typed(
325                        "cssImportInlines",
326                        string_snapshot.css_import_inline_values,
327                        ir_snapshot.css_import_inline_values,
328                        typed_snapshot.css_import_inline_values,
329                    ),
330                    shadow_field_report_with_typed(
331                        "cssModuleComposesExports",
332                        string_snapshot.css_module_composes_values,
333                        ir_snapshot.css_module_composes_values,
334                        typed_snapshot.css_module_composes_values,
335                    ),
336                    shadow_field_report_with_typed(
337                        "cssModuleEvaluation",
338                        string_snapshot.css_module_evaluation_values,
339                        ir_snapshot.css_module_evaluation_values,
340                        typed_snapshot.css_module_evaluation_values,
341                    ),
342                    shadow_field_report_with_typed(
343                        "designTokenRoutes",
344                        string_snapshot.design_token_route_values,
345                        ir_snapshot.design_token_route_values,
346                        typed_snapshot.design_token_route_values,
347                    ),
348                    shadow_field_report_with_typed(
349                        "irTransactionCommitCount",
350                        [expected_commit_flag.clone()],
351                        [actual_commit_flag],
352                        [expected_ir_transaction_commit_flag(
353                            typed_snapshot.mutation_count,
354                        )],
355                    ),
356                ],
357            )
358        }
359        (Err(ir_error), typed_result) => {
360            let ir_error = format!("irPathError:{ir_error}");
361            let typed_error = typed_result
362                .err()
363                .map(|error| format!("typedPathError:{error}"))
364                .unwrap_or_else(|| "typedPathUnavailableAfterIrError".to_string());
365            (
366                None,
367                None,
368                None,
369                0,
370                0,
371                vec![
372                    shadow_field_report_with_typed(
373                        "canonicalCssBytes",
374                        [string_snapshot.output_css],
375                        [ir_error.clone()],
376                        [typed_error.clone()],
377                    ),
378                    shadow_field_report_with_typed(
379                        "selectorSet",
380                        string_snapshot.selector_values,
381                        [ir_error.clone()],
382                        [typed_error.clone()],
383                    ),
384                    shadow_field_report_with_typed(
385                        "declarationSet",
386                        string_snapshot.declaration_values,
387                        [ir_error.clone()],
388                        [typed_error.clone()],
389                    ),
390                    shadow_field_report_with_typed(
391                        "cascadeOutcome",
392                        string_snapshot.cascade_values,
393                        [ir_error.clone()],
394                        [typed_error.clone()],
395                    ),
396                    shadow_field_report_with_typed(
397                        "mutationSpanRanges",
398                        string_snapshot.mutation_span_values,
399                        [ir_error.clone()],
400                        [typed_error.clone()],
401                    ),
402                    shadow_field_report_with_typed(
403                        "mutationCount",
404                        [string_snapshot.mutation_count.to_string()],
405                        [ir_error.clone()],
406                        [typed_error.clone()],
407                    ),
408                    shadow_field_report_with_typed(
409                        "semanticRemovals",
410                        string_snapshot.semantic_removal_values,
411                        [ir_error.clone()],
412                        [typed_error.clone()],
413                    ),
414                    shadow_field_report_with_typed(
415                        "cssImportInlines",
416                        string_snapshot.css_import_inline_values,
417                        [ir_error.clone()],
418                        [typed_error.clone()],
419                    ),
420                    shadow_field_report_with_typed(
421                        "cssModuleComposesExports",
422                        string_snapshot.css_module_composes_values,
423                        [ir_error.clone()],
424                        [typed_error.clone()],
425                    ),
426                    shadow_field_report_with_typed(
427                        "cssModuleEvaluation",
428                        string_snapshot.css_module_evaluation_values,
429                        [ir_error.clone()],
430                        [typed_error.clone()],
431                    ),
432                    shadow_field_report_with_typed(
433                        "designTokenRoutes",
434                        string_snapshot.design_token_route_values,
435                        [ir_error.clone()],
436                        [typed_error.clone()],
437                    ),
438                    shadow_field_report_with_typed(
439                        "irTransactionCommitCount",
440                        [expected_commit_flag],
441                        [ir_error.clone()],
442                        [typed_error.clone()],
443                    ),
444                ],
445            )
446        }
447        (Ok(ir_snapshot), Err(typed_error)) => {
448            let telemetry = ir_snapshot.ir_transaction_telemetry;
449            let actual_commit_flag = if telemetry.transaction_commit_count > 0 {
450                "1".to_string()
451            } else {
452                "0".to_string()
453            };
454            let typed_error = format!("typedPathError:{typed_error}");
455            (
456                Some(ir_snapshot.mutation_count),
457                None,
458                Some(telemetry.transaction_commit_count),
459                0,
460                0,
461                vec![
462                    shadow_field_report_with_typed(
463                        "canonicalCssBytes",
464                        [string_snapshot.output_css],
465                        [ir_snapshot.output_css],
466                        [typed_error.clone()],
467                    ),
468                    shadow_field_report_with_typed(
469                        "selectorSet",
470                        string_snapshot.selector_values,
471                        ir_snapshot.selector_values,
472                        [typed_error.clone()],
473                    ),
474                    shadow_field_report_with_typed(
475                        "declarationSet",
476                        string_snapshot.declaration_values,
477                        ir_snapshot.declaration_values,
478                        [typed_error.clone()],
479                    ),
480                    shadow_field_report_with_typed(
481                        "cascadeOutcome",
482                        string_snapshot.cascade_values,
483                        ir_snapshot.cascade_values,
484                        [typed_error.clone()],
485                    ),
486                    shadow_field_report_with_typed(
487                        "mutationSpanRanges",
488                        string_snapshot.mutation_span_values,
489                        ir_snapshot.mutation_span_values,
490                        [typed_error.clone()],
491                    ),
492                    shadow_field_report_with_typed(
493                        "mutationCount",
494                        [string_snapshot.mutation_count.to_string()],
495                        [ir_snapshot.mutation_count.to_string()],
496                        [typed_error.clone()],
497                    ),
498                    shadow_field_report_with_typed(
499                        "semanticRemovals",
500                        string_snapshot.semantic_removal_values,
501                        ir_snapshot.semantic_removal_values,
502                        [typed_error.clone()],
503                    ),
504                    shadow_field_report_with_typed(
505                        "cssImportInlines",
506                        string_snapshot.css_import_inline_values,
507                        ir_snapshot.css_import_inline_values,
508                        [typed_error.clone()],
509                    ),
510                    shadow_field_report_with_typed(
511                        "cssModuleComposesExports",
512                        string_snapshot.css_module_composes_values,
513                        ir_snapshot.css_module_composes_values,
514                        [typed_error.clone()],
515                    ),
516                    shadow_field_report_with_typed(
517                        "cssModuleEvaluation",
518                        string_snapshot.css_module_evaluation_values,
519                        ir_snapshot.css_module_evaluation_values,
520                        [typed_error.clone()],
521                    ),
522                    shadow_field_report_with_typed(
523                        "designTokenRoutes",
524                        string_snapshot.design_token_route_values,
525                        ir_snapshot.design_token_route_values,
526                        [typed_error.clone()],
527                    ),
528                    shadow_field_report_with_typed(
529                        "irTransactionCommitCount",
530                        [expected_commit_flag],
531                        [actual_commit_flag],
532                        [typed_error],
533                    ),
534                ],
535            )
536        }
537    };
538    let all_fields_match = fields.iter().all(|field| field.matches);
539    let all_typed_path_fields_match = all_fields_match;
540
541    TransformStructuralIrShadowFixtureReportV0 {
542        schema_version: "0",
543        product: "omena-transform-passes.structural-ir-shadow-fixture",
544        fixture: fixture.fixture.to_string(),
545        pass_id: fixture.pass.id(),
546        dialect: dialect_label(fixture.dialect),
547        string_path_mutation_count: Some(string_snapshot.mutation_count),
548        ir_path_mutation_count,
549        typed_path_mutation_count,
550        ir_path_transaction_commit_count,
551        typed_payload_projections_consumed,
552        typed_payload_memo_hits,
553        fields,
554        all_fields_match,
555        all_typed_path_fields_match,
556    }
557}
558
559fn structural_pipeline_shadow_report_for_fixture(
560    fixture: TransformStructuralIrPipelineShadowFixtureInputV0<'_>,
561) -> TransformStructuralIrShadowFixtureReportV0 {
562    let string_snapshot = string_pipeline_snapshot(fixture);
563    let expected_commit_flag = expected_ir_transaction_commit_flag(string_snapshot.mutation_count);
564    let (ir_path_mutation_count, ir_path_transaction_commit_count, fields) =
565        match ir_pipeline_snapshot(fixture) {
566            Ok(ir_snapshot) => {
567                let actual_commit_flag = if ir_snapshot
568                    .ir_transaction_telemetry
569                    .transaction_commit_count
570                    > 0
571                {
572                    "1".to_string()
573                } else {
574                    "0".to_string()
575                };
576                (
577                    Some(ir_snapshot.mutation_count),
578                    Some(
579                        ir_snapshot
580                            .ir_transaction_telemetry
581                            .transaction_commit_count,
582                    ),
583                    vec![
584                        shadow_field_report(
585                            "canonicalCssBytes",
586                            [string_snapshot.output_css.clone()],
587                            [ir_snapshot.output_css],
588                        ),
589                        shadow_field_report(
590                            "selectorSet",
591                            string_snapshot.selector_values,
592                            ir_snapshot.selector_values,
593                        ),
594                        shadow_field_report(
595                            "declarationSet",
596                            string_snapshot.declaration_values,
597                            ir_snapshot.declaration_values,
598                        ),
599                        shadow_field_report(
600                            "cascadeOutcome",
601                            string_snapshot.cascade_values,
602                            ir_snapshot.cascade_values,
603                        ),
604                        shadow_field_report(
605                            "mutationSpanRanges",
606                            string_snapshot.mutation_span_values,
607                            ir_snapshot.mutation_span_values,
608                        ),
609                        shadow_field_report(
610                            "mutationCount",
611                            [string_snapshot.mutation_count.to_string()],
612                            [ir_snapshot.mutation_count.to_string()],
613                        ),
614                        shadow_field_report(
615                            "semanticRemovals",
616                            string_snapshot.semantic_removal_values,
617                            ir_snapshot.semantic_removal_values,
618                        ),
619                        shadow_field_report(
620                            "cssImportInlines",
621                            string_snapshot.css_import_inline_values,
622                            ir_snapshot.css_import_inline_values,
623                        ),
624                        shadow_field_report(
625                            "cssModuleComposesExports",
626                            string_snapshot.css_module_composes_values,
627                            ir_snapshot.css_module_composes_values,
628                        ),
629                        shadow_field_report(
630                            "cssModuleEvaluation",
631                            string_snapshot.css_module_evaluation_values,
632                            ir_snapshot.css_module_evaluation_values,
633                        ),
634                        shadow_field_report(
635                            "designTokenRoutes",
636                            string_snapshot.design_token_route_values,
637                            ir_snapshot.design_token_route_values,
638                        ),
639                        shadow_field_report(
640                            "irTransactionCommitCount",
641                            [expected_commit_flag.clone()],
642                            [actual_commit_flag],
643                        ),
644                    ],
645                )
646            }
647            Err(error) => {
648                let error = format!("irPipelinePathError:{error}");
649                (
650                    None,
651                    None,
652                    vec![
653                        shadow_field_report(
654                            "canonicalCssBytes",
655                            [string_snapshot.output_css.clone()],
656                            [error.clone()],
657                        ),
658                        shadow_field_report(
659                            "selectorSet",
660                            string_snapshot.selector_values,
661                            [error.clone()],
662                        ),
663                        shadow_field_report(
664                            "declarationSet",
665                            string_snapshot.declaration_values,
666                            [error.clone()],
667                        ),
668                        shadow_field_report(
669                            "cascadeOutcome",
670                            string_snapshot.cascade_values,
671                            [error.clone()],
672                        ),
673                        shadow_field_report(
674                            "mutationSpanRanges",
675                            string_snapshot.mutation_span_values,
676                            [error.clone()],
677                        ),
678                        shadow_field_report(
679                            "mutationCount",
680                            [string_snapshot.mutation_count.to_string()],
681                            [error.clone()],
682                        ),
683                        shadow_field_report(
684                            "semanticRemovals",
685                            string_snapshot.semantic_removal_values,
686                            [error.clone()],
687                        ),
688                        shadow_field_report(
689                            "cssImportInlines",
690                            string_snapshot.css_import_inline_values,
691                            [error.clone()],
692                        ),
693                        shadow_field_report(
694                            "cssModuleComposesExports",
695                            string_snapshot.css_module_composes_values,
696                            [error.clone()],
697                        ),
698                        shadow_field_report(
699                            "cssModuleEvaluation",
700                            string_snapshot.css_module_evaluation_values,
701                            [error.clone()],
702                        ),
703                        shadow_field_report(
704                            "designTokenRoutes",
705                            string_snapshot.design_token_route_values,
706                            [error.clone()],
707                        ),
708                        shadow_field_report(
709                            "irTransactionCommitCount",
710                            [expected_commit_flag],
711                            [error],
712                        ),
713                    ],
714                )
715            }
716        };
717    let all_fields_match = fields.iter().all(|field| field.matches);
718    let all_typed_path_fields_match = all_fields_match;
719
720    TransformStructuralIrShadowFixtureReportV0 {
721        schema_version: "0",
722        product: "omena-transform-passes.structural-ir-shadow-fixture",
723        fixture: fixture.fixture.to_string(),
724        pass_id: "structural-pipeline",
725        dialect: dialect_label(fixture.dialect),
726        string_path_mutation_count: Some(string_snapshot.mutation_count),
727        ir_path_mutation_count,
728        typed_path_mutation_count: ir_path_mutation_count,
729        ir_path_transaction_commit_count,
730        typed_payload_projections_consumed: 0,
731        typed_payload_memo_hits: 0,
732        fields,
733        all_fields_match,
734        all_typed_path_fields_match,
735    }
736}
737
738fn expected_ir_transaction_commit_flag(mutation_count: usize) -> String {
739    if mutation_count > 0 {
740        "1".to_string()
741    } else {
742        "0".to_string()
743    }
744}
745
746fn string_path_snapshot(
747    fixture: TransformStructuralIrShadowFixtureInputV0<'_>,
748) -> StructuralShadowPathSnapshotV0 {
749    let reachability = reachability_for_fixture(fixture);
750    let module_context = module_context_for_fixture(fixture);
751    let (output_css, mutation_count, semantic_removal_values) = match fixture.pass {
752        TransformPassKind::NestingUnwrap => {
753            let (output_css, mutation_count) =
754                unwrap_css_nesting_with_lexer(fixture.source, fixture.dialect);
755            (output_css, mutation_count, Vec::new())
756        }
757        TransformPassKind::ScopeFlatten => {
758            if string_path_flatten_precondition_allows(
759                fixture,
760                TransformPassKind::ScopeFlatten,
761                &reachability,
762                &module_context,
763            ) {
764                let (output_css, mutation_count) =
765                    flatten_css_scopes_with_lexer(fixture.source, fixture.dialect);
766                (output_css, mutation_count, Vec::new())
767            } else {
768                (fixture.source.to_string(), 0, Vec::new())
769            }
770        }
771        TransformPassKind::LayerFlatten => {
772            if string_path_flatten_precondition_allows(
773                fixture,
774                TransformPassKind::LayerFlatten,
775                &reachability,
776                &module_context,
777            ) {
778                let (output_css, mutation_count) = flatten_css_layers_with_lexer(
779                    fixture.source,
780                    fixture.dialect,
781                    fixture.closed_bundle,
782                );
783                (output_css, mutation_count, Vec::new())
784            } else {
785                (fixture.source.to_string(), 0, Vec::new())
786            }
787        }
788        TransformPassKind::RuleDeduplication => {
789            let (output_css, mutation_count) =
790                dedupe_exact_css_rules_with_lexer(fixture.source, fixture.dialect);
791            (output_css, mutation_count, Vec::new())
792        }
793        TransformPassKind::RuleMerging => {
794            let (output_css, mutation_count) =
795                merge_adjacent_same_selector_css_rules_with_lexer(fixture.source, fixture.dialect);
796            (output_css, mutation_count, Vec::new())
797        }
798        TransformPassKind::SelectorMerging => {
799            let (output_css, mutation_count) =
800                merge_adjacent_same_block_css_selectors_with_lexer(fixture.source, fixture.dialect);
801            (output_css, mutation_count, Vec::new())
802        }
803        TransformPassKind::EmptyRuleRemoval => {
804            let (output_css, mutation_count) =
805                remove_empty_css_rules_with_lexer(fixture.source, fixture.dialect);
806            (output_css, mutation_count, Vec::new())
807        }
808        TransformPassKind::SupportsStaticEval | TransformPassKind::DeadSupportsBranchRemoval => {
809            let (output_css, mutation_count) = evaluate_static_supports_rules_with_lexer(
810                fixture.source,
811                fixture.dialect,
812                StaticSupportsAssumptionV0::ModernBrowser,
813            );
814            (output_css, mutation_count, Vec::new())
815        }
816        TransformPassKind::MediaStaticEval | TransformPassKind::DeadMediaBranchRemoval => {
817            let (output_css, mutation_count) = evaluate_static_media_rules_with_lexer(
818                fixture.source,
819                fixture.dialect,
820                StaticMediaEvaluationOptions::default(),
821            );
822            (output_css, mutation_count, Vec::new())
823        }
824        TransformPassKind::ContainerStaticEval => {
825            let (output_css, mutation_count) =
826                evaluate_static_container_rules_with_lexer(fixture.source, fixture.dialect);
827            (output_css, mutation_count, Vec::new())
828        }
829        TransformPassKind::NativeCssStaticEval => {
830            let (output_css, mutation_count) =
831                evaluate_native_css_static_values_with_plan(fixture.source, fixture.dialect);
832            (output_css, mutation_count, Vec::new())
833        }
834        TransformPassKind::TreeShakeClass => {
835            let (output_css, removals) = tree_shake_css_class_rules_with_lexer(
836                fixture.source,
837                fixture.dialect,
838                reachability.class_names.as_slice(),
839            );
840            let mutation_count = removals.len();
841            (
842                output_css,
843                mutation_count,
844                semantic_removal_values(removals),
845            )
846        }
847        TransformPassKind::TreeShakeKeyframes => {
848            let (output_css, removals) = tree_shake_css_keyframes_with_lexer(
849                fixture.source,
850                fixture.dialect,
851                reachability.keyframe_names.as_slice(),
852                reachability.class_names.as_slice(),
853            );
854            let mutation_count = removals.len();
855            (
856                output_css,
857                mutation_count,
858                semantic_removal_values(removals),
859            )
860        }
861        TransformPassKind::TreeShakeValue => {
862            let (output_css, removals) = tree_shake_css_modules_values_with_lexer(
863                fixture.source,
864                fixture.dialect,
865                reachability.value_names.as_slice(),
866                reachability.keyframe_names.as_slice(),
867                reachability.class_names.as_slice(),
868            );
869            let mutation_count = removals.len();
870            (
871                output_css,
872                mutation_count,
873                semantic_removal_values(removals),
874            )
875        }
876        TransformPassKind::TreeShakeCustomProperty => {
877            let (output_css, removals) = tree_shake_css_custom_properties_with_lexer(
878                fixture.source,
879                fixture.dialect,
880                reachability.custom_property_names.as_slice(),
881                reachability.keyframe_names.as_slice(),
882                reachability.class_names.as_slice(),
883            );
884            let mutation_count = removals.len();
885            (
886                output_css,
887                mutation_count,
888                semantic_removal_values(removals),
889            )
890        }
891        TransformPassKind::ImportInline => {
892            let (output_css, mutation_count) = inline_css_imports_with_lexer(
893                fixture.source,
894                fixture.dialect,
895                module_context.import_inlines.as_slice(),
896            );
897            (output_css, mutation_count, Vec::new())
898        }
899        TransformPassKind::ResolveCssModulesComposes => {
900            let resolutions = css_module_composes_resolutions_for_fixture(fixture, &module_context);
901            let (output_css, mutation_count) = strip_resolved_css_module_composes_with_lexer(
902                fixture.source,
903                fixture.dialect,
904                resolutions.as_slice(),
905            );
906            (output_css, mutation_count, Vec::new())
907        }
908        TransformPassKind::HashCssModuleClassNames => {
909            let (output_css, mutation_count) = rewrite_css_module_class_names_with_lexer(
910                fixture.source,
911                fixture.dialect,
912                module_context.class_name_rewrites.as_slice(),
913            );
914            (output_css, mutation_count, Vec::new())
915        }
916        TransformPassKind::DesignTokenRouting => {
917            let (output_css, mutation_count) = route_design_token_values_with_lexer(
918                fixture.source,
919                fixture.dialect,
920                module_context.design_token_routes.as_slice(),
921            );
922            (output_css, mutation_count, Vec::new())
923        }
924        _ => (fixture.source.to_string(), 0, Vec::new()),
925    };
926    let (output_css, mutation_count, semantic_removal_values) =
927        if string_path_product_runtime_allows(
928            fixture,
929            mutation_count,
930            &reachability,
931            &module_context,
932        ) && string_path_semantic_preservation_allows(
933            fixture,
934            output_css.as_str(),
935            mutation_count,
936            &reachability,
937        ) {
938            (output_css, mutation_count, semantic_removal_values)
939        } else {
940            (fixture.source.to_string(), 0, Vec::new())
941        };
942
943    path_snapshot_from_output(
944        fixture,
945        output_css,
946        mutation_count,
947        semantic_removal_values,
948        module_egress_values_for_fixture(fixture, &module_context),
949        TransformStructuralIrTransactionTelemetryV0::default(),
950    )
951}
952
953fn string_path_semantic_preservation_allows(
954    fixture: TransformStructuralIrShadowFixtureInputV0<'_>,
955    output_css: &str,
956    mutation_count: usize,
957    reachability: &StructuralShadowReachabilityV0,
958) -> bool {
959    if mutation_count == 0 || !semantic_preservation_applies(fixture.pass) {
960        return true;
961    }
962    let input_ir = lower_transform_ir_from_source(
963        fixture.source,
964        fixture.dialect,
965        "omena-transform-passes.structural-shadow.input",
966    );
967    let output_ir = lower_transform_ir_from_source(
968        output_css,
969        fixture.dialect,
970        "omena-transform-passes.structural-shadow.output",
971    );
972    let closed_bundle = fixture_requires_closed_world_bundle(fixture)
973        .then(|| closed_world_bundle_for_shadow_fixture(fixture.fixture, reachability))
974        .transpose()
975        .ok()
976        .flatten();
977    let projection = SemanticObservationProjectionV0::for_pass_input(
978        fixture.pass,
979        &input_ir,
980        fixture.dialect,
981        closed_bundle.as_ref(),
982        None,
983        None,
984    );
985    let input_scope = SemanticObservationScopeV0::for_pass(
986        fixture.pass,
987        fixture.dialect,
988        closed_bundle.as_ref(),
989        None,
990        None,
991        &projection,
992    );
993    let output_scope = input_scope.without_ignored_source_ranges();
994    compare_semantic_observation_for_pass_with_scopes(
995        fixture.pass.id(),
996        &input_ir,
997        &output_ir,
998        input_scope,
999        output_scope,
1000    )
1001    .preserved
1002}
1003
1004fn string_path_product_runtime_allows(
1005    fixture: TransformStructuralIrShadowFixtureInputV0<'_>,
1006    mutation_count: usize,
1007    reachability: &StructuralShadowReachabilityV0,
1008    module_context: &StructuralShadowModuleContextV0,
1009) -> bool {
1010    if mutation_count == 0 {
1011        return true;
1012    }
1013    let context = execution_context_for_fixture(reachability, module_context);
1014    let passes = [fixture.pass];
1015    let summary = if fixture_requires_closed_world_bundle(fixture) {
1016        let Ok(bundle) = closed_world_bundle_for_shadow_fixture(fixture.fixture, reachability)
1017        else {
1018            return false;
1019        };
1020        execute_transform_passes_on_source_with_dialect_context_and_closed_world_bundle(
1021            fixture.source,
1022            fixture.dialect,
1023            &passes,
1024            &context,
1025            &bundle,
1026        )
1027    } else {
1028        execute_transform_passes_on_source_with_dialect_and_context(
1029            fixture.source,
1030            fixture.dialect,
1031            &passes,
1032            &context,
1033        )
1034    };
1035    summary.mutation_count > 0
1036        && !summary
1037            .planned_only_pass_ids
1038            .iter()
1039            .any(|pass_id| *pass_id == fixture.pass.id())
1040}
1041
1042fn string_path_flatten_precondition_allows(
1043    fixture: TransformStructuralIrShadowFixtureInputV0<'_>,
1044    pass: TransformPassKind,
1045    reachability: &StructuralShadowReachabilityV0,
1046    module_context: &StructuralShadowModuleContextV0,
1047) -> bool {
1048    let context = execution_context_for_fixture(reachability, module_context);
1049    let closed_bundle = fixture
1050        .closed_bundle
1051        .then(|| closed_world_bundle_for_shadow_fixture(fixture.fixture, reachability))
1052        .transpose()
1053        .ok()
1054        .flatten();
1055    let obligations = collect_cascade_proof_obligations_for_pass_input(
1056        pass.id(),
1057        Some(pass),
1058        fixture.source,
1059        fixture.dialect,
1060        &context,
1061        closed_bundle.as_ref(),
1062    );
1063    obligations.is_empty()
1064        || obligations.iter().all(|obligation| {
1065            obligation.accepted
1066                && obligation
1067                    .discharge_ledger_lookup
1068                    .as_ref()
1069                    .is_some_and(|lookup| lookup.can_apply_family_stamp())
1070        })
1071}
1072
1073fn string_pipeline_snapshot(
1074    fixture: TransformStructuralIrPipelineShadowFixtureInputV0<'_>,
1075) -> StructuralShadowPathSnapshotV0 {
1076    let mut current_source = fixture.source.to_string();
1077    let mut mutation_count = 0;
1078    let mut semantic_removal_values = Vec::new();
1079    let mut css_import_inline_values = Vec::new();
1080    let mut css_module_composes_values = Vec::new();
1081    let mut css_module_evaluation_values = Vec::new();
1082    let mut design_token_route_values = Vec::new();
1083    let planned_pipeline_pass_ids = product_pipeline_execution_summary(fixture)
1084        .map(|summary| summary.planned_only_pass_ids)
1085        .unwrap_or_default();
1086
1087    for pass in structural_pipeline_passes() {
1088        if planned_pipeline_pass_ids
1089            .iter()
1090            .any(|pass_id| *pass_id == pass.id())
1091        {
1092            continue;
1093        }
1094        let pass_fixture = TransformStructuralIrShadowFixtureInputV0 {
1095            fixture: fixture.fixture,
1096            pass,
1097            dialect: fixture.dialect,
1098            source: current_source.as_str(),
1099            closed_bundle: fixture.closed_bundle,
1100        };
1101        let snapshot = string_path_snapshot(pass_fixture);
1102        mutation_count += snapshot.mutation_count;
1103        semantic_removal_values.extend(snapshot.semantic_removal_values);
1104        css_import_inline_values.extend(snapshot.css_import_inline_values);
1105        css_module_composes_values.extend(snapshot.css_module_composes_values);
1106        css_module_evaluation_values.extend(snapshot.css_module_evaluation_values);
1107        design_token_route_values.extend(snapshot.design_token_route_values);
1108        current_source = snapshot.output_css;
1109    }
1110
1111    path_snapshot_from_output(
1112        TransformStructuralIrShadowFixtureInputV0 {
1113            fixture: fixture.fixture,
1114            pass: TransformPassKind::NestingUnwrap,
1115            dialect: fixture.dialect,
1116            source: fixture.source,
1117            closed_bundle: fixture.closed_bundle,
1118        },
1119        current_source,
1120        mutation_count,
1121        semantic_removal_values,
1122        StructuralShadowModuleEgressValuesV0 {
1123            css_import_inline_values,
1124            css_module_composes_values,
1125            css_module_evaluation_values,
1126            design_token_route_values,
1127        },
1128        TransformStructuralIrTransactionTelemetryV0::default(),
1129    )
1130}
1131
1132fn product_pipeline_execution_summary(
1133    fixture: TransformStructuralIrPipelineShadowFixtureInputV0<'_>,
1134) -> Result<TransformExecutionSummaryV0, String> {
1135    let reachability = reachability_for_pipeline_fixture(fixture);
1136    let module_context = module_context_for_pipeline_fixture(fixture);
1137    let context = TransformExecutionContextV0 {
1138        reachable_class_names: reachability.class_names.clone(),
1139        reachable_keyframe_names: reachability.keyframe_names.clone(),
1140        reachable_value_names: reachability.value_names.clone(),
1141        reachable_custom_property_names: reachability.custom_property_names.clone(),
1142        import_inlines: module_context.import_inlines,
1143        class_name_rewrites: module_context.class_name_rewrites,
1144        css_module_composes_resolutions: module_context.css_module_composes_resolutions,
1145        design_token_routes: module_context.design_token_routes,
1146        ..TransformExecutionContextV0::default()
1147    };
1148    let bundle = closed_world_bundle_for_shadow_fixture(fixture.fixture, &reachability)?;
1149    Ok(
1150        execute_transform_passes_on_source_with_dialect_context_and_closed_world_bundle(
1151            fixture.source,
1152            fixture.dialect,
1153            structural_pipeline_passes().as_slice(),
1154            &context,
1155            &bundle,
1156        ),
1157    )
1158}
1159
1160fn ir_path_snapshot(
1161    fixture: TransformStructuralIrShadowFixtureInputV0<'_>,
1162) -> Result<StructuralShadowPathSnapshotV0, String> {
1163    let reachability = reachability_for_fixture(fixture);
1164    let module_context = module_context_for_fixture(fixture);
1165    let context = execution_context_for_fixture(&reachability, &module_context);
1166    let passes = [fixture.pass];
1167    let summary = if fixture_requires_closed_world_bundle(fixture) {
1168        let bundle = closed_world_bundle_for_shadow_fixture(fixture.fixture, &reachability)?;
1169        execute_transform_passes_on_source_with_dialect_context_and_closed_world_bundle(
1170            fixture.source,
1171            fixture.dialect,
1172            &passes,
1173            &context,
1174            &bundle,
1175        )
1176    } else {
1177        execute_transform_passes_on_source_with_dialect_and_context(
1178            fixture.source,
1179            fixture.dialect,
1180            &passes,
1181            &context,
1182        )
1183    };
1184
1185    Ok(path_snapshot_from_output(
1186        fixture,
1187        summary.output_css,
1188        summary.mutation_count,
1189        public_semantic_removal_values(summary.semantic_removals),
1190        StructuralShadowModuleEgressValuesV0 {
1191            css_import_inline_values: json_values(summary.css_import_inlines.as_slice()),
1192            css_module_composes_values: json_values(summary.css_module_composes_exports.as_slice()),
1193            css_module_evaluation_values: summary
1194                .css_module_evaluation
1195                .as_ref()
1196                .map(|evaluation| serde_json::to_string(evaluation).unwrap_or_default())
1197                .into_iter()
1198                .collect(),
1199            design_token_route_values: json_values(summary.design_token_routes.as_slice()),
1200        },
1201        summary.structural_ir_transaction_telemetry,
1202    ))
1203}
1204
1205fn typed_path_snapshot(
1206    fixture: TransformStructuralIrShadowFixtureInputV0<'_>,
1207) -> Result<StructuralShadowPathSnapshotV0, String> {
1208    if fixture.pass != TransformPassKind::NestingUnwrap {
1209        return ir_path_snapshot(fixture);
1210    }
1211
1212    let mut ir = lower_transform_ir_from_source(
1213        fixture.source,
1214        fixture.dialect,
1215        "omena-transform-passes.typed-payload-shadow",
1216    );
1217    let mut memo = TypedPayloadProjectionMemoV0::default();
1218    let revision = IncrementalRevisionV0 { value: 1 };
1219    let mut typed_payload_ready = false;
1220    for node_id in top_level_style_rule_node_ids(&ir) {
1221        let Some(node_source) = node_source_for_typed_payload(&ir, node_id) else {
1222            continue;
1223        };
1224        if let Some(projection) =
1225            memo.project_style_rule_payload(node_id, node_source, fixture.dialect, revision)
1226        {
1227            typed_payload_ready |= projection_supports_nesting_unwrap(&projection, node_id);
1228            let _ =
1229                memo.project_style_rule_payload(node_id, node_source, fixture.dialect, revision);
1230        }
1231    }
1232
1233    reset_structural_ir_transaction_telemetry();
1234    let mutation_count = if typed_payload_ready {
1235        unwrap_css_nesting_in_ir(&mut ir, fixture.dialect)
1236            .map_err(|error| format!("typed payload structural rewrite failed: {error:?}"))?
1237    } else {
1238        0
1239    };
1240    let telemetry = structural_ir_transaction_telemetry_snapshot();
1241    let output_css = print_transform_ir_css(&ir)
1242        .map_err(|error| format!("typed payload structural print failed: {error:?}"))?;
1243    let mut snapshot = path_snapshot_from_output(
1244        fixture,
1245        output_css,
1246        mutation_count,
1247        Vec::new(),
1248        StructuralShadowModuleEgressValuesV0::default(),
1249        telemetry,
1250    );
1251    snapshot.typed_payload_telemetry = memo.telemetry;
1252    Ok(snapshot)
1253}
1254
1255impl TypedPayloadProjectionMemoV0 {
1256    fn project_style_rule_payload(
1257        &mut self,
1258        node_id: IrNodeIdV0,
1259        source: &str,
1260        dialect: StyleDialect,
1261        revision: IncrementalRevisionV0,
1262    ) -> Option<TypedPayloadProjectionV0> {
1263        let key = TypedPayloadProjectionKeyV0 {
1264            node_id,
1265            revision,
1266            content_signature: typed_payload_content_signature(source),
1267        };
1268        self.telemetry.projections_consumed += 1;
1269        if let Some((_, projection)) = self.entries.iter().find(|(candidate, _)| *candidate == key)
1270        {
1271            self.telemetry.memo_hits += 1;
1272            return Some(projection.clone());
1273        }
1274
1275        let parsed = parse(source, dialect);
1276        let syntax = parsed.syntax();
1277        let has_stylesheet = syntax
1278            .descendants()
1279            .any(|node| node.kind() == SyntaxKind::Stylesheet);
1280        if !has_stylesheet {
1281            return None;
1282        }
1283        let mut typed_node_count = 0usize;
1284        let mut style_rule_count = 0usize;
1285        for node in syntax.descendants() {
1286            if node.kind().is_node() || node.kind().is_bogus() {
1287                typed_node_count += 1;
1288            }
1289            if matches!(node.kind(), SyntaxKind::Rule | SyntaxKind::QualifiedRule) {
1290                style_rule_count += 1;
1291            }
1292        }
1293
1294        let projection = TypedPayloadProjectionV0 {
1295            node_id,
1296            revision,
1297            content_signature: key.content_signature,
1298            typed_node_count,
1299            style_rule_count,
1300        };
1301        self.entries.push((key, projection.clone()));
1302        Some(projection)
1303    }
1304}
1305
1306fn projection_supports_nesting_unwrap(
1307    projection: &TypedPayloadProjectionV0,
1308    node_id: IrNodeIdV0,
1309) -> bool {
1310    projection.node_id == node_id
1311        && projection.revision.value > 0
1312        && projection.content_signature != 0
1313        && projection.typed_node_count > 0
1314        && projection.style_rule_count > 0
1315}
1316
1317fn top_level_style_rule_node_ids(ir: &TransformIrV0) -> Vec<IrNodeIdV0> {
1318    ir.nodes
1319        .iter()
1320        .filter(|node| {
1321            !node.deleted
1322                && node.kind == IrNodeKindV0::StyleRule
1323                && node
1324                    .parent
1325                    .and_then(|parent_id| ir.nodes.get(parent_id.index()))
1326                    .is_none_or(|parent| parent.deleted || parent.kind != IrNodeKindV0::StyleRule)
1327        })
1328        .map(|node| node.node_id)
1329        .collect()
1330}
1331
1332fn node_source_for_typed_payload(ir: &TransformIrV0, node_id: IrNodeIdV0) -> Option<&str> {
1333    let node = ir.nodes.get(node_id.index())?;
1334    ir.source_text()
1335        .get(node.source_span_start..node.source_span_end)
1336}
1337
1338fn typed_payload_content_signature(source: &str) -> u64 {
1339    let mut hasher = DefaultHasher::new();
1340    source.hash(&mut hasher);
1341    hasher.finish()
1342}
1343
1344fn ir_pipeline_snapshot(
1345    fixture: TransformStructuralIrPipelineShadowFixtureInputV0<'_>,
1346) -> Result<StructuralShadowPathSnapshotV0, String> {
1347    let summary = product_pipeline_execution_summary(fixture)?;
1348
1349    Ok(path_snapshot_from_output(
1350        TransformStructuralIrShadowFixtureInputV0 {
1351            fixture: fixture.fixture,
1352            pass: TransformPassKind::NestingUnwrap,
1353            dialect: fixture.dialect,
1354            source: fixture.source,
1355            closed_bundle: fixture.closed_bundle,
1356        },
1357        summary.output_css,
1358        summary.mutation_count,
1359        public_semantic_removal_values(summary.semantic_removals),
1360        StructuralShadowModuleEgressValuesV0 {
1361            css_import_inline_values: json_values(summary.css_import_inlines.as_slice()),
1362            css_module_composes_values: json_values(summary.css_module_composes_exports.as_slice()),
1363            css_module_evaluation_values: summary
1364                .css_module_evaluation
1365                .as_ref()
1366                .map(|evaluation| serde_json::to_string(evaluation).unwrap_or_default())
1367                .into_iter()
1368                .collect(),
1369            design_token_route_values: json_values(summary.design_token_routes.as_slice()),
1370        },
1371        summary.structural_ir_transaction_telemetry,
1372    ))
1373}
1374
1375fn execution_context_for_fixture(
1376    reachability: &StructuralShadowReachabilityV0,
1377    module_context: &StructuralShadowModuleContextV0,
1378) -> TransformExecutionContextV0 {
1379    TransformExecutionContextV0 {
1380        reachable_class_names: reachability.class_names.clone(),
1381        reachable_keyframe_names: reachability.keyframe_names.clone(),
1382        reachable_value_names: reachability.value_names.clone(),
1383        reachable_custom_property_names: reachability.custom_property_names.clone(),
1384        import_inlines: module_context.import_inlines.clone(),
1385        class_name_rewrites: module_context.class_name_rewrites.clone(),
1386        css_module_composes_resolutions: module_context.css_module_composes_resolutions.clone(),
1387        design_token_routes: module_context.design_token_routes.clone(),
1388        ..TransformExecutionContextV0::default()
1389    }
1390}
1391
1392fn fixture_requires_closed_world_bundle(
1393    fixture: TransformStructuralIrShadowFixtureInputV0<'_>,
1394) -> bool {
1395    fixture.closed_bundle
1396        || matches!(
1397            fixture.pass,
1398            TransformPassKind::TreeShakeClass
1399                | TransformPassKind::TreeShakeKeyframes
1400                | TransformPassKind::TreeShakeValue
1401                | TransformPassKind::TreeShakeCustomProperty
1402        )
1403}
1404
1405fn closed_world_bundle_for_shadow_fixture(
1406    fixture_name: &str,
1407    reachability: &StructuralShadowReachabilityV0,
1408) -> Result<ClosedWorldBundleV0, String> {
1409    let instance = ModuleInstanceKeyV0::new(
1410        ModuleIdV0::new(format!("omena-transform-passes.shadow.{fixture_name}")),
1411        ConfigurationHashV0::none(),
1412    );
1413    let mut module = ClosedWorldLinkedModuleV0::new(instance.clone());
1414    for name in &reachability.class_names {
1415        module = module.with_class_name(name.clone());
1416    }
1417    for name in &reachability.keyframe_names {
1418        module = module.with_keyframe_name(name.clone());
1419    }
1420    for name in &reachability.value_names {
1421        module = module.with_value_name(name.clone());
1422    }
1423    for name in &reachability.custom_property_names {
1424        module = module.with_custom_property_name(name.clone());
1425    }
1426
1427    let metadata = ClosedWorldModuleMetadataV0::new(instance.clone())
1428        .with_interface_hash(format!("structural-shadow:{fixture_name}"))
1429        .with_source_precision(ClosedWorldSourcePrecisionSummaryV0 {
1430            conservative_source_count: 1,
1431            ..ClosedWorldSourcePrecisionSummaryV0::default()
1432        })
1433        .with_reachability_evidence(ClosedWorldModuleReachabilityEvidenceV0::Supplied)
1434        .with_composes_scan_state(ClosedWorldComposesScanStateV0::ScannedClosed);
1435    ClosedWorldBundleV0::try_from_linked_modules_with_metadata(
1436        vec![instance],
1437        vec![module],
1438        vec![metadata],
1439    )
1440    .map_err(|err| format!("closed-world bundle construction failed: {err:?}"))
1441}
1442
1443fn structural_pipeline_passes() -> Vec<TransformPassKind> {
1444    let structural_passes = default_transform_pass_descriptors()
1445        .into_iter()
1446        .filter(|descriptor| descriptor.pass_class == TransformPassClassV0::Structural)
1447        .map(|descriptor| descriptor.kind)
1448        .collect::<Vec<_>>();
1449    plan_transform_passes(structural_passes.as_slice())
1450        .ordered_pass_ids
1451        .iter()
1452        .filter_map(|pass_id| transform_pass_kind_from_id(pass_id))
1453        .collect()
1454}
1455
1456fn path_snapshot_from_output(
1457    fixture: TransformStructuralIrShadowFixtureInputV0<'_>,
1458    output_css: String,
1459    mutation_count: usize,
1460    semantic_removal_values: Vec<String>,
1461    module_egress_values: StructuralShadowModuleEgressValuesV0,
1462    ir_transaction_telemetry: TransformStructuralIrTransactionTelemetryV0,
1463) -> StructuralShadowPathSnapshotV0 {
1464    let cascade_values = cascade_values_for_source(fixture, output_css.as_str());
1465    StructuralShadowPathSnapshotV0 {
1466        selector_values: selector_values_for_source(&output_css, fixture.dialect),
1467        declaration_values: declaration_values_for_source(&output_css, fixture.dialect),
1468        cascade_values,
1469        mutation_span_values: mutation_span_values(derive_transform_mutation_spans(
1470            fixture.source,
1471            output_css.as_str(),
1472        )),
1473        output_css,
1474        mutation_count,
1475        semantic_removal_values,
1476        css_import_inline_values: module_egress_values.css_import_inline_values,
1477        css_module_composes_values: module_egress_values.css_module_composes_values,
1478        css_module_evaluation_values: module_egress_values.css_module_evaluation_values,
1479        design_token_route_values: module_egress_values.design_token_route_values,
1480        ir_transaction_telemetry,
1481        typed_payload_telemetry: StructuralShadowTypedPayloadTelemetryV0::default(),
1482    }
1483}
1484
1485fn structural_shadow_fixtures() -> Vec<TransformStructuralIrShadowFixtureInputV0<'static>> {
1486    vec![
1487        TransformStructuralIrShadowFixtureInputV0 {
1488            fixture: "nesting-descendant-and-pseudo",
1489            pass: TransformPassKind::NestingUnwrap,
1490            dialect: StyleDialect::Css,
1491            source: ".card { color: red; & .title { color: blue; } &:hover { color: green; } }",
1492            closed_bundle: false,
1493        },
1494        TransformStructuralIrShadowFixtureInputV0 {
1495            fixture: "nesting-conditional-group",
1496            pass: TransformPassKind::NestingUnwrap,
1497            dialect: StyleDialect::Css,
1498            source: "@media (min-width: 40rem) { .card { color: red; & .title { color: blue; } } }",
1499            closed_bundle: false,
1500        },
1501        TransformStructuralIrShadowFixtureInputV0 {
1502            fixture: "scope-root-flatten",
1503            pass: TransformPassKind::ScopeFlatten,
1504            dialect: StyleDialect::Css,
1505            source: "@scope (:root) { .card { color: red; } }",
1506            closed_bundle: false,
1507        },
1508        TransformStructuralIrShadowFixtureInputV0 {
1509            fixture: "scope-limit-blocked",
1510            pass: TransformPassKind::ScopeFlatten,
1511            dialect: StyleDialect::Css,
1512            source: "@scope (.theme) to (.stop) { .card { color: red; } }",
1513            closed_bundle: false,
1514        },
1515        TransformStructuralIrShadowFixtureInputV0 {
1516            fixture: "layer-closed-bundle-flatten",
1517            pass: TransformPassKind::LayerFlatten,
1518            dialect: StyleDialect::Css,
1519            source: "@layer theme { .card { color: red; } }",
1520            closed_bundle: true,
1521        },
1522        TransformStructuralIrShadowFixtureInputV0 {
1523            fixture: "layer-open-bundle-blocked",
1524            pass: TransformPassKind::LayerFlatten,
1525            dialect: StyleDialect::Css,
1526            source: "@layer theme { .card { color: red; } }",
1527            closed_bundle: false,
1528        },
1529        TransformStructuralIrShadowFixtureInputV0 {
1530            fixture: "rule-dedup-overridden-declarations",
1531            pass: TransformPassKind::RuleDeduplication,
1532            dialect: StyleDialect::Css,
1533            source: ".a { color: red; color: blue; --tone: red; --tone: blue; color: green !important; color: black !important; } :export { token: red; token: blue; }",
1534            closed_bundle: false,
1535        },
1536        TransformStructuralIrShadowFixtureInputV0 {
1537            fixture: "rule-dedup-duplicate-rules",
1538            pass: TransformPassKind::RuleDeduplication,
1539            dialect: StyleDialect::Css,
1540            source: ".a { color: red; } .b { color: red; } .a { color: blue; } .a { color: red; }",
1541            closed_bundle: false,
1542        },
1543        TransformStructuralIrShadowFixtureInputV0 {
1544            fixture: "rule-merge-adjacent-ordinary",
1545            pass: TransformPassKind::RuleMerging,
1546            dialect: StyleDialect::Css,
1547            source: ".a { color: red; } .a { background: blue; } .a { outline: 0; } .b { color: red; }",
1548            closed_bundle: false,
1549        },
1550        TransformStructuralIrShadowFixtureInputV0 {
1551            fixture: "rule-merge-adjacent-conditional-wrappers",
1552            pass: TransformPassKind::RuleMerging,
1553            dialect: StyleDialect::Css,
1554            source: "@media (prefers-color-scheme: dark) { .card { color: white; } } @media (prefers-color-scheme: dark) { .card .title { color: #ddd; } } @supports (display: grid) { .grid { display: grid; } }",
1555            closed_bundle: false,
1556        },
1557        TransformStructuralIrShadowFixtureInputV0 {
1558            fixture: "selector-merge-adjacent-same-block",
1559            pass: TransformPassKind::SelectorMerging,
1560            dialect: StyleDialect::Css,
1561            source: ".a { color: red; } .b { color: red; } .c { color: red; } .d { color: blue; }",
1562            closed_bundle: false,
1563        },
1564        TransformStructuralIrShadowFixtureInputV0 {
1565            fixture: "selector-merge-nested-same-block",
1566            pass: TransformPassKind::SelectorMerging,
1567            dialect: StyleDialect::Css,
1568            source: "@media (min-width: 1px) { .m { color: black; } .n { color: black; } }",
1569            closed_bundle: false,
1570        },
1571        TransformStructuralIrShadowFixtureInputV0 {
1572            fixture: "empty-rule-ordinary-and-group",
1573            pass: TransformPassKind::EmptyRuleRemoval,
1574            dialect: StyleDialect::Css,
1575            source: ".a {} @media (min-width: 1px) { .b {} } @keyframes spin { from {} to { opacity: 1; } }",
1576            closed_bundle: false,
1577        },
1578        TransformStructuralIrShadowFixtureInputV0 {
1579            fixture: "empty-rule-preserves-comment-block",
1580            pass: TransformPassKind::EmptyRuleRemoval,
1581            dialect: StyleDialect::Css,
1582            source: ".a { /* keep */ } .b { color: red; }",
1583            closed_bundle: false,
1584        },
1585        TransformStructuralIrShadowFixtureInputV0 {
1586            fixture: "supports-static-true-unwrap",
1587            pass: TransformPassKind::SupportsStaticEval,
1588            dialect: StyleDialect::Css,
1589            source: "@supports (display: grid) { .a { display: grid; } }",
1590            closed_bundle: false,
1591        },
1592        TransformStructuralIrShadowFixtureInputV0 {
1593            fixture: "supports-static-false-remove",
1594            pass: TransformPassKind::DeadSupportsBranchRemoval,
1595            dialect: StyleDialect::Css,
1596            source: "@supports not (display: grid) { .a { display: grid; } } .b { color: red; }",
1597            closed_bundle: false,
1598        },
1599        TransformStructuralIrShadowFixtureInputV0 {
1600            fixture: "media-static-true-unwrap",
1601            pass: TransformPassKind::MediaStaticEval,
1602            dialect: StyleDialect::Css,
1603            source: "@media all { .a { color: red; } } @media (min-width: 40PX) { .b { color: blue; } }",
1604            closed_bundle: false,
1605        },
1606        TransformStructuralIrShadowFixtureInputV0 {
1607            fixture: "media-static-false-remove",
1608            pass: TransformPassKind::DeadMediaBranchRemoval,
1609            dialect: StyleDialect::Css,
1610            source: "@media not all { .a { color: red; } } .b { color: blue; }",
1611            closed_bundle: false,
1612        },
1613        TransformStructuralIrShadowFixtureInputV0 {
1614            fixture: "container-static-false-remove",
1615            pass: TransformPassKind::ContainerStaticEval,
1616            dialect: StyleDialect::Css,
1617            source: "@container (max-width: -1px) { .a { color: red; } } .b { color: blue; }",
1618            closed_bundle: false,
1619        },
1620        TransformStructuralIrShadowFixtureInputV0 {
1621            fixture: "native-css-static-when-fold",
1622            pass: TransformPassKind::NativeCssStaticEval,
1623            dialect: StyleDialect::Css,
1624            source: "@when supports(display: grid) { .grid { display: grid; } } @else { .fallback { display: block; } }",
1625            closed_bundle: false,
1626        },
1627        TransformStructuralIrShadowFixtureInputV0 {
1628            fixture: "tree-shake-class-reachable-owner",
1629            pass: TransformPassKind::TreeShakeClass,
1630            dialect: StyleDialect::Css,
1631            source: ".used { color: green; } .unused, .also-unused { color: red; } :global(.external) { color: black; }",
1632            closed_bundle: false,
1633        },
1634        TransformStructuralIrShadowFixtureInputV0 {
1635            fixture: "tree-shake-keyframes-referenced-animation",
1636            pass: TransformPassKind::TreeShakeKeyframes,
1637            dialect: StyleDialect::Css,
1638            source: "@keyframes spin { to { opacity: 1; } } @keyframes fade { to { opacity: 0; } } .used { animation: spin 1s; }",
1639            closed_bundle: false,
1640        },
1641        TransformStructuralIrShadowFixtureInputV0 {
1642            fixture: "tree-shake-css-modules-values",
1643            pass: TransformPassKind::TreeShakeValue,
1644            dialect: StyleDialect::Css,
1645            source: "@value keep: 1px; @value dead: 2px; @value imported, unused from \"./tokens.css\"; :export { keepExport: keep; deadExport: dead; }",
1646            closed_bundle: false,
1647        },
1648        TransformStructuralIrShadowFixtureInputV0 {
1649            fixture: "tree-shake-custom-properties",
1650            pass: TransformPassKind::TreeShakeCustomProperty,
1651            dialect: StyleDialect::Css,
1652            source: "@property --dead-reg { syntax: \"<color>\"; inherits: false; initial-value: red; } .used { color: var(--keep); --keep: green; --dead: red; } :export { keepExport: var(--keep); deadExport: var(--dead); }",
1653            closed_bundle: false,
1654        },
1655        TransformStructuralIrShadowFixtureInputV0 {
1656            fixture: "module-import-inline",
1657            pass: TransformPassKind::ImportInline,
1658            dialect: StyleDialect::Css,
1659            source: "@import \"./tokens.css\"; .used { color: var(--brand); }",
1660            closed_bundle: false,
1661        },
1662        TransformStructuralIrShadowFixtureInputV0 {
1663            fixture: "module-composes-resolution",
1664            pass: TransformPassKind::ResolveCssModulesComposes,
1665            dialect: StyleDialect::Css,
1666            source: ".button { composes: base utility; color: red; } .base { color: blue; } .utility { color: green; }",
1667            closed_bundle: false,
1668        },
1669        TransformStructuralIrShadowFixtureInputV0 {
1670            fixture: "module-class-hashing",
1671            pass: TransformPassKind::HashCssModuleClassNames,
1672            dialect: StyleDialect::Css,
1673            source: ".button { composes: base utility global(reset); color: red; } :local { .button { color: blue; } } @supports selector(.button) { .button { color: green; } }",
1674            closed_bundle: false,
1675        },
1676        TransformStructuralIrShadowFixtureInputV0 {
1677            fixture: "module-design-token-routing",
1678            pass: TransformPassKind::DesignTokenRouting,
1679            dialect: StyleDialect::Css,
1680            source: "@media (min-width: var(--pkg-breakpoint)) { .button { color: var(--pkg-brand); } }",
1681            closed_bundle: false,
1682        },
1683    ]
1684}
1685
1686fn compared_pass_ids() -> Vec<&'static str> {
1687    let mut pass_ids = default_transform_pass_descriptors()
1688        .into_iter()
1689        .filter(|descriptor| descriptor.pass_class == TransformPassClassV0::Structural)
1690        .map(|descriptor| descriptor.id)
1691        .collect::<Vec<_>>();
1692    pass_ids.sort_unstable();
1693    pass_ids
1694}
1695
1696fn reachability_for_fixture(
1697    fixture: TransformStructuralIrShadowFixtureInputV0<'_>,
1698) -> StructuralShadowReachabilityV0 {
1699    match fixture.fixture {
1700        "tree-shake-class-reachable-owner" => StructuralShadowReachabilityV0 {
1701            class_names: string_vec(["used"]),
1702            keyframe_names: Vec::new(),
1703            value_names: Vec::new(),
1704            custom_property_names: Vec::new(),
1705        },
1706        "tree-shake-keyframes-referenced-animation" => StructuralShadowReachabilityV0 {
1707            class_names: string_vec(["used"]),
1708            keyframe_names: Vec::new(),
1709            value_names: Vec::new(),
1710            custom_property_names: Vec::new(),
1711        },
1712        "tree-shake-css-modules-values" => StructuralShadowReachabilityV0 {
1713            class_names: Vec::new(),
1714            keyframe_names: Vec::new(),
1715            value_names: string_vec(["keepExport"]),
1716            custom_property_names: Vec::new(),
1717        },
1718        "tree-shake-custom-properties" => StructuralShadowReachabilityV0 {
1719            class_names: string_vec(["used"]),
1720            keyframe_names: Vec::new(),
1721            value_names: Vec::new(),
1722            custom_property_names: string_vec(["keepExport"]),
1723        },
1724        "pipeline-module-structural-interpass" => StructuralShadowReachabilityV0 {
1725            class_names: string_vec(["card", "card__icon", "base", "utility"]),
1726            keyframe_names: string_vec(["spin"]),
1727            value_names: Vec::new(),
1728            custom_property_names: string_vec(["pkg-brand", "local-tone"]),
1729        },
1730        "pipeline-rule-structural-interpass" => StructuralShadowReachabilityV0 {
1731            class_names: string_vec(["card", "card__icon", "dup", "grid", "media"]),
1732            keyframe_names: Vec::new(),
1733            value_names: Vec::new(),
1734            custom_property_names: Vec::new(),
1735        },
1736        _ => StructuralShadowReachabilityV0 {
1737            class_names: Vec::new(),
1738            keyframe_names: Vec::new(),
1739            value_names: Vec::new(),
1740            custom_property_names: Vec::new(),
1741        },
1742    }
1743}
1744
1745fn reachability_for_pipeline_fixture(
1746    fixture: TransformStructuralIrPipelineShadowFixtureInputV0<'_>,
1747) -> StructuralShadowReachabilityV0 {
1748    reachability_for_fixture(TransformStructuralIrShadowFixtureInputV0 {
1749        fixture: fixture.fixture,
1750        pass: TransformPassKind::TreeShakeClass,
1751        dialect: fixture.dialect,
1752        source: fixture.source,
1753        closed_bundle: fixture.closed_bundle,
1754    })
1755}
1756
1757fn module_context_for_pipeline_fixture(
1758    fixture: TransformStructuralIrPipelineShadowFixtureInputV0<'_>,
1759) -> StructuralShadowModuleContextV0 {
1760    module_context_for_fixture(TransformStructuralIrShadowFixtureInputV0 {
1761        fixture: fixture.fixture,
1762        pass: TransformPassKind::ImportInline,
1763        dialect: fixture.dialect,
1764        source: fixture.source,
1765        closed_bundle: fixture.closed_bundle,
1766    })
1767}
1768
1769fn module_context_for_fixture(
1770    fixture: TransformStructuralIrShadowFixtureInputV0<'_>,
1771) -> StructuralShadowModuleContextV0 {
1772    match fixture.fixture {
1773        "module-import-inline" => StructuralShadowModuleContextV0 {
1774            import_inlines: vec![TransformImportInlineV0 {
1775                import_source: "./tokens.css".to_string(),
1776                replacement_css: ":root { --brand: red; }".to_string(),
1777            }],
1778            ..StructuralShadowModuleContextV0::default()
1779        },
1780        "module-class-hashing" => StructuralShadowModuleContextV0 {
1781            class_name_rewrites: vec![
1782                TransformClassNameRewriteV0 {
1783                    original_name: "button".to_string(),
1784                    rewritten_name: "_button_hash".to_string(),
1785                },
1786                TransformClassNameRewriteV0 {
1787                    original_name: "base".to_string(),
1788                    rewritten_name: "_base_hash".to_string(),
1789                },
1790                TransformClassNameRewriteV0 {
1791                    original_name: "utility".to_string(),
1792                    rewritten_name: "_utility_hash".to_string(),
1793                },
1794            ],
1795            ..StructuralShadowModuleContextV0::default()
1796        },
1797        "module-design-token-routing" => StructuralShadowModuleContextV0 {
1798            design_token_routes: vec![
1799                TransformDesignTokenRouteV0 {
1800                    token_name: "--pkg-breakpoint".to_string(),
1801                    routed_value: "40rem".to_string(),
1802                },
1803                TransformDesignTokenRouteV0 {
1804                    token_name: "--pkg-brand".to_string(),
1805                    routed_value: "#123456".to_string(),
1806                },
1807            ],
1808            ..StructuralShadowModuleContextV0::default()
1809        },
1810        "pipeline-module-structural-interpass" => StructuralShadowModuleContextV0 {
1811            import_inlines: vec![TransformImportInlineV0 {
1812                import_source: "./tokens.css".to_string(),
1813                replacement_css: ":root { --pkg-brand: #123456; }".to_string(),
1814            }],
1815            class_name_rewrites: vec![
1816                TransformClassNameRewriteV0 {
1817                    original_name: "card".to_string(),
1818                    rewritten_name: "_card_hash".to_string(),
1819                },
1820                TransformClassNameRewriteV0 {
1821                    original_name: "card__icon".to_string(),
1822                    rewritten_name: "_card__icon_hash".to_string(),
1823                },
1824                TransformClassNameRewriteV0 {
1825                    original_name: "base".to_string(),
1826                    rewritten_name: "_base_hash".to_string(),
1827                },
1828                TransformClassNameRewriteV0 {
1829                    original_name: "utility".to_string(),
1830                    rewritten_name: "_utility_hash".to_string(),
1831                },
1832            ],
1833            css_module_composes_resolutions: vec![TransformCssModuleComposesResolutionV0 {
1834                local_class_name: "card".to_string(),
1835                exported_class_names: vec!["base".to_string(), "utility".to_string()],
1836            }],
1837            design_token_routes: vec![TransformDesignTokenRouteV0 {
1838                token_name: "--pkg-brand".to_string(),
1839                routed_value: "#123456".to_string(),
1840            }],
1841        },
1842        _ => StructuralShadowModuleContextV0::default(),
1843    }
1844}
1845
1846fn css_module_composes_resolutions_for_fixture(
1847    fixture: TransformStructuralIrShadowFixtureInputV0<'_>,
1848    module_context: &StructuralShadowModuleContextV0,
1849) -> Vec<TransformCssModuleComposesResolutionV0> {
1850    let mut merged =
1851        local_css_module_composes_resolutions_with_lexer(fixture.source, fixture.dialect);
1852    for resolution in &module_context.css_module_composes_resolutions {
1853        let Some(existing) = merged
1854            .iter_mut()
1855            .find(|existing| existing.local_class_name == resolution.local_class_name)
1856        else {
1857            merged.push(resolution.clone());
1858            continue;
1859        };
1860        for exported_class_name in &resolution.exported_class_names {
1861            if !existing
1862                .exported_class_names
1863                .iter()
1864                .any(|existing| existing == exported_class_name)
1865            {
1866                existing
1867                    .exported_class_names
1868                    .push(exported_class_name.clone());
1869            }
1870        }
1871    }
1872    merged.sort_by(|left, right| left.local_class_name.cmp(&right.local_class_name));
1873    merged
1874}
1875
1876fn module_egress_values_for_fixture(
1877    fixture: TransformStructuralIrShadowFixtureInputV0<'_>,
1878    module_context: &StructuralShadowModuleContextV0,
1879) -> StructuralShadowModuleEgressValuesV0 {
1880    match fixture.pass {
1881        TransformPassKind::ImportInline => StructuralShadowModuleEgressValuesV0 {
1882            css_import_inline_values: json_values(module_context.import_inlines.as_slice()),
1883            ..StructuralShadowModuleEgressValuesV0::default()
1884        },
1885        TransformPassKind::ResolveCssModulesComposes => StructuralShadowModuleEgressValuesV0 {
1886            css_module_composes_values: json_values(
1887                css_module_composes_resolutions_for_fixture(fixture, module_context).as_slice(),
1888            ),
1889            ..StructuralShadowModuleEgressValuesV0::default()
1890        },
1891        TransformPassKind::DesignTokenRouting => StructuralShadowModuleEgressValuesV0 {
1892            design_token_route_values: json_values(module_context.design_token_routes.as_slice()),
1893            ..StructuralShadowModuleEgressValuesV0::default()
1894        },
1895        _ => StructuralShadowModuleEgressValuesV0::default(),
1896    }
1897}
1898
1899fn json_values<T: serde::Serialize>(values: &[T]) -> Vec<String> {
1900    values
1901        .iter()
1902        .map(|value| serde_json::to_string(value).unwrap_or_default())
1903        .collect()
1904}
1905
1906fn selector_values_for_source(source: &str, dialect: StyleDialect) -> Vec<String> {
1907    let summary = summarize_omena_parser_style_facts(source, dialect);
1908    sorted_unique(
1909        summary
1910            .class_selector_names
1911            .into_iter()
1912            .map(|name| format!("class:{name}"))
1913            .chain(
1914                summary
1915                    .id_selector_names
1916                    .into_iter()
1917                    .map(|name| format!("id:{name}")),
1918            )
1919            .chain(
1920                summary
1921                    .placeholder_selector_names
1922                    .into_iter()
1923                    .map(|name| format!("placeholder:{name}")),
1924            )
1925            .collect::<Vec<_>>(),
1926    )
1927}
1928
1929fn declaration_values_for_source(source: &str, dialect: StyleDialect) -> Vec<String> {
1930    let summary = summarize_omena_parser_parity_lite(source, dialect);
1931    sorted_unique(vec![
1932        format!("ruleCount:{}", summary.rule_count),
1933        format!("declarationCount:{}", summary.declaration_count),
1934        format!(
1935            "declarationKindCounts:{}",
1936            serde_json::to_string(&summary.declaration_kind_counts).unwrap_or_default()
1937        ),
1938        format!(
1939            "atRuleKindCounts:{}",
1940            serde_json::to_string(&summary.at_rule_kind_counts).unwrap_or_default()
1941        ),
1942    ])
1943}
1944
1945fn cascade_values_for_source(
1946    fixture: TransformStructuralIrShadowFixtureInputV0<'_>,
1947    source: &str,
1948) -> Vec<String> {
1949    match fixture.pass {
1950        TransformPassKind::ScopeFlatten => sorted_unique(
1951            collect_scope_flatten_proof_candidates_with_lexer(source, fixture.dialect)
1952                .into_iter()
1953                .map(|candidate| {
1954                    format!(
1955                        "scope:{}..{}:accepted={}:blocked={:?}:root={}:witness={}",
1956                        candidate.source_span_start,
1957                        candidate.source_span_end,
1958                        candidate.proof.accepted,
1959                        candidate.proof.blocked_reason,
1960                        candidate.proof.root_selector,
1961                        candidate.proof.cascade_safe_witness
1962                    )
1963                })
1964                .collect::<Vec<_>>(),
1965        ),
1966        TransformPassKind::LayerFlatten => sorted_unique(
1967            collect_layer_flatten_proof_candidates_with_lexer(
1968                source,
1969                fixture.dialect,
1970                fixture.closed_bundle,
1971            )
1972            .into_iter()
1973            .map(|candidate| {
1974                format!(
1975                    "layer:{}..{}:accepted={}:blocked={:?}:name={:?}:witness={}",
1976                    candidate.source_span_start,
1977                    candidate.source_span_end,
1978                    candidate.proof.accepted,
1979                    candidate.proof.blocked_reason,
1980                    candidate.proof.layer_name,
1981                    candidate.proof.cascade_safe_witness
1982                )
1983            })
1984            .collect::<Vec<_>>(),
1985        ),
1986        _ => Vec::new(),
1987    }
1988}
1989
1990fn mutation_span_values(spans: Vec<TransformProvenanceMutationSpanV0>) -> Vec<String> {
1991    spans
1992        .into_iter()
1993        .map(|span| {
1994            format!(
1995                "{}..{}=>{}..{}",
1996                span.source_span_start,
1997                span.source_span_end,
1998                span.generated_span_start,
1999                span.generated_span_end
2000            )
2001        })
2002        .collect()
2003}
2004
2005fn semantic_removal_values(removals: Vec<TransformSemanticRemovalCandidate>) -> Vec<String> {
2006    removals
2007        .into_iter()
2008        .map(|removal| {
2009            format!(
2010                "{}:{}:{}..{}:{}",
2011                removal.symbol_kind,
2012                removal.name,
2013                removal.source_span_start,
2014                removal.source_span_end,
2015                removal.reason
2016            )
2017        })
2018        .collect()
2019}
2020
2021fn public_semantic_removal_values(removals: Vec<TransformSemanticRemovalV0>) -> Vec<String> {
2022    removals
2023        .into_iter()
2024        .map(|removal| {
2025            format!(
2026                "{}:{}:{}..{}:{}",
2027                removal.symbol_kind,
2028                removal.name,
2029                removal.source_span_start,
2030                removal.source_span_end,
2031                removal.reason
2032            )
2033        })
2034        .collect()
2035}
2036
2037fn string_vec<const N: usize>(values: [&str; N]) -> Vec<String> {
2038    values.into_iter().map(str::to_string).collect()
2039}
2040
2041fn shadow_field_report(
2042    field: &'static str,
2043    string_path_values: impl IntoIterator<Item = String>,
2044    ir_path_values: impl IntoIterator<Item = String>,
2045) -> TransformStructuralIrShadowFieldReportV0 {
2046    let ir_path_values = sorted_unique(ir_path_values);
2047    shadow_field_report_with_typed(
2048        field,
2049        string_path_values,
2050        ir_path_values.clone(),
2051        ir_path_values,
2052    )
2053}
2054
2055fn shadow_field_report_with_typed(
2056    field: &'static str,
2057    string_path_values: impl IntoIterator<Item = String>,
2058    ir_path_values: impl IntoIterator<Item = String>,
2059    typed_path_values: impl IntoIterator<Item = String>,
2060) -> TransformStructuralIrShadowFieldReportV0 {
2061    let string_path_values = sorted_unique(string_path_values);
2062    let ir_path_values = sorted_unique(ir_path_values);
2063    let typed_path_values = sorted_unique(typed_path_values);
2064    let matches = string_path_values == ir_path_values && string_path_values == typed_path_values;
2065    TransformStructuralIrShadowFieldReportV0 {
2066        field,
2067        string_path_values,
2068        ir_path_values,
2069        typed_path_values,
2070        matches,
2071    }
2072}
2073
2074fn sorted_unique(values: impl IntoIterator<Item = String>) -> Vec<String> {
2075    values
2076        .into_iter()
2077        .collect::<BTreeSet<_>>()
2078        .into_iter()
2079        .collect()
2080}
2081
2082fn dialect_label(dialect: StyleDialect) -> &'static str {
2083    match dialect {
2084        StyleDialect::Css => "css",
2085        StyleDialect::Scss => "scss",
2086        StyleDialect::Sass => "sass",
2087        StyleDialect::Less => "less",
2088    }
2089}
2090
2091#[cfg(test)]
2092mod tests {
2093    use std::collections::BTreeMap;
2094
2095    use serde::Deserialize;
2096
2097    use super::*;
2098
2099    const STRING_AUTHORITY_STRUCTURAL_GOLDEN: &str =
2100        include_str!("../../data/string-authority-structural-golden-v0.json");
2101
2102    #[derive(Debug, Deserialize)]
2103    #[serde(rename_all = "camelCase")]
2104    struct StringAuthorityStructuralGoldenEntryV0 {
2105        fixture: String,
2106        pass_id: String,
2107        dialect: String,
2108        output_css: String,
2109    }
2110
2111    #[test]
2112    fn structural_ir_output_matches_string_authority_golden() -> Result<(), String> {
2113        let entries = serde_json::from_str::<Vec<StringAuthorityStructuralGoldenEntryV0>>(
2114            STRING_AUTHORITY_STRUCTURAL_GOLDEN,
2115        )
2116        .map_err(|err| format!("String authority structural golden should parse: {err:?}"))?;
2117        let mut entries_by_key = BTreeMap::new();
2118        for entry in entries {
2119            let key = (entry.fixture.clone(), entry.pass_id.clone());
2120            if entries_by_key.insert(key.clone(), entry).is_some() {
2121                return Err(format!(
2122                    "String authority structural golden contains duplicate fixture/pass key {key:?}"
2123                ));
2124            }
2125        }
2126
2127        let fixtures = structural_shadow_fixtures();
2128        assert_eq!(
2129            entries_by_key.len(),
2130            fixtures.len(),
2131            "String authority structural golden must cover every structural shadow fixture"
2132        );
2133
2134        for fixture in fixtures {
2135            let key = (fixture.fixture.to_string(), fixture.pass.id().to_string());
2136            let Some(golden) = entries_by_key.remove(&key) else {
2137                return Err(format!(
2138                    "String authority structural golden is missing fixture/pass key {key:?}"
2139                ));
2140            };
2141            assert_eq!(golden.dialect, dialect_label(fixture.dialect));
2142            let snapshot = ir_path_snapshot(fixture)?;
2143            assert_eq!(
2144                snapshot.output_css, golden.output_css,
2145                "IR output drifted from String authority golden for {} / {}",
2146                fixture.fixture, golden.pass_id
2147            );
2148        }
2149
2150        assert!(
2151            entries_by_key.is_empty(),
2152            "String authority structural golden contains stale keys: {:?}",
2153            entries_by_key.keys().collect::<Vec<_>>()
2154        );
2155        Ok(())
2156    }
2157
2158    #[test]
2159    fn typed_payload_projection_memo_distinguishes_same_node_after_source_change() {
2160        let mut memo = TypedPayloadProjectionMemoV0::default();
2161        let node_id = IrNodeIdV0(7);
2162        let revision = IncrementalRevisionV0 { value: 1 };
2163
2164        let first = memo.project_style_rule_payload(
2165            node_id,
2166            ".card { &__icon { color: red; } }",
2167            StyleDialect::Scss,
2168            revision,
2169        );
2170        let second = memo.project_style_rule_payload(
2171            node_id,
2172            ".card { &__icon { color: red; } }",
2173            StyleDialect::Scss,
2174            revision,
2175        );
2176        let changed = memo.project_style_rule_payload(
2177            node_id,
2178            ".card { &__icon { color: blue; } }",
2179            StyleDialect::Scss,
2180            revision,
2181        );
2182
2183        assert!(first.is_some());
2184        assert!(second.is_some());
2185        assert!(changed.is_some());
2186        assert_eq!(memo.entries.len(), 2);
2187        assert_eq!(memo.telemetry.projections_consumed, 3);
2188        assert_eq!(memo.telemetry.memo_hits, 1);
2189    }
2190}