Skip to main content

fallow_types/
issue_meta.rs

1//! Shared issue-type contract metadata.
2
3use std::sync::LazyLock;
4
5use crate::suppress::IssueKind;
6
7/// Shared contract facts for issue-like diagnostics.
8///
9/// Curated prose stays with the surface that owns it. This table is only for
10/// stable machine-facing facts that otherwise drift across CLI schema, LSP,
11/// MCP, and suppression helpers.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct IssueKindMeta {
14    /// Backing suppression issue kind, when this row maps to one.
15    pub kind: Option<IssueKind>,
16    /// Canonical issue code used in output and diagnostic codes.
17    pub code: &'static str,
18    /// Accepted aliases for config, suppression, or migration compatibility.
19    pub aliases: &'static [&'static str],
20    /// User-facing label for editor and capability surfaces.
21    pub label: &'static str,
22    /// Canonical `[rules]` key when the issue is configurable.
23    pub config_key: Option<&'static str>,
24    /// Dead-code CLI filter flag, when one exists.
25    pub filter_flag: Option<&'static str>,
26    /// MCP `issue_types` selector, when this issue can be selected there.
27    pub mcp_issue_type: Option<&'static str>,
28    /// Suppression token agents should emit, when suppressible.
29    pub suppress_token: Option<&'static str>,
30    /// Whether the suppression comment should use `fallow-ignore-file`.
31    pub suppress_file_level: bool,
32    /// Whether the LSP exposes this row through initialization options and
33    /// `fallow/issueTypes`.
34    pub lsp: bool,
35    /// Broad documentation category for authoring and generated manifests.
36    pub docs_category: &'static str,
37}
38
39impl IssueKindMeta {
40    /// Return the filter flag as an MCP selector pair.
41    #[must_use]
42    pub const fn mcp_pair(self) -> Option<(&'static str, &'static str)> {
43        match (self.mcp_issue_type, self.filter_flag) {
44            (Some(issue_type), Some(flag)) => Some((issue_type, flag)),
45            _ => None,
46        }
47    }
48
49    /// Whether this row owns a serialized dead-code result contract.
50    #[must_use]
51    pub fn has_result_contract(self) -> bool {
52        issue_result_meta_by_code(self.code).is_some()
53    }
54
55    /// SARIF rule ids used by CI formatters for this issue row.
56    #[must_use]
57    pub fn sarif_rule_ids(self) -> Vec<String> {
58        issue_sarif_rule_ids(self.code)
59    }
60
61    /// Whether this issue row is eligible for SARIF rule metadata.
62    #[must_use]
63    pub fn sarif_enabled(self) -> bool {
64        self.has_result_contract()
65    }
66
67    /// CodeClimate check names used by CI formatters for this issue row.
68    #[must_use]
69    pub fn codeclimate_check_names(self) -> Vec<String> {
70        issue_codeclimate_check_names(self.code)
71    }
72
73    /// Whether this issue row is eligible for CodeClimate output.
74    #[must_use]
75    pub fn codeclimate_enabled(self) -> bool {
76        !self.codeclimate_check_names().is_empty()
77    }
78
79    /// Documentation anchor under `/explanations/dead-code`.
80    #[must_use]
81    pub fn docs_anchor(self) -> Option<&'static str> {
82        issue_docs_anchor(self.code)
83    }
84
85    /// Published TypeScript backwards-compat alias policy.
86    #[must_use]
87    pub fn ts_alias(self) -> Option<TsAliasMeta> {
88        issue_ts_alias(self.code)
89    }
90}
91
92/// All shared issue metadata rows.
93pub const ISSUE_KIND_META: &[IssueKindMeta] = &[
94    IssueKindMeta {
95        kind: Some(IssueKind::CodeDuplication),
96        code: "code-duplication",
97        aliases: &[],
98        label: "Code Duplication",
99        config_key: None,
100        filter_flag: None,
101        mcp_issue_type: None,
102        suppress_token: Some("code-duplication"),
103        suppress_file_level: false,
104        lsp: true,
105        docs_category: "dupes",
106    },
107    IssueKindMeta {
108        kind: Some(IssueKind::UnusedFile),
109        code: "unused-file",
110        aliases: &[],
111        label: "Unused Files",
112        config_key: Some("unused-files"),
113        filter_flag: Some("--unused-files"),
114        mcp_issue_type: Some("unused-files"),
115        suppress_token: Some("unused-file"),
116        suppress_file_level: true,
117        lsp: true,
118        docs_category: "source",
119    },
120    IssueKindMeta {
121        kind: Some(IssueKind::UnusedExport),
122        code: "unused-export",
123        aliases: &[],
124        label: "Unused Exports",
125        config_key: Some("unused-exports"),
126        filter_flag: Some("--unused-exports"),
127        mcp_issue_type: Some("unused-exports"),
128        suppress_token: Some("unused-export"),
129        suppress_file_level: false,
130        lsp: true,
131        docs_category: "source",
132    },
133    IssueKindMeta {
134        kind: Some(IssueKind::UnusedType),
135        code: "unused-type",
136        aliases: &[],
137        label: "Unused Types",
138        config_key: Some("unused-types"),
139        filter_flag: Some("--unused-types"),
140        mcp_issue_type: Some("unused-types"),
141        suppress_token: Some("unused-type"),
142        suppress_file_level: false,
143        lsp: true,
144        docs_category: "source",
145    },
146    IssueKindMeta {
147        kind: Some(IssueKind::PrivateTypeLeak),
148        code: "private-type-leak",
149        aliases: &[],
150        label: "Private Type Leaks",
151        config_key: Some("private-type-leaks"),
152        filter_flag: Some("--private-type-leaks"),
153        mcp_issue_type: Some("private-type-leaks"),
154        suppress_token: Some("private-type-leak"),
155        suppress_file_level: false,
156        lsp: true,
157        docs_category: "source",
158    },
159    IssueKindMeta {
160        kind: Some(IssueKind::UnusedDependency),
161        code: "unused-dependency",
162        aliases: &[],
163        label: "Unused Dependencies",
164        config_key: Some("unused-dependencies"),
165        filter_flag: Some("--unused-deps"),
166        mcp_issue_type: Some("unused-deps"),
167        suppress_token: None,
168        suppress_file_level: false,
169        lsp: true,
170        docs_category: "dependency",
171    },
172    IssueKindMeta {
173        kind: Some(IssueKind::UnusedDevDependency),
174        code: "unused-dev-dependency",
175        aliases: &["unused-dev-deps", "unused-dev-dependencies"],
176        label: "Unused Dev Dependencies",
177        config_key: Some("unused-dev-dependencies"),
178        filter_flag: Some("--unused-deps"),
179        mcp_issue_type: None,
180        suppress_token: None,
181        suppress_file_level: false,
182        lsp: true,
183        docs_category: "dependency",
184    },
185    IssueKindMeta {
186        kind: None,
187        code: "unused-optional-dependency",
188        aliases: &["unused-optional-deps", "unused-optional-dependencies"],
189        label: "Unused Optional Dependencies",
190        config_key: Some("unused-optional-dependencies"),
191        filter_flag: Some("--unused-deps"),
192        mcp_issue_type: None,
193        suppress_token: None,
194        suppress_file_level: false,
195        lsp: true,
196        docs_category: "dependency",
197    },
198    IssueKindMeta {
199        kind: Some(IssueKind::UnusedEnumMember),
200        code: "unused-enum-member",
201        aliases: &[],
202        label: "Unused Enum Members",
203        config_key: Some("unused-enum-members"),
204        filter_flag: Some("--unused-enum-members"),
205        mcp_issue_type: Some("unused-enum-members"),
206        suppress_token: Some("unused-enum-member"),
207        suppress_file_level: false,
208        lsp: true,
209        docs_category: "source",
210    },
211    IssueKindMeta {
212        kind: Some(IssueKind::UnusedClassMember),
213        code: "unused-class-member",
214        aliases: &[],
215        label: "Unused Class Members",
216        config_key: Some("unused-class-members"),
217        filter_flag: Some("--unused-class-members"),
218        mcp_issue_type: Some("unused-class-members"),
219        suppress_token: Some("unused-class-member"),
220        suppress_file_level: false,
221        lsp: true,
222        docs_category: "source",
223    },
224    IssueKindMeta {
225        kind: Some(IssueKind::UnusedStoreMember),
226        code: "unused-store-member",
227        aliases: &["unused-store-members"],
228        label: "Unused Store Members",
229        config_key: Some("unused-store-members"),
230        filter_flag: Some("--unused-store-members"),
231        mcp_issue_type: Some("unused-store-members"),
232        suppress_token: Some("unused-store-member"),
233        suppress_file_level: false,
234        lsp: true,
235        docs_category: "framework",
236    },
237    IssueKindMeta {
238        kind: Some(IssueKind::UnresolvedImport),
239        code: "unresolved-import",
240        aliases: &[],
241        label: "Unresolved Imports",
242        config_key: Some("unresolved-imports"),
243        filter_flag: Some("--unresolved-imports"),
244        mcp_issue_type: Some("unresolved-imports"),
245        suppress_token: Some("unresolved-import"),
246        suppress_file_level: false,
247        lsp: true,
248        docs_category: "dependency",
249    },
250    IssueKindMeta {
251        kind: Some(IssueKind::UnlistedDependency),
252        code: "unlisted-dependency",
253        aliases: &[],
254        label: "Unlisted Dependencies",
255        config_key: Some("unlisted-dependencies"),
256        filter_flag: Some("--unlisted-deps"),
257        mcp_issue_type: Some("unlisted-deps"),
258        suppress_token: None,
259        suppress_file_level: false,
260        lsp: true,
261        docs_category: "dependency",
262    },
263    IssueKindMeta {
264        kind: Some(IssueKind::DuplicateExport),
265        code: "duplicate-export",
266        aliases: &[],
267        label: "Duplicate Exports",
268        config_key: Some("duplicate-exports"),
269        filter_flag: Some("--duplicate-exports"),
270        mcp_issue_type: Some("duplicate-exports"),
271        suppress_token: Some("duplicate-export"),
272        suppress_file_level: true,
273        lsp: true,
274        docs_category: "source",
275    },
276    IssueKindMeta {
277        kind: Some(IssueKind::TypeOnlyDependency),
278        code: "type-only-dependency",
279        aliases: &[],
280        label: "Type-Only Dependencies",
281        config_key: Some("type-only-dependencies"),
282        filter_flag: Some("--unused-deps"),
283        mcp_issue_type: None,
284        suppress_token: None,
285        suppress_file_level: false,
286        lsp: true,
287        docs_category: "dependency",
288    },
289    IssueKindMeta {
290        kind: Some(IssueKind::TestOnlyDependency),
291        code: "test-only-dependency",
292        aliases: &[],
293        label: "Test-Only Dependencies",
294        config_key: Some("test-only-dependencies"),
295        filter_flag: Some("--unused-deps"),
296        mcp_issue_type: None,
297        suppress_token: None,
298        suppress_file_level: false,
299        lsp: true,
300        docs_category: "dependency",
301    },
302    IssueKindMeta {
303        kind: Some(IssueKind::DevDependencyInProduction),
304        code: "dev-dependency-in-production",
305        aliases: &[],
306        label: "Dev Dependencies Used in Production",
307        config_key: Some("dev-dependencies-in-production"),
308        filter_flag: Some("--unused-deps"),
309        mcp_issue_type: None,
310        suppress_token: None,
311        suppress_file_level: false,
312        lsp: true,
313        docs_category: "dependency",
314    },
315    IssueKindMeta {
316        kind: Some(IssueKind::CircularDependency),
317        code: "circular-dependency",
318        aliases: &["circular-dependencies"],
319        label: "Circular Dependencies",
320        config_key: Some("circular-dependencies"),
321        filter_flag: Some("--circular-deps"),
322        mcp_issue_type: Some("circular-deps"),
323        suppress_token: Some("circular-dependency"),
324        suppress_file_level: false,
325        lsp: true,
326        docs_category: "architecture",
327    },
328    IssueKindMeta {
329        kind: Some(IssueKind::ReExportCycle),
330        code: "re-export-cycle",
331        aliases: &["re-export-cycles", "reexport-cycle", "reexport-cycles"],
332        label: "Re-Export Cycles",
333        config_key: Some("re-export-cycle"),
334        filter_flag: Some("--re-export-cycles"),
335        mcp_issue_type: Some("re-export-cycles"),
336        suppress_token: Some("re-export-cycle"),
337        suppress_file_level: true,
338        lsp: true,
339        docs_category: "architecture",
340    },
341    IssueKindMeta {
342        kind: Some(IssueKind::BoundaryViolation),
343        code: "boundary-violation",
344        aliases: &[],
345        label: "Boundary Violations",
346        config_key: Some("boundary-violation"),
347        filter_flag: Some("--boundary-violations"),
348        mcp_issue_type: Some("boundary-violations"),
349        suppress_token: Some("boundary-violation"),
350        suppress_file_level: false,
351        lsp: true,
352        docs_category: "architecture",
353    },
354    IssueKindMeta {
355        kind: None,
356        code: "boundary-coverage",
357        aliases: &["boundary-coverage-violations"],
358        label: "Boundary Coverage",
359        config_key: Some("boundary-violation"),
360        filter_flag: Some("--boundary-violations"),
361        mcp_issue_type: None,
362        suppress_token: Some("boundary-violation"),
363        suppress_file_level: true,
364        lsp: false,
365        docs_category: "architecture",
366    },
367    IssueKindMeta {
368        kind: Some(IssueKind::BoundaryViolation),
369        code: "boundary-call-violation",
370        aliases: &["boundary-calls", "boundary-call-violations"],
371        label: "Boundary Call Violations",
372        config_key: Some("boundary-violation"),
373        filter_flag: Some("--boundary-violations"),
374        mcp_issue_type: None,
375        suppress_token: Some("boundary-call-violation"),
376        suppress_file_level: false,
377        lsp: false,
378        docs_category: "architecture",
379    },
380    IssueKindMeta {
381        kind: Some(IssueKind::PolicyViolation),
382        code: "policy-violation",
383        aliases: &["policy-violations"],
384        label: "Policy Violations",
385        config_key: Some("policy-violation"),
386        filter_flag: Some("--policy-violations"),
387        mcp_issue_type: Some("policy-violations"),
388        suppress_token: Some("policy-violation"),
389        suppress_file_level: false,
390        lsp: true,
391        docs_category: "architecture",
392    },
393    IssueKindMeta {
394        kind: Some(IssueKind::InvalidClientExport),
395        code: "invalid-client-export",
396        aliases: &["invalid-client-exports"],
397        label: "Invalid Client Exports",
398        config_key: Some("invalid-client-export"),
399        filter_flag: None,
400        mcp_issue_type: None,
401        suppress_token: Some("invalid-client-export"),
402        suppress_file_level: false,
403        lsp: true,
404        docs_category: "framework",
405    },
406    IssueKindMeta {
407        kind: Some(IssueKind::MixedClientServerBarrel),
408        code: "mixed-client-server-barrel",
409        aliases: &["mixed-client-server-barrels"],
410        label: "Mixed Client/Server Barrels",
411        config_key: Some("mixed-client-server-barrel"),
412        filter_flag: None,
413        mcp_issue_type: None,
414        suppress_token: Some("mixed-client-server-barrel"),
415        suppress_file_level: false,
416        lsp: true,
417        docs_category: "framework",
418    },
419    IssueKindMeta {
420        kind: Some(IssueKind::MisplacedDirective),
421        code: "misplaced-directive",
422        aliases: &["misplaced-directives"],
423        label: "Misplaced Directives",
424        config_key: Some("misplaced-directive"),
425        filter_flag: None,
426        mcp_issue_type: None,
427        suppress_token: Some("misplaced-directive"),
428        suppress_file_level: false,
429        lsp: true,
430        docs_category: "framework",
431    },
432    IssueKindMeta {
433        kind: Some(IssueKind::UnprovidedInject),
434        code: "unprovided-inject",
435        aliases: &["unprovided-injects"],
436        label: "Unprovided Injects",
437        config_key: Some("unprovided-injects"),
438        filter_flag: Some("--unprovided-injects"),
439        mcp_issue_type: Some("unprovided-injects"),
440        suppress_token: Some("unprovided-inject"),
441        suppress_file_level: false,
442        lsp: true,
443        docs_category: "framework",
444    },
445    IssueKindMeta {
446        kind: Some(IssueKind::UnrenderedComponent),
447        code: "unrendered-component",
448        aliases: &["unrendered-components"],
449        label: "Unrendered Components",
450        config_key: Some("unrendered-components"),
451        filter_flag: Some("--unrendered-components"),
452        mcp_issue_type: Some("unrendered-components"),
453        suppress_token: Some("unrendered-component"),
454        suppress_file_level: false,
455        lsp: true,
456        docs_category: "framework",
457    },
458    IssueKindMeta {
459        kind: Some(IssueKind::UnusedComponentProp),
460        code: "unused-component-prop",
461        aliases: &["unused-component-props"],
462        label: "Unused Component Props",
463        config_key: Some("unused-component-props"),
464        filter_flag: Some("--unused-component-props"),
465        mcp_issue_type: Some("unused-component-props"),
466        suppress_token: Some("unused-component-prop"),
467        suppress_file_level: false,
468        lsp: true,
469        docs_category: "framework",
470    },
471    IssueKindMeta {
472        kind: Some(IssueKind::UnusedComponentEmit),
473        code: "unused-component-emit",
474        aliases: &["unused-component-emits"],
475        label: "Unused Component Emits",
476        config_key: Some("unused-component-emits"),
477        filter_flag: Some("--unused-component-emits"),
478        mcp_issue_type: Some("unused-component-emits"),
479        suppress_token: Some("unused-component-emit"),
480        suppress_file_level: false,
481        lsp: true,
482        docs_category: "framework",
483    },
484    IssueKindMeta {
485        kind: Some(IssueKind::UnusedComponentInput),
486        code: "unused-component-input",
487        aliases: &["unused-component-inputs"],
488        label: "Unused Component Inputs",
489        config_key: Some("unused-component-inputs"),
490        filter_flag: Some("--unused-component-inputs"),
491        mcp_issue_type: Some("unused-component-inputs"),
492        suppress_token: Some("unused-component-input"),
493        suppress_file_level: false,
494        lsp: true,
495        docs_category: "framework",
496    },
497    IssueKindMeta {
498        kind: Some(IssueKind::UnusedComponentOutput),
499        code: "unused-component-output",
500        aliases: &["unused-component-outputs"],
501        label: "Unused Component Outputs",
502        config_key: Some("unused-component-outputs"),
503        filter_flag: Some("--unused-component-outputs"),
504        mcp_issue_type: Some("unused-component-outputs"),
505        suppress_token: Some("unused-component-output"),
506        suppress_file_level: false,
507        lsp: true,
508        docs_category: "framework",
509    },
510    IssueKindMeta {
511        kind: Some(IssueKind::UnusedSvelteEvent),
512        code: "unused-svelte-event",
513        aliases: &["unused-svelte-events"],
514        label: "Unused Svelte Events",
515        config_key: Some("unused-svelte-events"),
516        filter_flag: Some("--unused-svelte-events"),
517        mcp_issue_type: Some("unused-svelte-events"),
518        suppress_token: Some("unused-svelte-event"),
519        suppress_file_level: false,
520        lsp: true,
521        docs_category: "framework",
522    },
523    IssueKindMeta {
524        kind: Some(IssueKind::UnusedServerAction),
525        code: "unused-server-action",
526        aliases: &["unused-server-actions"],
527        label: "Unused Server Actions",
528        config_key: Some("unused-server-actions"),
529        filter_flag: Some("--unused-server-actions"),
530        mcp_issue_type: Some("unused-server-actions"),
531        suppress_token: Some("unused-server-action"),
532        suppress_file_level: false,
533        lsp: true,
534        docs_category: "framework",
535    },
536    IssueKindMeta {
537        kind: Some(IssueKind::UnusedLoadDataKey),
538        code: "unused-load-data-key",
539        aliases: &["unused-load-data-keys"],
540        label: "Unused Load Data Keys",
541        config_key: Some("unused-load-data-keys"),
542        filter_flag: Some("--unused-load-data-keys"),
543        mcp_issue_type: Some("unused-load-data-keys"),
544        suppress_token: Some("unused-load-data-key"),
545        suppress_file_level: false,
546        lsp: true,
547        docs_category: "framework",
548    },
549    IssueKindMeta {
550        kind: Some(IssueKind::RouteCollision),
551        code: "route-collision",
552        aliases: &["route-collisions"],
553        label: "Route Collisions",
554        config_key: Some("route-collision"),
555        filter_flag: None,
556        mcp_issue_type: None,
557        suppress_token: Some("route-collision"),
558        suppress_file_level: true,
559        lsp: true,
560        docs_category: "framework",
561    },
562    IssueKindMeta {
563        kind: Some(IssueKind::DynamicSegmentNameConflict),
564        code: "dynamic-segment-name-conflict",
565        aliases: &["dynamic-segment-name-conflicts"],
566        label: "Dynamic Segment Conflicts",
567        config_key: Some("dynamic-segment-name-conflict"),
568        filter_flag: None,
569        mcp_issue_type: None,
570        suppress_token: Some("dynamic-segment-name-conflict"),
571        suppress_file_level: true,
572        lsp: true,
573        docs_category: "framework",
574    },
575    IssueKindMeta {
576        kind: Some(IssueKind::StaleSuppression),
577        code: "stale-suppression",
578        aliases: &[],
579        label: "Stale Suppressions",
580        config_key: Some("stale-suppressions"),
581        filter_flag: Some("--stale-suppressions"),
582        mcp_issue_type: Some("stale-suppressions"),
583        suppress_token: None,
584        suppress_file_level: false,
585        lsp: true,
586        docs_category: "source",
587    },
588    IssueKindMeta {
589        kind: Some(IssueKind::StaleSuppression),
590        code: "missing-suppression-reason",
591        aliases: &["missing-suppression-reasons"],
592        label: "Missing Suppression Reasons",
593        config_key: Some("require-suppression-reason"),
594        filter_flag: Some("--stale-suppressions"),
595        mcp_issue_type: None,
596        suppress_token: None,
597        suppress_file_level: false,
598        lsp: false,
599        docs_category: "source",
600    },
601    IssueKindMeta {
602        kind: Some(IssueKind::PnpmCatalogEntry),
603        code: "unused-catalog-entry",
604        aliases: &["catalog", "unused-catalog-entries"],
605        label: "Unused Catalog Entries",
606        config_key: Some("unused-catalog-entries"),
607        filter_flag: Some("--unused-catalog-entries"),
608        mcp_issue_type: Some("unused-catalog-entries"),
609        suppress_token: None,
610        suppress_file_level: false,
611        lsp: true,
612        docs_category: "dependency",
613    },
614    IssueKindMeta {
615        kind: Some(IssueKind::EmptyCatalogGroup),
616        code: "empty-catalog-group",
617        aliases: &["empty-catalog", "empty-catalog-groups"],
618        label: "Empty Catalog Groups",
619        config_key: Some("empty-catalog-groups"),
620        filter_flag: Some("--empty-catalog-groups"),
621        mcp_issue_type: Some("empty-catalog-groups"),
622        suppress_token: None,
623        suppress_file_level: false,
624        lsp: true,
625        docs_category: "dependency",
626    },
627    IssueKindMeta {
628        kind: Some(IssueKind::UnresolvedCatalogReference),
629        code: "unresolved-catalog-reference",
630        aliases: &["unresolved-catalog", "unresolved-catalog-references"],
631        label: "Unresolved Catalog References",
632        config_key: Some("unresolved-catalog-references"),
633        filter_flag: Some("--unresolved-catalog-references"),
634        mcp_issue_type: Some("unresolved-catalog-references"),
635        suppress_token: None,
636        suppress_file_level: false,
637        lsp: true,
638        docs_category: "dependency",
639    },
640    IssueKindMeta {
641        kind: Some(IssueKind::UnusedDependencyOverride),
642        code: "unused-dependency-override",
643        aliases: &[
644            "unused-dependency-overrides",
645            "unused-override",
646            "unused-overrides",
647        ],
648        label: "Unused Dependency Overrides",
649        config_key: Some("unused-dependency-overrides"),
650        filter_flag: Some("--unused-dependency-overrides"),
651        mcp_issue_type: Some("unused-dependency-overrides"),
652        suppress_token: None,
653        suppress_file_level: false,
654        lsp: true,
655        docs_category: "dependency",
656    },
657    IssueKindMeta {
658        kind: Some(IssueKind::MisconfiguredDependencyOverride),
659        code: "misconfigured-dependency-override",
660        aliases: &[
661            "misconfigured-dependency-overrides",
662            "misconfigured-override",
663            "misconfigured-overrides",
664        ],
665        label: "Misconfigured Dependency Overrides",
666        config_key: Some("misconfigured-dependency-overrides"),
667        filter_flag: Some("--misconfigured-dependency-overrides"),
668        mcp_issue_type: Some("misconfigured-dependency-overrides"),
669        suppress_token: None,
670        suppress_file_level: false,
671        lsp: true,
672        docs_category: "dependency",
673    },
674    IssueKindMeta {
675        kind: Some(IssueKind::SecuritySink),
676        code: "security-sink",
677        aliases: &[],
678        label: "Security Sink Candidates",
679        config_key: Some("security-sink"),
680        filter_flag: None,
681        mcp_issue_type: None,
682        suppress_token: Some("security-sink"),
683        suppress_file_level: false,
684        lsp: true,
685        docs_category: "security",
686    },
687    IssueKindMeta {
688        kind: Some(IssueKind::SecurityClientServerLeak),
689        code: "security-client-server-leak",
690        aliases: &[],
691        label: "Security Client-Server Leaks",
692        config_key: Some("security-client-server-leak"),
693        filter_flag: None,
694        mcp_issue_type: None,
695        suppress_token: Some("security-client-server-leak"),
696        suppress_file_level: true,
697        lsp: true,
698        docs_category: "security",
699    },
700    IssueKindMeta {
701        kind: Some(IssueKind::CoverageGaps),
702        code: "coverage-gaps",
703        aliases: &[],
704        label: "Coverage Gaps",
705        config_key: Some("coverage-gaps"),
706        filter_flag: None,
707        mcp_issue_type: None,
708        suppress_token: Some("coverage-gaps"),
709        suppress_file_level: true,
710        lsp: false,
711        docs_category: "health",
712    },
713    IssueKindMeta {
714        kind: Some(IssueKind::FeatureFlag),
715        code: "feature-flag",
716        aliases: &[],
717        label: "Feature Flags",
718        config_key: Some("feature-flags"),
719        filter_flag: None,
720        mcp_issue_type: None,
721        suppress_token: Some("feature-flag"),
722        suppress_file_level: false,
723        lsp: false,
724        docs_category: "flags",
725    },
726    IssueKindMeta {
727        kind: Some(IssueKind::Complexity),
728        code: "complexity",
729        aliases: &[],
730        label: "Complexity",
731        config_key: None,
732        filter_flag: None,
733        mcp_issue_type: None,
734        suppress_token: Some("complexity"),
735        suppress_file_level: false,
736        lsp: false,
737        docs_category: "health",
738    },
739    IssueKindMeta {
740        kind: Some(IssueKind::PropDrilling),
741        code: "prop-drilling",
742        aliases: &[],
743        label: "Prop Drilling",
744        config_key: Some("prop-drilling"),
745        filter_flag: None,
746        mcp_issue_type: None,
747        suppress_token: Some("prop-drilling"),
748        suppress_file_level: false,
749        lsp: false,
750        docs_category: "source",
751    },
752    IssueKindMeta {
753        kind: Some(IssueKind::ThinWrapper),
754        code: "thin-wrapper",
755        aliases: &["thin-wrappers"],
756        label: "Thin Wrappers",
757        config_key: Some("thin-wrapper"),
758        filter_flag: None,
759        mcp_issue_type: None,
760        suppress_token: Some("thin-wrapper"),
761        suppress_file_level: false,
762        lsp: false,
763        docs_category: "source",
764    },
765    IssueKindMeta {
766        kind: Some(IssueKind::DuplicatePropShape),
767        code: "duplicate-prop-shape",
768        aliases: &["duplicate-prop-shapes"],
769        label: "Duplicate Prop Shapes",
770        config_key: Some("duplicate-prop-shape"),
771        filter_flag: None,
772        mcp_issue_type: None,
773        suppress_token: Some("duplicate-prop-shape"),
774        suppress_file_level: false,
775        lsp: false,
776        docs_category: "source",
777    },
778    IssueKindMeta {
779        kind: Some(IssueKind::CssTokenDrift),
780        code: "css-token-drift",
781        aliases: &[],
782        label: "CSS Token Drift",
783        config_key: Some("css-token-drift"),
784        filter_flag: None,
785        mcp_issue_type: None,
786        suppress_token: Some("css-token-drift"),
787        suppress_file_level: false,
788        lsp: false,
789        docs_category: "health",
790    },
791    IssueKindMeta {
792        kind: Some(IssueKind::CssDuplicateBlock),
793        code: "css-duplicate-block",
794        aliases: &[],
795        label: "CSS Duplicate Block",
796        config_key: Some("css-duplicate-block"),
797        filter_flag: None,
798        mcp_issue_type: None,
799        suppress_token: Some("css-duplicate-block"),
800        suppress_file_level: false,
801        lsp: false,
802        docs_category: "health",
803    },
804    IssueKindMeta {
805        kind: Some(IssueKind::CssSelectorComplexity),
806        code: "css-selector-complexity",
807        aliases: &[],
808        label: "CSS Selector Complexity",
809        config_key: Some("css-selector-complexity"),
810        filter_flag: None,
811        mcp_issue_type: None,
812        suppress_token: Some("css-selector-complexity"),
813        suppress_file_level: false,
814        lsp: false,
815        docs_category: "health",
816    },
817    IssueKindMeta {
818        kind: Some(IssueKind::CssDeadSurface),
819        code: "css-dead-surface",
820        aliases: &[],
821        label: "CSS Dead Surface",
822        config_key: Some("css-dead-surface"),
823        filter_flag: None,
824        mcp_issue_type: None,
825        suppress_token: Some("css-dead-surface"),
826        suppress_file_level: false,
827        lsp: false,
828        docs_category: "health",
829    },
830    IssueKindMeta {
831        kind: Some(IssueKind::CssBrokenReference),
832        code: "css-broken-reference",
833        aliases: &[],
834        label: "CSS Broken Reference",
835        config_key: Some("css-broken-reference"),
836        filter_flag: None,
837        mcp_issue_type: None,
838        suppress_token: Some("css-broken-reference"),
839        suppress_file_level: false,
840        lsp: false,
841        docs_category: "health",
842    },
843];
844
845/// Shared contract facts for serialized `AnalysisResults` arrays.
846#[derive(Debug, Clone, Copy, PartialEq, Eq)]
847#[non_exhaustive]
848pub struct IssueResultMeta {
849    /// Canonical issue code that owns this result array.
850    pub code: &'static str,
851    /// Short SARIF rule description.
852    pub sarif_description: &'static str,
853    /// Explanation emitted in dead-code `_meta.rules`.
854    pub meta_description: &'static str,
855    /// Documentation path emitted in dead-code `_meta.rules`.
856    pub meta_docs_path: &'static str,
857    /// Human-readable name emitted in dead-code `_meta.rules`.
858    pub meta_name: &'static str,
859    /// Label used by CI summary tables.
860    pub summary_label: &'static str,
861    /// Documentation anchor under `/explanations/dead-code`.
862    pub docs_anchor: &'static str,
863    /// Serialized `AnalysisResults` array key that carries this issue row.
864    pub result_key: &'static str,
865    /// Whether `result_key` contributes to `AnalysisResults::total_issues()`.
866    pub counts_in_total: bool,
867}
868
869/// TypeScript backwards-compat alias emitted for a dead-code result row.
870#[derive(Debug, Clone, Copy, PartialEq, Eq)]
871pub struct TsAliasMeta {
872    /// Bare alias name kept available from the published `fallow/types` subpath.
873    pub name: &'static str,
874    /// Generated `*Finding` wrapper type the alias resolves to.
875    pub parent: &'static str,
876}
877
878/// TypeScript backwards-compat alias row for a dead-code result code.
879#[derive(Debug, Clone, Copy, PartialEq, Eq)]
880pub struct IssueTsAliasMeta {
881    /// Canonical issue code that owns this alias.
882    pub code: &'static str,
883    /// Published TypeScript alias policy.
884    pub alias: TsAliasMeta,
885}
886
887/// Published TypeScript backwards-compat aliases.
888pub const ISSUE_TS_ALIAS_META: &[IssueTsAliasMeta] = &[
889    IssueTsAliasMeta {
890        code: "unused-file",
891        alias: TsAliasMeta {
892            name: "UnusedFile",
893            parent: "UnusedFileFinding",
894        },
895    },
896    IssueTsAliasMeta {
897        code: "unused-export",
898        alias: TsAliasMeta {
899            name: "UnusedExport",
900            parent: "UnusedExportFinding",
901        },
902    },
903    IssueTsAliasMeta {
904        code: "private-type-leak",
905        alias: TsAliasMeta {
906            name: "PrivateTypeLeak",
907            parent: "PrivateTypeLeakFinding",
908        },
909    },
910    IssueTsAliasMeta {
911        code: "unused-dependency",
912        alias: TsAliasMeta {
913            name: "UnusedDependency",
914            parent: "UnusedDependencyFinding",
915        },
916    },
917    IssueTsAliasMeta {
918        code: "unused-dev-dependency",
919        alias: TsAliasMeta {
920            name: "UnusedDependency",
921            parent: "UnusedDevDependencyFinding",
922        },
923    },
924    IssueTsAliasMeta {
925        code: "unused-optional-dependency",
926        alias: TsAliasMeta {
927            name: "UnusedDependency",
928            parent: "UnusedOptionalDependencyFinding",
929        },
930    },
931    IssueTsAliasMeta {
932        code: "unused-enum-member",
933        alias: TsAliasMeta {
934            name: "UnusedMember",
935            parent: "UnusedEnumMemberFinding",
936        },
937    },
938    IssueTsAliasMeta {
939        code: "unused-class-member",
940        alias: TsAliasMeta {
941            name: "UnusedMember",
942            parent: "UnusedClassMemberFinding",
943        },
944    },
945    IssueTsAliasMeta {
946        code: "unused-store-member",
947        alias: TsAliasMeta {
948            name: "UnusedMember",
949            parent: "UnusedStoreMemberFinding",
950        },
951    },
952    IssueTsAliasMeta {
953        code: "unresolved-import",
954        alias: TsAliasMeta {
955            name: "UnresolvedImport",
956            parent: "UnresolvedImportFinding",
957        },
958    },
959    IssueTsAliasMeta {
960        code: "unlisted-dependency",
961        alias: TsAliasMeta {
962            name: "UnlistedDependency",
963            parent: "UnlistedDependencyFinding",
964        },
965    },
966    IssueTsAliasMeta {
967        code: "duplicate-export",
968        alias: TsAliasMeta {
969            name: "DuplicateExport",
970            parent: "DuplicateExportFinding",
971        },
972    },
973    IssueTsAliasMeta {
974        code: "type-only-dependency",
975        alias: TsAliasMeta {
976            name: "TypeOnlyDependency",
977            parent: "TypeOnlyDependencyFinding",
978        },
979    },
980    IssueTsAliasMeta {
981        code: "test-only-dependency",
982        alias: TsAliasMeta {
983            name: "TestOnlyDependency",
984            parent: "TestOnlyDependencyFinding",
985        },
986    },
987    IssueTsAliasMeta {
988        code: "dev-dependency-in-production",
989        alias: TsAliasMeta {
990            name: "DevDependencyInProduction",
991            parent: "DevDependencyInProductionFinding",
992        },
993    },
994    IssueTsAliasMeta {
995        code: "circular-dependency",
996        alias: TsAliasMeta {
997            name: "CircularDependency",
998            parent: "CircularDependencyFinding",
999        },
1000    },
1001    IssueTsAliasMeta {
1002        code: "re-export-cycle",
1003        alias: TsAliasMeta {
1004            name: "ReExportCycle",
1005            parent: "ReExportCycleFinding",
1006        },
1007    },
1008    IssueTsAliasMeta {
1009        code: "boundary-violation",
1010        alias: TsAliasMeta {
1011            name: "BoundaryViolation",
1012            parent: "BoundaryViolationFinding",
1013        },
1014    },
1015    IssueTsAliasMeta {
1016        code: "unused-catalog-entry",
1017        alias: TsAliasMeta {
1018            name: "UnusedCatalogEntry",
1019            parent: "UnusedCatalogEntryFinding",
1020        },
1021    },
1022    IssueTsAliasMeta {
1023        code: "empty-catalog-group",
1024        alias: TsAliasMeta {
1025            name: "EmptyCatalogGroup",
1026            parent: "EmptyCatalogGroupFinding",
1027        },
1028    },
1029    IssueTsAliasMeta {
1030        code: "unresolved-catalog-reference",
1031        alias: TsAliasMeta {
1032            name: "UnresolvedCatalogReference",
1033            parent: "UnresolvedCatalogReferenceFinding",
1034        },
1035    },
1036    IssueTsAliasMeta {
1037        code: "unused-dependency-override",
1038        alias: TsAliasMeta {
1039            name: "UnusedDependencyOverride",
1040            parent: "UnusedDependencyOverrideFinding",
1041        },
1042    },
1043    IssueTsAliasMeta {
1044        code: "misconfigured-dependency-override",
1045        alias: TsAliasMeta {
1046            name: "MisconfiguredDependencyOverride",
1047            parent: "MisconfiguredDependencyOverrideFinding",
1048        },
1049    },
1050];
1051
1052/// All shared issue-to-result metadata rows.
1053pub const ISSUE_RESULT_META: &[IssueResultMeta] = &[
1054    IssueResultMeta {
1055        code: "unused-file",
1056        sarif_description: "File is not reachable from any entry point",
1057        meta_description: "Source files that are not imported by any other module and are not entry points. Detection uses graph reachability from configured entry points.",
1058        meta_docs_path: "explanations/dead-code#unused-files",
1059        meta_name: "Unused Files",
1060        summary_label: "Unused files",
1061        docs_anchor: "unused-files",
1062        result_key: "unused_files",
1063        counts_in_total: true,
1064    },
1065    IssueResultMeta {
1066        code: "unused-export",
1067        sarif_description: "Export is never imported",
1068        meta_description: "Named exports that are never imported by any other module in the project, including direct exports and re-exports through barrel files.",
1069        meta_docs_path: "explanations/dead-code#unused-exports",
1070        meta_name: "Unused Exports",
1071        summary_label: "Unused exports",
1072        docs_anchor: "unused-exports",
1073        result_key: "unused_exports",
1074        counts_in_total: true,
1075    },
1076    IssueResultMeta {
1077        code: "unused-type",
1078        sarif_description: "Type export is never imported",
1079        meta_description: "Type-only exports that are never imported. These do not generate runtime code but add maintenance burden.",
1080        meta_docs_path: "explanations/dead-code#unused-types",
1081        meta_name: "Unused Type Exports",
1082        summary_label: "Unused types",
1083        docs_anchor: "unused-types",
1084        result_key: "unused_types",
1085        counts_in_total: true,
1086    },
1087    IssueResultMeta {
1088        code: "private-type-leak",
1089        sarif_description: "Exported signature references a private type",
1090        meta_description: "Exported values or types whose public TypeScript signature references a same-file type declaration that is not exported.",
1091        meta_docs_path: "explanations/dead-code#private-type-leaks",
1092        meta_name: "Private Type Leaks",
1093        summary_label: "Private type leaks",
1094        docs_anchor: "private-type-leaks",
1095        result_key: "private_type_leaks",
1096        counts_in_total: true,
1097    },
1098    IssueResultMeta {
1099        code: "unused-dependency",
1100        sarif_description: "Dependency listed but never imported",
1101        meta_description: "Packages listed in dependencies that are never imported or required by any source file.",
1102        meta_docs_path: "explanations/dead-code#unused-dependencies",
1103        meta_name: "Unused Dependencies",
1104        summary_label: "Unused dependencies",
1105        docs_anchor: "unused-dependencies",
1106        result_key: "unused_dependencies",
1107        counts_in_total: true,
1108    },
1109    IssueResultMeta {
1110        code: "unused-dev-dependency",
1111        sarif_description: "Dev dependency listed but never imported",
1112        meta_description: "Packages listed in devDependencies that are never imported by test files, config files, or scripts.",
1113        meta_docs_path: "explanations/dead-code#unused-devdependencies",
1114        meta_name: "Unused Dev Dependencies",
1115        summary_label: "Unused devDependencies",
1116        docs_anchor: "unused-dependencies",
1117        result_key: "unused_dev_dependencies",
1118        counts_in_total: true,
1119    },
1120    IssueResultMeta {
1121        code: "unused-optional-dependency",
1122        sarif_description: "Optional dependency listed but never imported",
1123        meta_description: "Packages listed in optionalDependencies that are never imported.",
1124        meta_docs_path: "explanations/dead-code#unused-optionaldependencies",
1125        meta_name: "Unused Optional Dependencies",
1126        summary_label: "Unused optionalDependencies",
1127        docs_anchor: "unused-dependencies",
1128        result_key: "unused_optional_dependencies",
1129        counts_in_total: true,
1130    },
1131    IssueResultMeta {
1132        code: "unused-enum-member",
1133        sarif_description: "Enum member is never referenced",
1134        meta_description: "Enum members that are never referenced in the codebase.",
1135        meta_docs_path: "explanations/dead-code#unused-enum-members",
1136        meta_name: "Unused Enum Members",
1137        summary_label: "Unused enum members",
1138        docs_anchor: "unused-enum-members",
1139        result_key: "unused_enum_members",
1140        counts_in_total: true,
1141    },
1142    IssueResultMeta {
1143        code: "unused-class-member",
1144        sarif_description: "Class member is never referenced",
1145        meta_description: "Class methods and properties that are never referenced outside the class.",
1146        meta_docs_path: "explanations/dead-code#unused-class-members",
1147        meta_name: "Unused Class Members",
1148        summary_label: "Unused class members",
1149        docs_anchor: "unused-class-members",
1150        result_key: "unused_class_members",
1151        counts_in_total: true,
1152    },
1153    IssueResultMeta {
1154        code: "unused-store-member",
1155        sarif_description: "Store member is never accessed by any consumer",
1156        meta_description: "Pinia store members declared but never accessed by any consumer project-wide.",
1157        meta_docs_path: "explanations/dead-code#unused-store-members",
1158        meta_name: "Unused Store Members",
1159        summary_label: "Unused store members",
1160        docs_anchor: "unused-store-members",
1161        result_key: "unused_store_members",
1162        counts_in_total: true,
1163    },
1164    IssueResultMeta {
1165        code: "unresolved-import",
1166        sarif_description: "Import could not be resolved",
1167        meta_description: "Import specifiers that could not be resolved to a file on disk.",
1168        meta_docs_path: "explanations/dead-code#unresolved-imports",
1169        meta_name: "Unresolved Imports",
1170        summary_label: "Unresolved imports",
1171        docs_anchor: "unresolved-imports",
1172        result_key: "unresolved_imports",
1173        counts_in_total: true,
1174    },
1175    IssueResultMeta {
1176        code: "unlisted-dependency",
1177        sarif_description: "Dependency used but not in package.json",
1178        meta_description: "Packages imported in source code but not listed in package.json.",
1179        meta_docs_path: "explanations/dead-code#unlisted-dependencies",
1180        meta_name: "Unlisted Dependencies",
1181        summary_label: "Unlisted dependencies",
1182        docs_anchor: "unlisted-dependencies",
1183        result_key: "unlisted_dependencies",
1184        counts_in_total: true,
1185    },
1186    IssueResultMeta {
1187        code: "duplicate-export",
1188        sarif_description: "Export name appears in multiple modules",
1189        meta_description: "The same export name is defined in multiple modules.",
1190        meta_docs_path: "explanations/dead-code#duplicate-exports",
1191        meta_name: "Duplicate Exports",
1192        summary_label: "Duplicate exports",
1193        docs_anchor: "duplicate-exports",
1194        result_key: "duplicate_exports",
1195        counts_in_total: true,
1196    },
1197    IssueResultMeta {
1198        code: "type-only-dependency",
1199        sarif_description: "Production dependency only used via type-only imports",
1200        meta_description: "Production dependencies that are only imported via type-only imports.",
1201        meta_docs_path: "explanations/dead-code#type-only-dependencies",
1202        meta_name: "Type-only Dependencies",
1203        summary_label: "Type-only dependencies",
1204        docs_anchor: "type-only-dependencies",
1205        result_key: "type_only_dependencies",
1206        counts_in_total: true,
1207    },
1208    IssueResultMeta {
1209        code: "test-only-dependency",
1210        sarif_description: "Production dependency only imported by test files",
1211        meta_description: "Production dependencies that are only imported from test files.",
1212        meta_docs_path: "explanations/dead-code#test-only-dependencies",
1213        meta_name: "Test-only Dependencies",
1214        summary_label: "Test-only dependencies",
1215        docs_anchor: "test-only-dependencies",
1216        result_key: "test_only_dependencies",
1217        counts_in_total: true,
1218    },
1219    IssueResultMeta {
1220        code: "dev-dependency-in-production",
1221        sarif_description: "devDependency imported by production code with a runtime import",
1222        meta_description: "devDependencies imported by production source code via a runtime/value import; they should be promoted to dependencies.",
1223        meta_docs_path: "explanations/dead-code#dev-dependencies-in-production",
1224        meta_name: "Dev Dependencies Used in Production",
1225        summary_label: "Dev dependencies used in production",
1226        docs_anchor: "dev-dependencies-in-production",
1227        result_key: "dev_dependencies_in_production",
1228        counts_in_total: true,
1229    },
1230    IssueResultMeta {
1231        code: "circular-dependency",
1232        sarif_description: "Circular dependency chain detected",
1233        meta_description: "A cycle in the module import graph.",
1234        meta_docs_path: "explanations/dead-code#circular-dependencies",
1235        meta_name: "Circular Dependencies",
1236        summary_label: "Circular dependencies",
1237        docs_anchor: "circular-dependencies",
1238        result_key: "circular_dependencies",
1239        counts_in_total: true,
1240    },
1241    IssueResultMeta {
1242        code: "re-export-cycle",
1243        sarif_description: "Two or more barrel files re-export from each other in a loop",
1244        meta_description: "A barrel file re-exports from another barrel that ultimately re-exports back.",
1245        meta_docs_path: "explanations/dead-code#re-export-cycles",
1246        meta_name: "Re-Export Cycles",
1247        summary_label: "Re-export cycles",
1248        docs_anchor: "re-export-cycles",
1249        result_key: "re_export_cycles",
1250        counts_in_total: true,
1251    },
1252    IssueResultMeta {
1253        code: "boundary-violation",
1254        sarif_description: "Import crosses a configured architecture boundary",
1255        meta_description: "A module imports from a zone that its configured boundary rules do not allow.",
1256        meta_docs_path: "explanations/dead-code#boundary-violations",
1257        meta_name: "Boundary Violations",
1258        summary_label: "Boundary violations",
1259        docs_anchor: "boundary-violations",
1260        result_key: "boundary_violations",
1261        counts_in_total: true,
1262    },
1263    IssueResultMeta {
1264        code: "boundary-coverage",
1265        sarif_description: "Source file matches no configured architecture boundary zone",
1266        meta_description: "A reachable source file is not assigned to any configured boundary zone while boundary coverage is required.",
1267        meta_docs_path: "explanations/dead-code#boundary-violations",
1268        meta_name: "Boundary Coverage",
1269        summary_label: "Boundary coverage",
1270        docs_anchor: "boundary-violations",
1271        result_key: "boundary_coverage_violations",
1272        counts_in_total: true,
1273    },
1274    IssueResultMeta {
1275        code: "boundary-call-violation",
1276        sarif_description: "Zoned file calls a callee its zone forbids",
1277        meta_description: "A file classified into a boundary zone calls a callee matching one of the zone's forbidden call patterns.",
1278        meta_docs_path: "explanations/dead-code#boundary-violations",
1279        meta_name: "Boundary Call Violation",
1280        summary_label: "Boundary calls",
1281        docs_anchor: "boundary-violations",
1282        result_key: "boundary_call_violations",
1283        counts_in_total: true,
1284    },
1285    IssueResultMeta {
1286        code: "policy-violation",
1287        sarif_description: "Banned usage matched a rule-pack rule",
1288        meta_description: "A call site, import, or catalogue-derived effect matched a configured rule pack rule.",
1289        meta_docs_path: "explanations/dead-code#policy-violations",
1290        meta_name: "Policy Violation",
1291        summary_label: "Policy violations",
1292        docs_anchor: "policy-violations",
1293        result_key: "policy_violations",
1294        counts_in_total: true,
1295    },
1296    IssueResultMeta {
1297        code: "invalid-client-export",
1298        sarif_description: "\"use client\" file exports a server-only / route-config name",
1299        meta_description: "A file carrying the use client directive also exports a Next.js server-only or route-segment config name.",
1300        meta_docs_path: "explanations/dead-code#invalid-client-exports",
1301        meta_name: "Invalid client export",
1302        summary_label: "Invalid client exports",
1303        docs_anchor: "invalid-client-exports",
1304        result_key: "invalid_client_exports",
1305        counts_in_total: true,
1306    },
1307    IssueResultMeta {
1308        code: "mixed-client-server-barrel",
1309        sarif_description: "Barrel re-exports both a \"use client\" module and a server-only module",
1310        meta_description: "A barrel file forwards a name from a use client module alongside a name from a server-only module.",
1311        meta_docs_path: "explanations/dead-code#mixed-client-server-barrels",
1312        meta_name: "Mixed client/server barrel",
1313        summary_label: "Mixed client/server barrels",
1314        docs_anchor: "mixed-client-server-barrels",
1315        result_key: "mixed_client_server_barrels",
1316        counts_in_total: true,
1317    },
1318    IssueResultMeta {
1319        code: "misplaced-directive",
1320        sarif_description: "\"use client\" / \"use server\" directive is not in the leading position and is ignored",
1321        meta_description: "A use client or use server directive string appears after a non-directive statement and is ignored.",
1322        meta_docs_path: "explanations/dead-code#misplaced-directives",
1323        meta_name: "Misplaced directive",
1324        summary_label: "Misplaced directives",
1325        docs_anchor: "misplaced-directives",
1326        result_key: "misplaced_directives",
1327        counts_in_total: true,
1328    },
1329    IssueResultMeta {
1330        code: "unprovided-inject",
1331        sarif_description: "inject() / getContext() reads a key that no provide() / setContext() supplies",
1332        meta_description: "A Vue inject or Svelte getContext reads a dependency-injection key that no matching provider supplies.",
1333        meta_docs_path: "explanations/dead-code#unprovided-injects",
1334        meta_name: "Unprovided injects",
1335        summary_label: "Unprovided injects",
1336        docs_anchor: "unprovided-inject",
1337        result_key: "unprovided_injects",
1338        counts_in_total: true,
1339    },
1340    IssueResultMeta {
1341        code: "unrendered-component",
1342        sarif_description: "A Vue / Svelte component is reachable through a barrel but rendered nowhere",
1343        meta_description: "A Vue or Svelte single-file component is reachable through the graph but rendered nowhere in the project.",
1344        meta_docs_path: "explanations/dead-code#unrendered-components",
1345        meta_name: "Unrendered components",
1346        summary_label: "Unrendered components",
1347        docs_anchor: "unrendered-component",
1348        result_key: "unrendered_components",
1349        counts_in_total: true,
1350    },
1351    IssueResultMeta {
1352        code: "unused-component-prop",
1353        sarif_description: "A Vue, Svelte, or React component prop is referenced nowhere in its own component",
1354        meta_description: "A declared Vue, Svelte, React, or Preact component prop is referenced nowhere inside its own component.",
1355        meta_docs_path: "explanations/dead-code#unused-component-props",
1356        meta_name: "Unused component props",
1357        summary_label: "Unused component props",
1358        docs_anchor: "unused-component-prop",
1359        result_key: "unused_component_props",
1360        counts_in_total: true,
1361    },
1362    IssueResultMeta {
1363        code: "unused-component-emit",
1364        sarif_description: "A Vue <script setup> defineEmits event is emitted nowhere in its own component",
1365        meta_description: "A Vue script setup defineEmits event is emitted nowhere in its own component.",
1366        meta_docs_path: "explanations/dead-code#unused-component-emits",
1367        meta_name: "Unused component emits",
1368        summary_label: "Unused component emits",
1369        docs_anchor: "unused-component-emit",
1370        result_key: "unused_component_emits",
1371        counts_in_total: true,
1372    },
1373    IssueResultMeta {
1374        code: "unused-component-input",
1375        sarif_description: "An Angular @Input() / signal input() / model() input is read nowhere in its own component",
1376        meta_description: "An Angular input is read nowhere in its own component.",
1377        meta_docs_path: "explanations/dead-code#unused-component-inputs",
1378        meta_name: "Unused component inputs",
1379        summary_label: "Unused component inputs",
1380        docs_anchor: "unused-component-input",
1381        result_key: "unused_component_inputs",
1382        counts_in_total: true,
1383    },
1384    IssueResultMeta {
1385        code: "unused-component-output",
1386        sarif_description: "An Angular @Output() / signal output() output is emitted nowhere in its own component",
1387        meta_description: "An Angular output is emitted nowhere in its own component.",
1388        meta_docs_path: "explanations/dead-code#unused-component-outputs",
1389        meta_name: "Unused component outputs",
1390        summary_label: "Unused component outputs",
1391        docs_anchor: "unused-component-output",
1392        result_key: "unused_component_outputs",
1393        counts_in_total: true,
1394    },
1395    IssueResultMeta {
1396        code: "unused-svelte-event",
1397        sarif_description: "A Svelte component dispatches a createEventDispatcher event whose name is listened to nowhere in the project",
1398        meta_description: "A Svelte component dispatches a custom event whose name is listened to nowhere in the analyzed project.",
1399        meta_docs_path: "explanations/dead-code#unused-svelte-events",
1400        meta_name: "Unused Svelte events",
1401        summary_label: "Unused Svelte events",
1402        docs_anchor: "unused-svelte-event",
1403        result_key: "unused_svelte_events",
1404        counts_in_total: true,
1405    },
1406    IssueResultMeta {
1407        code: "unused-server-action",
1408        sarif_description: "A Next.js Server Action exported from a \"use server\" file is referenced by no code in the project",
1409        meta_description: "A Next.js Server Action exported from a use server file is referenced by no code in the project.",
1410        meta_docs_path: "explanations/dead-code#unused-server-actions",
1411        meta_name: "Unused server actions",
1412        summary_label: "Unused server actions",
1413        docs_anchor: "unused-server-action",
1414        result_key: "unused_server_actions",
1415        counts_in_total: true,
1416    },
1417    IssueResultMeta {
1418        code: "unused-load-data-key",
1419        sarif_description: "A SvelteKit load() return-object key is read by no consumer",
1420        meta_description: "A SvelteKit load return-object key is read by no route or project-wide consumer.",
1421        meta_docs_path: "explanations/dead-code#unused-load-data-keys",
1422        meta_name: "Unused load data keys",
1423        summary_label: "Unused load data keys",
1424        docs_anchor: "unused-load-data-key",
1425        result_key: "unused_load_data_keys",
1426        counts_in_total: true,
1427    },
1428    IssueResultMeta {
1429        code: "route-collision",
1430        sarif_description: "Two or more Next.js App Router route files resolve to the same URL",
1431        meta_description: "Two or more Next.js App Router route files resolve to the same URL within one app root.",
1432        meta_docs_path: "explanations/dead-code#route-collisions",
1433        meta_name: "Route collision",
1434        summary_label: "Route collisions",
1435        docs_anchor: "route-collisions",
1436        result_key: "route_collisions",
1437        counts_in_total: true,
1438    },
1439    IssueResultMeta {
1440        code: "dynamic-segment-name-conflict",
1441        sarif_description: "Sibling Next.js dynamic route segments use different slug names at the same position",
1442        meta_description: "Sibling Next.js dynamic route segments use different slug names at the same position.",
1443        meta_docs_path: "explanations/dead-code#dynamic-segment-name-conflicts",
1444        meta_name: "Dynamic segment name conflict",
1445        summary_label: "Dynamic segment conflicts",
1446        docs_anchor: "dynamic-segment-name-conflicts",
1447        result_key: "dynamic_segment_name_conflicts",
1448        counts_in_total: true,
1449    },
1450    IssueResultMeta {
1451        code: "stale-suppression",
1452        sarif_description: "Suppression comment or tag no longer matches any issue",
1453        meta_description: "A fallow suppression comment or tag no longer matches any active issue.",
1454        meta_docs_path: "explanations/dead-code#stale-suppressions",
1455        meta_name: "Stale Suppressions",
1456        summary_label: "Stale suppressions",
1457        docs_anchor: "stale-suppressions",
1458        result_key: "stale_suppressions",
1459        counts_in_total: true,
1460    },
1461    IssueResultMeta {
1462        code: "unused-catalog-entry",
1463        sarif_description: "Catalog entry not referenced by any workspace package",
1464        meta_description: "A package manager catalog entry is not referenced by any workspace package.json.",
1465        meta_docs_path: "explanations/dead-code#unused-catalog-entries",
1466        meta_name: "Unused catalog entry",
1467        summary_label: "Unused catalog entries",
1468        docs_anchor: "unused-catalog-entries",
1469        result_key: "unused_catalog_entries",
1470        counts_in_total: true,
1471    },
1472    IssueResultMeta {
1473        code: "empty-catalog-group",
1474        sarif_description: "Named catalog group has no entries",
1475        meta_description: "A named package manager catalog group has no package entries.",
1476        meta_docs_path: "explanations/dead-code#empty-catalog-groups",
1477        meta_name: "Empty catalog group",
1478        summary_label: "Empty catalog groups",
1479        docs_anchor: "empty-catalog-groups",
1480        result_key: "empty_catalog_groups",
1481        counts_in_total: true,
1482    },
1483    IssueResultMeta {
1484        code: "unresolved-catalog-reference",
1485        sarif_description: "package.json references a catalog that does not declare the package",
1486        meta_description: "A workspace package.json uses a catalog protocol reference that no catalog declares.",
1487        meta_docs_path: "explanations/dead-code#unresolved-catalog-references",
1488        meta_name: "Unresolved catalog reference",
1489        summary_label: "Unresolved catalog references",
1490        docs_anchor: "unresolved-catalog-references",
1491        result_key: "unresolved_catalog_references",
1492        counts_in_total: true,
1493    },
1494    IssueResultMeta {
1495        code: "unused-dependency-override",
1496        sarif_description: "pnpm.overrides entry targets a package not declared or resolved",
1497        meta_description: "A pnpm dependency override targets a package not declared by any workspace package and not present in the lockfile.",
1498        meta_docs_path: "explanations/dead-code#unused-dependency-overrides",
1499        meta_name: "Unused pnpm dependency override",
1500        summary_label: "Unused dependency overrides",
1501        docs_anchor: "unused-dependency-overrides",
1502        result_key: "unused_dependency_overrides",
1503        counts_in_total: true,
1504    },
1505    IssueResultMeta {
1506        code: "misconfigured-dependency-override",
1507        sarif_description: "pnpm.overrides entry has an unparsable key or value",
1508        meta_description: "A pnpm dependency override key or value does not parse as a valid override spec.",
1509        meta_docs_path: "explanations/dead-code#misconfigured-dependency-overrides",
1510        meta_name: "Misconfigured pnpm dependency override",
1511        summary_label: "Misconfigured dependency overrides",
1512        docs_anchor: "misconfigured-dependency-overrides",
1513        result_key: "misconfigured_dependency_overrides",
1514        counts_in_total: true,
1515    },
1516    IssueResultMeta {
1517        code: "prop-drilling",
1518        sarif_description: "A React/Preact prop is forwarded unchanged through 3+ pass-through components to a distant consumer",
1519        meta_description: "A React or Preact prop is forwarded unchanged through multiple pass-through components to a distant consumer.",
1520        meta_docs_path: "explanations/dead-code#prop-drilling",
1521        meta_name: "Prop drilling",
1522        summary_label: "Prop drilling",
1523        docs_anchor: "prop-drilling",
1524        result_key: "prop_drilling_chains",
1525        counts_in_total: false,
1526    },
1527    IssueResultMeta {
1528        code: "thin-wrapper",
1529        sarif_description: "A React/Preact component whose whole body is a single spread-forwarded child render (a candidate for inlining)",
1530        meta_description: "A React or Preact component is structural indirection around a single spread-forwarded child render.",
1531        meta_docs_path: "explanations/dead-code#thin-wrapper",
1532        meta_name: "Thin wrapper",
1533        summary_label: "Thin wrappers",
1534        docs_anchor: "thin-wrapper",
1535        result_key: "thin_wrappers",
1536        counts_in_total: false,
1537    },
1538    IssueResultMeta {
1539        code: "duplicate-prop-shape",
1540        sarif_description: "Three or more React/Preact components across two or more files declare an identical prop-name set (a missing shared Props type)",
1541        meta_description: "Multiple React or Preact components declare an identical significant prop-name set.",
1542        meta_docs_path: "explanations/dead-code#duplicate-prop-shape",
1543        meta_name: "Duplicate prop shape",
1544        summary_label: "Duplicate prop shapes",
1545        docs_anchor: "duplicate-prop-shape",
1546        result_key: "duplicate_prop_shapes",
1547        counts_in_total: false,
1548    },
1549];
1550
1551/// Canonical names and aliases accepted by `IssueKind::parse`.
1552pub static KNOWN_ISSUE_KIND_NAMES: LazyLock<Vec<&'static str>> =
1553    LazyLock::new(known_issue_kind_names_from_meta);
1554
1555/// CLI filter flags on `fallow dead-code` that scope output to one issue family.
1556pub static DEAD_CODE_FILTER_FLAGS: LazyLock<Vec<&'static str>> =
1557    LazyLock::new(dead_code_filter_flags_from_meta);
1558
1559/// MCP issue selector names mapped to dead-code CLI flags.
1560pub static MCP_ISSUE_TYPE_FLAGS: LazyLock<Vec<(&'static str, &'static str)>> =
1561    LazyLock::new(mcp_issue_type_flags_from_meta);
1562
1563/// Result issue codes emitted by the dead-code CodeClimate formatter.
1564pub static CODECLIMATE_RESULT_CODES: LazyLock<Vec<&'static str>> =
1565    LazyLock::new(codeclimate_result_codes_from_meta);
1566
1567fn known_issue_kind_names_from_meta() -> Vec<&'static str> {
1568    let mut names = Vec::new();
1569    for meta in ISSUE_KIND_META.iter().filter(|meta| meta.kind.is_some()) {
1570        push_unique(&mut names, meta.code);
1571        for alias in meta.aliases {
1572            push_unique(&mut names, *alias);
1573        }
1574    }
1575    names
1576}
1577
1578fn dead_code_filter_flags_from_meta() -> Vec<&'static str> {
1579    let mut flags = Vec::new();
1580    for meta in ISSUE_KIND_META {
1581        if let Some(flag) = meta.filter_flag {
1582            push_unique(&mut flags, flag);
1583        }
1584    }
1585    flags
1586}
1587
1588fn mcp_issue_type_flags_from_meta() -> Vec<(&'static str, &'static str)> {
1589    ISSUE_KIND_META
1590        .iter()
1591        .filter_map(|meta| meta.mcp_pair())
1592        .collect()
1593}
1594
1595fn codeclimate_result_codes_from_meta() -> Vec<&'static str> {
1596    ISSUE_RESULT_META
1597        .iter()
1598        .filter(|meta| meta.counts_in_total)
1599        .map(|meta| meta.code)
1600        .collect()
1601}
1602
1603fn push_unique<T: Copy + PartialEq>(items: &mut Vec<T>, item: T) {
1604    if !items.contains(&item) {
1605        items.push(item);
1606    }
1607}
1608
1609/// Lookup metadata by canonical code.
1610#[must_use]
1611pub fn issue_meta_by_code(code: &str) -> Option<&'static IssueKindMeta> {
1612    ISSUE_KIND_META.iter().find(|meta| meta.code == code)
1613}
1614
1615/// Lookup metadata by canonical code or alias.
1616#[must_use]
1617pub fn issue_meta_for_token(token: &str) -> Option<&'static IssueKindMeta> {
1618    ISSUE_KIND_META
1619        .iter()
1620        .find(|meta| meta.code == token || meta.aliases.contains(&token))
1621}
1622
1623/// Lookup metadata by any shared contract token: canonical code, alias,
1624/// config key, MCP selector, suppression token, or CLI filter flag.
1625#[must_use]
1626pub fn issue_meta_for_contract_token(token: &str) -> Option<&'static IssueKindMeta> {
1627    let normalized = token.trim().trim_start_matches("--").replace('_', "-");
1628    ISSUE_KIND_META
1629        .iter()
1630        .find(|meta| issue_meta_matches_contract_token(meta, &normalized))
1631}
1632
1633/// Whether a metadata row owns the provided shared contract token.
1634#[must_use]
1635pub fn issue_meta_matches_contract_token(meta: &IssueKindMeta, token: &str) -> bool {
1636    let normalized = token.trim().trim_start_matches("--").replace('_', "-");
1637    meta.code == normalized
1638        || meta.aliases.contains(&normalized.as_str())
1639        || meta.config_key == Some(normalized.as_str())
1640        || meta.mcp_issue_type == Some(normalized.as_str())
1641        || meta.suppress_token == Some(normalized.as_str())
1642        || meta.filter_flag.map(|flag| flag.trim_start_matches("--")) == Some(normalized.as_str())
1643}
1644
1645/// Lookup metadata by backing issue kind.
1646#[must_use]
1647pub fn issue_meta_by_kind(kind: IssueKind) -> Option<&'static IssueKindMeta> {
1648    ISSUE_KIND_META.iter().find(|meta| meta.kind == Some(kind))
1649}
1650
1651/// Lookup serialized result metadata by canonical issue code.
1652#[must_use]
1653pub fn issue_result_meta_by_code(code: &str) -> Option<&'static IssueResultMeta> {
1654    ISSUE_RESULT_META.iter().find(|meta| meta.code == code)
1655}
1656
1657/// SARIF rule ids used by CI formatters for a canonical issue code.
1658#[must_use]
1659pub fn issue_sarif_rule_ids(code: &str) -> Vec<String> {
1660    let mut ids = vec![format!("fallow/{code}")];
1661    if code == "stale-suppression" {
1662        ids.push("fallow/missing-suppression-reason".to_string());
1663    }
1664    ids
1665}
1666
1667/// Short SARIF rule description for a rule id emitted by dead-code output.
1668#[must_use]
1669pub fn issue_sarif_rule_description(rule_id: &str) -> Option<&'static str> {
1670    let code = rule_id.strip_prefix("fallow/")?;
1671    if code == "missing-suppression-reason" {
1672        return Some("Suppression comment omits a required reason");
1673    }
1674    issue_result_meta_by_code(code).map(|meta| meta.sarif_description)
1675}
1676
1677/// CodeClimate check names used by CI formatters for a canonical issue code.
1678#[must_use]
1679pub fn issue_codeclimate_check_names(code: &str) -> Vec<String> {
1680    if !CODECLIMATE_RESULT_CODES.contains(&code) {
1681        return Vec::new();
1682    }
1683    issue_sarif_rule_ids(code)
1684}
1685
1686/// Documentation anchor under `/explanations/dead-code` for a canonical issue
1687/// code.
1688#[must_use]
1689pub fn issue_docs_anchor(code: &str) -> Option<&'static str> {
1690    issue_result_meta_by_code(code).map(|meta| meta.docs_anchor)
1691}
1692
1693/// Published TypeScript alias policy for backwards-compatible bare names.
1694#[must_use]
1695pub fn issue_ts_alias(code: &str) -> Option<TsAliasMeta> {
1696    ISSUE_TS_ALIAS_META
1697        .iter()
1698        .find(|meta| meta.code == code)
1699        .map(|meta| meta.alias)
1700}
1701
1702/// Rows exposed by the LSP issue-type capability.
1703pub fn diagnostic_issue_metas() -> impl Iterator<Item = &'static IssueKindMeta> {
1704    ISSUE_KIND_META.iter().filter(|meta| meta.lsp)
1705}
1706
1707/// Rows that map to a serialized `AnalysisResults` array.
1708pub fn result_issue_metas() -> impl Iterator<Item = &'static IssueResultMeta> {
1709    ISSUE_RESULT_META.iter()
1710}
1711
1712/// Rows whose serialized `AnalysisResults` array contributes to `total_issues`.
1713pub fn counted_result_issue_metas() -> impl Iterator<Item = &'static IssueResultMeta> {
1714    result_issue_metas().filter(|meta| meta.counts_in_total)
1715}
1716
1717#[cfg(test)]
1718mod tests {
1719    use std::collections::BTreeSet;
1720
1721    use crate::results::TOTAL_ISSUE_RESULT_KEYS;
1722
1723    use super::*;
1724
1725    #[test]
1726    fn known_names_round_trip_through_metadata() {
1727        for name in KNOWN_ISSUE_KIND_NAMES.iter() {
1728            let meta = issue_meta_for_token(name)
1729                .unwrap_or_else(|| panic!("known issue name {name} missing metadata row"));
1730            assert!(
1731                meta.kind.is_some(),
1732                "known issue name {name} maps to non-IssueKind metadata"
1733            );
1734        }
1735    }
1736
1737    #[test]
1738    fn issue_kind_variants_have_metadata() {
1739        for &kind in IssueKind::ALL {
1740            assert!(
1741                issue_meta_by_kind(kind).is_some(),
1742                "IssueKind {kind:?} has no metadata row"
1743            );
1744        }
1745    }
1746
1747    #[test]
1748    fn dead_code_filter_flags_match_metadata() {
1749        let from_constants: BTreeSet<&str> = DEAD_CODE_FILTER_FLAGS.iter().copied().collect();
1750        let from_meta: BTreeSet<&str> = ISSUE_KIND_META
1751            .iter()
1752            .filter_map(|meta| meta.filter_flag)
1753            .collect();
1754        assert_eq!(from_constants, from_meta);
1755    }
1756
1757    #[test]
1758    fn mcp_issue_type_flags_match_metadata() {
1759        let from_constants: BTreeSet<(&str, &str)> = MCP_ISSUE_TYPE_FLAGS.iter().copied().collect();
1760        let from_meta: BTreeSet<(&str, &str)> = ISSUE_KIND_META
1761            .iter()
1762            .filter_map(|meta| meta.mcp_pair())
1763            .collect();
1764        assert_eq!(from_constants, from_meta);
1765    }
1766
1767    #[test]
1768    fn lsp_exposes_only_actual_diagnostic_codes() {
1769        let codes: BTreeSet<&str> = diagnostic_issue_metas().map(|meta| meta.code).collect();
1770        assert!(codes.contains("boundary-violation"));
1771        assert!(!codes.contains("boundary-coverage"));
1772        assert!(!codes.contains("boundary-call-violation"));
1773    }
1774
1775    #[test]
1776    fn issue_codes_are_unique() {
1777        let mut seen = BTreeSet::new();
1778        for meta in ISSUE_KIND_META {
1779            assert!(seen.insert(meta.code), "duplicate issue code {}", meta.code);
1780        }
1781    }
1782
1783    #[test]
1784    fn contract_tokens_resolve_through_metadata() {
1785        for meta in ISSUE_KIND_META {
1786            assert_eq!(
1787                issue_meta_for_contract_token(meta.code).map(|resolved| resolved.code),
1788                Some(meta.code),
1789                "canonical issue code {} should resolve through contract token lookup",
1790                meta.code
1791            );
1792            for token in meta.aliases {
1793                assert!(
1794                    issue_meta_matches_contract_token(meta, token),
1795                    "alias {token} should match {}",
1796                    meta.code
1797                );
1798            }
1799            for token in [
1800                meta.config_key,
1801                meta.mcp_issue_type,
1802                meta.suppress_token,
1803                meta.filter_flag,
1804            ]
1805            .into_iter()
1806            .flatten()
1807            {
1808                assert!(
1809                    issue_meta_for_contract_token(token).is_some(),
1810                    "contract token {token} should resolve through metadata"
1811                );
1812                assert!(
1813                    issue_meta_matches_contract_token(meta, token),
1814                    "contract token {token} should match {}",
1815                    meta.code
1816                );
1817            }
1818        }
1819    }
1820
1821    #[test]
1822    fn sarif_rule_descriptions_cover_result_metadata() {
1823        for meta in result_issue_metas() {
1824            for rule_id in issue_sarif_rule_ids(meta.code) {
1825                assert!(
1826                    issue_sarif_rule_description(&rule_id).is_some(),
1827                    "SARIF rule {rule_id} for {} needs a central description",
1828                    meta.code
1829                );
1830            }
1831        }
1832    }
1833
1834    #[test]
1835    fn result_meta_codes_have_issue_metadata() {
1836        for meta in ISSUE_RESULT_META {
1837            assert!(
1838                issue_meta_by_code(meta.code).is_some(),
1839                "result metadata code {} has no issue metadata row",
1840                meta.code
1841            );
1842        }
1843    }
1844
1845    #[test]
1846    fn result_meta_codes_have_docs_anchors() {
1847        for meta in ISSUE_RESULT_META {
1848            let issue = issue_meta_by_code(meta.code)
1849                .unwrap_or_else(|| panic!("result metadata code {} has no issue row", meta.code));
1850            assert_eq!(
1851                issue.docs_anchor(),
1852                Some(meta.docs_anchor),
1853                "result metadata code {} has mismatched docs anchor",
1854                meta.code
1855            );
1856        }
1857    }
1858
1859    #[test]
1860    fn result_meta_codes_have_summary_labels() {
1861        for meta in ISSUE_RESULT_META {
1862            assert!(
1863                !meta.summary_label.is_empty(),
1864                "result metadata code {} has no summary label",
1865                meta.code
1866            );
1867        }
1868    }
1869
1870    #[test]
1871    fn result_meta_codes_have_meta_names() {
1872        for meta in ISSUE_RESULT_META {
1873            assert!(
1874                !meta.meta_name.is_empty(),
1875                "result metadata code {} has no meta name",
1876                meta.code
1877            );
1878        }
1879    }
1880
1881    #[test]
1882    fn result_meta_codes_have_meta_docs_paths() {
1883        for meta in ISSUE_RESULT_META {
1884            assert!(
1885                meta.meta_docs_path.starts_with("explanations/dead-code#"),
1886                "result metadata code {} has invalid meta docs path",
1887                meta.code
1888            );
1889        }
1890    }
1891
1892    #[test]
1893    fn result_meta_codes_have_meta_descriptions() {
1894        for meta in ISSUE_RESULT_META {
1895            assert!(
1896                !meta.meta_description.is_empty(),
1897                "result metadata code {} has no meta description",
1898                meta.code
1899            );
1900        }
1901    }
1902
1903    #[test]
1904    fn ci_format_ids_are_prefixed_and_known() {
1905        let result_codes: BTreeSet<&str> = result_issue_metas().map(|meta| meta.code).collect();
1906        let codeclimate_codes: BTreeSet<&str> = CODECLIMATE_RESULT_CODES.iter().copied().collect();
1907        assert!(codeclimate_codes.is_subset(&result_codes));
1908
1909        for meta in result_issue_metas() {
1910            let issue = issue_meta_by_code(meta.code)
1911                .unwrap_or_else(|| panic!("result metadata code {} has no issue row", meta.code));
1912            assert!(issue.sarif_enabled());
1913            let sarif_ids = issue.sarif_rule_ids();
1914            assert!(sarif_ids.contains(&format!("fallow/{}", meta.code)));
1915            for rule_id in sarif_ids {
1916                assert!(
1917                    rule_id.starts_with("fallow/"),
1918                    "result metadata code {} has unprefixed SARIF rule id {rule_id}",
1919                    meta.code
1920                );
1921            }
1922            for check_name in issue.codeclimate_check_names() {
1923                assert!(
1924                    check_name.starts_with("fallow/"),
1925                    "result metadata code {} has unprefixed CodeClimate check name {check_name}",
1926                    meta.code
1927                );
1928            }
1929        }
1930    }
1931
1932    #[test]
1933    fn ci_summary_check_tables_match_result_metadata() {
1934        assert_summary_check_table_matches_result_metadata(
1935            include_str!("../../../action/jq/summary-check.jq"),
1936            "action/jq/summary-check.jq",
1937        );
1938        assert_summary_check_table_matches_result_metadata(
1939            include_str!("../../../ci/jq/summary-check.jq"),
1940            "ci/jq/summary-check.jq",
1941        );
1942    }
1943
1944    #[test]
1945    fn ci_summary_combined_tables_match_result_metadata() {
1946        assert_summary_combined_table_matches_result_metadata(
1947            include_str!("../../../action/jq/summary-combined.jq"),
1948            "action/jq/summary-combined.jq",
1949        );
1950        assert_summary_combined_table_matches_result_metadata(
1951            include_str!("../../../ci/jq/summary-combined.jq"),
1952            "ci/jq/summary-combined.jq",
1953        );
1954    }
1955
1956    fn assert_summary_check_table_matches_result_metadata(source: &str, path: &str) {
1957        for meta in counted_result_issue_metas() {
1958            let expected = format!(
1959                r#"table_row("{}"; "{}"; "{}")"#,
1960                meta.summary_label, meta.result_key, meta.docs_anchor
1961            );
1962            assert!(
1963                source.contains(&expected),
1964                "{path} must include registry row for {} as `{expected}`",
1965                meta.code
1966            );
1967        }
1968    }
1969
1970    fn assert_summary_combined_table_matches_result_metadata(source: &str, path: &str) {
1971        for meta in counted_result_issue_metas() {
1972            let row = source
1973                .lines()
1974                .find(|line| line.contains(&format!(".check.{}", meta.result_key)))
1975                .unwrap_or_else(|| {
1976                    panic!(
1977                        "{path} must include combined summary row for {} ({})",
1978                        meta.code, meta.result_key
1979                    )
1980                });
1981            assert!(
1982                row.contains(&format!("[{}]", meta.summary_label)),
1983                "{path} row for {} must use registry label `{}`: {row}",
1984                meta.code,
1985                meta.summary_label
1986            );
1987            assert!(
1988                row.contains(&format!(r#"docs("{}")"#, meta.docs_anchor)),
1989                "{path} row for {} must use registry docs anchor `{}`: {row}",
1990                meta.code,
1991                meta.docs_anchor
1992            );
1993        }
1994    }
1995
1996    #[test]
1997    fn ts_alias_policy_is_explicit() {
1998        let aliases: BTreeSet<(&str, &str)> = ISSUE_TS_ALIAS_META
1999            .iter()
2000            .map(|meta| (meta.alias.name, meta.alias.parent))
2001            .collect();
2002
2003        assert_eq!(
2004            BTreeSet::from([
2005                ("BoundaryViolation", "BoundaryViolationFinding"),
2006                ("CircularDependency", "CircularDependencyFinding"),
2007                (
2008                    "DevDependencyInProduction",
2009                    "DevDependencyInProductionFinding",
2010                ),
2011                ("DuplicateExport", "DuplicateExportFinding"),
2012                ("EmptyCatalogGroup", "EmptyCatalogGroupFinding"),
2013                (
2014                    "MisconfiguredDependencyOverride",
2015                    "MisconfiguredDependencyOverrideFinding",
2016                ),
2017                ("PrivateTypeLeak", "PrivateTypeLeakFinding"),
2018                ("ReExportCycle", "ReExportCycleFinding"),
2019                ("TestOnlyDependency", "TestOnlyDependencyFinding"),
2020                ("TypeOnlyDependency", "TypeOnlyDependencyFinding"),
2021                (
2022                    "UnresolvedCatalogReference",
2023                    "UnresolvedCatalogReferenceFinding",
2024                ),
2025                ("UnresolvedImport", "UnresolvedImportFinding"),
2026                ("UnlistedDependency", "UnlistedDependencyFinding"),
2027                ("UnusedCatalogEntry", "UnusedCatalogEntryFinding"),
2028                ("UnusedDependency", "UnusedDependencyFinding"),
2029                ("UnusedDependency", "UnusedDevDependencyFinding"),
2030                ("UnusedDependency", "UnusedOptionalDependencyFinding"),
2031                (
2032                    "UnusedDependencyOverride",
2033                    "UnusedDependencyOverrideFinding",
2034                ),
2035                ("UnusedExport", "UnusedExportFinding"),
2036                ("UnusedFile", "UnusedFileFinding"),
2037                ("UnusedMember", "UnusedClassMemberFinding"),
2038                ("UnusedMember", "UnusedEnumMemberFinding"),
2039                ("UnusedMember", "UnusedStoreMemberFinding"),
2040            ]),
2041            aliases
2042        );
2043    }
2044
2045    #[test]
2046    fn ts_alias_registry_rows_match_result_metadata() {
2047        let result_codes: BTreeSet<&str> = result_issue_metas().map(|meta| meta.code).collect();
2048        let mut seen_codes = BTreeSet::new();
2049        for meta in ISSUE_TS_ALIAS_META {
2050            assert!(
2051                seen_codes.insert(meta.code),
2052                "duplicate TypeScript alias row for {}",
2053                meta.code
2054            );
2055            assert!(
2056                result_codes.contains(meta.code),
2057                "TypeScript alias row {} has no result metadata",
2058                meta.code
2059            );
2060            assert!(
2061                meta.alias.parent.ends_with("Finding"),
2062                "TypeScript alias row {} must point at a generated Finding wrapper",
2063                meta.code
2064            );
2065            assert_eq!(
2066                Some(meta.alias),
2067                issue_meta_by_code(meta.code).and_then(|issue| issue.ts_alias()),
2068                "IssueKindMeta helper must resolve TypeScript alias row {} through the registry",
2069                meta.code
2070            );
2071        }
2072    }
2073
2074    #[test]
2075    fn result_keys_are_unique() {
2076        let mut seen = BTreeSet::new();
2077        for meta in ISSUE_RESULT_META {
2078            assert!(
2079                seen.insert(meta.result_key),
2080                "duplicate result key {}",
2081                meta.result_key
2082            );
2083        }
2084    }
2085
2086    #[test]
2087    fn counted_result_keys_match_total_issue_fields() {
2088        let from_total: BTreeSet<&str> = TOTAL_ISSUE_RESULT_KEYS.iter().copied().collect();
2089        let from_meta: BTreeSet<&str> = counted_result_issue_metas()
2090            .map(|meta| meta.result_key)
2091            .collect();
2092        assert_eq!(from_total, from_meta);
2093    }
2094
2095    #[test]
2096    fn advisory_result_keys_are_explicitly_excluded_from_total() {
2097        let expected = BTreeSet::from([
2098            "duplicate_prop_shapes",
2099            "prop_drilling_chains",
2100            "thin_wrappers",
2101        ]);
2102        let from_meta: BTreeSet<&str> = result_issue_metas()
2103            .filter(|meta| !meta.counts_in_total)
2104            .map(|meta| meta.result_key)
2105            .collect();
2106        assert_eq!(expected, from_meta);
2107    }
2108}