1use serde::{Deserialize, Serialize};
2use std::{fmt, path::PathBuf};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
6pub enum Safety {
7 Safe,
8 Review,
9 Unsafe,
10 Unsupported,
11 NoOp,
12}
13
14impl Safety {
15 pub const fn label(self) -> &'static str {
16 match self {
17 Self::Safe => "SAFE",
18 Self::Review => "REVIEW",
19 Self::Unsafe => "UNSAFE",
20 Self::Unsupported => "UNSUPPORTED",
21 Self::NoOp => "NO_OP",
22 }
23 }
24}
25
26impl fmt::Display for Safety {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 f.write_str(self.label())
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "kebab-case")]
34pub enum SafetyLevel {
35 AnalysisOnly,
36 FormattingOnly,
37 ProvenLocalRefactor,
38 SemanticReview,
39 Architectural,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[serde(rename_all = "kebab-case")]
44pub enum RuleId {
45 NestPseudoClass,
46 NestPseudoElement,
47 NestAttribute,
48 NestCompound,
49 NestDescendant,
50 NestCombinator,
51 NestMedia,
52 NestSupports,
53 NestContainer,
54 NestStartingStyle,
55 FactorSelectorList,
56 ConsolidateNot,
57 ModernizeIs,
58 ModernizeWhere,
59 ModernizeMediaRange,
60 MergeSameNamedLayer,
61 MergeAdjacentMedia,
62 MergeAdjacentSupports,
63 MergeAdjacentContainer,
64 MergeIdenticalScope,
65 MergeIdenticalStartingStyle,
66 MergeAdjacentIdenticalSelector,
67 MergeIdenticalRuleBodies,
68 FactorIdenticalStatesWithIs,
69 GatherRelatedSelectorRules,
70 NestLayerBySelector,
71 PruneOverriddenDeclarations,
72}
73
74impl RuleId {
75 pub const ALL: [RuleId; 27] = [
76 RuleId::NestPseudoClass,
77 RuleId::NestPseudoElement,
78 RuleId::NestAttribute,
79 RuleId::NestCompound,
80 RuleId::NestDescendant,
81 RuleId::NestCombinator,
82 RuleId::NestMedia,
83 RuleId::NestSupports,
84 RuleId::NestContainer,
85 RuleId::NestStartingStyle,
86 RuleId::FactorSelectorList,
87 RuleId::ConsolidateNot,
88 RuleId::ModernizeIs,
89 RuleId::ModernizeWhere,
90 RuleId::ModernizeMediaRange,
91 RuleId::MergeSameNamedLayer,
92 RuleId::MergeAdjacentMedia,
93 RuleId::MergeAdjacentSupports,
94 RuleId::MergeAdjacentContainer,
95 RuleId::MergeIdenticalScope,
96 RuleId::MergeIdenticalStartingStyle,
97 RuleId::MergeAdjacentIdenticalSelector,
98 RuleId::MergeIdenticalRuleBodies,
99 RuleId::FactorIdenticalStatesWithIs,
100 RuleId::GatherRelatedSelectorRules,
101 RuleId::NestLayerBySelector,
102 RuleId::PruneOverriddenDeclarations,
103 ];
104
105 pub const fn as_str(self) -> &'static str {
106 match self {
107 Self::NestPseudoClass => "nest-pseudo-class",
108 Self::NestPseudoElement => "nest-pseudo-element",
109 Self::NestAttribute => "nest-attribute",
110 Self::NestCompound => "nest-compound",
111 Self::NestDescendant => "nest-descendant",
112 Self::NestCombinator => "nest-combinator",
113 Self::NestMedia => "nest-media",
114 Self::NestSupports => "nest-supports",
115 Self::NestContainer => "nest-container",
116 Self::NestStartingStyle => "nest-starting-style",
117 Self::FactorSelectorList => "factor-selector-list",
118 Self::ConsolidateNot => "consolidate-not",
119 Self::ModernizeIs => "modernize-is",
120 Self::ModernizeWhere => "modernize-where",
121 Self::ModernizeMediaRange => "modernize-media-range-syntax",
122 Self::MergeSameNamedLayer => "merge-same-named-layer",
123 Self::MergeAdjacentMedia => "merge-adjacent-media",
124 Self::MergeAdjacentSupports => "merge-adjacent-supports",
125 Self::MergeAdjacentContainer => "merge-adjacent-container",
126 Self::MergeIdenticalScope => "merge-identical-scope",
127 Self::MergeIdenticalStartingStyle => "merge-identical-starting-style",
128 Self::MergeAdjacentIdenticalSelector => "merge-adjacent-identical-selector",
129 Self::MergeIdenticalRuleBodies => "merge-identical-rule-bodies",
130 Self::FactorIdenticalStatesWithIs => "factor-identical-states-with-is",
131 Self::GatherRelatedSelectorRules => "gather-related-selector-rules",
132 Self::NestLayerBySelector => "nest-layer-by-selector",
133 Self::PruneOverriddenDeclarations => "prune-overridden-declarations",
134 }
135 }
136}
137
138impl fmt::Display for RuleId {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 f.write_str(self.as_str())
141 }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
145#[serde(rename_all = "kebab-case")]
146pub enum RuleSection {
147 Modernize,
148 Refactor,
149}
150
151impl RuleSection {
152 pub const ALL: [RuleSection; 2] = [RuleSection::Modernize, RuleSection::Refactor];
153
154 pub const fn label(self) -> &'static str {
155 match self {
156 Self::Modernize => "MODERNIZE (Native Nesting, Range Syntax & Selectors)",
157 Self::Refactor => "REFACTOR (Consolidation, Deduplication & Structural Cleanup)",
158 }
159 }
160}
161
162#[derive(Debug, Clone)]
163pub struct RuleDefinition {
164 pub id: RuleId,
165 pub section: RuleSection,
166 pub title: &'static str,
167 pub category: &'static str,
168 pub safety_level: SafetyLevel,
169 pub description: &'static str,
170}
171
172pub fn rule_definitions() -> Vec<RuleDefinition> {
173 vec![
174 RuleDefinition {
175 id: RuleId::NestPseudoClass,
176 section: RuleSection::Modernize,
177 title: "Nest pseudo-classes",
178 category: "Nesting",
179 safety_level: SafetyLevel::ProvenLocalRefactor,
180 description: "Nest adjacent same-parent pseudo-class rules such as .button:hover -> &:hover.",
181 },
182 RuleDefinition {
183 id: RuleId::NestPseudoElement,
184 section: RuleSection::Modernize,
185 title: "Nest pseudo-elements",
186 category: "Nesting",
187 safety_level: SafetyLevel::ProvenLocalRefactor,
188 description: "Nest adjacent same-parent pseudo-elements such as .card::before -> &::before.",
189 },
190 RuleDefinition {
191 id: RuleId::NestAttribute,
192 section: RuleSection::Modernize,
193 title: "Nest attribute states",
194 category: "Nesting",
195 safety_level: SafetyLevel::ProvenLocalRefactor,
196 description: "Nest adjacent attribute states such as .button[disabled] -> &[disabled].",
197 },
198 RuleDefinition {
199 id: RuleId::NestCompound,
200 section: RuleSection::Modernize,
201 title: "Nest compound states",
202 category: "Nesting",
203 safety_level: SafetyLevel::ProvenLocalRefactor,
204 description: "Nest adjacent compound states such as .item.active -> &.active.",
205 },
206 RuleDefinition {
207 id: RuleId::NestDescendant,
208 section: RuleSection::Modernize,
209 title: "Nest descendants",
210 category: "Nesting",
211 safety_level: SafetyLevel::ProvenLocalRefactor,
212 description: "Nest adjacent descendant selectors (.card .title -> .title) with exact relationship proof.",
213 },
214 RuleDefinition {
215 id: RuleId::NestCombinator,
216 section: RuleSection::Modernize,
217 title: "Nest combinators",
218 category: "Nesting",
219 safety_level: SafetyLevel::ProvenLocalRefactor,
220 description: "Nest adjacent child and sibling combinators (> + ~) under their exact parent selector.",
221 },
222 RuleDefinition {
223 id: RuleId::NestMedia,
224 section: RuleSection::Modernize,
225 title: "Nest local @media",
226 category: "At-rules",
227 safety_level: SafetyLevel::ProvenLocalRefactor,
228 description: "Nest an immediately-following @media block containing matching selector rules.",
229 },
230 RuleDefinition {
231 id: RuleId::NestSupports,
232 section: RuleSection::Modernize,
233 title: "Nest local @supports",
234 category: "At-rules",
235 safety_level: SafetyLevel::ProvenLocalRefactor,
236 description: "Nest an immediately-following @supports block containing matching selector rules.",
237 },
238 RuleDefinition {
239 id: RuleId::NestContainer,
240 section: RuleSection::Modernize,
241 title: "Nest local @container",
242 category: "At-rules",
243 safety_level: SafetyLevel::SemanticReview,
244 description: "Nest an immediately-following @container block for matching selector rules.",
245 },
246 RuleDefinition {
247 id: RuleId::NestStartingStyle,
248 section: RuleSection::Modernize,
249 title: "Nest @starting-style",
250 category: "At-rules",
251 safety_level: SafetyLevel::ProvenLocalRefactor,
252 description: "Nest an immediately-following @starting-style block for the matching parent selector.",
253 },
254 RuleDefinition {
255 id: RuleId::FactorSelectorList,
256 section: RuleSection::Modernize,
257 title: "Factor selector lists",
258 category: "Nesting",
259 safety_level: SafetyLevel::ProvenLocalRefactor,
260 description: "Factor comma-separated selectors sharing a common base (.marker, .marker::before -> .marker { &, &::before }).",
261 },
262 RuleDefinition {
263 id: RuleId::ConsolidateNot,
264 section: RuleSection::Modernize,
265 title: "Consolidate :not() selectors",
266 category: "Selectors",
267 safety_level: SafetyLevel::SemanticReview,
268 description: "Consolidate chained :not() selectors like :not(a):not(b) into :not(a, b) (review required for additive specificity change).",
269 },
270 RuleDefinition {
271 id: RuleId::ModernizeIs,
272 section: RuleSection::Modernize,
273 title: "Factor with :is()",
274 category: "Selectors",
275 safety_level: SafetyLevel::ProvenLocalRefactor,
276 description: "Factor selector-list alternatives with uniform specificity into :is(...) grouping.",
277 },
278 RuleDefinition {
279 id: RuleId::ModernizeWhere,
280 section: RuleSection::Modernize,
281 title: "Modernize with :where()",
282 category: "Selectors",
283 safety_level: SafetyLevel::Architectural,
284 description: "Factor selector-list alternatives into :where(...) for zero-specificity defaults (review required).",
285 },
286 RuleDefinition {
287 id: RuleId::ModernizeMediaRange,
288 section: RuleSection::Modernize,
289 title: "Modernize media range syntax",
290 category: "At-rules",
291 safety_level: SafetyLevel::FormattingOnly,
292 description: "Convert min/max-width and min/max-height media features to CSS Range Syntax (e.g. (width >= 800px)).",
293 },
294 RuleDefinition {
295 id: RuleId::MergeSameNamedLayer,
296 section: RuleSection::Refactor,
297 title: "Merge same named @layer blocks",
298 category: "Structural Refactoring",
299 safety_level: SafetyLevel::ProvenLocalRefactor,
300 description: "Consolidate separated blocks belonging to the same named @layer into their canonical first occurrence.",
301 },
302 RuleDefinition {
303 id: RuleId::MergeAdjacentMedia,
304 section: RuleSection::Refactor,
305 title: "Merge adjacent @media queries",
306 category: "Structural Refactoring",
307 safety_level: SafetyLevel::ProvenLocalRefactor,
308 description: "Combine consecutive @media blocks having identical query conditions into a single block.",
309 },
310 RuleDefinition {
311 id: RuleId::MergeAdjacentSupports,
312 section: RuleSection::Refactor,
313 title: "Merge adjacent @supports queries",
314 category: "Structural Refactoring",
315 safety_level: SafetyLevel::ProvenLocalRefactor,
316 description: "Combine consecutive @supports blocks having identical feature conditions into a single block.",
317 },
318 RuleDefinition {
319 id: RuleId::MergeAdjacentContainer,
320 section: RuleSection::Refactor,
321 title: "Merge adjacent @container queries",
322 category: "Structural Refactoring",
323 safety_level: SafetyLevel::ProvenLocalRefactor,
324 description: "Combine consecutive @container blocks having identical container name and query conditions.",
325 },
326 RuleDefinition {
327 id: RuleId::MergeIdenticalScope,
328 section: RuleSection::Refactor,
329 title: "Merge adjacent @scope blocks",
330 category: "Structural Refactoring",
331 safety_level: SafetyLevel::ProvenLocalRefactor,
332 description: "Combine consecutive @scope blocks having identical root and limit parameters.",
333 },
334 RuleDefinition {
335 id: RuleId::MergeIdenticalStartingStyle,
336 section: RuleSection::Refactor,
337 title: "Merge adjacent @starting-style blocks",
338 category: "Structural Refactoring",
339 safety_level: SafetyLevel::ProvenLocalRefactor,
340 description: "Combine consecutive top-level @starting-style blocks into a single block.",
341 },
342 RuleDefinition {
343 id: RuleId::MergeAdjacentIdenticalSelector,
344 section: RuleSection::Refactor,
345 title: "Merge adjacent identical selectors",
346 category: "Structural Refactoring",
347 safety_level: SafetyLevel::ProvenLocalRefactor,
348 description: "Combine consecutive style rules sharing the exact same selector when no intervening rules exist.",
349 },
350 RuleDefinition {
351 id: RuleId::MergeIdenticalRuleBodies,
352 section: RuleSection::Refactor,
353 title: "Merge identical rule bodies",
354 category: "Structural Refactoring",
355 safety_level: SafetyLevel::ProvenLocalRefactor,
356 description: "Combine selectors sharing identical declaration bodies into a unified comma-separated rule.",
357 },
358 RuleDefinition {
359 id: RuleId::FactorIdenticalStatesWithIs,
360 section: RuleSection::Refactor,
361 title: "Factor identical states with :is()",
362 category: "Structural Refactoring",
363 safety_level: SafetyLevel::ProvenLocalRefactor,
364 description: "Combine multiple states of the same element sharing identical bodies into &:is(:hover, :focus, ...) form.",
365 },
366 RuleDefinition {
367 id: RuleId::GatherRelatedSelectorRules,
368 section: RuleSection::Refactor,
369 title: "Gather related selector rules",
370 category: "Structural Refactoring",
371 safety_level: SafetyLevel::SemanticReview,
372 description: "Gather scattered related rules into the strongest existing parent within the same cascade layer (specificity wins; prefix nest beats appended `&` on a tie). Busy @media/@supports blocks with mixed selectors stay grouped. Does not move declarations between named layers.",
373 },
374 RuleDefinition {
375 id: RuleId::NestLayerBySelector,
376 section: RuleSection::Refactor,
377 title: "Nest named layers under a shared selector",
378 category: "Structural Refactoring",
379 safety_level: SafetyLevel::SemanticReview,
380 description: "Factor the exact same selector living in multiple named @layer blocks into `.sel { @layer a { … } @layer b { … } }`, preserving layer identity and first-declared layer order. Never nests @layer inside another @layer (that would create a child layer).",
381 },
382 RuleDefinition {
383 id: RuleId::PruneOverriddenDeclarations,
384 section: RuleSection::Refactor,
385 title: "Prune overridden declarations & rules",
386 category: "Structural Refactoring",
387 safety_level: SafetyLevel::ProvenLocalRefactor,
388 description: "Remove dead declarations and entire rules overridden by later identical selectors in the cascade.",
389 },
390 ]
391}
392
393#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
394#[serde(rename_all = "kebab-case")]
395pub enum Preset {
396 Analysis,
397 Conservative,
398 Modern,
399 Refactor,
400 Aggressive,
401 Custom,
402}
403
404impl Preset {
405 pub const ALL: [Preset; 6] = [
406 Preset::Analysis,
407 Preset::Conservative,
408 Preset::Modern,
409 Preset::Refactor,
410 Preset::Aggressive,
411 Preset::Custom,
412 ];
413
414 pub const fn label(self) -> &'static str {
415 match self {
416 Self::Analysis => "Analysis",
417 Self::Conservative => "Conservative",
418 Self::Modern => "Modern",
419 Self::Refactor => "Refactor",
420 Self::Aggressive => "Aggressive",
421 Self::Custom => "Custom",
422 }
423 }
424
425 pub fn enabled_rules(self) -> Vec<RuleId> {
426 match self {
427 Self::Analysis => vec![],
428 Self::Conservative => vec![
429 RuleId::NestPseudoClass,
430 RuleId::NestPseudoElement,
431 RuleId::NestAttribute,
432 RuleId::NestCompound,
433 RuleId::NestDescendant,
434 RuleId::NestCombinator,
435 RuleId::NestMedia,
436 RuleId::NestSupports,
437 RuleId::NestStartingStyle,
438 RuleId::FactorSelectorList,
439 RuleId::ModernizeIs,
440 RuleId::ModernizeMediaRange,
441 RuleId::MergeSameNamedLayer,
442 RuleId::MergeAdjacentMedia,
443 RuleId::MergeAdjacentSupports,
444 RuleId::MergeAdjacentIdenticalSelector,
445 RuleId::MergeIdenticalRuleBodies,
446 RuleId::FactorIdenticalStatesWithIs,
447 ],
448 Self::Modern => vec![
449 RuleId::NestPseudoClass,
450 RuleId::NestPseudoElement,
451 RuleId::NestAttribute,
452 RuleId::NestCompound,
453 RuleId::NestDescendant,
454 RuleId::NestCombinator,
455 RuleId::NestMedia,
456 RuleId::NestSupports,
457 RuleId::NestContainer,
458 RuleId::NestStartingStyle,
459 RuleId::FactorSelectorList,
460 RuleId::ConsolidateNot,
461 RuleId::ModernizeIs,
462 RuleId::ModernizeMediaRange,
463 RuleId::PruneOverriddenDeclarations,
464 ],
465 Self::Refactor => vec![
466 RuleId::NestPseudoClass,
467 RuleId::NestPseudoElement,
468 RuleId::NestAttribute,
469 RuleId::NestCompound,
470 RuleId::NestDescendant,
471 RuleId::NestCombinator,
472 RuleId::NestMedia,
473 RuleId::NestSupports,
474 RuleId::NestContainer,
475 RuleId::NestStartingStyle,
476 RuleId::FactorSelectorList,
477 RuleId::ConsolidateNot,
478 RuleId::ModernizeIs,
479 RuleId::ModernizeMediaRange,
480 RuleId::MergeSameNamedLayer,
481 RuleId::MergeAdjacentMedia,
482 RuleId::MergeAdjacentSupports,
483 RuleId::MergeAdjacentContainer,
484 RuleId::MergeIdenticalScope,
485 RuleId::MergeIdenticalStartingStyle,
486 RuleId::MergeAdjacentIdenticalSelector,
487 RuleId::MergeIdenticalRuleBodies,
488 RuleId::FactorIdenticalStatesWithIs,
489 RuleId::GatherRelatedSelectorRules,
490 RuleId::NestLayerBySelector,
491 RuleId::PruneOverriddenDeclarations,
492 ],
493 Self::Aggressive => RuleId::ALL.to_vec(),
494 Self::Custom => vec![],
495 }
496 }
497}
498
499impl fmt::Display for Preset {
500 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
501 f.write_str(self.label())
502 }
503}
504
505#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
506#[serde(rename_all = "kebab-case")]
507pub enum OutputMode {
508 DryRun,
509 NewFile,
510 OutDir,
511 OverwriteWithBackup,
512 Overwrite,
513 Patch,
514 Stdout,
515}
516
517impl OutputMode {
518 pub const ALL: [OutputMode; 7] = [
519 Self::DryRun,
520 Self::NewFile,
521 Self::OutDir,
522 Self::OverwriteWithBackup,
523 Self::Overwrite,
524 Self::Patch,
525 Self::Stdout,
526 ];
527
528 pub const fn label(self) -> &'static str {
529 match self {
530 Self::DryRun => "Dry run",
531 Self::NewFile => "New file (*.modern.css)",
532 Self::OutDir => "Output directory",
533 Self::OverwriteWithBackup => "Overwrite + backup",
534 Self::Overwrite => "Overwrite",
535 Self::Patch => "Patch file",
536 Self::Stdout => "stdout",
537 }
538 }
539}
540
541impl fmt::Display for OutputMode {
542 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
543 f.write_str(self.label())
544 }
545}
546
547#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
548pub struct SourceRange {
549 pub start: usize,
550 pub end: usize,
551}
552
553#[derive(Debug, Clone, Default, Serialize, Deserialize)]
554pub struct Proof {
555 pub selector_set_equivalent: bool,
556 pub specificity_equivalent: bool,
557 pub cascade_context_equivalent: bool,
558 pub source_order_equivalent: bool,
559 pub layer_equivalent: bool,
560 pub scope_equivalent: bool,
561 pub declarations_exact: bool,
562 pub important_exact: bool,
563}
564
565impl Proof {
566 pub fn safe_local() -> Self {
567 Self {
568 selector_set_equivalent: true,
569 specificity_equivalent: true,
570 cascade_context_equivalent: true,
571 source_order_equivalent: true,
572 layer_equivalent: true,
573 scope_equivalent: true,
574 declarations_exact: true,
575 important_exact: true,
576 }
577 }
578
579 pub fn all_pass(&self) -> bool {
580 self.selector_set_equivalent
581 && self.specificity_equivalent
582 && self.cascade_context_equivalent
583 && self.source_order_equivalent
584 && self.layer_equivalent
585 && self.scope_equivalent
586 && self.declarations_exact
587 && self.important_exact
588 }
589}
590
591#[derive(Debug, Clone, Serialize, Deserialize)]
592pub struct PlanEntry {
593 pub id: String,
594 pub file: PathBuf,
595 pub rules: Vec<RuleId>,
596 pub safety: Safety,
597 pub source_range: SourceRange,
598 pub original: String,
599 pub proposed: String,
600 pub proof: Proof,
601 pub warnings: Vec<String>,
602 pub reason: String,
603 #[serde(default = "default_true")]
604 pub selected: bool,
605}
606
607fn default_true() -> bool {
608 true
609}
610
611#[derive(Debug, Clone, Default, Serialize, Deserialize)]
612pub struct AnalysisStats {
613 pub bytes: usize,
614 pub top_level_style_rules: usize,
615 pub top_level_at_rules: usize,
616 pub declarations: usize,
617 pub important_declarations: usize,
618 pub custom_properties: usize,
619 pub duplicate_selectors: usize,
620 pub media_rules: usize,
621 pub supports_rules: usize,
622 pub container_rules: usize,
623 pub layer_rules: usize,
624 pub scope_rules: usize,
625 pub starting_style_rules: usize,
626 pub parse_errors: usize,
627}
628
629#[derive(Debug, Clone, Serialize, Deserialize)]
630pub struct Finding {
631 pub safety: Safety,
632 pub title: String,
633 pub detail: String,
634}
635
636#[derive(Debug, Clone, Serialize, Deserialize)]
637pub struct FileReport {
638 pub path: PathBuf,
639 pub parse_ok: bool,
640 pub parse_error: Option<String>,
641 pub stats: AnalysisStats,
642 pub findings: Vec<Finding>,
643 pub plans: Vec<PlanEntry>,
644}
645
646#[derive(Debug, Clone, Default, Serialize, Deserialize)]
647pub struct WorkspaceSummary {
648 pub files: usize,
649 pub parse_errors: usize,
650 pub rules_analyzed: usize,
651 pub safe: usize,
652 pub review: usize,
653 pub unsafe_count: usize,
654 pub unsupported: usize,
655 pub no_op: usize,
656 pub specificity_sensitive: usize,
657 pub cascade_sensitive: usize,
658 pub layer_sensitive: usize,
659 pub scope_sensitive: usize,
660}
661
662#[derive(Debug, Clone, Serialize, Deserialize)]
663pub struct WorkspaceReport {
664 pub tool_version: String,
665 pub spec_baseline: String,
666 pub root: PathBuf,
667 pub enabled_rules: Vec<RuleId>,
668 pub files: Vec<FileReport>,
669 pub summary: WorkspaceSummary,
670}