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