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