cargo-judge 0.7.0

Deterministic post-refactoring analysis for Rust workspaces
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
//! Static, versioned documentation for every rule id judge can emit (todo.md
//! §17.5: "Für jede Regel in der Registry Evidenzklasse, Voraussetzungen,
//! Ausschlussgründe, zulässige Formulierungen und Verdict-Effekt fest
//! hinterlegen"). This is a pure lookup table, not a detector — the text
//! here is consolidated from each rule's own module/function doc comments,
//! not invented, and is consulted by `cargo judge explain-rule <id>`.
//!
//! Every rule-id constant defined anywhere in this crate (`grep -rn 'pub
//! const.*_RULE\b.*: &str = "' src/*.rs`) has exactly one entry here,
//! including the three `crate::rules::pattern` aggregation rules — those never
//! produce a `Finding` (see that module's doc comment) and so are always
//! `Heuristic`/[`VerdictEffect::AdvisoryOnly`] here, consistent with
//! [`crate::finding::evidence_class_for_rule`]'s fallback for any rule id it
//! doesn't recognize.

use crate::finding::EvidenceClass;

/// Whether a rule can affect a verdict/exit code or the health score.
/// Documentation-only mirror of [`EvidenceClass::is_gating`] — see the
/// consistency test below, which is the single place that keeps the two from
/// drifting apart.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerdictEffect {
    Gating,
    AdvisoryOnly,
}

impl VerdictEffect {
    pub const fn label(self) -> &'static str {
        match self {
            Self::Gating => "gating",
            Self::AdvisoryOnly => "advisory_only",
        }
    }
}

/// One rule's fixed documentation: evidence class, when it runs, its known
/// limitations, the wording discipline its findings must follow (todo.md
/// §17.4), and whether it can gate a verdict.
#[derive(Debug, Clone, Copy)]
pub struct RuleMetadata {
    pub id: &'static str,
    pub evidence_class: EvidenceClass,
    /// When the rule is evaluated at all — e.g. "always evaluated", "opt-in
    /// via `--features deep`", "requires `judge.toml` config".
    pub preconditions: &'static str,
    /// Known exclusion reasons / scope limits (false-positive sources, scope
    /// boundaries) — taken from the rule's own module/function doc comments.
    /// `"none documented"` where no module doc calls out a limitation, so an
    /// empty entry is never mistaken for "nothing was checked".
    pub exclusions: &'static str,
    /// Wording this rule's findings are allowed to use (todo.md §17.4: never
    /// an absolute factual claim for a `heuristic`/`bounded_semantic` rule).
    pub allowed_wording: &'static str,
    pub verdict_effect: VerdictEffect,
    /// A minimal, self-contained illustration of what triggers this rule,
    /// for an audience outside judge itself (e.g. a project landing page) —
    /// deliberately separate from `allowed_wording`, which constrains a
    /// finding's own printed text, not marketing/documentation copy. `None`
    /// where no curated example exists yet; not every rule has one.
    pub example: Option<RuleExample>,
}

/// One rule's curated example: minimal source plus a plain-language reason
/// it matters. `before` is meant to be kept identical to (or copied by) a
/// canonical positive test for the same rule — see each usage site below —
/// so an example can never silently drift from what the rule actually
/// flags: if the detector's behavior changes enough to stop matching, that
/// shared test fails.
#[derive(Debug, Clone, Copy)]
pub struct RuleExample {
    pub before: &'static str,
    pub why_it_matters: &'static str,
}

const DERIVED_FACT_WORDING: &str = "State as an exact fact of the declared inputs (e.g. an occurrence count) — never as a quality judgment. The syntax/manifest fact is certain; whether it constitutes a real problem is not (todo.md §17.3).";

const BOUNDED_SEMANTIC_WORDING: &str = "State as 'no reference found within the examined workspace/view', scoped explicitly to what was searched — never as an absolute 'unused' or 'dead'; usage outside the examined view is not_inferable (todo.md §17.3, §17.4).";

const EXTERNAL_MEASUREMENT_WORDING: &str = "State as the result of the imported snapshot/lookup at the time it ran — valid for that snapshot, never a timeless truth; re-running later can change the result (todo.md §17.2).";

const HEURISTIC_WORDING: &str = "State as a hint or possible reading, never as proof — advisory by default, no exit code 1 (todo.md §17.2). Never phrase as an absolute claim about design correctness, authorship, or code quality (todo.md §17.4).";

/// The single authoritative documentation table — one entry per rule id
/// defined anywhere in this crate. See the module doc comment for the
/// completeness guarantee and the consistency test below for the
/// evidence-class/verdict-effect invariant.
pub const RULE_REGISTRY: &[RuleMetadata] = &[
    // -- api_surface.rs ---------------------------------------------------
    RuleMetadata {
        id: "undocumented-public-item",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; `cargo judge api-surface`, subcommand-only).",
        exclusions: "Checks only whether the item itself is written `pub`, not the full visibility chain up through enclosing modules — a `pub fn` inside a private `mod` is not actually reachable from outside the crate but is still checked (see `crate::rules::api_surface` module docs). Scoped to free `fn`/`struct`/`enum`/`trait`/`const`/`static`/`type` at module level plus inherent-impl methods; methods inside `impl Trait for Type` are exempt (typically inherit the trait's own documentation), as are `#[test]`-attributed functions and anything gated by `#[cfg(test)]`.",
        allowed_wording: "State only that no doc comment was found on this `pub` item — never that its documentation is 'bad' or 'incomplete' (todo.md §17.4).",
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "pub fn calculate_shipping_cost(weight_kg: f64, distance_km: f64) -> f64 {\n    weight_kg * distance_km * 0.12\n}\n",
            why_it_matters: "A public function with no doc comment forces every downstream caller to read its implementation just to find out what it does, and generated docs render an empty description for it.",
        }),
    },
    RuleMetadata {
        id: "semver-hazard",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; `cargo judge api-surface`, subcommand-only).",
        exclusions: "Covers two of the three todo.md §I sub-cases via `evidence.kind`: a `pub enum` with at least two variants and no `#[non_exhaustive]` attribute (`missing_non_exhaustive_enum`; a single-variant enum is exempt), and a `pub struct` with at least one `pub` field and no `#[non_exhaustive]` attribute (`missing_non_exhaustive_struct_fields`; a unit struct or one with only private fields is exempt). The third sub-case — a dependency's type leaking through a public signature — needs type resolution across crate boundaries the Fast Tier doesn't have and is not implemented. Same `#[cfg(test)]`/generated-code exemptions as `undocumented-public-item`.",
        allowed_wording: "State only the exact syntax fact (attribute absence plus variant/field count) — never that the type is 'badly designed'; adding a variant/field is a known Rust API-evolvability fact, not this crate's opinion (todo.md §17.4).",
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "/// Supported export formats for a report.\npub enum ExportFormat {\n    Json,\n    Csv,\n}\n",
            why_it_matters: "Adding a third export format later is a breaking change for every downstream `match` on this enum, but nothing here warns callers that more variants may arrive.",
        }),
    },
    // -- boundaries.rs --------------------------------------------------
    RuleMetadata {
        id: "crate-boundary-violation",
        evidence_class: EvidenceClass::BoundedSemantic,
        preconditions: "Requires a `judge.toml` with `[[boundary]]`/`[layers]` config; opt-in — `cargo judge boundaries` (and the boundaries block of bare `cargo judge`/`audit`) does nothing without it.",
        exclusions: "Scoped to crate-level dependency edges only, fully knowable from `cargo_metadata` without a build; module-level boundaries need semantic module resolution the Fast Tier doesn't have yet.",
        allowed_wording: BOUNDED_SEMANTIC_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: None,
    },
    RuleMetadata {
        id: "dependency-cycle",
        evidence_class: EvidenceClass::BoundedSemantic,
        preconditions: "Requires a `judge.toml` with `[[boundary]]`/`[layers]` config; opt-in — `cargo judge boundaries` (and the boundaries block of bare `cargo judge`/`audit`) does nothing without it.",
        exclusions: "Scoped to crate-level dependency edges only, fully knowable from `cargo_metadata` without a build; module-level cycles need semantic module resolution the Fast Tier doesn't have yet.",
        allowed_wording: BOUNDED_SEMANTIC_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: None,
    },
    RuleMetadata {
        id: "feature-graph-cycle",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`/`audit`) — deliberately not gated behind `judge.toml` the way `dependency-cycle` is: a `[features]` table is either cyclic or it isn't, needing no project-intent config to interpret (see `crate::rules::boundaries` module docs 'feature-graph-cycle').",
        exclusions: "Reuses `dependency-cycle`'s own cycle-finding algorithm over a different graph: nodes are one crate's own declared feature names, edges are implication-list entries that exactly match another feature of the same package. A `dep:foo`/`pkg/feat`/`pkg?/feat` entry (a dependency activation, not a sibling feature) is excluded. Cargo tolerates a cyclic feature graph at resolution time — this is a structural-hygiene signal, not a claim the build is broken.",
        allowed_wording: "State only that this cyclic chain of feature implications exists — never that the crate 'fails to build' or that the cycle is 'a bug' (todo.md §17.4).",
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "[features]\nasync-runtime = [\"tokio-support\"]\ntokio-support = [\"async-runtime\"]\n",
            why_it_matters: "A cyclic feature-implication chain means enabling one feature silently pulls in another that implies the first right back, making it impossible to reason about what turning on a single feature actually activates.",
        }),
    },
    RuleMetadata {
        id: "module-boundary-violation",
        evidence_class: EvidenceClass::BoundedSemantic,
        preconditions: "Requires a `judge.toml` with `[[module_boundary]]` config; opt-in — `cargo judge boundaries` (and the boundaries block of bare `cargo judge`/`audit`) does nothing without it.",
        exclusions: "Module path resolution is a directory-convention heuristic, not `mod`-graph resolution — a file wired into the build unconventionally (e.g. a `#[path = \"...\"]` attribute) is missed (see `crate::rules::boundaries` module docs 'Module-level boundaries'). Only `direct` reach is supported — `transitive` would need a real module call graph, which the Fast Tier doesn't have; requesting it is a config error, not a silent downgrade. Only `forbidden` is supported, not `required` (crate-level boundaries' other half).",
        allowed_wording: BOUNDED_SEMANTIC_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: None,
    },
    // -- api_surface_deep.rs (Deep Tier, `--features deep`) ---------------
    RuleMetadata {
        id: "internal-leak",
        evidence_class: EvidenceClass::BoundedSemantic,
        preconditions: "Requires `--features deep` and `cargo judge api-surface` (same subcommand as `semver-hazard`'s `leaked_dependency_type` sub-case, which this rule reuses the type resolution of), plus a `judge.toml` with a non-empty `internal_crates` list (see `crate::rules::boundaries::BoundaryConfig::internal_crates`). With `internal_crates` empty or absent (the default), this rule performs no analysis at all and emits zero findings — that must never be read as 'no internal leaks found', only that none were checked for (todo.md §17 'Kein Raten von Projektabsicht': an architecture rule needs explicit config, not a guess).",
        exclusions: "Same resolution and the same documented boundary as `semver-hazard`'s `leaked_dependency_type` sub-case (see `crate::rules::api_surface_deep` module docs 'Ehrliche Grenze'): only direct parameter/return types plus one level of generic unwrapping through a `std`/`core`/`alloc` container are checked; a `dyn Trait` receiver, raw pointer, function pointer, tuple, or slice/array element type is not unwrapped.",
        allowed_wording: "State only that this pub item's signature resolves to a type defined in `<crate>`, which is configured as internal — never that crossing this boundary is 'unintentional' or that the crate's public API is 'broken' (todo.md §17.4); judge does not know whether crossing the boundary was deliberate.",
        verdict_effect: VerdictEffect::Gating,
        example: None,
    },
    RuleMetadata {
        id: "module-boundary-violation-deep",
        evidence_class: EvidenceClass::BoundedSemantic,
        preconditions: "Requires `--features deep` and a `judge.toml` with `[[module_boundary]]` config; runs alongside (not instead of) the Fast-Tier `module-boundary-violation` check in `cargo judge boundaries` — see `crate::rules::boundaries_deep` module docs.",
        exclusions: "Real Deep-Tier symbol reference resolution replaces the Fast Tier's `syn`-based text scan for the *reference edge* itself (catching a re-export or aliased `use` the text scan misses), but the `from`/`forbidden` module-path *scoping* is still the same directory-convention heuristic as the Fast-Tier rule. Only free functions, inherent/trait-impl methods, and trait default methods are checked as the referenced item — unlike the Fast-Tier text scan, which is item-kind-agnostic, this Deep-Tier pass does not yet cover structs/enums/traits/consts/statics. `Reach::Transitive` is not supported here either, same restriction as the Fast Tier.",
        allowed_wording: BOUNDED_SEMANTIC_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: None,
    },
    RuleMetadata {
        id: "re-export-chain",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Requires `--features deep` and `cargo judge api-surface` (same subcommand as `semver-hazard`/`internal-leak`); always evaluated when the Deep Tier is available, no `judge.toml` config needed.",
        exclusions: "Only a plain, non-glob, non-braced `pub use path::Item;` (optionally renamed) at a file's own module-root level is considered a candidate or an intermediate hop — a `pub use` nested inside an inline `mod { .. }` block in the same file, a glob (`pub use foo::*;`), or a braced group (`pub use foo::{A, B};`) is invisible to this scan (see `crate::rules::api_surface_deep` module docs). A hop count capped at 5 (`RE_EXPORT_CHAIN_MAX_HOPS`) is reported as `evidence.capped: true` rather than an exact count — chosen to bound the walk against a `pub use` cycle, not derived from a study of real-world chain depths. A single direct re-export (hop count 1) is deliberately never flagged — only 2 or more hops are, since curated top-level re-exports, prelude modules, and workspace umbrella crates routinely add exactly one hop and are not themselves a sign of obscured ownership.",
        allowed_wording: "State only that this item's public path resolves through `<hop_count>` `pub use` hops before reaching its defining module `<defining_path>` — when `evidence.capped` is true, phrase `<hop_count>` as 'at least 5', not an exact count; never phrase a chain's existence as 'bad practice' or as 'hiding implementation details' (todo.md §17.4) — re-export facades are a common, legitimate pattern judge cannot tell apart from an unintentional one.",
        verdict_effect: VerdictEffect::AdvisoryOnly,
        // Three crates, one re-export hop each — this is what the rule
        // actually walks (a `pub use` chain across a multi-crate
        // workspace). The `// crate: <name>` markers are not Rust syntax;
        // the paired drift-guard test in `api_surface_deep.rs` splits on
        // them to materialize each section as its own workspace member.
        example: Some(RuleExample {
            before: "// crate: gateway_core\npub struct PaymentGateway;\n\n// crate: gateway_facade\npub use gateway_core::PaymentGateway;\n\n// crate: gateway_api\npub use gateway_facade::PaymentGateway;\n",
            why_it_matters: "A caller importing `PaymentGateway` from `gateway_api` can't tell from that import alone that it's actually implemented two crates away in `gateway_core` — tracing a bug back to its owner means manually unwinding every re-export hop.",
        }),
    },
    // -- complexity.rs ------------------------------------------------------
    RuleMetadata {
        id: "signature-complexity",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block, same wiring as `complexity-inflation` — reuses the same `Vec<FunctionInfo>` already computed over the loaded workspace, no extra parse pass).",
        exclusions: "Fires on any of three independent signature-shape measures, no LOC floor (unlike `complexity-inflation`, a one-line function can still have a complex signature): return-type nesting depth over 3 (`Result<Option<Vec<T>>, E>` is 3 and does not fire; one more level of nesting does), more than 4 generic type parameters, or more than 5 total trait bounds (inline `T: Clone + Debug` plus `where`-clause bounds summed together). Lifetime parameters are counted and reported in `evidence.lifetime_param_count` but never gate on their own — lifetimes rarely indicate complexity by themselves in idiomatic Rust. Does not distinguish a genuinely generic library/framework signature (where breadth is the point) from an accidentally over-parameterized one.",
        allowed_wording: HEURISTIC_WORDING,
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "pub fn fetch<T: Clone + Debug, U, E>(id: &str) -> Option<T>\nwhere\n    T: Send + Sync + Serialize,\n    U: Default + PartialEq,\n    E: std::error::Error,\n{\n    todo!()\n}\n",
            why_it_matters: "A signature carrying eight trait bounds across three generic parameters demands the caller understand an enormous contract before calling it at all — one glance at the call site tells you nothing about what behavior actually varies across `T`, `U`, and `E`.",
        }),
    },
    RuleMetadata {
        id: "maintainability-index",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block, same wiring as `signature-complexity`/`complexity-inflation` — reuses the same `Vec<FunctionInfo>` already computed over the loaded workspace for its per-file `lines_of_code`/`cyclomatic` sums, plus one additional `syn` parse per file for Halstead operator/operand counts). Computes the standard SEI-derived Maintainability Index, the same 0-100-normalized formula Visual Studio and most modern tooling use: `MI_raw = 171 - 5.2*ln(HalsteadVolume) - 0.23*CyclomaticComplexity - 16.2*ln(LinesOfCode)`, `MI = max(0, MI_raw * 100 / 171)`. Fires at the widely-cited 'yellow or worse' threshold: `MI < 10` is 'red' (hard to maintain), `10-19` is 'yellow' (moderately maintainable), `20+` is 'green' — this rule fires below 20.",
        exclusions: "Halstead's operator/operand counting is a C-era definition that does not map 1:1 onto Rust; this rule's counting rules are a documented, reproducible approximation, not the canonical software-science definition (see `crate::rules::complexity::HalsteadVisitor`). Operators counted: `syn::BinOp`/`syn::UnOp` variants (a binary and a unary use of the same token, e.g. `*`, count as two distinct kinds, not one), `if`/`match`/`for`/`while`/`loop`, `?`, plain `=` (compound assignment like `+=` is already its own `BinOp` variant), a path call and a method call as two distinct kinds, and any macro invocation. Operands counted: `Expr::Path` idents and `Expr::Lit` literal values, deduplicated by token-stream text within the file — a type-position path (e.g. a parameter's declared type) is not counted, only paths referenced in expression position. Only Halstead Volume is computed; Difficulty/Effort/Time are out of scope (Maintainability Index only needs Volume). A file with zero Halstead vocabulary or zero summed lines of code is skipped entirely rather than scored, to avoid a degenerate/misleading MI.",
        allowed_wording: HEURISTIC_WORDING,
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "fn currency_name(code: &str) -> &'static str {\n    let mut name = \"unknown currency\";\n    if code == \"USD\" {\n        name = \"US Dollar\";\n    } else if code == \"EUR\" {\n        name = \"Euro\";\n    } else if code == \"JPY\" {\n        name = \"Japanese Yen\";\n    } else if code == \"GBP\" {\n        name = \"British Pound\";\n    } else if code == \"AUD\" {\n        name = \"Australian Dollar\";\n    } else if code == \"CAD\" {\n        name = \"Canadian Dollar\";\n    } else if code == \"CHF\" {\n        name = \"Swiss Franc\";\n    } else if code == \"CNY\" {\n        name = \"Chinese Yuan\";\n    } else if code == \"HKD\" {\n        name = \"Hong Kong Dollar\";\n    } else if code == \"NZD\" {\n        name = \"New Zealand Dollar\";\n    } else if code == \"SEK\" {\n        name = \"Swedish Krona\";\n    } else if code == \"KRW\" {\n        name = \"South Korean Won\";\n    } else if code == \"SGD\" {\n        name = \"Singapore Dollar\";\n    } else if code == \"NOK\" {\n        name = \"Norwegian Krone\";\n    } else if code == \"MXN\" {\n        name = \"Mexican Peso\";\n    } else if code == \"INR\" {\n        name = \"Indian Rupee\";\n    } else if code == \"RUB\" {\n        name = \"Russian Ruble\";\n    } else if code == \"ZAR\" {\n        name = \"South African Rand\";\n    } else if code == \"TRY\" {\n        name = \"Turkish Lira\";\n    } else if code == \"BRL\" {\n        name = \"Brazilian Real\";\n    } else if code == \"TWD\" {\n        name = \"Taiwan Dollar\";\n    } else if code == \"DKK\" {\n        name = \"Danish Krone\";\n    } else if code == \"PLN\" {\n        name = \"Polish Zloty\";\n    } else if code == \"THB\" {\n        name = \"Thai Baht\";\n    } else if code == \"IDR\" {\n        name = \"Indonesian Rupiah\";\n    } else if code == \"HUF\" {\n        name = \"Hungarian Forint\";\n    } else if code == \"CZK\" {\n        name = \"Czech Koruna\";\n    } else if code == \"ILS\" {\n        name = \"Israeli Shekel\";\n    } else if code == \"CLP\" {\n        name = \"Chilean Peso\";\n    } else if code == \"PHP\" {\n        name = \"Philippine Peso\";\n    } else if code == \"AED\" {\n        name = \"UAE Dirham\";\n    } else if code == \"COP\" {\n        name = \"Colombian Peso\";\n    } else if code == \"SAR\" {\n        name = \"Saudi Riyal\";\n    } else if code == \"MYR\" {\n        name = \"Malaysian Ringgit\";\n    } else if code == \"RON\" {\n        name = \"Romanian Leu\";\n    } else if code == \"VND\" {\n        name = \"Vietnamese Dong\";\n    } else if code == \"EGP\" {\n        name = \"Egyptian Pound\";\n    } else if code == \"BGN\" {\n        name = \"Bulgarian Lev\";\n    } else if code == \"HRK\" {\n        name = \"Croatian Kuna\";\n    } else if code == \"PKR\" {\n        name = \"Pakistani Rupee\";\n    } else if code == \"QAR\" {\n        name = \"Qatari Riyal\";\n    } else if code == \"PEN\" {\n        name = \"Peruvian Sol\";\n    } else if code == \"KWD\" {\n        name = \"Kuwaiti Dinar\";\n    } else if code == \"NGN\" {\n        name = \"Nigerian Naira\";\n    } else if code == \"UAH\" {\n        name = \"Ukrainian Hryvnia\";\n    } else if code == \"MAD\" {\n        name = \"Moroccan Dirham\";\n    } else if code == \"DZD\" {\n        name = \"Algerian Dinar\";\n    } else if code == \"OMR\" {\n        name = \"Omani Rial\";\n    } else if code == \"BHD\" {\n        name = \"Bahraini Dinar\";\n    } else if code == \"LKR\" {\n        name = \"Sri Lankan Rupee\";\n    } else if code == \"KES\" {\n        name = \"Kenyan Shilling\";\n    } else if code == \"GHS\" {\n        name = \"Ghanaian Cedi\";\n    } else if code == \"TZS\" {\n        name = \"Tanzanian Shilling\";\n    } else if code == \"UGX\" {\n        name = \"Ugandan Shilling\";\n    } else if code == \"ETB\" {\n        name = \"Ethiopian Birr\";\n    } else if code == \"XOF\" {\n        name = \"West African CFA Franc\";\n    } else if code == \"XAF\" {\n        name = \"Central African CFA Franc\";\n    } else if code == \"ISK\" {\n        name = \"Icelandic Krona\";\n    } else if code == \"BDT\" {\n        name = \"Bangladeshi Taka\";\n    } else if code == \"JOD\" {\n        name = \"Jordanian Dinar\";\n    } else if code == \"LBP\" {\n        name = \"Lebanese Pound\";\n    } else if code == \"TND\" {\n        name = \"Tunisian Dinar\";\n    } else if code == \"KZT\" {\n        name = \"Kazakhstani Tenge\";\n    } else if code == \"AZN\" {\n        name = \"Azerbaijani Manat\";\n    } else if code == \"GEL\" {\n        name = \"Georgian Lari\";\n    } else if code == \"AMD\" {\n        name = \"Armenian Dram\";\n    } else if code == \"BYN\" {\n        name = \"Belarusian Ruble\";\n    } else if code == \"MDL\" {\n        name = \"Moldovan Leu\";\n    } else if code == \"RSD\" {\n        name = \"Serbian Dinar\";\n    } else if code == \"MKD\" {\n        name = \"Macedonian Denar\";\n    } else if code == \"ALL\" {\n        name = \"Albanian Lek\";\n    } else if code == \"BAM\" {\n        name = \"Bosnia Mark\";\n    } else if code == \"UYU\" {\n        name = \"Uruguayan Peso\";\n    } else if code == \"PYG\" {\n        name = \"Paraguayan Guarani\";\n    } else if code == \"BOB\" {\n        name = \"Bolivian Boliviano\";\n    } else if code == \"GTQ\" {\n        name = \"Guatemalan Quetzal\";\n    } else if code == \"HNL\" {\n        name = \"Honduran Lempira\";\n    } else if code == \"NIO\" {\n        name = \"Nicaraguan Cordoba\";\n    } else if code == \"CRC\" {\n        name = \"Costa Rican Colon\";\n    } else if code == \"PAB\" {\n        name = \"Panamanian Balboa\";\n    } else if code == \"DOP\" {\n        name = \"Dominican Peso\";\n    } else if code == \"JMD\" {\n        name = \"Jamaican Dollar\";\n    } else if code == \"TTD\" {\n        name = \"Trinidad Dollar\";\n    } else if code == \"BBD\" {\n        name = \"Barbados Dollar\";\n    } else if code == \"BSD\" {\n        name = \"Bahamian Dollar\";\n    } else if code == \"BZD\" {\n        name = \"Belize Dollar\";\n    } else if code == \"XCD\" {\n        name = \"East Caribbean Dollar\";\n    } else if code == \"FJD\" {\n        name = \"Fiji Dollar\";\n    } else if code == \"PGK\" {\n        name = \"Papua New Guinea Kina\";\n    } else if code == \"WST\" {\n        name = \"Samoan Tala\";\n    } else if code == \"TOP\" {\n        name = \"Tongan Paanga\";\n    } else if code == \"VUV\" {\n        name = \"Vanuatu Vatu\";\n    } else if code == \"SBD\" {\n        name = \"Solomon Islands Dollar\";\n    } else if code == \"MOP\" {\n        name = \"Macanese Pataca\";\n    } else if code == \"BND\" {\n        name = \"Brunei Dollar\";\n    } else if code == \"MMK\" {\n        name = \"Myanmar Kyat\";\n    } else if code == \"KHR\" {\n        name = \"Cambodian Riel\";\n    } else if code == \"LAK\" {\n        name = \"Lao Kip\";\n    }\n    name\n}\n",
            why_it_matters: "A currency-code lookup written as a flat if/else-if chain instead of a match or a table scores low on Maintainability Index even though no single branch looks complicated on its own — the file as a whole packs enough cyclomatic complexity, size, and Halstead volume into one function that adding, removing, or reordering a single currency safely requires reading the whole thing.",
        }),
    },
    // -- coverage.rs ------------------------------------------------------
    RuleMetadata {
        id: "untested-hotspot",
        evidence_class: EvidenceClass::ExternalMeasurement,
        preconditions: "Requires `cargo judge coverage` and a parsed LCOV report.",
        exclusions: "Code outside the analyzed coverage report or functions with cyclomatic complexity below threshold.",
        allowed_wording: "State that the function has high complexity and low test coverage in the imported LCOV report.",
        verdict_effect: VerdictEffect::Gating,
        example: None,
    },
    // -- mutants.rs ---------------------------------------------------------
    RuleMetadata {
        id: "mutation-survivor",
        evidence_class: EvidenceClass::ExternalMeasurement,
        preconditions: "Requires `cargo judge coverage --mutants-json PATH`, an already-generated `cargo-mutants` `outcomes.json` report (opt-in; judge never runs `cargo-mutants` itself). Same evidence class and verdict effect as `untested-hotspot` — both are external-tool-derived test-strength signals.",
        exclusions: "A mutant that is semantically identical to the original code (an 'equivalent mutant') can never be caught by any test, however thorough — there is no observable behavior difference to assert on. This is a well-known, unavoidable limitation of mutation testing itself, not a defect in this rule; a finding here is never proof that a test is missing, only that no test in the imported run distinguished the mutated behavior from the original. The parsed `\"MissedMutant\"` count is cross-checked against the report's own top-level `missed` field; a mismatch (e.g. a stale or partially malformed report) is recorded as a non-fatal error rather than dropping findings — see `crate::advisory::mutants` module docs.",
        allowed_wording: "State only that this mutant was not caught by a failing test in the imported cargo-mutants run — never that the mutated code is 'untested' or 'broken' (todo.md §17.4); an equivalent mutant would survive no matter how thorough the tests are.",
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "{\"outcomes\":[{\"scenario\":{\"Mutant\":{\"name\":\"src/discount.rs:8:5: replace apply_discount -> f64 with 0.0\",\"package\":\"shop-core\",\"file\":\"src/discount.rs\",\"function\":{\"function_name\":\"apply_discount\",\"return_type\":\"-> f64\",\"span\":{\"start\":{\"line\":6,\"column\":1},\"end\":{\"line\":10,\"column\":1}}},\"span\":{\"start\":{\"line\":8,\"column\":5},\"end\":{\"line\":8,\"column\":30}},\"replacement\":\"0.0\",\"genre\":\"FnValue\"}},\"summary\":\"MissedMutant\",\"log_path\":\"mutants.out/log/discount.rs-line8.log\",\"diff_path\":\"mutants.out/diff/discount.rs-line8.diff\",\"phase_results\":[]}],\"total_mutants\":1,\"missed\":1,\"caught\":0,\"timeout\":0,\"unviable\":0,\"success\":0,\"start_time\":\"2026-01-01T00:00:00Z\",\"end_time\":\"2026-01-01T00:05:00Z\",\"cargo_mutants_version\":\"25.0.0\"}",
            why_it_matters: "A mutant that replaces a function's real return value with a dummy constant and still passes the entire test suite means no test actually asserts on that function's output — the coverage tooling may still call this line \"covered\", but nothing checks whether it computed the right thing.",
        }),
    },
    // -- dead_code.rs (Deep Tier, `--features deep`) -----------------------
    RuleMetadata {
        id: "unused-pub-workspace",
        evidence_class: EvidenceClass::BoundedSemantic,
        preconditions: "Requires `--features deep` and `cargo judge dead-code` (Deep Tier; semantic reachability isn't available at the Fast Tier).",
        exclusions: "Every workspace crate is treated as workspace-internal; a crate whose resolved `publish` field allows publishing gets `unused-pub-api` instead of this rule for the same underlying condition (see `crate::rules::dead_code::publishable_crates`).",
        allowed_wording: "State as 'no reference found in the loaded workspace' — never as 'unused' outright or as clearance for deletion; external ecosystem usage is not_inferable (todo.md §17.3, §17.4).",
        verdict_effect: VerdictEffect::Gating,
        // `publish = false` is what routes a dead item to this rule rather
        // than `unused-pub-api` — see `crate::rules::dead_code::publishable_crates`.
        example: Some(RuleExample {
            before: "pub fn migrate_legacy_user_ids(raw: &str) -> String {\n    raw.trim().to_string()\n}\n",
            why_it_matters: "A public function nobody in the workspace calls anymore still has to be read, understood, and kept compiling through every future refactor — maintenance cost with no corresponding benefit.",
        }),
    },
    RuleMetadata {
        id: "unused-pub-api",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Requires `--features deep` and `cargo judge dead-code` (Deep Tier; semantic reachability isn't available at the Fast Tier). Only emitted for items belonging to a crate whose resolved `publish` field allows publishing (`None`, or `Some` of a non-empty registry list — only `Some(vec![])`, i.e. `publish = false`, is excluded).",
        exclusions: "Same reachability query and same 'every workspace crate is workspace-internal' scope as `unused-pub-workspace`; a published crate's whole purpose is exposing API to consumers outside the loaded workspace, so 'zero internal reference' is the expected normal state for most of a healthy library's public surface, not a defect signal — that is why this is `Heuristic`/advisory rather than `unused-pub-workspace`'s `BoundedSemantic`/gating.",
        allowed_wording: "State as 'no reference found within the examined workspace; this crate is published, so external ecosystem usage is not inferable and expected' — never as 'unused' or as clearance for deletion (todo.md §17.3, §17.4).",
        verdict_effect: VerdictEffect::AdvisoryOnly,
        // No `publish` field set — publishable by default, so this routes
        // through `unused-pub-api`, not `unused-pub-workspace`.
        example: Some(RuleExample {
            before: "pub fn compute_legacy_checksum(data: &[u8]) -> u32 {\n    data.iter().map(|b| *b as u32).sum()\n}\n",
            why_it_matters: "A published crate's dead public function might still be used by external downstream consumers judge can't see from inside this workspace — but if it truly isn't, every version bump keeps supporting an API surface nobody needs.",
        }),
    },
    RuleMetadata {
        id: "dead-enum-variant",
        evidence_class: EvidenceClass::BoundedSemantic,
        preconditions: "Requires `--features deep` and `cargo judge dead-code` (Deep Tier; semantic reachability isn't available at the Fast Tier).",
        exclusions: "Every workspace crate is treated as workspace-internal, same simplification as `unused-pub-workspace`. Only a `pub` enum's variants are checked. Construction-vs-pattern classification is a `syn` re-parse of each file `crate::deep::referencing_files` reports as referencing the variant, looking for `Expr::Path`/`Expr::Call`/`Expr::Struct` occurrences of the variant's trailing path segment; `syn` parses a macro invocation's body as an opaque token stream, so a variant constructed only inside a macro call (e.g. `some_macro!(MyEnum::Variant)`) is invisible to this scan and can be misreported as having no construction site.",
        allowed_wording: "State as 'no construction site found in the examined workspace view' — never 'never constructed' or 'dead' outright (todo.md §17.3, §17.4).",
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "pub enum PaymentStatus {\n    Pending,\n    Settled,\n}\n\npub fn make_payment() -> PaymentStatus {\n    PaymentStatus::Pending\n}\n\npub fn describe(status: PaymentStatus) -> &'static str {\n    match status {\n        PaymentStatus::Pending => \"pending\",\n        PaymentStatus::Settled => \"settled\",\n    }\n}\n",
            why_it_matters: "An enum variant that's only ever matched against, never constructed, usually means the code path that used to produce it was deleted — but nothing forces the variant itself to be cleaned up too.",
        }),
    },
    RuleMetadata {
        id: "test-only-pub",
        evidence_class: EvidenceClass::BoundedSemantic,
        preconditions: "Requires `--features deep` and `cargo judge dead-code` (Deep Tier; semantic reachability isn't available at the Fast Tier).",
        exclusions: "Every workspace crate is treated as workspace-internal, same simplification as `unused-pub-workspace`; does not narrow by a crate's `publish` field the way `unused-pub-api` does. Runs the entry-point reachability query twice per checked item (once production-only, once counting tests) — real, accepted extra Deep Tier query volume.",
        allowed_wording: "State as 'reachable only through #[cfg(test)]/test-target code in the examined workspace view' — never a prescriptive claim like 'should be pub(crate)' (todo.md §17.3, §17.4).",
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "pub fn reset_test_fixtures() -> bool {\n    true\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn fixtures_reset() {\n        assert!(reset_test_fixtures());\n    }\n}\n",
            why_it_matters: "A `pub` function reachable only from the crate's own tests isn't really part of the public API — keeping it `pub` invites outside code to depend on something that was only ever meant for internal test setup.",
        }),
    },
    RuleMetadata {
        id: "unreachable-from-entry",
        evidence_class: EvidenceClass::BoundedSemantic,
        preconditions: "Requires `--features deep` and `cargo judge dead-code` (Deep Tier; semantic reachability isn't available at the Fast Tier). Scoped to non-`pub` items only (private, `pub(crate)`, `pub(super)`, `pub(in path)`) — a `pub` item is checked by `unused-pub-workspace`/`unused-pub-api`/`test-only-pub` instead, never by this rule.",
        exclusions: "Scoped to non-`pub` items only, so it never runs `check_item`'s cross-crate reference check at all — a non-`pub` item can never be referenced from another crate by Rust's own visibility rules, so that check would be vacuously false here; only entry-point reachability is checked. Inherits every entry-point-detection limitation `crate::reachability`'s module docs describe: a workspace-internal crate's own `pub` API is not itself an automatic root, and registration macros (`inventory::submit!`, `linkme::distributed_slice`, `ctor`, …) are not recognized as entry points at all. Unlike `crate::rules::slop_structural_deep`'s fan-in check, does not exclude trait-impl methods (`FunctionSite::in_trait_impl`) — that exclusion exists there because its literal-reference search can't see calls through operator/macro sugar, but this rule's `incoming_calls`-based call-hierarchy BFS does resolve calls through trait dispatch.",
        allowed_wording: "State as 'not reachable from any recognized entry point in the examined reachability view' — never as 'unused' or 'dead' outright or as clearance for deletion; usage the analysis can't see (e.g. through an unresolved macro expansion or a registration macro) is not_inferable (todo.md §17.3, §17.4).",
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "fn recalculate_shipping_estimate(distance_km: f64) -> f64 {\n    distance_km * 0.4\n}\n",
            why_it_matters: "A private function with no caller anywhere in the analyzed reachability view still has to be read, understood, and kept compiling through every future refactor — and unlike a `pub` item, it can't be excused as future external API judge simply can't see.",
        }),
    },
    RuleMetadata {
        id: "crate-coupling",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Requires `--features deep` and `cargo judge dead-code` (Deep Tier; semantic reachability isn't available at the Fast Tier). No `judge.toml` config needed — runs unconditionally, folded into the same per-pub-item `referencing_files` query `unused-pub-workspace`/`unused-pub-api` already run (`check_item`'s `referencing` set), not a second workspace pass.",
        exclusions: "Only counts workspace-internal crate-to-crate coupling — an external dependency's coupling is a different, already-solved concern (see `heavy-dependency`, todo.md §B 'Dependency Hygiene'). Same 'every workspace crate is workspace-internal' scope and the same `crate::deep::referencing_files` resolution limitations as `unused-pub-workspace` (e.g. a reference only visible through proc-macro expansion is invisible to this scan). A crate with zero observed cross-crate coupling (`Ca + Ce == 0`) is skipped, not flagged — there is nothing to report for it.",
        allowed_wording: "State only the exact `Ce`/`Ca` counts and the resulting Instability ratio for this crate within the examined workspace (e.g. 'Ce=2 distinct crates referenced, Ca=1 distinct crate references back, Instability=0.67') — never that the crate is 'too coupled', 'badly designed', or 'violates the architecture' (todo.md §17.4); high afferent coupling is often the expected, healthy shape for a shared core crate, not a defect.",
        verdict_effect: VerdictEffect::AdvisoryOnly,
        // `core` is referenced by both `app` and `plugin` (Ca=2) and
        // references nothing cross-crate itself (Ce=0) — Instability 0.0,
        // the shape expected of a shared, stable core crate.
        example: Some(RuleExample {
            before: "// crate: core\npub fn shared_helper() -> i32 {\n    1\n}\n\n// crate: app\npub fn run() -> i32 {\n    core::shared_helper()\n}\n\n// crate: plugin\npub fn run() -> i32 {\n    core::shared_helper()\n}\n",
            why_it_matters: "core is called from two other crates but calls into none of them itself — the low-instability shape a shared foundation crate is expected to have, worth knowing before adding a new outward dependency to it.",
        }),
    },
    RuleMetadata {
        id: "module-coupling",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Requires `--features deep` and `cargo judge dead-code` (Deep Tier; semantic reachability isn't available at the Fast Tier). No `judge.toml` config needed — runs unconditionally, folded into the same per-pub-item `referencing_files` query `unused-pub-workspace`/`unused-pub-api`/`crate-coupling` already run (`check_item`'s `referencing` set), not a second workspace pass.",
        exclusions: "`crate-coupling` generalized down to *top-level module* granularity: a module is `krate_name::first_module_segment` (e.g. `mycrate::parser`), not the full nested module path — an item several levels deep is folded into its crate's top-level bucket, and an item declared directly at a crate's root (not inside any named module) gets its own `krate_name::<root>` bucket. Broader scope than `crate-coupling`: a reference from module A to module B counts whether A and B are in the same crate or two different workspace crates, not only across a crate boundary. A module's identity is resolved from the file it's declared in via the same directory/`mod.rs` convention `module-boundary-violation`/`module-boundary-violation-deep` already use, not a full `mod`-graph walk — a source file outside `src/` (e.g. `build.rs`) has no resolvable module and is simply excluded. Same `crate::deep::referencing_files` resolution limitations as `unused-pub-workspace` (e.g. a reference only visible through proc-macro expansion is invisible to this scan). A module with zero observed coupling (`Ca + Ce == 0`) is skipped, not flagged — there is nothing to report for it.",
        allowed_wording: "State only the exact `Ce`/`Ca` counts and the resulting Instability ratio for this module within the examined workspace (e.g. 'Ce=2 distinct modules referenced, Ca=1 distinct module references back, Instability=0.67') — never that the module is 'too coupled', 'badly designed', or 'violates the architecture' (todo.md §17.4); high afferent coupling is often the expected, healthy shape for a shared core module, not a defect.",
        verdict_effect: VerdictEffect::AdvisoryOnly,
        // Single crate, three top-level modules: `core` is referenced by
        // both `consumer_a` and `consumer_b` (Ca=2) and references nothing
        // itself (Ce=0) — Instability 0.0, the shape expected of a shared,
        // stable core module.
        example: Some(RuleExample {
            before: "// file: core.rs\npub fn shared_helper() -> i32 {\n    1\n}\n\n// file: consumer_a.rs\npub fn run() -> i32 {\n    crate::core::shared_helper()\n}\n\n// file: consumer_b.rs\npub fn run() -> i32 {\n    crate::core::shared_helper()\n}\n",
            why_it_matters: "core is called from two other modules in the same crate but calls into none of them itself — the low-instability shape a shared foundation module is expected to have, worth knowing before adding a new outward dependency to it.",
        }),
    },
    // -- feature_matrix.rs (Deep Tier, `--features deep`) -------------------
    RuleMetadata {
        id: "feature-gated-dead-code",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Requires `--features deep` and `cargo judge dead-code`, plus a `judge.toml` with a non-empty `[feature_matrix] combinations` list (see `crate::rules::boundaries::FeatureMatrixConfig`). With `combinations` absent or empty (the default), this rule performs no analysis at all and emits zero findings — that must never be read as 'no feature-gated dead code found', only that none were checked for (todo.md §17 'Kein Raten von Projektabsicht'). Re-loads the whole workspace once per configured combination (a real, accepted extra cost per combination — seconds to minutes each, same order as any other Deep Tier load) via `CargoFeatures::Selected { .., no_default_features: true }`, so a large configured matrix is a real, up-front performance trade-off the user opts into by listing more combinations.",
        exclusions: "Correctness depends entirely on the configured matrix being representative of real downstream usage: a feature combination not listed in `combinations` is never checked at all, and an item reachable only under an unconfigured combination is indistinguishable from genuinely dead code to this rule — this is why it is `Heuristic`/advisory rather than `unreachable-from-entry`'s `bounded_semantic`/gating classification, even though both share the same underlying entry-point BFS. Scoped to both `pub` and non-`pub` items alike (unlike `unreachable-from-entry`'s pub/non-pub split, which exists only to avoid duplicating `unused-pub-workspace`'s cross-crate reference check — a different concern this rule doesn't have). Inherits every entry-point-detection limitation `crate::reachability`'s module docs describe (registration macros like `inventory::submit!`/`ctor` are not recognized entry points).",
        allowed_wording: "State as 'not reachable from any recognized entry point under any of the configured [feature_matrix] combinations, in the examined reachability view' — never as 'unused', 'dead', or 'unreachable under all feature combinations' outright; a combination the config omits was never checked (todo.md §17.3, §17.4).",
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: None,
    },
    // -- dead_trait_impl.rs (Deep Tier, `--features deep`) -------------------
    RuleMetadata {
        id: "dead-trait-impl",
        evidence_class: EvidenceClass::BoundedSemantic,
        preconditions: "Requires `--features deep` (Deep Tier; attributing a `.method()` call site to the specific impl it dispatches to needs real semantic resolution). Scoped to `impl Trait for Type` blocks where `Type` is a concrete named type and `Trait` is itself defined within the analyzed workspace's own source — never a trait from `std`/`core`/`alloc` or an external dependency crate (see `crate::rules::dead_trait_impl` module docs for why this scoping is a deliberate, permanent cut, not a gap to fill later).",
        exclusions: "Scoped only to traits defined within the analyzed workspace — impls of std/external-crate traits (`Drop`, the `std::ops::*` operator traits, `Display`/`Debug`, `Default`/`From`, `Hash`, `Iterator`, serde's `Serialize`/`Deserialize`, …) are never checked by this rule at all; this is an intentional, permanent scope boundary, not a gap to fill later, because those traits are invoked through compiler/operator/macro sugar this rule's plain `.method()` call-site scan cannot see. Inherits the same generic-dispatch blind spot `crate::reachability::classify_call_kind` documents: a call through a generic type bound (`fn foo<T: Trait>(x: T) { x.bar() }`) is not concretely dispatched at the call site, so it never marks a candidate impl as used. Blanket impls (`impl<T: Trait> Trait for T`) are excluded entirely, as is any impl gated by `#[cfg(test)]` (on itself or an enclosing item), and an impl overriding none of the trait's methods (relying entirely on default method bodies) — it has no assoc items of its own for a call site to resolve to, so it is never a candidate.",
        allowed_wording: BOUNDED_SEMANTIC_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "trait Notifier {\n    fn notify(&self, message: &str);\n}\n\nstruct EmailNotifier;\n\nimpl Notifier for EmailNotifier {\n    fn notify(&self, message: &str) {\n        println!(\"email: {message}\");\n    }\n}\n",
            why_it_matters: "EmailNotifier's Notifier impl is never invoked through `.notify(..)` anywhere in the workspace — the compiler keeps compiling and dispatching through it, but no real call path exercises it, so it's read and kept compiling for a trait conformance nobody actually uses.",
        }),
    },
    // -- dep_graph.rs -----------------------------------------------------
    RuleMetadata {
        id: "duplicate-crate-versions",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `cargo judge deps`).",
        exclusions: "Requires a resolved `Cargo.lock`; runs its own full `cargo_metadata` resolve (not `--no-deps`), separate from the workspace-only ingest used elsewhere.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "# api-gateway/Cargo.toml\nimage-resizer = \"1.4.2\"\n\n# batch-worker/Cargo.toml\nimage-resizer = \"2.0.0\"\n",
            why_it_matters: "Two workspace crates pinning incompatible major versions of the same dependency both get compiled and linked into the final build, doubling compile time and binary size for code that's supposed to be shared.",
        }),
    },
    RuleMetadata {
        id: "msrv-drift",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `cargo judge deps`).",
        exclusions: "Requires a resolved `Cargo.lock`; runs its own full `cargo_metadata` resolve (not `--no-deps`), separate from the workspace-only ingest used elsewhere.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "[package]\nname = \"image-codec\"\nversion = \"0.4.2\"\nrust-version = \"1.80\"\n",
            why_it_matters: "A dependency that requires a newer Rust toolchain than the workspace promises silently breaks the build for anyone still on the workspace's stated minimum-supported Rust version.",
        }),
    },
    RuleMetadata {
        id: "workspace-dep-drift",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `cargo judge deps`).",
        exclusions: "Requires a resolved `Cargo.lock`; runs its own full `cargo_metadata` resolve (not `--no-deps`), separate from the workspace-only ingest used elsewhere.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "# api-gateway/Cargo.toml\ntokio = \"1.35\"\n\n# report-worker/Cargo.toml\ntokio = \"1.40\"\n",
            why_it_matters: "When workspace members pin different version requirements for the same dependency, cargo can end up resolving two separate copies of it, and the two crates silently drift out of sync with each other over time.",
        }),
    },
    // -- deps.rs ------------------------------------------------------------
    RuleMetadata {
        id: "misplaced-dependency-kind",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `cargo judge deps`).",
        exclusions: "Only two unambiguous cases are implemented: a `normal` dependency used exclusively from `Dev`-domain files, and a `build` dependency never referenced from `build.rs`. Directory-convention classification (`tests/`/`examples/`/`benches/`) is heuristic, not module-graph resolution — an unconventionally wired file can be misclassified. A dependency with more than one declared feature is excluded from the `Dev`-domain case, since a longer feature list is itself weak evidence of broader use than identifier scanning can see.",
        allowed_wording: HEURISTIC_WORDING,
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "[dependencies]\nassert-json-diff = \"2.0\"\n",
            why_it_matters: "A helper crate declared as a normal dependency but only ever called from the test suite still ships inside every consumer's production build, expanding the dependency tree for code that never runs outside tests.",
        }),
    },
    RuleMetadata {
        id: "unused-dev-dependency",
        evidence_class: EvidenceClass::BoundedSemantic,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `cargo judge deps`).",
        exclusions: "No usage found in `Dev`-domain files (`tests/`, `examples/`, `benches/`) or `#[cfg(test)]` modules of the declaring package; doctests are not scanned.",
        allowed_wording: BOUNDED_SEMANTIC_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "[dev-dependencies]\npretty-assertions = \"1.4\"\n",
            why_it_matters: "A dev-dependency that's declared but never referenced from any test still gets resolved and compiled on every `cargo test` run, adding to CI time for tooling nothing actually exercises.",
        }),
    },
    RuleMetadata {
        id: "heavy-dependency",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `cargo judge deps`).",
        exclusions: "Transitive-count and used-item thresholds are first-cut, adjustable constants, not a calibrated cost model.",
        allowed_wording: HEURISTIC_WORDING,
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "[dependencies]\nimage-resizer = \"1.4\"\n",
            why_it_matters: "Pulling in a dependency for one small feature still compiles its entire transitive dependency tree into the build, adding compile time and attack surface for functionality the code barely touches.",
        }),
    },
    RuleMetadata {
        id: "unused-feature-flag",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `cargo judge deps`).",
        exclusions: "Does not cover well-known 'bundle' features (e.g. tokio's 'full' feature) when the dependency itself is used — recognizing those needs a per-dependency feature vocabulary judge does not maintain. Only fires for a dependency with zero usage found anywhere in the examined view.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "[dependencies]\nimage-processing = { version = \"3.2\", features = [\"simd-acceleration\"] }\n",
            why_it_matters: "Turning on a dependency's feature flag activates its extra code paths (and any dependencies that feature pulls in) even though nothing in the codebase calls into the dependency at all.",
        }),
    },
    RuleMetadata {
        id: "default-features-unused",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `cargo judge deps`).",
        exclusions: "Does not cover 'used, but only non-default features' — telling default from non-default usage apart needs per-dependency feature-to-symbol knowledge judge does not have. Only fires when the manifest text explicitly sets `default-features = true` and zero usage was found anywhere in the examined view.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "[dependencies]\nimage-processing = { version = \"3.2\", default-features = true }\n",
            why_it_matters: "Explicitly keeping a dependency's default features on, for a dependency the code never actually calls, compiles in functionality nobody asked for and hides what the manifest's real feature footprint is.",
        }),
    },
    RuleMetadata {
        id: "unused-feature",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `cargo judge deps`).",
        exclusions: "About a crate's own declared `[features]` table, not a dependency's features (see `unused-feature-flag`, the opposite direction). Never fires for `default`, or for a feature whose own value list is non-empty (an umbrella/bundle feature enabling other features/deps — a real effect even with no direct `cfg` reference). The same-crate reference check is a plain substring scan for `feature = \"x\"`/`feature=\"x\"` across the crate's own authored source, not a `syn`/token-tree parse of the `cfg` predicate — unusual whitespace around the `=` would be missed.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "[features]\nlegacy-api = []\n",
            why_it_matters: "A feature flag that's declared but never gated behind a single `#[cfg(feature = ...)]` anywhere gives downstream users a switch that silently does nothing, wasting their time when they try to use it.",
        }),
    },
    RuleMetadata {
        id: "unused-dependency",
        evidence_class: EvidenceClass::BoundedSemantic,
        preconditions: "Opt-in only: `cargo judge deps --check-rustc-lints` — runs a full `cargo check --workspace --all-targets` with rustc's stable `unused_crate_dependencies` lint enabled; never part of bare `cargo judge`, `audit`, or `cargo judge deps` without the flag (a full compile is a different order of cost than this module's other, instant syntactic passes).",
        exclusions: "Restricted to `normal` dependencies (`dev`/`build` are out of scope; `dev-dependencies` has its own `unused-dev-dependency` detector). Only fires when rustc's lint reports the dependency unused in every target compiled for the package — a dependency used by only one target (e.g. only from a `[[test]]`) is a known, documented multi-target false positive of the raw lint and is deliberately not flagged (see `crate::rules::deps` module docs). A workspace that does not currently compile produces a report error from this detector, never a finding.",
        allowed_wording: BOUNDED_SEMANTIC_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: None,
    },
    RuleMetadata {
        id: "dep-without-repo",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `cargo judge deps`). Reads a dependency's own manifest via the same full, non `--no-deps` `cargo_metadata` resolve as `heavy-dependency`, since the primary `--no-deps` ingest cannot see a dependency's own manifest fields.",
        exclusions: "Fires when the dependency's own `repository` field is absent or blank. A missing field is not itself a defect — private/internal crates legitimately omit it, and the finding never claims otherwise (`Severity::Info`).",
        allowed_wording: "State only that no `repository` field was found in the dependency's own manifest — never that the dependency is 'untrustworthy' or 'suspicious' (that framing belongs to the separate `fresh-low-reputation-dep`/`phantom-crate` rules).",
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "[package]\nname = \"internal-metrics\"\nversion = \"0.3.0\"\n",
            why_it_matters: "Without a `repository` field in its own manifest, this dependency's source can't be traced or reviewed by anyone auditing the build — including automated supply-chain scanners looking for where the code actually comes from.",
        }),
    },
    // -- duplication.rs -------------------------------------------------
    RuleMetadata {
        id: "duplicate-code",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`/`audit` in `Mild` mode by default, and `cargo judge dupes` for any `--mode`).",
        exclusions: "This entry reflects the default `Strict`/`Mild` classification. `Weak` mode normalizes literal values to placeholders; `Semantic` mode additionally normalizes local variable/parameter identifiers — both are overridden to `Heuristic` at the finding-creation site (see `crate::rules::duplication::CloneMember::to_finding`), not `derived_fact`.",
        allowed_wording: "For `Strict`/`Mild` matches: state as an exact token-equality fact (todo.md §17.3). For `Weak`/`Semantic` matches: phrase as a possible/similar match, never an exact duplicate — those modes normalize literals and/or identifiers, so the underlying code is not byte-identical.",
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "fn calculate_shipping_cost(weight_kg: f64, distance_km: f64) -> f64 {\n    let base_rate = 2.5;\n    let mut cost = base_rate;\n    for _ in 0..(distance_km as i32) {\n        cost += weight_kg * 0.01;\n    }\n    cost\n}\n\nfn calculate_freight_cost(weight_kg: f64, distance_km: f64) -> f64 {\n    let base_rate = 2.5;\n    let mut cost = base_rate;\n    for _ in 0..(distance_km as i32) {\n        cost += weight_kg * 0.01;\n    }\n    cost\n}\n",
            why_it_matters: "Two functions with the same logic under different names mean every future bug fix has to be remembered and applied twice — and it usually isn't.",
        }),
    },
    // -- git.rs -----------------------------------------------------------
    RuleMetadata {
        id: "size-distribution",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Always evaluated (Fast Tier, no git history needed — pure per-file LOC plus per-crate aggregation over the loaded workspace; part of bare `cargo judge` and `audit`).",
        exclusions: "First-cut, adjustable Gini threshold (`SIZE_DISTRIBUTION_GINI_THRESHOLD`, 0.6, mirrors `crate::rules::duplication::DEFAULT_MIN_TOKENS`'s style); only fires when a file's LOC is in its crate's top decile *and* the crate's own file-size Gini coefficient exceeds the threshold — a large, concentrated file (e.g. a CLI dispatch table or an enum-heavy config module) is routinely legitimate, not a defect. A crate with only one authored file always has Gini `0.0` by construction and never fires.",
        allowed_wording: "State only the file's LOC, the crate's file count, and the crate's Gini coefficient against the threshold — never that the file 'is too big' or 'needs refactoring' (todo.md §17.4).",
        verdict_effect: VerdictEffect::AdvisoryOnly,
        // A single outlier file's real content, shown against a crate whose
        // other files are much smaller (see the paired drift-guard test in
        // `crate::git` for the companion files that make this Gini-exceed).
        example: Some(RuleExample {
            before: r#"//! Fee and discount calculations for order processing. This file has grown
//! organically as new pricing rules were added over several years.

pub struct OrderPricing;

impl OrderPricing {
    pub fn apply_new_customer_discount(&self, total: f64) -> f64 {
        total * 0.95
    }

    pub fn apply_loyalty_discount(&self, total: f64) -> f64 {
        total * 0.9
    }

    pub fn apply_bulk_order_discount(&self, total: f64, quantity: u32) -> f64 {
        if quantity > 100 {
            total * 0.85
        } else {
            total
        }
    }

    pub fn apply_seasonal_discount(&self, total: f64, month: u32) -> f64 {
        if month == 11 || month == 12 {
            total * 0.92
        } else {
            total
        }
    }

    pub fn apply_membership_discount(&self, total: f64, tier: u8) -> f64 {
        match tier {
            1 => total * 0.98,
            2 => total * 0.95,
            3 => total * 0.9,
            _ => total,
        }
    }

    pub fn apply_shipping_surcharge(&self, total: f64, weight_kg: f64) -> f64 {
        if weight_kg > 20.0 {
            total + 15.0
        } else {
            total + 5.0
        }
    }

    pub fn apply_fragile_handling_fee(&self, total: f64, fragile: bool) -> f64 {
        if fragile {
            total + 8.0
        } else {
            total
        }
    }

    pub fn apply_rush_delivery_fee(&self, total: f64, rush: bool) -> f64 {
        if rush {
            total * 1.25
        } else {
            total
        }
    }

    pub fn apply_gift_wrap_fee(&self, total: f64, gift_wrap: bool) -> f64 {
        if gift_wrap {
            total + 3.5
        } else {
            total
        }
    }

    pub fn apply_regional_tax(&self, total: f64, region: &str) -> f64 {
        match region {
            "CA" => total * 1.0725,
            "NY" => total * 1.08,
            "TX" => total * 1.0625,
            _ => total,
        }
    }

    pub fn apply_import_duty(&self, total: f64, imported: bool) -> f64 {
        if imported {
            total * 1.15
        } else {
            total
        }
    }

    pub fn apply_restocking_fee(&self, total: f64, returned: bool) -> f64 {
        if returned {
            total * 0.9
        } else {
            total
        }
    }

    pub fn apply_price_match_adjustment(&self, total: f64, competitor_price: Option<f64>) -> f64 {
        match competitor_price {
            Some(price) if price < total => price,
            _ => total,
        }
    }

    pub fn apply_currency_conversion(&self, total: f64, rate: f64) -> f64 {
        total * rate
    }

    pub fn apply_coupon_code(&self, total: f64, coupon_value: f64) -> f64 {
        (total - coupon_value).max(0.0)
    }

    pub fn apply_installment_fee(&self, total: f64, installments: u32) -> f64 {
        if installments > 1 {
            total * 1.03
        } else {
            total
        }
    }

    pub fn apply_warranty_fee(&self, total: f64, warranty_months: u32) -> f64 {
        total + (warranty_months as f64) * 1.5
    }

    pub fn apply_insurance_fee(&self, total: f64, insured: bool) -> f64 {
        if insured {
            total + 6.0
        } else {
            total
        }
    }

    pub fn apply_charity_roundup(&self, total: f64) -> f64 {
        total.ceil()
    }

    pub fn apply_employee_discount(&self, total: f64, is_employee: bool) -> f64 {
        if is_employee {
            total * 0.8
        } else {
            total
        }
    }

    pub fn apply_referral_credit(&self, total: f64, credit: f64) -> f64 {
        (total - credit).max(0.0)
    }

    pub fn apply_subscription_discount(&self, total: f64, subscriber: bool) -> f64 {
        if subscriber {
            total * 0.93
        } else {
            total
        }
    }

    pub fn apply_holiday_surcharge(&self, total: f64, is_holiday: bool) -> f64 {
        if is_holiday {
            total * 1.1
        } else {
            total
        }
    }

    pub fn apply_minimum_order_fee(&self, total: f64) -> f64 {
        if total < 10.0 {
            total + 2.0
        } else {
            total
        }
    }
}
"#,
            why_it_matters: "A single file that's grown to dominate its crate's size is a sign the module boundary hasn't kept up with the code — accumulated logic in one place is harder to navigate, review, and safely change than the same logic split along its natural seams.",
        }),
    },
    RuleMetadata {
        id: "complexity-concentration",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge` and `audit`) — reuses the same `Vec<FunctionInfo>` `complexity-inflation` and `hotspot` already compute over the loaded workspace, no extra parse pass.",
        exclusions: "First-cut, adjustable Gini threshold (`COMPLEXITY_CONCENTRATION_GINI_THRESHOLD`, 0.6, same value as `size-distribution`'s for consistency between the two same-shaped distributional signals); only fires when a file's total cyclomatic complexity is in its crate's top decile *and* the crate's own per-file complexity Gini coefficient exceeds the threshold — a large, concentrated-complexity file (e.g. a state machine or a CLI dispatch table) is routinely legitimate, not a defect. A crate with only one authored file always has Gini `0.0` by construction and never fires. A file with no functions (or that failed to parse) contributes `0` complexity rather than being excluded.",
        allowed_wording: "State only the file's total cyclomatic complexity, the crate's file count, and the crate's Gini coefficient against the threshold — never that the file 'is too complex' or 'needs refactoring' (todo.md §17.4).",
        verdict_effect: VerdictEffect::AdvisoryOnly,
        // A single outlier file's real content, shown against a crate whose
        // other files are trivial (see the paired drift-guard test in
        // `crate::git` for the companion files that make this Gini-exceed).
        example: Some(RuleExample {
            before: r#"//! Order status transitions and validation for the fulfillment pipeline.

pub enum OrderStatus {
    Pending,
    Paid,
    Packed,
    Shipped,
    Delivered,
    Cancelled,
    Refunded,
}

pub fn validate_transition(
    from: &OrderStatus,
    to: &OrderStatus,
    has_stock: bool,
    is_prepaid: bool,
) -> bool {
    match (from, to) {
        (OrderStatus::Pending, OrderStatus::Paid) => is_prepaid,
        (OrderStatus::Pending, OrderStatus::Cancelled) => true,
        (OrderStatus::Paid, OrderStatus::Packed) => has_stock,
        (OrderStatus::Paid, OrderStatus::Cancelled) => true,
        (OrderStatus::Packed, OrderStatus::Shipped) => true,
        (OrderStatus::Packed, OrderStatus::Cancelled) => has_stock,
        (OrderStatus::Shipped, OrderStatus::Delivered) => true,
        (OrderStatus::Delivered, OrderStatus::Refunded) => true,
        (OrderStatus::Cancelled, OrderStatus::Refunded) => is_prepaid,
        _ => false,
    }
}

pub fn required_notice(status: &OrderStatus, is_international: bool, is_expedited: bool) -> &'static str {
    match status {
        OrderStatus::Pending => "awaiting payment",
        OrderStatus::Paid if is_expedited && is_international => "customs prep, expedited",
        OrderStatus::Paid if is_international => "customs prep",
        OrderStatus::Paid => "queued for packing",
        OrderStatus::Packed if is_international => "export docs pending",
        OrderStatus::Packed => "ready to ship",
        OrderStatus::Shipped if is_expedited => "in transit, expedited",
        OrderStatus::Shipped => "in transit",
        OrderStatus::Delivered => "complete",
        OrderStatus::Cancelled => "cancelled",
        OrderStatus::Refunded => "refunded",
    }
}

pub fn can_cancel(status: &OrderStatus, refund_issued: bool) -> bool {
    match status {
        OrderStatus::Pending | OrderStatus::Paid => true,
        OrderStatus::Packed => !refund_issued,
        OrderStatus::Shipped | OrderStatus::Delivered => false,
        OrderStatus::Cancelled | OrderStatus::Refunded => false,
    }
}
"#,
            why_it_matters: "A single file that concentrates most of its crate's branching logic is harder to reason about test coverage for and riskier to change than the same logic split along its natural seams — the same intuition `size-distribution` applies to line count, just measured by branch count instead.",
        }),
    },
    // -- module_graph.rs ------------------------------------------------
    RuleMetadata {
        id: "unlinked-file",
        evidence_class: EvidenceClass::BoundedSemantic,
        preconditions: "Always evaluated (Fast Tier; `cargo judge module-graph`, subcommand-only).",
        exclusions: "Resolves `mod` declarations (including `#[path = \"...\"]`) from every recognized Cargo target root (`lib`, `bin`, `test`, `example`, `bench`, `build.rs`); a file spliced in only via `include!(...)` has no `mod` declaration and is invisible to this scan, so it is misreported as unlinked (see module docs 'Known blind spot: include!'). Generated files are excluded by default (see `crate::ingest::SourceKind`).",
        allowed_wording: BOUNDED_SEMANTIC_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "pub fn parse_legacy_config(raw: &str) -> Vec<(String, String)> {\n    raw.lines()\n        .filter_map(|line| line.split_once('='))\n        .map(|(key, value)| (key.trim().to_string(), value.trim().to_string()))\n        .collect()\n}\n",
            why_it_matters: "A source file that exists on disk but is never `mod`-declared silently drops out of the build — its code never compiles or runs, and nobody notices until they go looking for it.",
        }),
    },
    RuleMetadata {
        id: "orphan-module",
        evidence_class: EvidenceClass::BoundedSemantic,
        preconditions: "Always evaluated (Fast Tier; `cargo judge module-graph`, subcommand-only).",
        exclusions: "Only resolves `crate::`/`super::`/`<crate-name>::`-qualified references, plus the narrow same-file `mod foo; use foo::...;` bare-reference exception (see module docs); any other bare/self-relative reference is not resolved, so a module referenced only that way can be misreported as orphaned. Modules containing a recognized entry point (`fn main`, a `#[test]`-like function) are exempt. Scoped to file-backed (`mod foo;`) module nodes; inline `mod foo { .. }` blocks have no file of their own and are not checked.",
        allowed_wording: BOUNDED_SEMANTIC_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "pub fn normalize_whitespace(input: &str) -> String {\n    input.split_whitespace().collect::<Vec<_>>().join(\" \")\n}\n\npub fn trim_and_normalize(input: &str) -> String {\n    normalize_whitespace(input.trim())\n}\n",
            why_it_matters: "A module that's declared but never referenced from outside its own file is dead weight in the module tree — nothing else can reach it, yet it still gets compiled, reviewed, and maintained.",
        }),
    },
    // -- ownership.rs -------------------------------------------------------

    // -- pattern.rs (advisory-only design-pattern recommendations; never a
    // Finding — see that module's doc comment) ----------------------------
    RuleMetadata {
        id: "stringly-error-boundary",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Requires `cargo judge patterns` (or `explain-pattern`/`fix-preview`); not part of bare `cargo judge`, `audit`, or any Finding-producing report — never wired into `evidence_class_for_rule`, the health score, or a baseline verdict.",
        exclusions: "Requires ≥2 `catch-all-error` findings in the same crate plus at least one crate-local typed error definition (independently sourced signals); a single symptom, or symptoms without a typed error already present, produce no candidate. The boundary can be a deliberate compatibility shim rather than a design gap.",
        allowed_wording: "Every claim must be phrased as an observation with checkable evidence locations, never an absolute claim (todo.md §16.7 'Sprachdisziplin'); never state this is 'the best' pattern or that the current structure is definitely wrong (todo.md §17.4). Always pair with a contraindication.",
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "pub fn fetch_user(id: u64) -> Result<User, Box<dyn std::error::Error>> {\n    let raw = http_get(&format!(\"/users/{id}\"))?;\n    Ok(parse_user(&raw)?)\n}\n\npub fn fetch_order(id: u64) -> Result<Order, Box<dyn std::error::Error>> {\n    let raw = http_get(&format!(\"/orders/{id}\"))?;\n    Ok(parse_order(&raw)?)\n}\n\npub enum ApiError {\n    NotFound,\n    Timeout,\n}\n",
            why_it_matters: "Two boundary functions already erase their errors into `Box<dyn Error>` even though this crate has a typed error ready to use, which forces every caller to match on error text instead of a variant.",
        }),
    },
    RuleMetadata {
        id: "primitive-domain-value",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Requires `cargo judge patterns` (or `explain-pattern`/`fix-preview`); not part of bare `cargo judge`, `audit`, or any Finding-producing report — never wired into `evidence_class_for_rule`, the health score, or a baseline verdict.",
        exclusions: "Fast-Tier-reachable narrowing of the full todo.md §16.3 rule: only the same (parameter name, type) pair across ≥2 `pub fn` signatures in the same crate, restricted to primitive numeric/`String`/`&str` types (`bool` excluded — see `boolean-state-cluster`), with at least one signature guarding the parameter. No cross-crate reasoning, no non-syntactic evidence. A shared name/type pair can have different meanings across functions despite matching structurally.",
        allowed_wording: "Every claim must be phrased as an observation with checkable evidence locations, never an absolute claim (todo.md §16.7 'Sprachdisziplin'); never state this is 'the best' pattern or that the current structure is definitely wrong (todo.md §17.4). Always pair with a contraindication.",
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "pub fn set_retry_timeout(timeout_ms: u32) {\n    println!(\"timeout set to {timeout_ms}\");\n}\n\npub fn schedule_backoff(timeout_ms: u32) -> Result<(), String> {\n    if timeout_ms > 60_000 {\n        return Err(\"timeout_ms must be at most 60 seconds\".to_string());\n    }\n    Ok(())\n}\n",
            why_it_matters: "The same `timeout_ms: u32` parameter appears in two public functions, but only one of them checks that the value is sane, so callers can't tell from the type alone which functions expect an already-validated timeout.",
        }),
    },
    RuleMetadata {
        id: "boolean-state-cluster",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Requires `cargo judge patterns` (or `explain-pattern`/`fix-preview`); not part of bare `cargo judge`, `audit`, or any Finding-producing report — never wired into `evidence_class_for_rule`, the health score, or a baseline verdict.",
        exclusions: "Fast-Tier-reachable narrowing of the full todo.md §16.3 rule, scoped to a single function rather than cross-call-site: needs ≥3 `bool` parameters plus a condition/`match` combining ≥2 of them within the same function body; does not aggregate evidence about how bool parameters are combined across call sites.",
        allowed_wording: "Every claim must be phrased as an observation with checkable evidence locations, never an absolute claim (todo.md §16.7 'Sprachdisziplin'); never state this is 'the best' pattern or that the current structure is definitely wrong (todo.md §17.4). Always pair with a contraindication.",
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "pub fn configure_logger(verbose: bool, json_output: bool, include_timestamps: bool) {\n    if verbose && json_output {\n        enable_debug_json_format();\n    }\n    let _ = include_timestamps;\n}\n\nfn enable_debug_json_format() {}\n",
            why_it_matters: "Three boolean flags on one function signature already need a combined check to make sense of, and every new flag doubles the number of state combinations callers and maintainers have to reason about.",
        }),
    },
    RuleMetadata {
        id: "public-invariant-bypass",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Requires `cargo judge patterns` (or `explain-pattern`/`fix-preview`); not part of bare `cargo judge`, `audit`, or any Finding-producing report — never wired into `evidence_class_for_rule`, the health score, or a baseline verdict.",
        exclusions: "Fast-Tier-reachable narrowing of the full todo.md §16.3 rule, deliberately without the full rule's consumer-side analysis: needs a `pub struct` with ≥2 `pub` fields and no `#[non_exhaustive]` attribute, plus at least one crate-local constructor-shaped `pub fn` (return type `Self`/the struct name, optionally `Result`-wrapped) that jointly validates ≥2 of those fields via matching parameter names. No cross-crate reasoning, no consumer call-site analysis.",
        allowed_wording: "Every claim must be phrased as an observation with checkable evidence locations, never an absolute claim (todo.md §16.7 'Sprachdisziplin'); never state this is 'the best' pattern or that the current structure is definitely wrong (todo.md §17.4). Always pair with a contraindication.",
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "pub struct PriceRange {\n    pub min_price: u32,\n    pub max_price: u32,\n}\n\nimpl PriceRange {\n    pub fn new(min_price: u32, max_price: u32) -> Result<Self, String> {\n        if min_price >= max_price {\n            return Err(\"min_price must be less than max_price\".to_string());\n        }\n        Ok(Self { min_price, max_price })\n    }\n}\n",
            why_it_matters: "The constructor enforces that the minimum stays below the maximum, but both fields are `pub`, so any caller can still build a `PriceRange` directly and skip that check entirely.",
        }),
    },
    RuleMetadata {
        id: "manual-resource-lifecycle",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Requires `cargo judge patterns` (or `explain-pattern`/`fix-preview`); not part of bare `cargo judge`, `audit`, or any Finding-producing report — never wired into `evidence_class_for_rule`, the health score, or a baseline verdict.",
        exclusions: "Fast-Tier-reachable narrowing of the full todo.md §16.3 rule, with no ownership/lifetime analysis: needs one function calling both an acquire-shaped operation and a release-shaped counterpart by call name alone, plus a crate with no `impl Drop for ...` anywhere. Cannot show that ownership and lifetime of the resource are actually bound to a single guard — acquire/release name matches can be coincidental and couple unrelated calls.",
        allowed_wording: "Every claim must be phrased as an observation with checkable evidence locations, never an absolute claim (todo.md §16.7 'Sprachdisziplin'); never state this is 'the best' pattern or that the current structure is definitely wrong (todo.md §17.4). Always pair with a contraindication.",
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "pub fn run_migration(host: &str) {\n    connect(host);\n    disconnect();\n}\n\nfn connect(_host: &str) {}\nfn disconnect() {}\n",
            why_it_matters: "The connection is opened and closed by explicit function calls instead of a guard whose `Drop` impl closes it automatically, so any early return or panic between the two calls leaks the connection.",
        }),
    },
    // -- provenance.rs ------------------------------------------------------

    // -- security.rs (Fast Tier security-shaped signals) -------------------
    RuleMetadata {
        id: "unsafe-surface",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`).",
        exclusions: "Scoped to `unsafe { .. }` expression blocks only — `unsafe fn`/`unsafe impl`/`unsafe trait` declarations are out of scope (a different existing convention: a `# Safety` doc section). The adjacency check for a `// SAFETY:` comment is a line-range heuristic (immediately preceding line, or the first inner line of the block) over `crate::rules::slop_text`'s raw-source-text comment scan, not a semantic link between the comment and the block — a `SAFETY:` comment placed elsewhere (e.g. at the top of the enclosing function) is not credited.",
        allowed_wording: "State only that no `SAFETY:` comment was found adjacent to this unsafe block — never that the code is 'unsound' or 'a vulnerability' (todo.md §17.4).",
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "pub fn first_byte(buf: &[u8]) -> u8 {\n    unsafe { *buf.as_ptr() }\n}\n",
            why_it_matters: "An unsafe block with no adjoining SAFETY comment leaves the next reader with no record of why dereferencing this raw pointer is actually sound, turning a real safety invariant into unwritten tribal knowledge.",
        }),
    },
    RuleMetadata {
        id: "unsafe-density",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`).",
        exclusions: "A whole-file companion metric to `unsafe-surface` (which flags one `unsafe { .. }` block per site), not a replacement for it: this rule aggregates every `unsafe { .. }` block in the file into `unsafe_density` (the fraction of the file's lines sitting inside any `unsafe { .. }` block) and `max_unsafe_block_size` (the single largest block's own line count), and fires if either crosses its threshold (10% density, or a single block over 30 lines). Only files with at least one `unsafe` block are considered — a file with none is skipped, not reported at 0%. A lexically nested `unsafe { .. }` block found inside another one is not separately counted, to avoid double-counting the same lines. Crossing either threshold is not itself a defect: an FFI wrapper, a `no_std` allocator, or SIMD code can legitimately need a high, necessary `unsafe` density — unlike `unsafe-surface`'s per-site missing-SAFETY-comment check (a concrete, actionable documentation gap worth gating on), this is a distributional signal about a file's overall shape, advisory only, never a violation on its own.",
        allowed_wording: "State only the measured `unsafe_density` ratio and/or `max_unsafe_block_size` in lines for this file — never that the file is 'unsafe', 'dangerous', or 'risky' (todo.md §17.4).",
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "pub fn read_first_four(buf: &[u8]) -> u32 {\n    unsafe {\n        let ptr = buf.as_ptr() as *const u32;\n        *ptr\n    }\n}\n",
            why_it_matters: "When almost an entire function's body sits inside one unsafe block, the compiler has stopped checking nearly everything that function does, and a reviewer can no longer tell which lines are the actual reason it needed to be unsafe at all.",
        }),
    },
    RuleMetadata {
        id: "integer-cast-risk",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`).",
        exclusions: "A syntax-only proxy, not a truncation proof: true truncation detection needs the source expression's real type (a type checker), not available at the Fast Tier — the same limitation already documented for `silent-default`/`context-free-propagation` in `crate::rules::slop`'s module doc. Only the cast's written target type is checked (`u8`/`i8`/`u16`/`i16`/`u32`/`i32`/`usize`/`isize`); false-positives on an already-narrow source (e.g. `byte_var as u8`), and false-negatives on a float cast to `u64`/`i64`/`u128`/`i128` (still narrowing, but not covered by this v1 target-type list). A cast whose direct inner expression is itself a call to `clamp`/`min`/`max`/one of the `saturating_*` methods is treated as already bounded and not flagged (a syntax-only check on the cast's immediate child only, not a binary expression's operands or deeper nesting) — added after a 2026-07-24 precision audit against a real 135k-LOC corpus found this the dominant false-positive source (311 of 312 findings, nearly all otherwise-bounded scoring/percentage arithmetic; GitHub issue #9).",
        allowed_wording: "State only that this is a possible truncation candidate based on the cast's target type — never 'this truncates' or 'this is a bug' (todo.md §17.4).",
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "pub fn scale_score(raw: i64) -> i32 {\n    (raw * 100) as i32\n}\n",
            why_it_matters: "Casting a widened score computation down to i32 truncates silently the moment the value exceeds i32's range, producing a wrong-but-plausible number instead of an error.",
        }),
    },
    RuleMetadata {
        id: "panic-in-lib",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`).",
        exclusions: "Scoped to a function whose own written visibility is `pub` — item-level only, same simplification as `undocumented-public-item` (a `pub fn` inside a private `mod` is still checked as if reachable; a trait default method, with no visibility of its own, is never checked). `.unwrap()`/`.expect(..)`/indexing are name/operator matches, not resolved against a real `Option`/`Result`/`Index` type — a type defining its own non-panicking method or operator of the same name is not distinguished from the standard, panicking one (same accepted imprecision as `swallowed-result`'s `.ok()` match). Does not distinguish a `[lib]` target's public API from a `[[bin]]`-only crate's `pub` items, which are never actually reachable by another crate. `#[test]`-attributed functions are excluded directly; a `#[cfg(test)] mod tests {..}` block is not tracked, but its functions are almost never themselves `pub`. An indexing finding's `evidence.kind` further distinguishes the index operand's shape: a range (`expr[a..b]`) is `range_slice` (real bounds and, for a `str`, char-boundary risk), a string literal (`expr[\"key\"]`) is `string_key_indexing` (in practice almost always a non-panicking `serde_json::Value`/`toml::Value`-style accessor, though a `HashMap<String, _>` would still genuinely panic and cannot be told apart from this without type information), anything else stays plain `indexing` — added after a 2026-07-24 precision audit found ~100 of 116 findings in a real corpus were `serde_json::Value` string-key access, none a real panic risk, while the only real bug found was a range-slice (GitHub issue #10).",
        allowed_wording: "State only that a panic-shaped construct (`.unwrap()`/`.expect(..)`/`panic!(..)`/indexing) exists on a `pub` path — never that it 'will panic', 'crashes', or 'is a bug' (todo.md §17.4).",
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "pub fn read_config(raw: Option<&str>) -> &str {\n    raw.unwrap()\n}\n",
            why_it_matters: "A public function that unwraps its input hands every caller a landmine: a missing config value doesn't return an error — it takes down the whole program.",
        }),
    },
    RuleMetadata {
        id: "hardcoded-secret",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`).",
        exclusions: "Two lanes (`evidence.kind`): `known_pattern` matches a string literal against a small, publicly documented list of secret-provider formats (AWS/GitHub/Slack/Google, PEM headers) anywhere in the file — a provider's own documented example value (e.g. AWS's `AKIAIOSFODNN7EXAMPLE`) matches identically to a live key. `high_entropy_assignment` requires a string literal to be the direct initializer of a `let`/`const`/`static` whose own name contains a suspicious marker, plus a minimum length and Shannon entropy — ordinary high-entropy strings (hashes, UUIDs, encoded blobs) are not flagged unless bound to such a name, but a placeholder/rotated/revoked credential is indistinguishable from a live one either way. `#[test]`-attributed functions and `#[cfg(test)]`-gated items are excluded from both lanes. `evidence` never includes the literal's own text, only its kind/pattern/length.",
        allowed_wording: "State only that a string literal matches a known secret-provider format, or is bound to a suspiciously-named binding with high entropy — never that it 'is a secret', 'is leaked', or 'is a vulnerability' (todo.md §17.4).",
        verdict_effect: VerdictEffect::AdvisoryOnly,
        // Deliberately NOT a real provider's key shape (e.g. Google's `AIza`
        // + 39 chars) — a byte-for-byte match would trip GitHub's own
        // secret scanning on this very file. This demonstrates the entropy
        // lane instead: a suspiciously-named binding plus a high-entropy,
        // non-provider-shaped literal.
        example: Some(RuleExample {
            before: "const API_SECRET: &str = \"Kx7$mQ2#Lp9@Rn4^Wz6&Tb3!\";\n",
            why_it_matters: "A credential committed as a literal ends up in git history forever, readable by anyone with clone access, long after it's rotated out of the running config.",
        }),
    },
    // -- slop.rs (G1 error-masking, G2 stub/theater-code, G3 lexical) -------
    RuleMetadata {
        id: "swallowed-result",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block).",
        exclusions: "Syntax-only: only `let _ = fallible();` and a bare `.ok();` statement are matched; other ways of discarding a `Result` are not. Exempt inside `Drop::drop`'s own body (`impl Drop for _ { fn drop(&mut self) { .. } }`, matched by trait path's last segment): `drop` cannot return a `Result`, so `let _ = fallible();` is the only correct idiom there, not a discarded error.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "fn save_settings(path: &std::path::Path, data: &str) {\n    let _ = std::fs::write(path, data);\n}\n",
            why_it_matters: "Discarding a `Result` with `let _ = ...` throws away the one signal that the operation could fail — a failed disk write here looks exactly like a successful save to every caller downstream.",
        }),
    },
    RuleMetadata {
        id: "empty-error-arm",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block).",
        exclusions: "Only an empty `Err(_)`/`Err(..)` match arm, or an `if let Err(_) = ... { }` with no `else`, is matched.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "fn send_notification(payload: &str) -> Result<(), String> {\n    match deliver(payload) {\n        Err(_) => {}\n        Ok(_) => {}\n    }\n    Ok(())\n}\nfn deliver(_payload: &str) -> Result<(), String> {\n    Ok(())\n}\n",
            why_it_matters: "Swallowing an error in an empty match arm hides a failed delivery from every caller — a failed send looks identical to a successful one until a user notices something never arrived.",
        }),
    },
    RuleMetadata {
        id: "catch-all-error",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block).",
        exclusions: "Only `pub fn`/`impl` method boundaries whose error type is erased (`Box<dyn Error>` / `anyhow::Error`, unless `[rules.catch-all-error] allow-anyhow-at-boundary = true` in `judge.toml`) *and* whose body contains at least one `.map_err(|_| ..)` call with a wildcard closure parameter are matched — added after a 2026-07-24 precision audit found 0 of 8 findings real in a codebase that only ever used plain `?`/`anyhow!` propagation (which preserves the source error chain) through such a boundary; a type-erased return type alone is normal, often-recommended `anyhow`/`thiserror` style, not evidence of discarded error information (GitHub issue #11). Internal (non-`pub`) error erasure remains out of scope, and this body check is itself a syntax-only proxy — a `.map_err` closure that uses its bound (non-wildcard) parameter, or discards information through some other pattern (a `match` collapsing distinct arms to one message, for instance), is not distinguished either way.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "pub fn load_settings(path: &std::path::Path) -> Result<String, Box<dyn std::error::Error>> {\n    let raw = std::fs::read_to_string(path).map_err(|_| \"failed to load settings\")?;\n    Ok(raw)\n}\n",
            why_it_matters: "Erasing the error type at a public boundary *and* discarding the original error in `.map_err` forces every caller to match on a string or downcast blindly instead of handling specific, known failure modes — the original failure reason is gone for good.",
        }),
    },
    RuleMetadata {
        id: "suppression-debt",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block). Reported as `Severity::Info` for the current state only — trend-against-baseline is handled by the existing baseline/delta system.",
        exclusions: "Counts `#[allow(...)]`/`#[expect(...)]` attribute occurrences; does not judge whether any individual suppression is justified.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "#[allow(clippy::too_many_arguments)]\nfn build_report(a: i32, b: i32, c: i32, d: i32, e: i32, f: i32, g: i32) -> i32 {\n    a + b + c + d + e + f + g\n}\n",
            why_it_matters: "Each suppressed lint is a small, permanent exception to the project's own quality bar that nobody revisits once the deadline pressure that created it has passed.",
        }),
    },
    RuleMetadata {
        id: "merged-stub",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block).",
        exclusions: "Only bare `todo!()`/`unimplemented!()` outside a `#[cfg(feature = ...)]`-gated scope; feature-gated stubs are excluded by design.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "fn export_report(_format: &str) -> Vec<u8> {\n    todo!()\n}\n",
            why_it_matters: "A `todo!()` left in merged code compiles cleanly and looks finished, but panics the moment a caller actually exercises that path in production.",
        }),
    },
    RuleMetadata {
        id: "empty-impl",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block).",
        exclusions: "Only a function/method/trait-default with a doc comment and a literally empty body is matched; an empty body without a doc comment is not flagged. Exempt for methods overriding one of the standard `syn` AST-visitor traits (`Visit`/`VisitMut`/`Fold`, matched by trait path's last segment): an empty override of one of these traits' hooks is a deliberate 'skip descending into this AST node type' choice constrained by the trait's own contract, not a stub. Other trait-impl method overrides are not exempted.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "/// Sends the confirmation email to the customer.\nfn send_confirmation_email(_customer_id: u64) {}\n",
            why_it_matters: "A doc comment describing behavior on a function whose body is empty is a promise the code doesn't keep — anything calling it silently does nothing.",
        }),
    },
    RuleMetadata {
        id: "assertion-free-test",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block).",
        exclusions: "Only the literal `#[test]` attribute (without `#[should_panic]`) is matched, not third-party test-framework attributes (`#[tokio::test]`, `#[rstest]`, ...). Syntactically assertion-free does not mean the test is ineffective — macros, propagated return errors, and helper functions can still exercise behavior.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "#[test]\nfn loads_default_config() {\n    let _config = Config::default();\n}\n",
            why_it_matters: "A test with no assertion always passes regardless of whether the code under test actually works — it only proves the function didn't panic.",
        }),
    },
    RuleMetadata {
        id: "tautological-test",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block).",
        exclusions: "Only the literal `assert!(true)` / `assert_eq!(x, x)` shapes are matched.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "#[test]\nfn retry_count_matches_itself() {\n    let retry_count = 3;\n    assert_eq!(retry_count, retry_count);\n}\n",
            why_it_matters: "`assert_eq!(x, x)` can never fail, so the test provides zero regression protection while still counting toward the suite's confidence and coverage numbers.",
        }),
    },
    RuleMetadata {
        id: "ignored-test-accumulation",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block). Reported as `Severity::Info` for the current state only — trend-against-baseline is handled by the existing baseline/delta system.",
        exclusions: "Only the literal `#[ignore]`/`#[ignore = \"...\"]` attribute is matched, not third-party test-framework equivalents.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "#[test]\n#[ignore = \"flaky in CI\"]\nfn retries_on_timeout() {\n    assert_eq!(1 + 1, 2);\n}\n",
            why_it_matters: "An ignored test stops running in CI but keeps looking like coverage in the test count, hiding the fact that its behavior is no longer actually checked.",
        }),
    },
    RuleMetadata {
        id: "conversational-artifact",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block).",
        exclusions: "Only plain `//`/`/* */` comments are scanned (raw source-text scan in `crate::rules::slop_text`, since `syn` discards non-doc comments entirely); `///`/`//!` doc comments are out of scope for this rule. A trigger phrase immediately enclosed by matching quote marks (`\"` or a backtick) is treated as quoted meta-discussion of the phrase, not a live disclaimer, and does not fire — which is why this very comment can quote \"as an AI\" below without self-triggering.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        // The trigger phrase ("as an AI") lives inside a Rust string literal
        // here, not a real `//` comment in this file — judge's own raw-text
        // comment scanner only sees actual `//`/`/* */` tokens, so this
        // literal cannot self-trigger the rule against rule_registry.rs.
        example: Some(RuleExample {
            before: "fn validate_payload(payload: &str) -> bool {\n    // As an AI, I can't verify external formats, so this only checks length.\n    !payload.is_empty()\n}\n",
            why_it_matters: "A stray AI-assistant disclaimer left in a comment signals the code was pasted from a chat session without review, and means nothing to the next engineer maintaining it.",
        }),
    },
    RuleMetadata {
        id: "restating-comment",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block).",
        exclusions: "Only plain `//`/`/* */` comments are scanned (raw source-text scan in `crate::rules::slop_text`); `///`/`//!` doc comments are out of scope for this rule (see `doc-restates-signature`).",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "struct Wallet {\n    wallet_balance_field: i64,\n}\nimpl Wallet {\n    fn deposit(&mut self, given_amount: i64) {\n        // update the wallet balance field to the given amount\n        self.wallet_balance_field = given_amount;\n    }\n}\n",
            why_it_matters: "A comment that only restates the line below it in English adds reading time without adding information, and goes stale the moment the code changes since nothing forces it to update.",
        }),
    },
    RuleMetadata {
        id: "step-comment-inflation",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block).",
        exclusions: "Only plain `//`/`/* */` comments are scanned (raw source-text scan in `crate::rules::slop_text`); requires a chain of three or more `// Step N:`-shaped comments.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "fn process_order(order_id: u64) {\n    // Step 1: validate the order\n    let is_valid = validate(order_id);\n    // Step 2: charge the payment\n    let charged = charge(order_id);\n    // Step 3: send the confirmation\n    notify(order_id);\n}\nfn validate(_order_id: u64) -> bool {\n    true\n}\nfn charge(_order_id: u64) -> bool {\n    true\n}\nfn notify(_order_id: u64) {}\n",
            why_it_matters: "A `// Step N:` comment for each line restates the control flow the code already makes obvious, and the whole chain has to be renumbered by hand every time a step is added, removed, or reordered.",
        }),
    },
    RuleMetadata {
        id: "generic-naming",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block).",
        exclusions: "Only an identifier that is exactly a fixed placeholder word (`data`, `temp`, `handler`, ...) is flagged; a poorly named identifier outside that list is not.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "pub fn processor(input: &str) -> String {\n    input.to_uppercase()\n}\n",
            why_it_matters: "A public function named after its category rather than what it actually does forces every caller to open the implementation just to learn what it's for.",
        }),
    },
    RuleMetadata {
        id: "doc-restates-signature",
        evidence_class: EvidenceClass::DerivedFact,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block).",
        exclusions: "Only a doc comment that is a pure signature echo is flagged; a doc comment that adds any information beyond the signature is not.",
        allowed_wording: DERIVED_FACT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "/// Returns the balance.\npub fn balance() -> f64 {\n    0.0\n}\n",
            why_it_matters: "A doc comment that only repeats the function's own name and return type gives readers nothing they couldn't already infer from the signature, while looking documented in a coverage report.",
        }),
    },
    RuleMetadata {
        id: "silent-default",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block).",
        exclusions: "A syntax-only proxy, not a taint proof: a complete check would need to confirm the observed error is really the same one being defaulted away, which needs real value-flow tracking not available at the Fast Tier — the same limitation `context-free-propagation`/`debug-format-leak` document, deliberately sidestepped here instead of solved. Only two call shapes match: `.unwrap_or_default()`, and `.unwrap_or_else(|_| ..)` where the closure takes a single wildcard/unused parameter and its body is exactly a `Default::default()`/`<Type>::default()` call — both are inherently `Option<T>`/`Result<T, E>`-only methods in std, so no receiver-type check is needed. The corroborating signal (\"no error-observing call anywhere in this function\": no `.inspect_err(`, no `log::`/`tracing::`-qualified call, no unqualified `eprintln!`/`warn!`/`error!` macro, no `if let Err(..)`) is function-granularity, not call-site granularity — a function that observes a *different* fallible call's error elsewhere still suppresses a finding on this one, since judge cannot tell the two apart without real data-flow analysis.",
        allowed_wording: HEURISTIC_WORDING,
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "fn parse_retry_count(raw: Option<&str>) -> u32 {\n    raw.and_then(|s| s.parse().ok()).unwrap_or_default()\n}\n",
            why_it_matters: "An invalid or missing retry count silently becomes 0 instead of surfacing anywhere, so a misconfigured value looks identical to a deliberately disabled retry.",
        }),
    },
    RuleMetadata {
        id: "context-free-propagation",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block).",
        exclusions: "A syntax-only proxy: only matches a function's own *written* return type against a fixed list of syntactically recognizable opaque-error idioms (`anyhow::Result<_>`/`anyhow::Error`, `eyre::Result<_>`/`eyre::Report`, `Box<dyn std::error::Error ..>`) — a type alias that resolves to one of these shapes without spelling it out is not recognized, since that needs a type checker, not available at the Fast Tier. Only fires with 2 or more `?`-sites on distinct underlying calls (deduped by token text, so the exact same call written twice counts once) — a function with a single fallible call needs no context to disambiguate which operation failed, so it is never flagged. Fires only when the function's body contains *zero* `.context(`/`.with_context(` calls anywhere — a function that already calls `.context()` for some of its `?`-sites but not all of them is not flagged, since the author is clearly already following that practice in this function; this is a deliberate, coarser-than-call-site choice, not an oversight.",
        allowed_wording: HEURISTIC_WORDING,
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "pub fn load_user_profile(path: &str) -> anyhow::Result<String> {\n    let raw = std::fs::read_to_string(path)?;\n    let parsed = raw.parse::<i32>()?;\n    Ok(parsed.to_string())\n}\n",
            why_it_matters: "When either the file read or the parse fails, the caller only ever sees the bare underlying error (a plain `io::Error` or `ParseIntError`) with no indication of which of the two operations — or which file/value — actually failed.",
        }),
    },
    RuleMetadata {
        id: "debug-format-leak",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block).",
        exclusions: "The sink is identified by the `impl std::fmt::Display for T` block boundary itself — Rust's own convention that `Display`/`{}`/`.to_string()` is the user-facing representation and `Debug`/`{:?}` is the diagnostic one — not by tracing a value's flow into it, so this does not need the general cross-function taint tracking `silent-default`/`context-free-propagation` also don't have. Only a `write!(f, ..)`/`format!(..)` call whose first string-literal argument contains the literal substring `{:?}`/`{:#?}` is matched; a captured/positional debug placeholder written as `{value:?}`/`{0:?}` is not recognized. A `format!`/`write!` invocation nested inside another macro's own argument tokens (e.g. `write!(f, \"{}\", format!(\"{:?}\", x))`) is invisible to this rule — `syn` does not parse into a macro's argument tokens as part of the file-level AST, only this rule's own explicit top-level macro visits are checked, same macro-opacity limitation already documented for `merged-stub`/`suppression-debt` elsewhere in this file.",
        allowed_wording: HEURISTIC_WORDING,
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "struct OrderId(u64);\n\nimpl std::fmt::Display for OrderId {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{:?}\", self.0)\n    }\n}\n",
            why_it_matters: "Every user-facing rendering of `OrderId` (`{}`, `.to_string()`, anywhere it's shown in a report or error message) now goes through `Debug` formatting instead of a deliberate display representation, so the moment the field's own type changes its `Debug` output, customer-visible text silently changes with it.",
        }),
    },
    // -- slop_structural.rs (G4, Fast Tier subset) ---------------------------
    RuleMetadata {
        id: "complexity-inflation",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block).",
        exclusions: "Flags a long function that either has implausibly low branching (cyclomatic complexity), is deeply nested/hard to follow relative to its length (Cognitive Complexity, SonarSource's approximated metric, threshold 15), nests `async` blocks/closures more than 2 levels deep (`async_nesting_depth`, a distinct dimension from the function's own `async fn` status), or contains a single expression with more than 6 direct operands (`max_expression_width` — a call's argument count, a tuple/array/struct literal's element count, or a flattened same-operator binary chain); does not distinguish a genuinely simple long function (e.g. a large match/data table) from a padded one, and does not distinguish deliberately layered control flow (or deliberately wide data literals) from code that would benefit from flattening.",
        allowed_wording: HEURISTIC_WORDING,
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "pub fn map_webhook_payload(raw: &RawPayload) -> WebhookEvent {\n    let mut event = WebhookEvent::default();\n    event.id = raw.id.clone();\n    event.event_type = raw.event_type.clone();\n    event.account_id = raw.account_id.clone();\n    event.timestamp = raw.timestamp.clone();\n    event.source_ip = raw.source_ip.clone();\n    event.user_agent = raw.user_agent.clone();\n    event.session_id = raw.session_id.clone();\n    event.request_id = raw.request_id.clone();\n    event.status = raw.status.clone();\n    event.amount = raw.amount.clone();\n    event.currency = raw.currency.clone();\n    event.method = raw.method.clone();\n    event.description = raw.description.clone();\n    event.reference = raw.reference.clone();\n    event.customer_id = raw.customer_id.clone();\n    event.customer_email = raw.customer_email.clone();\n    event.customer_name = raw.customer_name.clone();\n    event.billing_country = raw.billing_country.clone();\n    event.billing_postcode = raw.billing_postcode.clone();\n    event.card_brand = raw.card_brand.clone();\n    event.card_last4 = raw.card_last4.clone();\n    event.risk_score = raw.risk_score.clone();\n    event.gateway = raw.gateway.clone();\n    event.gateway_response = raw.gateway_response.clone();\n    event.metadata = raw.metadata.clone();\n    event.created_at = raw.created_at.clone();\n    event.updated_at = raw.updated_at.clone();\n    event.processed_at = raw.processed_at.clone();\n    event.retry_count = raw.retry_count.clone();\n    event.webhook_version = raw.webhook_version.clone();\n    event.signature = raw.signature.clone();\n    event.idempotency_key = raw.idempotency_key.clone();\n    event.notes = raw.notes.clone();\n    event.tags = raw.tags.clone();\n    event.locale = raw.locale.clone();\n    event.channel = raw.channel.clone();\n    event.environment = raw.environment.clone();\n    event.priority = raw.priority.clone();\n    event.correlation_id = raw.correlation_id.clone();\n    event\n}\n",
            why_it_matters: "A function that's long only because it repeats the same field-by-field assignment forty times over hides any real logic change in a wall of boilerplate that reviewers stop reading carefully.",
        }),
    },
    RuleMetadata {
        id: "abstraction-inflation",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block).",
        exclusions: "Covers three sub-patterns (single-impl trait, delegating wrapper, builder for a small struct) via `evidence.kind`; a deliberate abstraction seam kept for testability/future extension looks structurally identical to an unnecessary one. `single-impl-trait` only fires for traits declared within the analyzed workspace, so implementing a foreign trait (std or an external crate, e.g. `Write`, `Drop`, `Iterator`, `From`) exactly once is never flagged, regardless of impl count.",
        allowed_wording: HEURISTIC_WORDING,
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "pub struct MetricsStore(HashMap<String, u64>);\n\nimpl MetricsStore {\n    pub fn get(&self, key: &str) -> Option<&u64> {\n        self.0.get(key)\n    }\n}\n",
            why_it_matters: "A wrapper struct whose every method just forwards to the field it wraps adds a layer of indirection with no behavior of its own, making every caller trace through an extra hop for nothing.",
        }),
    },
    RuleMetadata {
        id: "fragile-substring-classification",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Always evaluated (Fast Tier; part of bare `cargo judge`, `audit`, and `health`'s slop block).",
        exclusions: "Only if/else-if chains of 2+ conditions are considered, and a condition is only flagged for a missing word-boundary check within that same condition expression; whether the string literal ever actually collides with an unrelated substring in real input is not evaluated — a shape-based hint, not a misclassification proof.",
        allowed_wording: HEURISTIC_WORDING,
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "pub fn detect_log_level(line: &str) -> &'static str {\n    if line.contains(\"ERROR\") {\n        \"error\"\n    } else if line.contains(\"WARN\") {\n        \"warn\"\n    } else {\n        \"info\"\n    }\n}\n",
            why_it_matters: "Classifying a log line by whether it merely contains \"ERROR\" also matches unrelated text like \"ERROR_RATE resolved\", misfiling a routine info line as an error.",
        }),
    },
    // -- slop_structural_deep.rs (G4 remainder, Deep Tier, `--features deep`)
    RuleMetadata {
        id: "duplicative-reinvention",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Requires `--features deep` and `cargo judge dead-code` (Deep Tier; needs `find_all_refs` cross-file reference data). Reported as `Severity::Info` for the current state only — trend-against-baseline is handled by the existing baseline/delta system.",
        exclusions: "Test/bench-attributed functions and methods inside `impl TraitName for SomeType` blocks are excluded from the candidate set entirely, not down-weighted — trait-impl methods are routinely invoked through operator/macro sugar `find_all_refs` can't see (e.g. `Display::fmt`, `Iterator::next`, `Drop::drop`), so they would otherwise look structurally unwired even when used everywhere.",
        allowed_wording: HEURISTIC_WORDING,
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "fn calculate_discount_v1(price: f64) -> f64 {\n    price * 0.9\n}\n\nfn calculate_discount_v2(price: f64) -> f64 {\n    price * 0.9\n}\n",
            why_it_matters: "When the same logic gets rewritten under two different names instead of reused, a future bug fix only patches one copy, leaving the other silently wrong.",
        }),
    },
    RuleMetadata {
        id: "connectivity-drop",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Requires `--features deep` and `cargo judge dead-code` (Deep Tier; needs `find_all_refs` cross-file reference data). Reported as `Severity::Info` for the current state only — trend-against-baseline is handled by the existing baseline/delta system.",
        exclusions: "Test/bench-attributed functions and methods inside `impl TraitName for SomeType` blocks are excluded from the candidate set entirely, not down-weighted — trait-impl methods are routinely invoked through operator/macro sugar `find_all_refs` can't see (e.g. `Display::fmt`, `Iterator::next`, `Drop::drop`), so they would otherwise look structurally unwired even when used everywhere.",
        allowed_wording: HEURISTIC_WORDING,
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "fn compute_shipping_estimate(weight_kg: f64) -> f64 {\n    weight_kg * 2.5\n}\n",
            why_it_matters: "A function nobody outside its own file ever calls is either dead code that plain reachability analysis missed, or a duplicate implementation nobody wired up — either way it is worth a second look.",
        }),
    },
    RuleMetadata {
        id: "monomorphization-load",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Requires `--features deep` and `cargo judge dead-code` (Deep Tier; needs `find_all_refs` cross-file reference data), plus the Fast Tier's already-computed `Vec<FunctionInfo>` (`generic_param_count`, the same field `signature-complexity` uses). Reported as `Severity::Info` for the current state only — trend-against-baseline is handled by the existing baseline/delta system.",
        exclusions: "A proxy for a proxy, not a measurement: `cargo-llvm-lines` measures actual generated-LLVM-IR line counts per monomorphized instantiation via a real compile; judge never runs `cargo-llvm-lines` itself and never resolves real type arguments at call sites. `monomorphization_load_score = generic_param_count * cross_file_call_sites` is a cheap, explicitly approximate stand-in — call-site *count* substitutes for distinct-instantiation-context diversity, which would need real type resolution this rule doesn't have; two call sites instantiating the exact same concrete types score identically to two call sites instantiating completely different ones. The `> 20` threshold is an illustrative starting point, not a rigorously derived cutoff (e.g. 2 generic params × 11 call sites, or 4 params × 6 sites) — there is no ground truth to calibrate it against short of actually running `cargo-llvm-lines`, so it is explicitly subject to revision. Only functions with at least one generic type parameter are candidates; a non-generic function is skipped outright rather than reported at a load of 0. Fan-in is computed with `include_tests: false` regardless of the caller's own reachability mode — a test-only call site produces no production binary bloat. Shares every `collect_function_fan_in`/`find_all_refs` limitation `connectivity-drop`/`duplicative-reinvention` already document: test/bench-attributed functions and methods inside `impl TraitName for SomeType` blocks are excluded from the candidate set entirely, and a reference only visible through unexpanded proc-macro output is invisible to this scan. A user who wants a real, measured answer should run `cargo llvm-lines` directly — this rule is a free, always-on early warning, not a replacement for it.",
        allowed_wording: "State only the measured `generic_param_count`, `cross_file_call_sites`, and the resulting `monomorphization_load_score` — an explicitly approximate proxy score, never a measured compile-time or binary-size fact, and never that the function 'causes bloat', 'is too generic', or 'should be refactored' (todo.md §17.4); run `cargo llvm-lines` for an actual measurement.",
        verdict_effect: VerdictEffect::AdvisoryOnly,
        // 3 generic params (T, U, E) x 7 distinct caller files = 21, just
        // over the threshold — see `monomorphization_load_registry_example_
        // still_triggers_the_rule` in `slop_structural_deep.rs`.
        example: Some(RuleExample {
            before: "// file: registry.rs\npub fn pair_with_tag<T, U, E>(value: T, other: U, _tag: E) -> (T, U) {\n    (value, other)\n}\n\n// file: billing.rs\npub fn run() -> (i32, i32) {\n    crate::registry::pair_with_tag(1, 2, \"billing\")\n}\n\n// file: shipping.rs\npub fn run() -> (i32, i32) {\n    crate::registry::pair_with_tag(1, 2, \"shipping\")\n}\n\n// file: inventory.rs\npub fn run() -> (i32, i32) {\n    crate::registry::pair_with_tag(1, 2, \"inventory\")\n}\n\n// file: users.rs\npub fn run() -> (i32, i32) {\n    crate::registry::pair_with_tag(1, 2, \"users\")\n}\n\n// file: orders.rs\npub fn run() -> (i32, i32) {\n    crate::registry::pair_with_tag(1, 2, \"orders\")\n}\n\n// file: analytics.rs\npub fn run() -> (i32, i32) {\n    crate::registry::pair_with_tag(1, 2, \"analytics\")\n}\n\n// file: reports.rs\npub fn run() -> (i32, i32) {\n    crate::registry::pair_with_tag(1, 2, \"reports\")\n}\n",
            why_it_matters: "pair_with_tag is called from seven different modules, each instantiating its three generic parameters separately — the compiler generates a fresh copy of the function body for every distinct combination, and cargo-llvm-lines is the only way to see how much that actually costs in the final binary.",
        }),
    },
    // -- slopsquat.rs (G5) ----------------------------------------------------
    RuleMetadata {
        id: "name-collision-risk",
        evidence_class: EvidenceClass::Heuristic,
        preconditions: "Always evaluated (Fast Tier, fully local/offline; part of bare `cargo judge`, `audit`, and `cargo judge deps`).",
        exclusions: "Levenshtein-distance match against a manually curated, potentially stale static list of well-known crates (`data/popular_crates.txt`); neither exhaustive nor auto-updated.",
        allowed_wording: HEURISTIC_WORDING,
        verdict_effect: VerdictEffect::AdvisoryOnly,
        example: Some(RuleExample {
            before: "serde_yml = \"1.0\"\n",
            why_it_matters: "A dependency name that's just one character off from a well-known crate can slip past a quick glance at Cargo.toml and pull in a completely different, unvetted package instead of the one actually intended.",
        }),
    },
    RuleMetadata {
        id: "phantom-crate",
        evidence_class: EvidenceClass::ExternalMeasurement,
        preconditions: "Requires `cargo judge deps --check-crates-io` (opt-in network access to the crates.io sparse index).",
        exclusions: "A snapshot at lookup time — a crate published moments after the check ran is indistinguishable from one that never existed.",
        allowed_wording: EXTERNAL_MEASUREMENT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "llm-prompt-toolkit = \"0.3\"\n",
            why_it_matters: "A dependency name that doesn't exist on crates.io today could be registered by an attacker tomorrow, so a later `cargo build` could silently start compiling and running code nobody on the team ever reviewed.",
        }),
    },
    RuleMetadata {
        id: "phantom-version",
        evidence_class: EvidenceClass::ExternalMeasurement,
        preconditions: "Requires `cargo judge deps --check-crates-io` (opt-in network access to the crates.io sparse index).",
        exclusions: "A snapshot at lookup time — a matching version published or un-yanked moments after the check ran is indistinguishable from one that never existed.",
        allowed_wording: EXTERNAL_MEASUREMENT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "data-pipeline-core = \"4.0\"\n",
            why_it_matters: "A version requirement that no published release actually satisfies means the dependency can never resolve the way the manifest implies — often a sign the version was guessed rather than checked against what crates.io actually has.",
        }),
    },
    RuleMetadata {
        id: "fresh-low-reputation-dep",
        evidence_class: EvidenceClass::ExternalMeasurement,
        preconditions: "Requires `cargo judge deps --check-crates-io` (opt-in network access to the crates.io REST API).",
        exclusions: "Download counts and repository-link presence are the crates.io REST API's own signals, not something judge independently verifies; a snapshot at lookup time.",
        allowed_wording: EXTERNAL_MEASUREMENT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "speedy-json-utils = \"0.1\"\n",
            why_it_matters: "A dependency that's only days old, barely downloaded, and has no linked source repository has had far less community scrutiny than an established crate, making it an easier place to slip in malicious code unnoticed.",
        }),
    },
    RuleMetadata {
        id: "yanked-dependency",
        evidence_class: EvidenceClass::ExternalMeasurement,
        preconditions: "Requires `cargo judge deps --check-crates-io` (opt-in network access to the crates.io sparse index). Runs its own full, non-`--no-deps` `cargo metadata` resolve to see actual resolved versions, not just declared requirements — see `crate::rules::slopsquat::analyze_yanked_dependencies`.",
        exclusions: "Checked against every resolved, non-workspace-member package (direct and transitive), not just directly declared dependencies — distinct from `phantom-version`, which checks whether the declared *requirement* has any non-yanked satisfying version at all. A snapshot at lookup time — a publisher un-yanking a version moments after the check ran is indistinguishable from one that was never yanked.",
        allowed_wording: EXTERNAL_MEASUREMENT_WORDING,
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "legacy-config-loader = \"2.3.1\"\n",
            why_it_matters: "Cargo doesn't automatically move a project off a version once it's locked in Cargo.lock, so a build can keep depending on a release its own publisher has since pulled for a security or correctness problem.",
        }),
    },
    RuleMetadata {
        id: "dep-single-maintainer",
        evidence_class: EvidenceClass::ExternalMeasurement,
        preconditions: "Requires `cargo judge deps --check-crates-io` (opt-in network access to the crates.io REST owners endpoint).",
        exclusions: "Checked against directly declared dependencies only, not the full resolved graph (unlike `yanked-dependency`) — a transitive dependency's own maintainer count is not checked. A raw crates.io owner count (`< 2` fires), with no insight into each owner's actual activity — two owners who are both inactive score the same as two active ones; a snapshot at lookup time.",
        allowed_wording: "State only the owner count and login names crates.io reports — never that the crate is 'abandoned', 'unmaintained', or 'risky' (todo.md §17.4).",
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "tiny-cli-helper = \"1.0\"\n",
            why_it_matters: "A crate with only one crates.io owner has no redundancy: if that single account is compromised or simply goes silent, there's no one else positioned to publish a fix or respond to a report.",
        }),
    },
    RuleMetadata {
        id: "known-vulnerability",
        evidence_class: EvidenceClass::ExternalMeasurement,
        preconditions: "Requires `cargo judge deps --audit-json PATH`, an already-generated `cargo audit --json` report (opt-in; judge never runs `cargo-audit` itself). Runs its own full, non-`--no-deps` `cargo metadata` resolve to cross-reference reachability — see `crate::advisory::advisories` module docs.",
        exclusions: "Reachability is a dependency-graph classification (`production`/`dev_only`/`unknown` in `evidence.reachability`), not a call-graph one — RUSTSEC advisories are scoped to crate+version, not specific functions, so there is no function-level target for the Deep Tier `--why-live` engine to check. `production` is `Severity::Fail`; `dev_only`/`unknown` are `Severity::Warn` — never silently dropped, just not asserted with `Fail`-level confidence. A package/version cargo-audit reported that judge's own resolve doesn't find at all (a stale report, or a workspace-root mismatch) is `unknown`, still reported. Only `cargo audit --json`'s format is imported; `cargo deny --format json` is not.",
        allowed_wording: "State only the advisory id, the reachability classification, and its basis — never that the crate is 'exploited' or 'unsafe to use' beyond what the advisory itself claims (todo.md §17.4).",
        verdict_effect: VerdictEffect::Gating,
        example: Some(RuleExample {
            before: "{\"vulnerabilities\":{\"list\":[{\"advisory\":{\"id\":\"RUSTSEC-2024-0031\",\"title\":\"Improper output sanitization allows script injection via crafted attribute names\",\"url\":\"https://rustsec.org/advisories/RUSTSEC-2024-0031\"},\"package\":{\"name\":\"html-sanitizer-lite\",\"version\":\"1.3.0\"}}]}}",
            why_it_matters: "A dependency with a published RUSTSEC advisory that's actually reachable from production code means the vulnerable code path ships in the built artifact, not just in tests.",
        }),
    },
];

/// Looks up one rule's fixed documentation by id. `None` for an id not in
/// [`RULE_REGISTRY`] — the CLI turns that into a usage error, not a panic.
pub fn lookup(rule_id: &str) -> Option<&'static RuleMetadata> {
    RULE_REGISTRY.iter().find(|entry| entry.id == rule_id)
}

/// Rule ids with no curated `example` yet, and why — every entry in
/// [`RULE_REGISTRY`] must either set `example: Some(_)` or be listed here
/// with a documented reason (see the completeness test below). This is what
/// keeps a curated example from being an optional afterthought that quietly
/// never happens: a newly added rule id with neither an example nor an
/// exemption fails `cargo test`. See
/// `.claude/skills/curate-rule-example/SKILL.md` for how to add one, and add
/// a new rule id here — with a real reason, not a placeholder — only when a
/// single self-contained snippet genuinely cannot trigger it (needs
/// `judge.toml` config, real git commit history, a network-backed
/// crates.io lookup's own resolved-graph shape, an externally imported
/// report, or an expensive full-workspace compile). Only ever read from the
/// completeness tests below, hence `#[cfg(test)]`.
#[cfg(test)]
const NO_EXAMPLE_YET: &[(&str, &str)] = &[
    (
        "crate-boundary-violation",
        "needs a multi-crate workspace plus a judge.toml [[boundary]]/[layers] config, not a single source snippet",
    ),
    (
        "dependency-cycle",
        "needs a multi-crate workspace plus a judge.toml [[boundary]]/[layers] config",
    ),
    (
        "module-boundary-violation",
        "needs a multi-crate workspace plus a judge.toml [[module_boundary]] config",
    ),
    (
        "internal-leak",
        "needs --features deep plus a judge.toml internal_crates config plus a multi-crate workspace",
    ),
    (
        "module-boundary-violation-deep",
        "needs --features deep plus a judge.toml [[module_boundary]] config",
    ),
    (
        "feature-gated-dead-code",
        "needs --features deep plus a judge.toml [feature_matrix] combinations config and a real multi-load reachability run per combination — not expressible as a single source snippet",
    ),
    (
        "untested-hotspot",
        "needs an externally generated cargo-llvm-cov LCOV report import — judge never measures coverage itself",
    ),
    (
        "unused-dependency",
        "opt-in --check-rustc-lints; triggering it for real needs a full `cargo check --workspace --all-targets` compile, too expensive for an illustrative snippet",
    ),
];

#[cfg(test)]
mod tests {
    use super::*;

    /// Every registry entry's `verdict_effect` must agree with
    /// `evidence_class.is_gating()` — the single place this policy actually
    /// lives (see [`EvidenceClass::is_gating`]). Prevents the two fields from
    /// drifting apart as rules are added or reclassified.
    #[test]
    fn verdict_effect_matches_evidence_class_is_gating_for_every_entry() {
        for entry in RULE_REGISTRY {
            let expected = if entry.evidence_class.is_gating() {
                VerdictEffect::Gating
            } else {
                VerdictEffect::AdvisoryOnly
            };
            assert_eq!(
                entry.verdict_effect, expected,
                "rule `{}`: verdict_effect does not match evidence_class.is_gating()",
                entry.id
            );
        }
    }

    /// Every rule id constant defined outside the Deep Tier (`--features
    /// deep`) has a registry entry — the completeness guarantee from todo.md
    /// §17.5. Constants are imported explicitly, one per rule, so a new rule
    /// id added anywhere in the crate without a matching registry entry fails
    /// this test instead of silently falling through `lookup`.
    #[test]
    fn every_fast_tier_rule_id_has_a_registry_entry() {
        let ids: &[&str] = &[
            crate::rules::api_surface::UNDOCUMENTED_PUBLIC_ITEM_RULE,
            crate::rules::api_surface::SEMVER_HAZARD_RULE,
            crate::rules::boundaries::BOUNDARY_VIOLATION_RULE,
            crate::rules::boundaries::DEPENDENCY_CYCLE_RULE,
            crate::rules::boundaries::MODULE_BOUNDARY_VIOLATION_RULE,
            crate::rules::boundaries::FEATURE_GRAPH_CYCLE_RULE,
            crate::rules::complexity::SIGNATURE_COMPLEXITY_RULE,
            crate::rules::complexity::MAINTAINABILITY_INDEX_RULE,
            crate::advisory::coverage::UNTESTED_HOTSPOT_RULE,
            crate::rules::dep_graph::DUPLICATE_CRATE_VERSIONS_RULE,
            crate::rules::dep_graph::MSRV_DRIFT_RULE,
            crate::rules::dep_graph::WORKSPACE_DEP_DRIFT_RULE,
            crate::rules::deps::MISPLACED_DEPENDENCY_KIND_RULE,
            crate::rules::deps::UNUSED_DEV_DEPENDENCY_RULE,
            crate::rules::deps::HEAVY_DEPENDENCY_RULE,
            crate::rules::deps::UNUSED_FEATURE_FLAG_RULE,
            crate::rules::deps::DEFAULT_FEATURES_UNUSED_RULE,
            crate::rules::deps::UNUSED_FEATURE_RULE,
            crate::rules::deps::UNUSED_DEPENDENCY_RULE,
            crate::rules::duplication::DUPLICATE_RULE,
            crate::rules::module_graph::UNLINKED_FILE_RULE,
            crate::rules::module_graph::ORPHAN_MODULE_RULE,
            crate::advisory::mutants::MUTATION_SURVIVOR_RULE,
            crate::rules::pattern::STRINGLY_ERROR_BOUNDARY_RULE,
            crate::rules::security::UNSAFE_SURFACE_RULE,
            crate::rules::security::UNSAFE_DENSITY_RULE,
            crate::rules::security::INTEGER_CAST_RISK_RULE,
            crate::rules::security::PANIC_IN_LIB_RULE,
            crate::rules::security::HARDCODED_SECRET_RULE,
            crate::rules::slop::SWALLOWED_RESULT_RULE,
            crate::rules::slop::EMPTY_ERROR_ARM_RULE,
            crate::rules::slop::CATCH_ALL_ERROR_RULE,
            crate::rules::slop::SUPPRESSION_DEBT_RULE,
            crate::rules::slop::MERGED_STUB_RULE,
            crate::rules::slop::EMPTY_IMPL_RULE,
            crate::rules::slop::ASSERTION_FREE_TEST_RULE,
            crate::rules::slop::TAUTOLOGICAL_TEST_RULE,
            crate::rules::slop::IGNORED_TEST_ACCUMULATION_RULE,
            crate::rules::slop::CONVERSATIONAL_ARTIFACT_RULE,
            crate::rules::slop::RESTATING_COMMENT_RULE,
            crate::rules::slop::STEP_COMMENT_INFLATION_RULE,
            crate::rules::slop::GENERIC_NAMING_RULE,
            crate::rules::slop::DOC_RESTATES_SIGNATURE_RULE,
            crate::rules::slop::SILENT_DEFAULT_RULE,
            crate::rules::slop::CONTEXT_FREE_PROPAGATION_RULE,
            crate::rules::slop::DEBUG_FORMAT_LEAK_RULE,
            crate::rules::slop_structural::COMPLEXITY_INFLATION_RULE,
            crate::rules::slop_structural::ABSTRACTION_INFLATION_RULE,
            crate::rules::slop_structural::FRAGILE_SUBSTRING_CLASSIFICATION_RULE,
            crate::rules::slopsquat::NAME_COLLISION_RISK_RULE,
            crate::rules::slopsquat::PHANTOM_CRATE_RULE,
            crate::rules::slopsquat::PHANTOM_VERSION_RULE,
            crate::rules::slopsquat::FRESH_LOW_REPUTATION_DEP_RULE,
            crate::rules::slopsquat::YANKED_DEPENDENCY_RULE,
            crate::rules::slopsquat::DEP_SINGLE_MAINTAINER_RULE,
            crate::advisory::advisories::KNOWN_VULNERABILITY_RULE,
        ];
        for id in ids {
            assert!(
                lookup(id).is_some(),
                "rule id `{id}` has no RULE_REGISTRY entry"
            );
        }
    }

    /// Same completeness guarantee for the three rule ids only defined when
    /// the crate is built with `--features deep` (`dead_code`,
    /// `slop_structural_deep`) — kept in a separate, `cfg`-gated test since
    /// those constants don't exist in a Fast-Tier-only build.
    #[cfg(feature = "deep")]
    #[test]
    fn every_deep_tier_rule_id_has_a_registry_entry() {
        let ids: &[&str] = &[
            crate::rules::dead_code::UNUSED_PUB_WORKSPACE_RULE,
            crate::rules::dead_code::UNUSED_PUB_API_RULE,
            crate::rules::dead_code::DEAD_ENUM_VARIANT_RULE,
            crate::rules::dead_code::TEST_ONLY_PUB_RULE,
            crate::rules::dead_code::UNREACHABLE_FROM_ENTRY_RULE,
            crate::rules::dead_code::CRATE_COUPLING_RULE,
            crate::rules::dead_code::MODULE_COUPLING_RULE,
            crate::rules::feature_matrix::FEATURE_GATED_DEAD_CODE_RULE,
            crate::rules::dead_trait_impl::DEAD_TRAIT_IMPL_RULE,
            crate::rules::slop_structural_deep::DUPLICATIVE_REINVENTION_RULE,
            crate::rules::slop_structural_deep::CONNECTIVITY_DROP_RULE,
            crate::rules::api_surface_deep::INTERNAL_LEAK_RULE,
            crate::rules::api_surface_deep::RE_EXPORT_CHAIN_RULE,
            crate::rules::boundaries_deep::MODULE_BOUNDARY_VIOLATION_DEEP_RULE,
        ];
        for id in ids {
            assert!(
                lookup(id).is_some(),
                "rule id `{id}` has no RULE_REGISTRY entry"
            );
        }
    }

    /// (a) A known rule id resolves, with the evidence class matching the
    /// authoritative mapping in [`crate::finding::evidence_class_for_rule`]
    /// (for rules that mapping actually classifies — `duplicate-code`'s
    /// default classification and the three `pattern` rules deliberately
    /// diverge, see their entries' `exclusions`/module docs).
    #[test]
    fn known_rule_id_resolves_with_expected_fields() {
        let entry = lookup(crate::rules::slop::CATCH_ALL_ERROR_RULE).expect("catch-all-error entry");
        assert_eq!(entry.id, "catch-all-error");
        assert_eq!(entry.evidence_class, EvidenceClass::DerivedFact);
        assert_eq!(entry.verdict_effect, VerdictEffect::Gating);
        assert!(!entry.preconditions.is_empty());
        assert!(!entry.exclusions.is_empty());
        assert!(!entry.allowed_wording.is_empty());
    }

    /// (b) An unknown rule id resolves to `None` — the CLI turns this into
    /// exit code 2, never a panic or a silent empty result.
    #[test]
    fn unknown_rule_id_does_not_resolve() {
        assert!(lookup("not-a-real-rule").is_none());
    }

    /// Every registry entry has a curated `example` or a documented
    /// exemption in [`NO_EXAMPLE_YET`] — see that constant's doc comment.
    /// This is the enforcement mechanism: a newly added rule with neither
    /// fails here, so a curated example can't be silently forgotten.
    #[test]
    fn every_registry_entry_has_an_example_or_a_documented_exemption() {
        for entry in RULE_REGISTRY {
            if entry.example.is_none() {
                assert!(
                    NO_EXAMPLE_YET.iter().any(|(id, _)| *id == entry.id),
                    "rule `{}` has no curated `example` and no documented exemption in \
                     NO_EXAMPLE_YET — add a RuleExample (see \
                     .claude/skills/curate-rule-example/SKILL.md) or add a reasoned \
                     exemption entry",
                    entry.id
                );
            }
        }
    }

    /// Every [`NO_EXAMPLE_YET`] id is a real, still-exampleless registry
    /// entry — a stale/misspelled exemption would silently stop being
    /// checked, and a rule that later gains an example should have its
    /// exemption removed, not left to accumulate.
    #[test]
    fn every_exemption_is_a_real_rule_id_still_missing_an_example() {
        for (id, reason) in NO_EXAMPLE_YET {
            assert!(!reason.is_empty(), "exemption `{id}` has an empty reason");
            let entry =
                lookup(id).unwrap_or_else(|| panic!("exemption `{id}` is not a real rule id"));
            assert!(
                entry.example.is_none(),
                "rule `{id}` is listed in NO_EXAMPLE_YET but already has a curated example — remove the stale exemption"
            );
        }
    }
}