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