rustqual 1.2.2

Comprehensive Rust code quality analyzer — seven dimensions: IOSP, Complexity, DRY, SRP, Coupling, Test Quality, Architecture
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
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.2.2] - in development

Patch release: **Reporter-Trait sealed two-trait + Snapshot pattern**,
**Call-parity anchor model + Orphan-suppression in trait contract**.

Late-cycle additions (post 2026-04-30 tag):

### Anchor model — unified target-capability rule (Codex 2026-05-04 P1-P4)

- **Single rule for walker + Check B/D**: `is_anchor_target_capability`
  in `anchor_index` is the only source of truth for "is this anchor
  a target capability". Walker (`is_target_boundary`) and Check B/D
  (`target_anchor_capabilities`) share it; previously each side
  re-implemented the rule and drifted (parallel-path inconsistency).
  Rule: anchor passes iff (a) declaring layer is NOT a peer adapter,
  AND (b) declaring layer IS the target with a callable body
  (default OR overriding impl), OR at least one overriding impl
  lives in the target layer.
- **Peer-adapter anchor rejection** (P2): anchors whose declaring
  trait lives in a configured peer-adapter layer are excluded from
  target capabilities. Prevents `cli` from inheriting `mcp::Handler`
  coverage via the anchor side-channel.
- **Default-only target-layer anchors** (P3): trait declared in
  target with a default body and no overriding impls is now
  enumerated as a capability — Check A used to accept the touchpoint
  (anchor in target layer) while Check B/D refused to enumerate
  ("no overriding impl"). Pure-signature trait methods (no default,
  no impl) stay rejected (uncallable).
- **Concrete impl-method skip in Check B/D** (P1): when an anchor is
  enumerated as target capability, its overriding impl-method
  canonicals (`<Impl>::<method>`) are skipped in the concrete
  pub-fn iteration **only when no adapter has the concrete in its
  coverage**. If at least one adapter calls the concrete directly
  (`LoggingHandler::handle()` UFCS or static-method form) while
  another adapter dispatches via `dyn Trait`, the concrete pass
  still runs — the mixed-form drift then surfaces as a concrete
  finding plus an anchor finding for the adapter that uses the
  other form. Cross-form synonym handling stays intentionally out
  of scope; without the gating refinement, mixed-form drift was
  silently masked behind a single false-positive anchor-orphan.
  Same conditional skip is mirrored in Check D (`check_d::check_multiplicity_mismatch`)
  via `any_adapter_counts_concrete` — without it, all-direct-call
  multiplicity drift (cli=2 vs mcp=1, both calling concrete
  directly with no dispatch) was silently dropped because Check D's
  `is_anchor_backed_concrete` skip ran unconditionally.
- **Anchor findings carry real source line** (P4): `AnchorInfo` now
  stores the trait method's source location captured at
  type-index-build time (`MethodLocation { file, line, column }`).
  Anchor findings (Check B `CallParityMissingAdapter`, Check D
  `CallParityMultiplicityMismatch`) report the trait method's
  declaration line instead of `line: 0`. Suppression-window
  matching, the orphan detector's window scan, and SARIF
  `startLine` validity all work for anchor-level findings.

Second-pass review (Codex 2026-05-04 round 2):

- **Anchor-only target surface defensive guard** (P1): Check B's
  early-return on missing target-layer entry in `pub_fns_by_layer`
  is replaced with an empty-slice fallback. The target-anchor
  enumeration runs unconditionally. Empirical workspaces always
  carry an entry (the pub-fn collector's `or_default()` ensures it),
  but the fallback locks in the invariant against future refactors —
  an anchor-only target surface (e.g. ports trait impl'd by a
  private application type, or default-only trait declared in
  target) cannot silently lose missing-adapter findings.
- **Reachable-target BFS recognises trait anchors** (P2):
  `build_adapter_reachable_targets` now treats a callee as a
  target-capability node when EITHER its resolved layer matches
  `target_layer` OR it is a synthetic anchor that passes
  `is_anchor_target_capability` for `(target_layer, adapter_layers)`.
  Previously, an anchor reached transitively via an adapter-touched
  target fn (adapter → target fn → `dyn Trait.method()`) was
  invisible to the BFS (anchor's `layer_of()` is the trait
  declaration layer, e.g. `ports`), and Check B fired a false
  orphan. Post-boundary plumbing wired up via at least one adapter
  now stays silent for trait anchors too.
- **cfg-test trait method filter** (P2): per-method `#[cfg(test)]`
  / `#[test]` attributes inside an otherwise-production trait now
  exclude the method from `WorkspaceTypeIndex.trait_methods`,
  `trait_method_locations`, and `trait_methods_with_default_body`.
  Without this, a `#[cfg(test)] fn helper(&self) {}` with a default
  body would promote the method to a target anchor capability
  even though it is invisible in production builds, and
  `trait_has_method` would accept dispatch calls that should stay
  unresolved.

Third-pass review (Codex 2026-05-04 round 3):

- **Private trait anchor exclusion** (P1): `WorkspaceTypeIndex` now
  captures the trait declaration's effective workspace visibility in
  `trait_visibility: HashMap<String, bool>`, threaded into
  `AnchorInfo.trait_visible`, and consulted as a precondition by
  `is_anchor_target_capability`. Without this, `trait Internal { fn
  run(&self) {} }` (no `pub`) and `trait Hidden { fn run(&self); }
  impl Hidden for X` (private trait + target impl) surfaced as
  Check B/D capabilities and produced orphan findings for what is
  architecturally implementation detail. Effective visibility is the
  trait's own `vis == Public` ANDed with the trait collector's
  `enclosing_mod_visible` (mirroring `pub_fns::PubFnCollector`'s mod
  visibility tracking) — so a `pub trait T { … }` declared inside a
  private `mod inner { … }` is also rejected, since it isn't
  reachable from outside its own module and thus isn't part of the
  architectural surface.
- **Anchor orphan suppression for direct-concrete coverage** (P1):
  `check_b::inspect_anchor` adds a second arm to the
  `reached.is_empty()` suppression: when at least one of
  `info.impl_method_canonicals` is in some adapter's coverage or in
  the reachable set, the anchor finding is silenced. Closes the
  all-direct-concrete false-positive — every adapter calls
  `LoggingHandler::handle()` via UFCS, none dispatches via
  `dyn Trait`, the concrete pass is silent (all reach concrete),
  and the anchor pass no longer fires "missing all adapters" since
  the concrete coverage IS the capability coverage.
- **`exclude_targets` matches impl path on anchor findings** (P2):
  new `is_anchor_excluded` helper tests the configured globs against
  the anchor canonical AND every `impl_method_canonical` it backs.
  A user-friendly `exclude_targets = ["application::admin::*"]`
  glob now silences the matching anchor finding (e.g.
  `ports::handler::Handler::handle`) when the impl lives in
  `application::admin::*`, instead of requiring a parallel
  ports-path entry. Concrete-pass exclusion is unchanged (already
  matched against the concrete canonical).
- **Stale `line=0` anchor wording** (P3): the v1.2.2 "Added —
  Anchors as target capabilities for Check B/D" entry promised a
  heuristic file path with `line=0` until span info was added —
  contradicting the round-2 P4 fix that already captures the trait
  method's source location. Wording updated to reference the
  round-2 P4 entry that delivered real `MethodLocation` capture.

Fourth-pass review (Codex 2026-05-04 round 4):

- **Trait visibility uses the shared workspace-canonical set** (P1):
  the round-3 trait visibility filter implemented a private
  `node.vis == Public` check (later patched with `enclosing_mod_visible`
  tracking), which diverged from the rest of call-parity's
  visibility model — `pub(crate)`, `pub(super)`, `pub(in <path>)`,
  file-backed module visibility, and `pub use` re-exports were all
  missed. `pub(crate) trait Handler` with a target impl was rejected
  as invisible; conversely a `pub trait` in a private file-backed
  module could still slip through. Fix: `populate_anchor_index` now
  reuses the workspace-wide `visible_canonicals` set built by
  `pub_fns_visibility::collect_visible_type_canonicals_workspace`
  (the same set `pub_fns::collect_pub_fns_by_layer` consumes), so
  trait visibility agrees with the rest of call-parity. Removed the
  redundant `WorkspaceTypeIndex.trait_visibility` map and the
  `TraitCollector.enclosing_mod_visible` tracking — both subsumed
  by the canonical-set lookup.
- **Inherited-default capability gap surfaced** (P2 — superseded
  in round 5, replaced with edge-rewrite in round 7): round 4
  noted that `pub trait Handler { fn handle(&self) {} } impl
  Handler for AppHandler {}` (no override, inherits default body)
  left adapter coverage with no visible target capability. The
  round-4 fix (`callable_impls_for` widening of `impl_layers` /
  `impl_method_canonicals`) was reverted in round 5 because it
  caused canonical collisions with inherent methods of the same
  name and promoted non-target default bodies through empty target
  impls. The final fix lives in the round-7 edge-rewrite pass
  (see below) — see the seventh-pass review entry.
- **Stale `add_anchor_to_impl_edges` reference** (P3): the
  `trait_dispatch_edges` doc comment in `calls.rs` claimed
  reachability from anchor to impl bodies was wired by
  `workspace_graph::add_anchor_to_impl_edges` — function never
  existed; the design intentionally keeps the anchor as a leaf
  in the graph. Comment updated to reflect the actual behaviour.
- **Default-only target anchor in book summary** (P3): the
  short summary in `book/adapter-parity.md` (the type-inference
  capability list) still said anchors are recognised "when at least
  one overriding impl lives in the target layer". The detailed
  anchor section already documents the default-OR-overriding rule,
  but the summary contradicted it. Updated.

Fifth-pass review (Codex 2026-05-12 round 5):

- **Revert of round-4 `callable_impls_for` widening** (P1 #1 + #2):
  the round-4 expansion of `impl_method_canonicals` to absorb
  non-overriding impls when the trait method has a default body
  caused two distinct bugs. First, `<Impl>::<method>` canonicals
  fabricated for inherited-default impls collide with unrelated
  inherent methods of the same name (`impl X { fn handle … }` +
  `impl T for X {}`), so Check B silently treats the real inherent
  method as anchor-backed and skips it. Second, ports-declared
  default methods with empty target-layer impls falsely promoted
  the anchor to a target capability — the executable body lives on
  the ports trait, not target, so Check A/B/D would require parity
  for code that never crosses into target. Fix: revert to strict
  overriding-only via the restored `overriding_impls_for` accessor;
  inherited-default impls no longer contribute to `impl_layers` or
  `impl_method_canonicals`. Promotion to target capability now
  requires either (a) the trait is declared in the target layer
  with a callable body (default body in target OR an overriding
  impl somewhere), or (b) at least one overriding impl lives in the
  target layer; default bodies declared OUTSIDE target don't promote
  through empty target impls. Unambiguous inherited-default concrete
  calls are folded onto the trait anchor by the round-7 edge-rewrite
  pass, so drift on them is counted against the anchor. The remaining
  blind spot is the ambiguous multi-trait default case (a type
  implementing two traits with the same default method name): the
  round-8 ambiguity guard leaves those phantoms in place rather than
  guessing.
- **Book visibility wording aligned with shared canonical set**
  (P3): the detailed anchor definition in `book/adapter-parity.md`
  still said workspace-visible means "the trait's own `vis` is
  `pub` AND every enclosing inline `mod` is `pub`". The code uses
  the shared `visible_canonicals` set (covering `pub(crate)`,
  `pub(super)`, `pub(in <path>)`, file-backed module visibility,
  and `pub use` re-exports). Wording updated to match.

Sixth-pass review (Codex 2026-05-12 round 6):

- **Walker phantom-canonical gate** (P1): `populate_layer_cache`
  caches `layer_of` for every canonical that appears in the graph,
  including edge sinks. A fabricated `<Impl>::<method>` from an
  inherited-default impl (no override, body lives on the trait)
  therefore got `layer_of == target_layer` and was accepted as a
  target boundary by `is_target_boundary` even though no real fn
  node existed with that canonical. Check A would pass on the
  phantom touchpoint while Check B/D had no way to enumerate the
  same capability consistently. Fix: `is_target_boundary` (in
  `touchpoints.rs`) and the sister `is_target_capability_node` (in
  `check_b_coverage.rs`) now require `graph.forward.contains_key`
  in addition to the layer match for concrete canonicals. Trait
  anchors continue through the unified `is_anchor_target_capability`
  rule untouched. Regression tests
  `touchpoints_reject_phantom_inherited_default_concrete_canonical`
  and `touchpoints_recognise_real_target_fn_node`.
- **Anchor docs round-5 leftover** (P3): the short anchor summary
  in `book/adapter-parity.md`'s type-inference list still said
  "at least one impl in the target layer makes the method callable
  (overriding the signature, or inheriting a default body declared
  elsewhere)" — that was the round-4 widening, reverted in round 5.
  Updated to strict "at least one overriding impl lives in target",
  plus an explicit note that inherited-default impls don't promote.

Seventh-pass review (Codex 2026-05-12 round 7):

- **Phantom inherited-default edge rewrite** (P2): the round-6
  walker-phantom-gate correctly rejected fabricated
  `<Impl>::<method>` canonicals as touchpoints, but never emitted
  an alternative — a target-layer trait declared with a default
  body + empty target impl + adapter UFCS call would silently look
  non-delegating, even though the trait anchor IS a valid target
  capability. New post-build pass
  `workspace_graph::edge_rewrite::rewrite_phantom_inherited_default_edges`
  scans every emitted edge after `FileFnCollector` completes,
  identifies phantom callees that match an inherited-default impl
  (impl is in `trait_impls[T]`, method has default body, impl
  doesn't override), and rewrites the edge to point at the trait
  anchor `<Trait>::<method>`. Concrete inherent methods stay
  untouched (their canonical IS a real graph node), and overriding
  impls stay untouched (their override registers a real fn body).
  Regression test
  `touchpoints_route_inherited_default_concrete_to_anchor`.
- **`call_depth` describes edge depth, not helper hops** (P3): the
  Rustdoc on `CallParityConfig::call_depth`, the
  `book/adapter-parity.md` walk description, the
  `book/reference-configuration.md` table entry, and the
  `docs/internals.md` summary all said "max helper hops", which
  was off-by-one — direct callees are seeded at depth 1, so
  `call_depth = 3` reaches `handler → h1 → h2 → target`
  (three edges, two intermediate helpers). All four sources
  updated with explicit edge-count wording + the example.
- **README stale `mod foo;` limitation** (P3): the "External file
  modules" entry in `README.md`'s Known Limitations claimed
  `mod foo;` declarations weren't followed and only inline modules
  were analysed recursively. That hasn't been true since the
  `file_visibility::collect_file_root_visibility` pre-pass
  shipped with regression tests for crate-root `mod`, private
  file modules, and ancestor chains. Entry removed.

Eighth-pass review (Codex 2026-05-12 round 8):

- **Edge-rewrite ambiguity guard** (P2): the round-7
  `inherited_default_anchor_for` returned the first HashMap match
  when a type implemented multiple traits with the same default
  method name (e.g. `pub trait Greeting { fn handle(&self) {} }`
  and `pub trait Logging { fn handle(&self) {} }` both implemented
  by `AppHandler`). Rewrite choice depended on map iteration order
  — non-deterministic. Rust itself requires UFCS disambiguation
  in that case, and the canonical alone doesn't tell us which
  trait was selected. Fix: rewrite only when EXACTLY ONE
  inherited-default candidate exists; otherwise leave the phantom
  canonical in place (the walker phantom-gate suppresses it).
  Regression test
  `touchpoints_skip_rewrite_when_multiple_traits_share_default_method_name`.
- **CHANGELOG round-4 anchor semantics superseded** (P3): the
  "Inherited-default impls count as target capability" entry from
  round 4 described the `callable_impls_for` widening that was
  reverted in round 5 and properly fixed via edge-rewrite in
  round 7. The CHANGELOG now reads as if two contradictory anchor
  models are active. The round-4 entry is reworded as
  "superseded" with a pointer to the round-7 entry that delivered
  the real fix.
- **CHANGELOG anchor model summary aligned** (P3): the Added-
  section blurb on the round-1 anchor model still framed target
  capability around "overriding impls" and treated inherited
  defaults as sharing the same target semantics. Updated to the
  current dual-rule (target-declared default body OR overriding
  impl in target) plus an explicit note that inherited-default
  UFCS calls are routed via edge-rewrite and that calls inside the
  default-method body itself stay invisible.
- **`inspect_anchor` comment refreshed** (P3): the doc on
  `inspect_anchor` still said "all-direct inherited-default drift
  stays undetected — the impl-method canonical is phantom". With
  the round-7 edge-rewrite, those phantom canonicals are folded
  onto the anchor before coverage/counting, so the limitation no
  longer applies. Comment updated to reflect the active behaviour.

Ninth-pass review (Codex 2026-05-12 round 9, doc-only):

- **Round-5 limitation note narrowed** (P3): the round-5 entry
  describing "mixed-form multiplicity drift on inherited defaults
  remains undetected" was reworded to reflect the round-7
  edge-rewrite — only the ambiguous multi-trait default case (the
  round-8 ambiguity guard) leaves edges phantom now.
- **Top-level anchor summary aligned** (P3): the primary
  `### Added` blurb on the anchor model was framing target boundary
  status around "overriding impl in target" only. Updated to the
  full dual-rule (target-declared callable body OR overriding impl
  in target) plus an explicit note on the edge-rewrite folding for
  unambiguous inherited-default UFCS calls.

Tenth-pass review (Codex 2026-05-12 round 10, doc/comment-only):

- **Ambiguous-multi-trait-default added to known limitations** (P3):
  `book/adapter-parity.md` Limitations list gained a sixth entry
  documenting that UFCS calls like `X::handle(&x)` are left
  unresolved when `X` implements multiple traits with the same
  default method name (Rust requires UFCS disambiguation; the
  canonical alone is ambiguous). Workaround: rename, override on
  the impl, or call through `dyn Trait`.
- **`docs/internals.md` anchor summary refreshed** (P3): the
  contributor-facing summary still framed target-boundary status
  around "at least one overriding impl in target". Rewritten to
  reference `is_anchor_target_capability` directly, list the dual
  rule, mention visibility / peer-adapter constraints, and call out
  the round-7 edge-rewrite for inherited-default UFCS calls.
- **`calls.rs` Rustdoc comments aligned** (P3): the
  `resolve_method_targets` doc still said trait-dispatch inference
  "may return multiple (one per impl of the trait)" — that was
  round-1 behaviour, before the synthetic-anchor collapse. The
  `canonical_edges_for_method` doc had the same "overriding impl
  in target" framing. Both updated to the current single-anchor
  semantics + dual-rule capability predicate.

Eleventh-pass review (Codex 2026-05-12 round 11, doc-only):

- **Limitations section heading + intro generalised** (P3): the
  `book/adapter-parity.md` Limitations subsection was titled
  "Limitations: type aliases" with an intro saying "two alias
  patterns currently disagree", but the list had grown to six
  bullets covering re-exports, function re-exports, trait
  default-body internals, and ambiguous inherited-default UFCS
  calls. Renamed to "Known limitations" with an intro that
  classifies each bullet's topic, so readers no longer assume only
  the first two items are in scope.

Eighteenth-pass review (Codex 2026-05-13 round 18):

- **External aliased trait bounds shadowed later workspace
  bounds** (P2): after the round-17 marker fix, the bound resolver
  in `resolve_bound_list` still accepted any successfully-canonicalised
  path as a `TraitBound`. With `use serde::Serialize;` and
  `fn make() -> impl Serialize + Handler`, the first bound expanded
  to `["serde", "Serialize"]` and returned, so the later workspace
  `Handler` bound was never visited — `make().handle()` stayed
  unresolved. Fully-qualified `serde::Serialize` without the `use`
  alias already returned `None` from canonicalisation and was
  correctly skipped; the alias-expanded form took a different path
  and slipped through. Fix: gate the `TraitBound` return on
  `canonical.first() == Some("crate")` so only workspace-rooted
  bounds win — external aliases now skip exactly like the
  fully-qualified external form. The std-marker special case
  (`resolve_marker::is_marker_trait`) and the Future special case
  (`future_bound_args`) still run first, so `Send` / `Sync` /
  `Future<Output = T>` keep their existing handling. Regression
  test `test_impl_trait_external_aliased_bound_skipped_workspace_bound_wins`.

Seventeenth-pass review (Codex 2026-05-13 round 17):

- **Marker-trait skip discarded workspace traits with marker-style
  leaf names** (P2): `resolve_bound_list` skipped each bound via
  `is_marker_trait`, which checked the raw last segment against a
  hard-coded `MARKER_TRAITS` list before alias canonicalisation.
  Workspace traits or aliases like `dyn crate::ports::Send` or
  `use crate::ports::Handler as Send; dyn Send` therefore got
  discarded as if they were the std marker, so `h.handle()` never
  became a trait anchor. Same root cause as round 16 P2 (aliased
  `Future` bound) — a sister-fix-site that should have been caught
  in the same pass. Fix: extracted `is_marker_trait` into a new
  `resolve_marker` module that canonicalises the bound first and
  skips only when the canonical leaf is in `MARKER_TRAITS` AND the
  canonical path is stdlib-prefixed (`std`/`core`/`alloc`).
  Unresolvable paths still skip bare single-segment markers
  (`dyn Send` via prelude) and explicitly stdlib-rooted forms
  (`dyn std::marker::Send`) — multi-segment workspace paths that
  failed to canonicalise are treated as real bounds. Regression
  tests `test_impl_trait_local_send_named_trait_resolves_not_skipped`
  + `test_impl_trait_bare_std_send_marker_still_skipped` cover both
  directions.

Sixteenth-pass review (Codex 2026-05-13 round 16):

- **Aliased `Future` bound on `impl Trait` lost its `Output`**
  (P2): `resolve_bound_list` in
  `src/adapters/analyzers/architecture/call_parity_rule/type_infer/resolve.rs`
  checked the raw bound leaf with `last.ident == "Future"` before
  alias canonicalisation. With
  `use std::future::Future as Fut; fn make() -> impl Fut<Output = Session>`,
  the leaf was `Fut` so the Future-detection branch missed, the
  bound got recorded as `TraitBound(std::future::Future)` instead,
  and `make().await.diff()` stayed unresolved because the canonical
  type no longer exposed the `Output = Session` shape. Fix: routed
  the bound through `identify_wrapper_name` (the same alias-aware
  probe `resolve_path` uses for path-form `Future<Output = T>`),
  keeping the original `Output = T` args from the trait bound so
  `wrap_future_output` can resolve them. Regression test
  `test_impl_aliased_future_resolves_to_future_with_output`
  asserts `Future(Session)` for the aliased form.
- **Check-A diagnostic still said "hops"** (P3): round 11 renamed
  `call_depth` to call-edge depth in the config doc + book to
  remove the off-by-one ambiguity (`3` = three call edges, two
  intermediate helpers — not three nodes). The emitted Check-A
  message in `rendering.rs` still said
  "within {call_depth} hops", keeping the ambiguity alive in real
  user-facing diagnostics. Reworded to
  "within {call_depth} adapter-internal call edges". The example
  in `book/adapter-parity.md:194` was synced. Regression test
  `no_delegation_message_uses_call_edge_wording_not_hops` locks
  the new wording.

Fifteenth-pass review (Codex 2026-05-13 round 15):

- **Repeated-match dedup leaked into text/HTML via shared
  projection** (P2): round 13's fix routed the JSON repeated-match
  builder to a `(enum_name, sorted participant locations)` dedup
  key, but the shared `split_dry_findings` projection
  (`src/adapters/report/projections/dry.rs`) — consumed by the
  text and HTML reporters — still deduped by `enum_name` alone.
  Two distinct repeated-match patterns over the same enum
  therefore collapsed into one rendered group outside JSON, so
  reporter parity regressed in the very next pass. Fix:
  `build_repeated_match_groups` now goes through the existing
  `dedup_by_locations` helper (same path that `build_duplicate_groups`
  and `build_fragment_groups` use), keying on the participant
  location set. Regression tests
  `split_dry_findings_keeps_distinct_repeated_match_groups_over_same_enum`
  + `split_dry_findings_collapses_duplicate_repeated_match_group_emissions`
  in `src/adapters/report/tests/projections_dry.rs` lock the dedup
  contract at the projection layer so every reporter benefits.

Fourteenth-pass review (user-driven proactive A21 sweep
2026-05-12 round 14):

- **All 24 reporter `_no_panic` smoke tests converted to
  value-asserting tests** (proactive A21-class elimination): rounds
  12-13 surfaced three v1.2.1 typed-reporter refactor drops that
  smoke tests had masked (JSON `logic_count`/`call_count`,
  NearDuplicate `similarity`, RepeatedMatch `arm_count`, SRP
  `composite_score`/`clusters`/`length_score`). Rather than wait
  for Codex to discover the remaining smoke tests one round at a
  time, the user directed a full sweep. All 24 `_no_panic` tests
  across eight reporters (sarif=3, ai=3, dot=3, findings_list=2,
  github=2, json=4 remaining, text=6, pipeline=1) were replaced
  with tests that assert actual output values and renamed to
  describe the asserted behavior (e.g.
  `test_print_json_carries_violation_logic_and_call_locations`,
  `test_print_sarif_emits_violation_with_location`,
  `ai_value_includes_complexity_finding_metric_and_location`). New
  helper `format_findings(&[FindingEntry]) -> String` in
  `findings_list/mod.rs` (string-returning variant of
  `print_findings`) makes the findings-list reporter testable
  without stdout capture. Reporter test fixtures now populate
  `findings.iosp` via `project_iosp` so the projection path is
  actually exercised. Test count unchanged (1611 → 1611, 1-for-1
  replacement). No production code changes — the conversion is
  purely a test-suite hardening to prevent future projection
  drops from staying silent. `grep -rn "fn .*_no_panic\|fn
  .*_no_crash" src/ tests/` returns nothing after this sweep, so
  the smoke-test category is effectively eliminated from the
  reporter suite.

Thirteenth-pass review (Codex 2026-05-12 round 13):

- **NearDuplicate similarity dropped from `DryFindingDetails::Duplicate`**
  (P2): the v1.2.1 typed-reporter refactor projected
  `DuplicateKind::NearDuplicate { similarity }` to
  `DryFindingDetails::Duplicate { participants }` and dropped the
  similarity score. JSON output hardcoded `similarity: None` for
  every group, so machine consumers couldn't distinguish a 0.91
  near-duplicate from an unscored exact group. Fix: added
  `similarity: Option<f64>` to the details variant, copied it in
  `project_duplicate_group`, and read it in the JSON builder. `Eq`
  derives dropped on `DryFinding` / `DryFindingDetails`. Regression
  test `test_print_json_carries_near_duplicate_similarity`.
- **RepeatedMatch `arm_count` dropped and groups collapsed by
  enum name** (P2): the typed `RepeatedMatchParticipant` carried
  no `arm_count`, so JSON `entries[].arm_count` was hardcoded to
  `0`; the JSON builder additionally de-duplicated groups by
  `enum_name` alone, collapsing two distinct repeated patterns over
  the same enum into one group. Fix: added `arm_count: usize` to
  `RepeatedMatchParticipant`, copied it in projection, and read it
  in the JSON builder. JSON de-dup keyed by
  `(enum_name, sorted participant locations)`. Regression test
  `test_print_json_carries_repeated_match_arm_count_and_distinct_groups`.
- **SRP `composite_score`, responsibility clusters, and
  `length_score` dropped** (P2): `SrpFindingDetails::StructCohesion`
  was missing the analyzer's `composite_score` + `clusters`
  (responsibility groups), and `ModuleLength` was missing
  `length_score`; JSON consumers filled placeholder zeros / empty
  arrays / wrapped-one-element-arrays. Fix: added the missing
  fields plus a new `ResponsibilityCluster` domain type
  (re-exported from `domain::findings`), copied them in
  `project_struct` / `project_module`, and pulled them through the
  JSON builder. The intermediate `SrpModuleRow` (text/html-friendly)
  still flattens each cluster's member list with `", "`; JSON
  preserves the per-cluster grouping. `Eq` derives dropped on
  `SrpFinding` / `SrpFindingDetails` (the new `f64` fields are not
  `Eq`). Regression test
  `test_print_json_carries_srp_composite_score_clusters_length_score`.
- **Integration test renamed and reinforced** (proactive A21
  sweep): `test_json_output_parseable` was a schema-only smoke
  test. Renamed to `test_json_output_schema_and_complexity_values`
  and extended with a value assertion that at least one
  `functions[].complexity.logic_count` is non-zero on
  `examples/sample.rs`. Catches future projection drops in the
  JSON path because `sample.rs` deterministically has Operations
  with non-zero logic counts.

Twelfth-pass review (Codex 2026-05-12 round 12):

- **JSON reporter dropped `logic_count` + `call_count`** (P2): the
  v1.2.1 typed-reporter refactor split `FunctionAnalysis.complexity`
  (legacy IOSP type carrying every metric) into
  `ComplexityMetricsRecord` (typed dimension state), but
  `project_metrics` did not carry the IOSP `logic_count` /
  `call_count` fields across and `json::functions::build_functions`
  hard-coded `JsonComplexity.logic_count` / `call_count` to `0`.
  Every JSON consumer therefore saw zeros for every function since
  v1.2.1, even though the analyzer measured non-zero counts. The
  existing `test_print_json_with_complexity_no_panic` set non-zero
  inputs but only asserted "no panic" — the smoke-test masked the
  data loss for eleven Codex passes. Fix: added the two counts to
  `ComplexityMetricsRecord`, copied them in `project_metrics`, and
  pulled them through `build_functions`. Regression test
  `json_complexity_carries_logic_count_and_call_count` parses the
  produced JSON and asserts the non-zero values survive the
  projection + reporter round-trip.

### Added

- **Trait-method anchor model for call-parity dispatch**: `dyn
  Trait.method()` now emits a single synthetic
  `<Trait>::<method>` anchor instead of one edge per overriding
  workspace impl. The boundary walker recognises the anchor as a
  target boundary when (a) the trait is declared in the target
  layer with a callable body (default body in target OR an
  overriding impl), or (b) at least one overriding impl lives in
  the target layer; non-target default bodies are not promoted
  through empty target impls (`CallGraph::trait_method_anchors`
  populated by `populate_anchor_index`). Concrete impl-method
  canonicals never enter the touchpoint set via dispatch, so
  Check C doesn't fire on what is semantically a single boundary
  call. Unambiguous inherited-default UFCS calls are routed to
  the same anchor by the edge-rewrite post-pass.
- **Anchors as target capabilities for Check B/D**:
  `CallGraph::target_anchor_capabilities(target)` enumerates
  trait-method anchors that pass the unified target-capability
  rule (target-declared callable body OR overriding impl in
  target). Check B iterates them alongside concrete
  `pub_fns_by_layer[target]`, so dispatch-only adapter coverage
  is checked for parity and orphan status; Check D counts handlers
  per anchor for multiplicity. Anchor findings carry the trait
  method's actual source location (file + 1-based line + column)
  — see the round-2 P4 entry above for the `MethodLocation`
  capture path.
- **Walker peer-adapter check before anchor promotion**:
  `TouchpointWalk::run` now checks `is_peer_adapter` BEFORE
  `is_target_boundary`. A trait anchor declared in a peer-adapter
  layer (e.g. `mcp::Handler`) with overriding impls in the target
  layer no longer leaks peer-adapter coverage into the origin
  adapter's set.
- **`OrphanSuppression` Finding type in `domain::findings`** with
  `AnalysisFindings::orphan_suppressions` field. Cross-cutting
  Finding (not tied to a single dimension) carrying
  `// qual:allow(...)` markers that matched no finding in their
  annotation window.
- **`ReporterImpl::OrphanView` + `build_orphans` method** plus
  `Snapshot::orphans` field. Per-reporter discretion: dot stays
  `OrphanView = ()` (intentional no-op for the data-only graph
  format), the seven diagnostic reporters (text, html, json, sarif,
  github, ai, findings_list) declare meaningful view types and
  consume `snapshot.orphans` exclusively. Future reporters MUST
  implement `build_orphans` (compile-force) and consciously decide
  what to do with orphans.

### Fixed

- **cfg-test impl-block leak in graph + pub-fn visitors**:
  `file_fn_collector::visit_item_impl` and `pub_fns::visit_item_impl`
  now skip `#[cfg(test)] impl X { … }` blocks entirely. Previously
  the cfg attribute lived on the impl block while child methods had
  no attrs of their own, so test-only methods leaked into the
  production call graph and pub-fn surface.
- **`record_trait_impl` filters cfg-test / `#[test]` overrides**:
  `WorkspaceTypeIndex.trait_impl_overrides` no longer records
  test-only methods, so production dispatch can't route to a phantom
  `Type::method` for a test-only override.
- **Strict self-type visibility in pub_fns**: trait-impl method
  registration was relaxed in an interim fix to register
  `<Hidden>::<method>` for `impl PubTrait for Hidden` even when
  `Hidden` is private. With the anchor refactor that relaxation is
  no longer needed and produced over-coverage; the strict visibility
  gate is restored. The trait method's anchor carries the public
  capability instead.

### Removed

- **`AnalysisResult.orphan_suppressions` field** — orphan rendering
  flows exclusively through `findings.orphan_suppressions`
  (consumed by `Snapshot::orphans` per reporter). The legacy
  struct-field bypass and per-reporter `orphan_suppressions: &'a [_]`
  fields are gone.
- **`OrphanSuppressionWarning`** alias removed. The canonical type is
  `domain::findings::OrphanSuppression`; the adapter-layer alias served
  as a transition step and is no longer needed.

## [1.2.2] - 2026-04-30

Patch release: **Reporter-Trait sealed two-trait + Snapshot pattern**.

Internal refactor — no user-visible behaviour change. Every output
format (text, html, json, sarif, github, ai, dot, findings_list) now
goes through a single `Reporter::render()` entry point backed by a
sealed `ReporterImpl` trait. The compile-time Reporter-Parity guarantee
(adding a new dimension forces every reporter to address it) is now
proven by three orthogonal failure modes simultaneously: trait method
set, snapshot constructor, and exhaustive `publish` destructuring —
verified in Phase 11 by introducing a synthetic 8th dimension and
observing 18 compile errors across all 9 reporter sites.

### Changed

- **Sealed two-trait design** in `src/ports/reporter.rs`: public
  `Reporter` trait with single `render()` method (only entry point
  external code can invoke), crate-internal `ReporterImpl` with
  per-dim `build_*` projections and `publish()` composition. The
  `sealed::Sealed` supertrait lives in a private module so no external
  crate can implement `Reporter` directly. `Snapshot<R>` aggregates
  all 10 per-dim views with `pub(crate)` fields, locking
  `ReporterImpl::publish` to crate-internal callers.
- **Per-reporter pure-data Views**: every reporter projects findings
  into typed row structs (`HtmlIospView`, `SarifResultRow`,
  `AiIospRow`, etc.); `publish()` formats them into the final string.
  No reporter pre-renders markup in `build_*` anymore — composition
  decisions (card-then-table-then-cross-section in HTML, summary-then-
  details in text, etc.) live in `publish()`.
- **Cross-reporter shared projections** in
  `src/adapters/report/projections/{srp, coupling, dry, tq}.rs`:
  text/html/sarif/json/ai/findings_list reporters all consume the
  same dimension-bucket projections (`SrpBuckets`, `CouplingBuckets`,
  `DryBuckets`, etc.). Removed twelve transitional cross-reporter
  duplicate findings via these helpers.
- **Pipeline.rs** (`src/app/pipeline.rs`): every output-format branch
  follows the unified `<Reporter>.render(&findings, &data)` shape.
  Print wrappers stay as the boundary entry points.

### Removed

- Legacy `DeprecatedReporter` + `DeprecatedAnalysisReporter` traits
  and the `deprecated_render_findings` / `deprecated_render_analysis_data`
  helpers — fully replaced by the sealed design.

### Fixed

- **Trait-dispatch collapses to synthetic anchor.**
  `calls::trait_dispatch_edges` previously emitted `<impl>::<method>`
  for every workspace impl of a dispatched trait method. A single
  `h.handle()` on `dyn Handler` with N overriding impls produced N
  edges, expanding into N touchpoints in the boundary walker, which
  triggered Check C `multi_touchpoint` warnings for what is
  semantically a single boundary call. Dispatch now emits ONE
  synthetic anchor `<Trait>::<method>` representing the logical
  capability. The touchpoint walker recognises the anchor as a
  target boundary when (a) the trait is declared in the target
  layer with a callable body (default OR overriding impl), OR
  (b) at least one overriding impl lives in the target layer —
  non-target default bodies are NOT promoted through empty target
  impls (the executable body lives outside target). Concrete UFCS
  calls into inherited-default impls are routed to the anchor at
  graph build time via the edge-rewrite post-pass, so dispatch and
  direct-concrete forms share the same anchor. Calls **inside**
  the default-method body itself stay invisible to Check A/B/D
  (the trait method's body isn't a graph node).
- **`record_trait_impl` filters cfg-test / `#[test]` overrides.**
  `WorkspaceTypeIndex.trait_impl_overrides` used to record every
  `ImplItem::Fn`, including test-only methods. Production dispatch
  then routed to a phantom `Type::method` for a test-only override
  while the workspace call graph + `method_returns` index correctly
  skipped those items. The override set is now filtered with
  `has_cfg_test` + `has_test_attr`, mirroring `methods.rs`.
- **PubFnCollector keeps strict self-type visibility.** Earlier
  v1.2.2 relaxed visibility to register `<Hidden>::<method>` for
  `impl PubTrait for Hidden` even when `Hidden` is private, so
  dispatch-emitted impl-edges had matching pub-fn entries. With
  the anchor refactor dispatch no longer emits per-impl edges, so
  the relaxation is unnecessary and the visibility gate is
  restored to its strict form: only impls on visible self-types
  contribute concrete target pub-fns. Private impls are still
  reachable through the anchor — the public capability they
  fulfill — without polluting the per-handler-type pub-fn surface.
  Regression test `test_collect_pub_fns_skips_trait_impl_method_on_private_self_type`.

### Documented limitations

- Function re-exports (`pub use private::op` for `pub fn op()`) are
  intentionally filtered from the visible-types set so private
  same-named types don't leak. The trade-off — pub-use-only
  functions are blind to Check B/D — is documented as Limitation #4
  in `book/adapter-parity.md`. Workaround: declare the function at
  a publicly-reachable path directly.

### Internal

- 1565 tests, 100% quality across all seven dimensions, 0 findings,
  0 clippy warnings.
- All `qual:allow(dry)` and `qual:allow(srp)` markers added during
  the migration phases removed: github helpers refactored to a
  generic `GithubDetailRow<D>` + `build_detail_view` /
  `format_detail_view`, html dry tables share a generic
  `render_table<T>`, sarif `SarifResultRow` holds the whole `Finding`
  (single clone) instead of destructured fields, html coupling
  introduces a private `format_subsections` helper to merge the three
  sub-formatters into one cluster, and ai is split into
  `ai/{mod, rows, format, details, output}.rs`.
- New regression test `helper_reached_via_trait_blanket_dispatch_is_not_dead_code`
  in `src/adapters/analyzers/dry/tests/dead_code.rs` documents that
  the `call_targets` visitor handles the trait-blanket-dispatch case
  via flat method-name capture; the v1.2.2 `sarif_rules` workaround
  was unnecessary and has been reverted.

## [1.2.1] - 2026-04-27

Patch release: **`call_parity` boundary semantic + new Checks C/D**.

The v1.2.0 `call_parity` rule walked transitive reachability across
the entire target layer up to `call_depth` hops. On a clean codebase
with zero genuine adapter asymmetries, this still produced findings
for every application-internal helper that wasn't directly touched
by every adapter (e.g. `record_operation`, `impact_count`). The
findings pointed *inward* at application plumbing rather than at
real adapter drift.

v1.2.1 reframes Check B's semantic to **boundary-only**: walk forward
from each adapter pub-fn until the target layer is hit, record that
node as the adapter's touchpoint, then stop. Compare touchpoint sets
across adapters. Application-internal helpers are no longer inspected
for parity — that's `DRY-002`'s concern, not `call_parity`'s.

### Added

- **Check C — multi-touchpoint** (`architecture/call_parity/multi_touchpoint`):
  flags adapter pub-fns that orchestrate across multiple application
  calls themselves. Configurable severity via
  `[architecture.call_parity] single_touchpoint = "off" | "warn" | "error"`,
  default `"warn"` (emits as `Severity::Low`).
- **Check D — multiplicity mismatch**
  (`architecture/call_parity/multiplicity_mismatch`): flags target
  pub-fns reached by every adapter but with divergent per-adapter
  handler counts (e.g. cli has 2 handlers → `session.search`, mcp
  has 1).
- **Deprecated-handler exclusion**: adapter pub-fns marked
  `#[deprecated]` (in any form) are excluded from Checks A/B/C/D.
  Aliases that are explicitly being phased out shouldn't drag the
  parity report.
- Regression tests pinning correct turbofish + inferred-generic call
  resolution behavior in the canonical-call collector.

### Changed

- **Check B — boundary semantic**. A target pub-fn is flagged when:
  - it appears in some adapter's coverage but is missing from another
    (mismatch case — adapter feature drift), OR
  - it isn't transitively reachable from any adapter touchpoint
    through target-internal callers (orphan case — application
    capability not wired to any adapter, including dead target-layer
    islands where only other unreachable target fns call it).
  Internal application chains wired up via at least one adapter
  (`session.search → record_operation → impact_count` when an adapter
  reaches `session.search`) are silent.
- `call_depth` semantic narrowed: now bounds **adapter-internal**
  traversal depth only. Once the target layer is reached, the walk
  stops descending into target callees. Default unchanged (3); no
  config breakage.

### Migration notes

If you saw v1.2.0 fire findings on application-internal helpers
(`record_operation`, `impact_count`, etc.) that ARE wired up through
some adapter, those silently disappear under v1.2.1. The legitimate
adapter-asymmetry findings remain. Genuinely orphaned target pub-fns
— including those only callable via other dead target-layer code —
still produce findings under Check B's orphan branch.

If you want to detect "internal application helpers reached
asymmetrically through other application code", that semantic is no
longer covered by `call_parity`; use `DRY-002` (dead code) plus the
existing per-target visibility audit in code review.

### Architecture refactor: typed per-dimension Findings

Alongside the call_parity bugfix, v1.2.1 introduces a **typed
per-dimension Finding architecture** that fixes a long-standing
"shotgun surgery" pattern: when a new dimension was added, every
reporter had to be touched manually and gaps went unnoticed (e.g.
`architecture_findings` only appeared in JSON/SARIF/findings_list,
silently missing from HTML/AI/text/github).

#### Added

- `domain::findings::*` — seven typed Finding structs (`IospFinding`,
  `ComplexityFinding`, `DryFinding`, `SrpFinding`, `CouplingFinding`,
  `TqFinding`, `ArchitectureFinding`) plus `AnalysisFindings`
  aggregate. Each typed Finding embeds `domain::Finding` as `common`
  for shared metadata (file/line/column/dimension/rule_id/message/
  severity/suppressed) and adds dimension-specific detail.
- `domain::analysis_data::*` — typed state structures (`FunctionRecord`,
  `ModuleCouplingRecord`) that carry per-function classification +
  complexity metrics and per-module coupling metrics for reporters.
- `ports::reporter::Reporter` trait with one method per dimension
  (no default implementations). The compile-time guarantee: when a new
  dimension is added, every reporter that hasn't been migrated fails
  to compile. `render_report` helper visits all dimensions in
  canonical order.
- `app::projection` module with per-dimension projection adapters that
  build typed Findings + AnalysisData from the analyzer outputs.
  Pipeline populates `AnalysisResult.findings` and
  `AnalysisResult.data` directly.
- Architecture findings now visible in **all reporters** (HTML, AI,
  JSON, SARIF, findings_list, text-verbose, github). Previously
  rendered only by JSON/SARIF/findings_list.
- AI reporter: `map_category("ARCHITECTURE") → "architecture"`
  (previously fell through unmapped).
- Per-kind metadata helpers consolidate label lookups: `DryFindingKind::meta()`,
  `TqFindingKind::meta()`, `ComplexityFindingKind::meta()`,
  `Severity::levels()` — replaces the kind→string match statements
  that used to be duplicated across reporters.

#### Changed

- `AnalysisResult` reduced to 5 fields: `results` (FunctionAnalysis
  records), `summary`, `orphan_suppressions`, `findings` (typed
  per-dimension), `data` (typed per-dimension state). The legacy
  per-dimension fields (`coupling`, `duplicates`, `dead_code`,
  `fragments`, `boilerplate`, `wildcard_warnings`, `repeated_matches`,
  `srp`, `tq`, `structural`, `architecture_findings`) are removed —
  every reporter now consumes the typed findings/data exclusively.

#### Migration notes

For consumers of the JSON output: no breaking changes — JSON shape is
unchanged. The typed `findings` and `data` aggregates are the internal
input the pipeline projects from; the JSON envelope is built from
them with the same shape as before.

For maintainers: when adding a new dimension, the migration path is
now (1) define the typed `*Finding` struct in `domain::findings`,
(2) add the projection adapter in `app::projection`, (3) extend the
`Reporter` trait with `report_<new_dim>`, (4) every reporter
implementing the trait fails to compile until updated. This replaces
the old practice of grepping for "where do reporters consume this
dimension?" and hoping nothing was missed.

## [1.2.0] - 2026-04-24

Minor release: **shallow type-inference** for `call_parity` receiver
resolution across three dimensions:

1. **Return-type propagation** (method chains, field access, stdlib
   Result/Option/Future combinators, destructuring patterns) —
   eliminates the dominant false-positive class that made v1.1.0
   unusable on any Session/Context/Handle-pattern Rust codebase.
2. **Trait dispatch over-approximation** — `dyn Trait` / `&dyn Trait` /
   `Box<dyn Trait>` receivers fan out to every workspace impl of the
   trait. Makes the tool structurally sound for Ports&Adapters
   architectures, where dependency inversion via trait objects is the
   core abstraction.
3. **Framework & type-alias config** — type-alias expansion,
   user-configurable transparent wrapper types (Axum `State<T>`,
   Actix `Data<T>`, tower `Router<T>`, …), and attribute-macro
   transparency (with a default starter-pack for `tracing::instrument`,
   `async_trait`, `tokio::main`/`test`, etc.).

No breaking changes; existing `[architecture.call_parity]` configs
keep working without modification — the new resolution paths are all
additive and the legacy fast-path stays intact as a safety net.

### Fixed
- **`call_parity` method-chain constructor resolution.** v1.1.0's
  resolver only extracted binding types from direct constructor calls
  (`let s = T::ctor()`). Real-world Rust code more often wraps the
  constructor in a `?` / `.unwrap()` / `.map_err(…)?` chain, which
  returned `None` from the legacy extractor and left the downstream
  method call as a layer-unknown `<method>:name`. On rlm (the reference
  adopter codebase), this produced 93 of 116 false-positive findings —
  roughly 80 % of the total. Symptom: every CLI handler shaped like
  ```rust
  pub fn cmd_diff(path: &str) -> Result<(), Error> {
      let session = RlmSession::open_cwd().map_err(map_err)?;
      session.diff(path).map_err(map_err)?;
      Ok(())
  }
  ```
  was reported as "not delegating to application" even though it
  obviously did.
- **`self` receiver resolution inside impl methods.** Signature seeding
  only iterated typed `FnArg::Typed` params, never the `self` receiver.
  As a result `self.helper()` and `self.field.method()` fell through to
  `<method>:…` even when the enclosing impl's canonical type was known
  via `self_type`. The collector now binds `self` to the impl's
  canonical segments alongside the typed params, so ordinary
  method-internal delegation routes through `method_returns` /
  `struct_fields` like any other receiver.

### Added
- **`call_parity_rule::type_infer`** — new module implementing shallow
  type inference over `syn::Expr`. Exposes `infer_type(expr, ctx) ->
  Option<CanonicalType>` as the public entry point. Built on three
  layers:
  - `workspace_index`: single pre-pass over the workspace collecting
    struct-field types, impl-method return types, and free-fn return
    types into a lookup index. Runs once per `build_call_graph` call.
  - `infer`: dispatch over expression variants — `Path`, `Call`,
    `MethodCall`, `Field`, `Try` (`?`), `Await`, `Cast`, `Unary(Deref)`,
    plus transparent `Paren` / `Reference` / `Group`. Supports
    `Self::xxx` substitution in impl-method contexts.
  - `combinators`: stdlib table covering `Result<T,E>` / `Option<T>` /
    `Future<T>` — `unwrap`, `expect`, `unwrap_or*`, `ok`, `err`,
    `map_err`, `or_else`, `ok_or`, `filter`, `as_ref` etc. Closure-
    dependent methods (`map`, `and_then`, `then`) intentionally stay
    unresolved rather than fabricate an edge.
- **Pattern-binding walker** (`type_infer::patterns`) — extracts
  `(name, type)` pairs from `let` / `if let` / `while let` / `let …
  else` / `match`-arm / `for` patterns. Handles tuple-struct
  destructuring (`Some(x)`, `Ok(x)`, `Err(_)`), named-field struct
  patterns (`Ctx { session }`, `Ctx { session: s }`, `Ctx { a, .. }`),
  slice patterns with rest, and disambiguates `None` as a variant
  against `Option<_>` instead of binding it as a variable name.
- **Fallback wiring in `calls::CanonicalCallCollector`** — both
  `visit_local` (for binding extraction) and `visit_expr_method_call`
  (for method resolution) now invoke `type_infer` as a fallback after
  the legacy fast-path fails. The fast path (direct constructor
  extraction, signature-parameter types, explicit `let x: T = …`
  annotation) is preserved for unit-test fixtures that don't build a
  workspace index, so no existing tests regressed.
- **`BindingLookup` trait** bridges the legacy `Vec<String>` scope
  stack into the inference engine's `CanonicalType` vocabulary via
  the `CollectorBindings` adapter. Returns owned `Option<CanonicalType>`
  so adapters can synthesize types on the fly without lifetime
  gymnastics.

### Changed
- **`FnContext` in `call_parity_rule::calls`** gained a new
  `workspace_index: Option<&'a WorkspaceTypeIndex>` field. The full
  `build_call_graph` pipeline always passes `Some(&index)`; unit-test
  fixtures pass `None` and fall back to the legacy fast-path only.
  Additive change — no public-API break for existing
  `collect_canonical_calls` call sites.
- **`build_call_graph`** now pre-builds the workspace type-index once
  before the per-file walk. The index shares the same `cfg_test_files`
  filter as the call-graph itself, so the two stay consistent.
- **`iosp::analyze_file`** — bugfix discovered during Task 1.3:
  `file_in_test` was propagated only to free-fn analysis, not to
  `Item::Impl` / `Item::Trait` / `Item::Mod`. This meant any impl-method
  helper inside a `#[cfg(test)] mod tests;` file incorrectly had
  `is_test = false` and got flagged by ERROR_HANDLING / MAGIC_NUMBER /
  LONG_FN checks. Now matches `analyze_mod`'s already-correct
  propagation.

### Documentation
- **`docs/rustqual-design-receiver-type-inference.md`** — the
  normative spec for the multi-stage receiver-resolution work
  (v1.2.0 → v1.3.0 → v1.4.0). Contains the type-inference grammar
  (§3), full stdlib-combinator table (§4), pattern-binding catalog
  (§5), workspace-index schema (§6), trait-dispatch plan (§7),
  config-schema additions (§8), documented Stage-1 limits (§9), and
  test-matrix (§10). Every PR modifying `type_infer/` is reviewed
  against this doc.

### Added — Trait-Dispatch (Stage 2)
- **`dyn Trait` / `&dyn Trait` / `Box<dyn Trait>` receivers** fan out
  to every workspace impl. `fn dispatch(h: &dyn Handler) { h.handle() }`
  records one edge per `impl Handler for X` — sound over-approximation
  that makes call-parity structurally correct for Ports&Adapters
  architectures. Marker traits (`Send`, `Sync`, `Unpin`, `Copy`,
  `Clone`, `Sized`, `Debug`, `Display`) are skipped when picking the
  dispatch-relevant bound from `dyn T1 + T2`.
- **Trait-method gate**: dispatch only fires when the method is in the
  trait's declared method set. `dyn Handler.unrelated_method()` still
  falls through to `<method>:name` rather than fabricating edges.
- **`trait_impls` + `trait_methods` index** built once per
  `build_call_graph`. `impls_of_trait(trait)` and
  `trait_has_method(trait, method)` are the public query methods.
- **Turbofish-as-return-type**: `get::<Session>()` where `get` is a
  generic fn with no concrete workspace return infers `Session` from
  the turbofish arg. Narrow by design — only single-ident paths
  trigger, so `Vec::<u32>::new()` (turbofish on type segment) isn't
  over-approximated.

### Added — Framework & Config Layer (Stage 3)
- **Type-alias expansion.** `type Repo = Arc<Box<Store>>;` recorded in
  the workspace index; `fn h(r: Repo) { r.insert(..) }` expands `Repo`
  → `Arc<Box<Store>>` → `Store` (Arc/Box are Deref-transparent) and
  resolves `insert` against Store's method index. Aliases wrapping
  non-Deref types like `RwLock` / `Mutex` / `RefCell` / `Cell` still
  expand the alias itself, but those wrappers aren't peeled by default
  (their `read` / `lock` / `borrow` methods don't live on the inner
  type) — list them in `transparent_wrappers` if your codebase genuinely
  treats them as Deref-transparent.
- **User-configurable transparent wrappers** via
  `[architecture.call_parity]::transparent_wrappers`:
  ```toml
  [architecture.call_parity]
  transparent_wrappers = ["State", "Extension", "Json", "Data"]
  ```
  Peeled identically to `Arc`/`Box` during resolution. Unblocks
  Axum/Actix-style framework-extractor patterns where
  `fn h(State(db): State<Db>) { db.query() }` would otherwise stay
  unresolved.
- **Attribute-macro transparency** via
  `[architecture.call_parity]::transparent_macros` with a starter-pack
  (`instrument`, `async_trait`, `main`, `test`, `rstest`, `test_case`,
  `pyfunction`, `pymethods`, `wasm_bindgen`, `cfg_attr`) applied by
  default. Current effect is config-schema groundwork + authorial
  intent — the syn-based AST walk already treats attribute macros as
  transparent, so listed entries compile but don't change today's
  behaviour. Retained for future macro-expansion integrations that
  can consult the list without a config-schema break.

### Known Limits
Patterns that intentionally stay unresolved and produce `<method>:name`
fallback markers rather than fabricate edges:
- `Session::open().map(|r| r.m())` — closure-body argument type is
  unknown. Inner method call stays `<method>:m`.
- `fn get<T>() -> T { … }; let x = get(); x.m()` without annotation
  or turbofish. Use `let x: T = get();` or `get::<T>()`.
- `fn make() -> impl Trait { … }; make().inherent_method()` —
  `impl Trait` hides the concrete type by design. Methods declared on
  the trait resolve via trait-dispatch over-approximation; inherent
  methods stay `<method>:name`.
- `fn make() -> impl Future<Output = T> + Handler { … }` — multi-bound
  intersection returns. `CanonicalType` carries one type per receiver,
  so `resolve_bound_list` keeps the first non-marker bound only;
  `.await` propagation *or* trait-dispatch fires, never both. Marker
  traits (`Send` / `Sync` / `Unpin` / `Copy` / `Clone` / `Sized` /
  `Debug` / `Display`) are filtered first, so the common
  `impl Future<Output = T> + Send` shape is unaffected.
- `pub mod outer { … pub use self::private::Hidden; }` followed by
  `fn h(x: outer::Hidden) { x.op() }` — the receiver-type resolver
  doesn't follow workspace-wide `pub use` re-exports inside nested
  modules, so the parameter resolves to `crate::…::outer::Hidden`
  while methods on the impl (inside `mod private`) are indexed under
  `crate::…::outer::private::Hidden`. Visibility recognises both
  paths, but the call-graph edge collapses to `<method>:op`.
  Workaround: write the impl at the file-level qualified path
  (`impl outer::Hidden { … }`) so impl-canonical and caller-canonical
  agree, or `qual:allow(architecture)` at the call-site.
- `pub type Public = private::Hidden; impl Public { pub fn op() }` —
  the impl method is indexed under `crate::…::Public::op` (impl
  self-type via path canonicaliser), but a caller `fn h(x: Public)
  { x.op() }` resolves `x` via type-alias expansion to
  `crate::…::private::Hidden` and emits a `Hidden::op` edge.
  Visibility sees `Public`, but the edges disagree so Check B
  flags `Public::op` as unreached. Workaround: write `impl
  private::Hidden { … }` directly so impl-canonical and
  caller-canonical agree, or `qual:allow(architecture)` on the
  affected impl.
- `type Id<T> = T; pub type Public = Id<private::Hidden>;` — the
  visibility pass doesn't substitute use-site generic args into
  alias bodies (the workspace alias-index runs after pub-fn
  enumeration). `Id` enters `visible_canonicals`, but
  `private::Hidden` doesn't, so Check B can drop public methods on
  `Hidden`. Receiver-side resolution does substitute, so callers
  still reach `Hidden::op`. Workaround: skip the generic-alias
  indirection (`pub type Public = private::Hidden;`), or
  `qual:allow(architecture)` on the affected impl.
- Arbitrary proc-macros that alter the call graph without being in
  `transparent_macros` config. User-annotate via
  `// qual:allow(architecture)` on the enclosing fn.

### Infrastructure
- **`tests/rlm_snapshot.rs`** — end-to-end regression snapshot with a
  3-file rlm-shape fixture (application/session, cli/handlers,
  mcp/handlers). Asserts a budget of **0 Check A findings + 5 Check B
  findings** (the 5 legitimate asymmetries / dead-code items). Any
  drift in this count is a clear regression signal.
- **`tests/regressions.rs`** — unit-level tests covering every rlm
  Group-2 / Group-3 pattern plus Stage-2 trait-dispatch /
  turbofish cases and Stage-3 type-alias / user-wrapper cases.
  Negative tests pin documented limits in place.
- **~160 new unit tests** across `type_infer/tests/` covering
  `CanonicalType`, `resolve_type`, workspace-index building, inference
  dispatch, pattern binding, the stdlib-combinator table, trait
  collection, and type-alias collection.

## [1.1.0] - 2026-04-24

Minor release: zero-annotation cross-adapter delegation check for
N-peer-adapter architectures (CLI + MCP + REST + …). No breaking
changes; the new check only fires when `[architecture.call_parity]`
is explicitly configured, and inert otherwise.

### Added
- **`[architecture.call_parity]`** — cross-adapter delegation drift
  check driven entirely by the existing `[architecture.layers]`
  configuration. No per-function annotation required: every `pub fn`
  in a configured adapter layer is checked automatically, and every
  new adapter handler participates in the check from its first commit.
  Two complementary rules run under one config section:
  - `architecture/call_parity/no_delegation` — each `pub fn` in an
    adapter layer must transitively (up to `call_depth` hops) call
    into the configured target layer. Catches inlined business logic.
  - `architecture/call_parity/missing_adapter` — each `pub fn` in
    the target layer must be transitively reached from every
    adapter layer. Catches asymmetric feature coverage (e.g. CLI
    + MCP both call `application::do_thing`, REST doesn't).
- **Receiver-type tracking** (`session.search(…)` resolution) — the
  call collector walks `let` bindings, signature parameters, and
  constructor returns to resolve method calls on Session / Service /
  Context objects. `Arc<T>`, `Box<T>`, `Rc<T>`, `&T`, `&mut T`,
  `Cow<'_, T>` wrappers are stripped. Critical for Session-pattern
  architectures, where method calls would otherwise stay
  `<method>:name` and the check would 100% false-positive.
- **`exclude_targets` glob escape** — legitimate asymmetric target
  fns (setup routines, debug-only endpoints) can be grouped under a
  glob pattern in the config, keeping the escape in one place instead
  of scattering `qual:allow(architecture)` markers across files.
- **`// qual:allow(architecture)`** as the secondary escape for
  individual fn-level asymmetries. Counts against
  `max_suppression_ratio` — overuse surfaces in the report.
- **`LayerDefinitions::layer_of_crate_path`** — resolves canonical
  call targets (`crate::a::b::c`) back to layer names. Internal API,
  reusable across future workspace-wide architecture rules.

### Infrastructure
- New `#[ignore]`-gated `benchmark_call_parity_on_self_analysis` test.
  Runs the full pipeline against rustqual's own ~200-file source tree
  and asserts the pass stays under a 3-second wall-time ceiling.
  Execute via `cargo test -- --ignored` before release.

## [1.0.1] - 2026-04-20

Patch release addressing five bugs reported against v0.5.6 (verified
against v1.0) plus one pre-existing CI gap uncovered during
investigation. No breaking changes; drop-in upgrade.

Self-analysis: `cargo run -- . --fail-on-warnings --coverage
coverage.lcov` reports 1913 functions, 100.0% quality score across all
7 dimensions, 0 findings. 1176 tests pass (35 new).

### Added
- **`// qual:test_helper` annotation** — narrow marker for
  integration-test helpers. Suppresses **only** the DRY-002 `testonly`
  dead-code finding and TQ-003 (`untested` production functions); all other
  checks (DRY duplicates, complexity, SRP, coupling, structural) keep
  applying. Does **not** count against `max_suppression_ratio`.
  Replaces the overly broad `ignore_functions` entry for the
  integration-test-helper use case.
- **Multi-line `qual:allow` rationale** — suppressions placed above a
  multi-line `//` comment block (a common pattern: marker on the first
  line, rationale on subsequent lines, then `#[derive]` + item) now
  work. The annotation window is measured from the block's last
  comment line, not the marker itself. Blank lines still break the
  block — misplaced markers don't reach their target.
- **Orphan-suppression findings** — `// qual:allow(...)` markers that
  match no finding in their annotation window are emitted as
  first-class `ORPHAN_SUPPRESSION` findings, visible in every output
  format (text, JSON, AI, SARIF, `--findings`). The AI format surfaces
  the marker's original reason string so the agent can tell whether
  it was a stale leftover or a misplaced annotation. Orphan findings
  contribute to `total_findings()` and thus to default-fail (they do
  not currently trigger `--fail-on-warnings`, which only gates on
  `suppression_ratio_exceeded`) — the user experience is: run
  rustqual, see the orphan in the list, delete or correct the marker,
  rerun. The
  detector reads raw complexity metrics (not the `*_warning` flags
  that suppressions clear), so a `// qual:allow(complexity)` marker
  on a genuinely over-threshold function is correctly recognized as
  non-orphan even after the suppression has silenced the user-visible
  finding. Coupling-only markers are skipped only when the file has
  no line-anchored Coupling finding to match by line window; when a
  line-anchored Coupling position exists (for example, a Structural
  warning with `dimension == Coupling`), the marker is verifiable.
- **`apply_parameter_warnings` marks suppressed entries instead of
  dropping them** — internal change that lets the orphan-suppression
  detector see SRP-param suppressions as matching targets. User-
  visible behavior unchanged (`srp_param_warnings` count still only
  tallies non-suppressed entries).

### Fixed
- **Test-companion files missed by cfg-test detection**. The
  `#[cfg(test)] #[path = "foo_tests.rs"] mod tests;` pattern — common
  for co-locating unit tests next to their production module — was
  not recognized as cfg-test because (a) `ChildPathResolver` only
  tried the naming-convention paths (`foo/tests.rs`,
  `foo/tests/mod.rs`) and ignored the `#[path]` override, and (b)
  top-level `#![cfg(test)]` inner attributes on the companion file
  itself were never scanned. Both gaps closed: `#[path]` is now
  resolved relative to the parent file's directory (rustc
  semantics), and `file.attrs` is inspected for inner
  `#![cfg(test)]`. Fixes systematic SRP_MODULE false-positives on
  test-companion files whose many-test-one-cluster-each layout
  triggers `max_independent_clusters` by design.
- **Bug 2 — SRP LCOM4 false-positives via macro-wrapped method
  calls**. `MethodBodyVisitor` in the SRP cohesion analyzer now
  descends into macro token streams, so `self.method()` references
  inside `debug_assert!(...)`, `assert_eq!(...)`, `format!(...)`
  etc. count as inter-method edges. Paired reader/mutator patterns
  where a mutator calls a reader via `debug_assert!` are now
  correctly united into a single LCOM4 cluster.
- **Bug 4 — AI format omitted SRP_MODULE cluster driver**.
  `enrich_detail()` in the AI reporter now names both the length
  driver (`N lines (max M)`) and the cluster driver (`N independent
  clusters (max M)`) when either triggers, and combines both when
  both fire. Extended the same completeness discipline to six more
  finding categories: SDP (instability values), BOILERPLATE
  (description + suggested fix), DEAD_CODE (full suggestion text),
  STRUCTURAL (rule detail not just code), and kept the pre-existing
  enrichers for VIOLATION, DUPLICATE, FRAGMENT, SRP_STRUCT,
  COGNITIVE, CYCLOMATIC, LONG_FN, NESTING, SRP_PARAMS. Goal: a
  single `--format ai` invocation is always enough — no JSON
  fallback.
- **Bug 1 — DEAD_CODE/testonly suggestion was hard to act on**. The
  suggestion text now explicitly names both escape hatches:
  `// qual:api` (for truly public API functions) and
  `// qual:test_helper` (for test-only helpers in `src/`).
- **CI/release workflow self-analysis gap (pre-existing)** —
  `.github/workflows/ci.yml` and `release.yml` now run
  `cargo run -- . --fail-on-warnings --coverage coverage.lcov` with
  `.` as the analysis root (was `src/`). Architecture globs like
  `src/adapters/**` only match when paths are relative to the
  project root; running with `src/` stripped the prefix and silently
  disabled architecture-rule checking. The gap was uncovered when
  Bug 4's investigation revealed a forbidden-edge violation
  (`structural::oi` → `coupling::file_to_module`) that had been
  merged under this blind spot.
- **Pre-existing architecture violation** — moved `file_to_module`
  helper from `adapters::analyzers::coupling` to
  `adapters::shared::file_to_module`. Dimension analyzers now don't
  cross-import each other (forbidden-edge rule honored).

### Internal
- `cargo test` in CI/release replaced with `cargo nextest run` to
  match local-development discipline.
- New module `src/app/orphan_suppressions.rs` encapsulates the
  verification pass; `src/app/warnings.rs` shrank from 475 to ~270
  lines after the extraction.
- `run_dry_detection` signature refactored: the two annotation-line
  maps (`api` + `test_helper`) are passed as a single
  `AnnotationLines<'a>` struct to keep parameter count under the
  SRP_PARAMS threshold.

## [1.0.0] - 2026-04-20

Clean-Architecture refactor and seventh quality dimension, **fully
enforced** against rustqual's own codebase. **Breaking**: the
`[weights]` config schema now has 7 fields instead of 6 (new `architecture`
weight); projects with an explicit `[weights]` section must add it and
re-balance so the weights sum to 1.0.

Self-analysis: `cargo run -- . --fail-on-warnings --coverage coverage.lcov`
reports 1805 functions, 100.0% quality score across all 7 dimensions,
0 findings, 27 suppressions (qual:allow + `#[allow]`). 1114 tests pass.

### Added
- **Architecture dimension** — seventh quality dimension with four rule
  types: Layer Rule (rank-based import ordering), Forbidden Rule
  (from/to/except glob triplets), Symbol Patterns (7 matcher families:
  `forbid_path_prefix`, `forbid_glob_import`, `forbid_method_call`,
  `forbid_function_call`, `forbid_macro_call`, `forbid_item_kind`,
  `forbid_derive`), and Trait-Signature Rule (7 checks:
  `receiver_may_be`, `methods_must_be_async`, `forbidden_return_type_contains`,
  `required_param_type_contains`, `required_supertraits_contain`,
  `must_be_object_safe` conservative, `forbidden_error_variant_contains`).
- **`--explain <FILE>` CLI mode** — diagnostic output per file showing
  layer assignment, classified imports, and rule hits; makes config
  tuning in new repos tractable.
- **Golden example crates** at `examples/architecture/<rule>/` covering
  every matcher and rule with fixture + minimal rustqual.toml + snapshot
  test.

### Changed — Clean-Architecture refactor
- **Five-rank layered module structure** with explicit dependency
  direction (`domain → port → infrastructure → analysis → application`):
  - `src/domain/` — pure value types (`Dimension`, `Finding`,
    `Severity`, `SourceUnit`, `Suppression`, `PERCENTAGE_MULTIPLIER`).
    No `syn`, no I/O, no adapter-specific types.
  - `src/ports/` — trait contracts (`DimensionAnalyzer`, `SourceLoader`,
    `SuppressionParser`, `Reporter`). Carry `ParsedFile` DTOs.
  - `src/adapters/config/`, `src/adapters/source/`,
    `src/adapters/suppression/` — **infrastructure** adapters (I/O,
    TOML parsing, filesystem, suppression parsing).
  - `src/adapters/analyzers/` + `src/adapters/shared/` +
    `src/adapters/report/` — **analysis** layer: the seven dimension
    analyzers, their shared helpers (cfg-test detection, AST
    normalization, use-tree walker), and the eight report renderers.
    Reports sit at the same rank as analyzers so they may read rich
    analyzer DTOs (FunctionAnalysis, DeadCodeWarning) without
    ceremonial Finding-only projections.
  - `src/app/` — **application** use-cases: `pipeline` (full-pipeline
    orchestrator), `secondary` (per-dimension passes bundled through
    `SecondaryContext`), `metrics`/`tq_metrics`/`structural_metrics`
    (per-category helpers), `warnings` (complexity, leaf reclass,
    suppression ratio), `exit_gates`, `setup`, `analyze_codebase`
    (port-based).
  - `src/cli/` (`mod`, `handlers`, `explain`) + `src/main.rs` +
    `src/bin/cargo-qual/` + `src/lib.rs` + `tests/**` —
    composition root / re-export points.
- **Pipeline module dissolved** — the 1223-line `src/pipeline/` tree
  from the Phase-1–4 era is now fully absorbed into `src/app/`; the
  orchestrator is split between `pipeline.rs` (221 lines) and
  `secondary.rs` (179 lines, one helper per dimension pass).
- **Strict architecture enforcement** — `[architecture] enabled = true`,
  `unmatched_behavior = "strict_error"` (every production file must be
  in a layer). The full rule set runs in CI.
- **Workspace-root `tests/**` now analyzed** — previously excluded
  wholesale. Cargo's integration-test binaries are detected as
  test-only files by `adapters/shared/cfg_test_files`, so
  `is_test`-aware checks (LONG_FN, MAGIC_NUMBER, ERROR_HANDLING) skip
  them correctly while dead-code and structural checks still apply.
- **Test co-location** — every `#[cfg(test)] mod tests { … }` extracted
  into `<dir>/tests/<name>.rs` companions. Production files report
  honest length metrics (all < 500 lines, most < 300).
- **Architecture analyzer wired through the port** — first dimension to
  implement `DimensionAnalyzer`; `analyze_codebase` iterates
  `&[Box<dyn DimensionAnalyzer>]`.
- **7-dimension weights** (`[f64; 7]`): default
  `iosp=0.22, complexity=0.18, dry=0.13, srp=0.18, coupling=0.09,
  test_quality=0.10, architecture=0.10`.
- **`test` → `test_quality` rename** in `[weights]` config (old `test`
  field rejected with a deserialize error; migrate to `test_quality`).
- **`allow_expect = false`** by default — consistent with the
  architecture rule `no_panic_helpers_in_production`.

### Fixed
- **Cross-analyzer helper leakage** — `has_cfg_test`, `has_test_attr`,
  and `DeclaredFunction`-related cfg-test-file detection moved from
  `adapters/analyzers/dry/` into `adapters/shared/` so TQ and
  structural analyzers no longer import DRY internals.
- **Test-aware classification gap** — helper functions inside companion
  `tests/` subtrees weren't always flagged as `is_test=true` (only
  `#[test]`-attributed ones were). `Analyzer::with_cfg_test_files`
  now initialises `in_test=true` for every function in a cfg-test
  file, eliminating a class of false positives in complexity /
  error-handling checks.
- **Doc-duplicate `Config::load`** — `Config::load` now delegates to
  `Config::load_from_file` after an ancestor-search helper
  (`find_config_file`); removed the inline read+parse duplication.
- **Panic-helper redundancy** — 7 `.expect()` / `unwrap!` /
  `unreachable!` call sites in production code replaced with safe
  fallbacks (`GlobSet::empty()`, `layer_and_rank_for_file` pairing,
  `_ => continue` for non-exhaustive syn matches, `unwrap_or_else`
  for infallible JSON serialization).

## [0.5.6] - 2026-04-16

### Changed
- **Extracted TOON encoder into dedicated [`toon-encode`](https://github.com/SaschaOnTour/toon-encode) crate** for reuse in other projects. `src/report/ai.rs` now delegates to `toon_encode::encode_toon()` instead of hosting its own encoder.
- Removed ~280 lines of duplicated code from `ai.rs`: `encode_toon`, `is_tabular`, `encode_tabular`, `encode_list`, `toon_quote` + `INDENT`/`TOON_SPECIAL` constants + 18 pure encoder tests. Rustqual-specific enrichment (`build_ai_value`, `enrich_detail`, `map_category`) remains.
- Added `toon-encode` as a crates.io dependency (`toon-encode = "0.1"`).
- Test count: 882 — Function count: 488

## [0.5.5] - 2026-04-10

### Added
- **`--format ai` (TOON output)**: Token-optimized output for AI agents using [TOON format](https://toonformat.dev/). Findings are grouped by file (file paths appear once), categories use human-readable snake_case (`magic_number`, `duplicate`, `violation`), and details are enriched with actionable context (partner locations for duplicates/fragments, logic/call line numbers for violations, threshold values for complexity findings). ~66% fewer tokens than JSON.
- **`--format ai-json` (compact JSON)**: Same enriched structure as `--format ai` but serialized as JSON — fallback for AI tools that don't support TOON.
- Custom minimal TOON encoder (~80 lines, no new dependencies).
- `output_results()` now takes `&Config` instead of `&CouplingConfig`, enabling AI format to include threshold information in enriched details.
- 29 new tests for AI output (TOON encoder, category mapping, finding grouping, detail enrichment, serialization).
- Test count: 899 — Function count: 496

## [0.5.4] - 2026-04-10

### Fixed
- **Inconsistent findings count**: Summary header reported fewer findings than the Findings section. `total_findings()` counted magic numbers per-function (1) and duplicates/fragments/repeated matches per-group (1), while the findings list counted per-occurrence (2) and per-entry (2). Now both use per-occurrence/per-entry counting, making the numbers consistent.
- **Missing coupling findings in findings list**: Coupling threshold warnings and circular dependencies were counted in `total_findings()` but not emitted by `collect_all_findings()`. Added `warning: bool` flag on `CouplingMetrics` (set by `count_coupling_warnings`), new `COUPLING` and `CYCLE` categories in `collect_coupling_findings`.
- Extracted `count_dry_findings()` Operation in `pipeline/metrics.rs` to consolidate DRY entry counting and keep `run_secondary_analysis` under the function length threshold.
- Removed redundant pre-suppression counts for duplicates, fragments, and boilerplate in `run_dry_detection` (overwritten after suppression marking).
- 5 new consistency tests verifying `total_findings() == collect_all_findings().len()`.
- Test count: 868 — Function count: 477

## [0.5.3] - 2026-04-09

### Fixed
- **`./src/` path rejected on Windows**: The dot-directory filter excluded `.` (current directory) because `".".starts_with('.')` is true. Now skips hidden dirs (`.git`, `.tmp`) while preserving `.` and `..`.
- **OI false positives on Windows**: `top_level_module()` only split on `/`, causing backslash paths to be treated as different modules. Now normalizes `\` to `/`.
- **Internal path normalization**: `display_path` in `read_and_parse_files` and `rel` in `collect_filtered_files` now normalize backslashes at the source. Ensures consistent forward-slash paths across all dimensions and reports.
- **Empty location in findings**: Findings without file location (e.g. SDP) no longer render as `:0`.
- 4 new tests for path handling: dot-prefix path, hidden dir exclusion, target dir exclusion, forward-slash normalization.
- Test count: 862 — Function count: 476

## [0.5.2] - 2026-04-09

### Changed
- **Cleaner default output**: Summary shown first with total findings count in header line. File-grouped output only with `--verbose`. Default mode shows compact findings list with "═══ N Findings ═══" heading. Removed "Loaded config from ..." message, "N quality findings. Run with --verbose" footer, and file headers without context.
- **Coupling section**: Explanation text ("Incoming = modules depending on this one...") and "Modules analyzed: N" only shown with `--verbose`.
- **Windows path support**: Backslash paths (e.g., `.\src\` from PowerShell) are normalized to forward slashes on input.

### Fixed
- **OI false positives on Windows**: `top_level_module()` in the Orphaned Impl check only split on `/`, causing backslash paths like `db\queries\chunks.rs` to be treated as a different module than `db\connection.rs`. Now normalizes `\` to `/` before splitting. This caused 9 false OI findings on Windows that didn't appear on Linux/WSL.
- Test count: 858 — Function count: 476

## [0.5.1] - 2026-04-09

### Added
- **`// qual:allow(unsafe)` annotation**: Suppresses unsafe-block warnings on individual functions without affecting other complexity findings. Not parsed as a blanket suppression — does not count against suppression ratio.
- **Boilerplate suppression**: `BoilerplateFind` now has `suppressed: bool`. `qual:allow(dry)` on any boilerplate finding suppresses it. `DrySuppressible` trait extended with impl for `BoilerplateFind`.
- **SARIF BP-001..BP-010 rule definitions**: All 10 boilerplate patterns now have proper SARIF rule entries in `sarif_rules()`. SARIF ruleId uses `b.pattern_id` directly (e.g., `BP-003`).
- `is_within_window()` and `has_annotation_in_window()` utility functions in `findings.rs` — consolidates 5+ duplicated annotation-window check patterns.

### Fixed
- **BP-003 reports per getter, not per struct**: Each trivial getter/setter is now a separate finding on the function line, enabling `qual:allow(dry)` suppression per function.
- **`qual:allow(unsafe)` no longer parsed as blanket suppression**: Previously, `qual:allow(unsafe)` was silently treated as `qual:allow` (suppress all) because "unsafe" wasn't a recognized dimension. Now intercepted before suppression parsing.
- **SARIF boilerplate ruleId**: Was `BP-BP-003` (double prefix), now correctly `BP-003`.

### Changed
- `is_unsafe_allowed()` extracted as standalone function in `pipeline/warnings.rs`.
- `apply_extended_warnings()` accepts `unsafe_allow_lines` parameter.
- `pipeline/dry_suppressions.rs`: `DrySuppressible` impl for `BoilerplateFind`.
- Text/HTML DRY section headers respect suppressed state for all finding types.
- Test count: 857 — Function count: 475

## [0.5.0] - 2026-04-09

### Changed
- **BREAKING: Quality score formula rescaled**. The old formula dampened findings because each dimension independently divided by total analyzed functions. With 20 findings / 100 functions, the old score was ~90%; now it correctly reflects ~73%. Formula: `score = 1 - active_dims * (1 - weighted_avg)`, clamped to [0, 1]. Only active (non-zero weight) dimensions count. 100% is only achievable with 0 findings. 100% violations now scores 0% (was 75%).
- Test count: 852 — Function count: 468

## [0.4.6] - 2026-04-08

### Fixed
- **`qual:allow(dry)` now suppresses all DRY findings**: RepeatedMatchGroup (DRY-005) and FragmentGroup now have `suppressed: bool` fields. `qual:allow(dry)` on any member suppresses the finding. Previously only DuplicateGroup was suppressible.
- All 6 report formats filter suppressed fragments and repeated matches.

### Changed
- `DrySuppressible` trait + generic `mark_dry_suppressions()` replaces 3 duplicate suppression functions. Extracted to `pipeline/dry_suppressions.rs`.
- Test count: 849 — Function count: 468

## [0.4.5] - 2026-04-08

### Fixed
- **Struct field function pointers**: Bare function names in struct initialization (`Config { handler: my_function }`) are now recognized as usage by `CallTargetCollector` via `visit_expr_struct`. Fixes false-positive dead code warnings (DRY-003).

### Changed
- README: removed duplicate Recursive Annotation section.
- Test count: 847 — Function count: 462

## [0.4.4] - 2026-04-08

### Changed
- **Safe targets extended to non-Violations**: `apply_leaf_reclassification()` now treats ALL non-Violation functions as safe call targets — not just C=0 leaves. Calls to Integrations (L=0, C>0) no longer trigger Violations in the caller. Only calls to other Violations (mutually recursive or genuinely tangled functions) remain true Violations. This is a pragmatic IOSP relaxation documented in README.
- **`// qual:recursive` annotation**: Marks intentionally recursive functions. Self-calls are removed from own-call lists before reclassification. Does not count against suppression ratio.
- README: design note documenting safe-target reclassification as pragmatic IOSP relaxation.
- Test count: 844 — Function count: 459

## [0.4.2] - 2026-04-08

### Added
- **Automatic leaf detection**: Functions classified as Operation (C=0) or Trivial are automatically recognized as "leaves". Calls to leaf functions no longer count as own calls for the caller, eliminating false IOSP violations when mixing logic with calls to simple helpers (e.g., `get_config()`, `map_err()`). Iterates until stable for cascading leaf detection.
- `apply_leaf_reclassification()` in `pipeline/warnings.rs` — post-processing step that reclassifies Violations calling only leaves as Operations.
- 5 new unit tests for leaf detection (single leaf, multiple leaves, non-leaf still violation, pure integration unchanged, cascading).

### Changed
- Test count: 841 — Function count: 459
- Showcase and integration test fixtures updated to use non-leaf helpers where Violations are expected.

## [0.4.1] - 2026-04-08

### Added
- **Type-aware method-call resolution**: `.method()` calls now use receiver type info (self type, parameter types) to determine if a call is own or external. Eliminates false-positive IOSP violations from std method name collisions.
- `methods_by_type` on `ProjectScope`, `extract_param_types()`, `resolve_receiver_type()`, `is_type_resolved_own_method()` on `BodyVisitor`.
- **PascalCase enum variant exclusion**: `Type::Variant(...)` not counted as own calls.

### Changed
- **BREAKING: `external_prefixes` removed** from config. Type-aware resolution replaces manual prefix lists. Remove `external_prefixes` from `rustqual.toml` to fix.
- **BREAKING: `UNIVERSAL_METHODS` removed**. `trait_only_methods` + type-aware resolution handle all cases.
- `classify_function()` accepts `type_context` tuple for receiver resolution.
- `BodyVisitor` gains `parent_type` and `param_types` fields.
- Test count: 836 — Function count: 458

## [0.4.0] - 2026-04-08

### Added
- **`// qual:inverse(fn_name)` annotation**: Marks inverse method pairs (e.g., `as_str`/`parse`, `encode`/`decode`). Suppresses near-duplicate DRY findings between paired functions without counting against the suppression ratio. Parsed by `parse_inverse_marker()` in `findings.rs`, collected by `collect_inverse_lines()` in `pipeline/discovery.rs`.
- **`qual:allow(dry)` suppression for duplicate groups**: `// qual:allow(dry)` on any member of a duplicate pair now correctly suppresses the finding. Previously only single-function findings were suppressible.
- `suppressed: bool` field on `DuplicateGroup` — enables per-group suppression.
- `mark_duplicate_suppressions()` and `mark_inverse_suppressions()` in `pipeline/metrics.rs`.
- **LCOM4 self-method-call resolution**: Methods calling `self.conn()` now transitively share the field accesses of the called method. `self_method_calls` tracked per method, resolved one level deep in `build_field_method_index()`. Fixes false high LCOM4 for types using accessor methods.
- `self_method_calls: HashSet<String>` field on `MethodFieldData`.
- `build_field_method_index()` extracted as Operation in `srp/cohesion.rs`.
- `collect_per_file()` generic helper in `pipeline/discovery.rs` — eliminates near-duplicate code in `collect_suppression_lines`, `collect_api_lines`, `collect_inverse_lines`.
- 20 new unit tests across all fixed areas.

### Fixed
- **`#[cfg(test)] impl` propagation**: Methods inside `#[cfg(test)] impl Type { ... }` blocks are now correctly recognized as test code (`in_test = true`). Fixes DRY-003 false positives for test helpers in cfg-test impl blocks. Both `DeclaredFnCollector` and `FunctionCollector` (dry) and the IOSP analyzer now propagate the flag.
- **`matches!(self, ...)` SLM detection**: The SLM (Self-less Methods) check now recognizes `matches!(self, ...)` as a self-reference by inspecting macro token streams. Previously flagged as "self never referenced".
- **`qual:api` TQ-003 pipeline fix**: `compute_tq()` now calls `mark_api_declarations()` on its declared functions, so `// qual:api` correctly excludes functions from untested-function detection. Previously, TQ analysis collected fresh `DeclaredFunction` objects without API markings.
- **Function pointer references in dead code**: `&function_name` passed as an argument is now recognized as a usage by `CallTargetCollector`. `record_path_args()` unwraps `Expr::Reference` to extract the inner path.
- **Enum variant constructors**: `ChunkKind::Other(...)`, `RefKind::Call` etc. no longer counted as own calls (PascalCase heuristic).
- **Error-handling dispatch**: `match op() { Ok(r) => ..., Err(e) => ... }` patterns benefit from the type-aware resolution — std method calls in arms no longer flagged.
- All 6 report formats (text, JSON, SARIF, HTML, GitHub annotations, findings list) now filter suppressed duplicate groups.

### Changed
- **BREAKING: `external_prefixes` removed** from config. Type-aware method resolution replaces the manual prefix lists. Old `rustqual.toml` files with `external_prefixes` will error — remove the field to fix.
- **BREAKING: `UNIVERSAL_METHODS` removed** from scope. `trait_only_methods` + type-aware resolution handle all cases previously covered by the hardcoded list.
- **SRP refactoring**: `FunctionCollector` moved from `dry/mod.rs` to `dry/functions.rs`, `DeclaredFnCollector` moved to `dry/dead_code.rs`. Reduces `dry/mod.rs` production lines from 304 to ~125.
- `mark_api_declarations()` changed from private to `pub(crate)`, signature changed to `&mut [DeclaredFunction]` (was by-value).
- `classify_function()` accepts `type_context: (Option<&str>, &Signature)` for receiver type resolution.
- `BodyVisitor` gains `parent_type` and `param_types` fields for type-aware method classification.
- Test count: 836 tests (829 unit + 4 integration + 3 showcase)
- Function count: 458

## [0.3.9] - 2026-04-02

### Fixed
- **Stacked annotations**: Multiple `// qual:*` annotations before a function now all work (e.g., `// qual:api` + `// qual:allow(iosp)`). Expanded adjacency window from 1 line to 3 lines (`ANNOTATION_WINDOW` constant in `findings.rs`).
- **NMS false positive**: `self.field[index].method()` (indexed field method call) is now correctly recognized as a mutation of `&mut self`. Previously only `self.field.method()` was detected.

## [0.3.6] - 2026-03-29

### Added
- **`// qual:api` annotation**: Mark public API functions to exclude them from dead code detection (DRY-003) and untested function detection (TQ-003) without counting against the suppression ratio. API functions are meant to be called by external consumers and may be tested via integration tests outside the project.
- `is_api: bool` field on `DeclaredFunction` — tracks whether a function has a `// qual:api` marker.
- `is_api_marker()` in `findings.rs` — parses `// qual:api` comments.
- `collect_api_lines()` in `pipeline/discovery.rs` — collects API marker line numbers per file.
- `mark_api_declarations()` in `dry/dead_code.rs` — marks declared functions with API annotations.
- 7 new unit tests for API marker parsing, dead code exclusion, and suppression non-counting.
- **`--findings` CLI flag**: One-line-per-finding output with `file:line category detail in function_name`, sorted by file and line. Ideal for CI integration and quick diagnosis.
- **Summary inline locations**: When total findings ≤ 10, the summary shows `→ file:line (detail)` sub-lines under each dimension with findings, making locations visible without `--verbose`.
- **TRIVIAL findings visible**: `--verbose` now shows `⚠` warning lines for TRIVIAL functions that have findings (magic numbers, complexity, etc.) — previously these were hidden.
- `FindingEntry` struct and `collect_all_findings()` in `report/findings_list.rs` — unified finding collection reused by both `--findings` and summary locations.
- 5 new unit tests for `collect_all_findings()`.

### Changed
- `detect_dead_code()` now accepts `api_lines` parameter for API exclusion.
- `should_exclude()` checks `d.is_api` alongside `is_main`, `is_test`, etc.
- `detect_untested_functions()` (TQ-003) excludes API-marked functions.
- Test count: 821 tests (814 unit + 4 integration + 3 showcase)
- Function count: 441

## [0.3.5] - 2026-03-29

### Added
- **Test-aware IOSP analysis**: Functions with `#[test]` attribute or inside `#[cfg(test)]` modules are now automatically recognized as test code. IOSP violations in test functions are reclassified as Trivial — tests inherently mix calls and assertions (Arrange-Act-Assert pattern), which is not a design defect.
- **Test-aware error handling**: `unwrap()`, `panic!()`, `todo!()`, and `expect()` in test functions no longer produce error-handling findings. These are idiomatic Rust test patterns.
- `is_test: bool` field on `FunctionAnalysis` — tracks whether a function is test code.
- `exclude_test_violations()` pipeline function — reclassifies test violations before counting.
- `has_error_handling_issue()` extracted as standalone Operation for IOSP compliance.
- `finalize_summary()` extracted from `run_analysis()` for IOSP compliance.
- 7 new unit tests for `is_test` detection, test violation exclusion, and error handling gating.
- **Array index magic number exclusion**: Numeric literals inside array index expressions (`values[3]`, `matrix[3][4]`) are no longer flagged as magic numbers. Array indices are positional — the index IS the meaning. Uses `in_index_context` depth counter (same pattern as `in_const_context`). 3 new unit tests.

### Changed
- `has_test_attr()` and `has_cfg_test()` promoted from `pub(super)` to `pub(crate)` in `dry/mod.rs` for reuse in analyzer.
- Test count: 809 tests (802 unit + 4 integration + 3 showcase)
- Function count: 426

## [0.3.4] - 2026-03-26

### Fixed
- **TQ-003 false positive** for functions called only inside macro invocations (`assert!()`, `assert_eq!()`, `format!()`, etc.) — `CallTargetCollector` now parses macro token streams as comma-separated expressions, extracting embedded function calls for both `test_calls` and `production_calls`. Same pattern as `TestCallCollector` in `sut.rs`. This also fixes potential false positives in dead code detection (DRY-003/DRY-004) where production calls inside macros were missed.

### Changed
- Test count: 799 tests (792 unit + 4 integration + 3 showcase)

## [0.3.3] - 2026-03-26

### Added
- **DRY-005: Repeated match pattern detection** — detects identical `match` blocks (≥3 arms, ≥3 instances across ≥2 functions) by normalizing and hashing match expressions. New file `src/dry/match_patterns.rs` with `MatchPatternCollector` visitor, `detect_repeated_matches()` Integration, and `group_repeated_patterns()` Operation. Enum name is extracted from arm patterns (best effort).
- `detect_repeated_matches` field in `[duplicates]` config (default: `true`)
- DRY-005 output in all 6 report formats (text, JSON, GitHub, HTML, SARIF, dot)
- `StructuralWarningKind::code()` and `StructuralWarningKind::detail()` methods — centralizes the `(code, detail)` extraction that was previously duplicated across 5 report files

### Changed
- `print_dry_section` and `print_dry_annotations` now take `&AnalysisResult` instead of 6 separate slice parameters, matching the pattern used by `print_json` and `print_html`
- 5 report files (text/structural, json_structural, github, html/structural_table, sarif/structural_collector) refactored to use `code()`/`detail()` methods instead of duplicated match blocks
- Test count: 797 tests (790 unit + 4 integration + 3 showcase)
- Function count: 422

## [0.3.2] - 2026-03-26

### Removed
- **SSM (Scattered Match) structural check** — redundant with DRY fragment detection and Rust's exhaustive matching. SSM produced false positives in most real-world cases (7/10 not actionable) and rustqual itself required 8 enums in `ssm_exclude_enums`. The `check_ssm` and `ssm_exclude_enums` config options have been removed.

### Changed
- Structural binary checks reduced from 8 to 7 rules (BTC, SLM, NMS, OI, SIT, DEH, IET)
- Test count: 787 tests (780 unit + 4 integration + 3 showcase)
- Function count: 412

## [0.3.1] - 2026-03-26

### Fixed
- **BP-006 false positive on or-patterns** — `match` arms with `Pat::Or` (e.g. `A | B => ...`) are no longer flagged as repetitive enum mapping boilerplate. The new `is_simple_enum_pattern()` rejects or-patterns, top-level wildcards, tuple patterns, and variable bindings.
- **BP-006 false positive on dispatch with bindings** — `match` arms that bind variables (e.g. `Msg::A(x) => handle(x)`) are no longer flagged. Only unit variants (`Color::Red`) and tuple-struct variants with wildcard sub-patterns (`Action::Add(_)`) are accepted as repetitive mapping patterns.
- **BP-006 false positive on tuple scrutinees** — `match (a, b) { ... }` expressions are now skipped by the repetitive match detector, since tuple scrutinees indicate multi-variable dispatch, not enum-to-enum mapping.
- **TQ-001 false positive on custom assertion macros** — `assert_relative_eq!`, `assert_approx_eq!`, and all other `assert_*`/`debug_assert_*` macros are now recognized via prefix matching instead of exact-match against a hardcoded list. For non-assert-prefixed macros (e.g. `verify!`), use the new `extra_assertion_macros` config option.

### Added
- `extra_assertion_macros` field in `[test]` config — list of additional macro names to treat as assertions for TQ-001 detection (for macros that don't start with `assert` or `debug_assert`)

### Changed
- `is_all_path_arms()` renamed to `is_repetitive_enum_mapping()` with stricter pattern validation (guards, or-patterns, wildcards, and variable bindings now rejected)
- Test count: 790 tests (783 unit + 4 integration + 3 showcase)
- Function count: 417

## [0.3.0] - 2026-03-25

### Added

#### Structural Binary Checks (8 rules)
- **BTC (Broken Trait Contract)** — flags impl blocks that are missing required trait methods (SRP dimension)
- **SLM (Self-less Methods)** — flags methods in impl blocks that don't use `self` and could be free functions (SRP dimension)
- **NMS (Needless &mut self)** — flags methods that take `&mut self` but only read from self (SRP dimension)
- **SSM (Scattered Match)** — flags enums matched in 3+ separate locations, suggesting missing method on enum (SRP dimension) *(removed in 0.3.2)*
- **OI (Orphaned Impl)** — flags impl blocks in files that don't define the type they implement (Coupling dimension)
- **SIT (Single-Impl Trait)** — flags traits with exactly one implementation, suggesting unnecessary abstraction (Coupling dimension)
- **DEH (Downcast Escape Hatch)** — flags usage of `.downcast_ref()` / `.downcast_mut()` / `.downcast()` indicating broken abstraction (Coupling dimension)
- **IET (Inconsistent Error Types)** — flags modules returning 3+ different error types, suggesting missing unified error type (Coupling dimension)
- Integrated into existing SRP and Coupling dimensions (no new quality dimension)
- `[structural]` config section with `enabled` and per-rule `check_*` bools
- New module: `structural/` with `mod.rs`, `btc.rs`, `slm.rs`, `nms.rs`, `oi.rs`, `sit.rs`, `deh.rs`, `iet.rs`
- New pipeline module: `pipeline/structural_metrics.rs`
- New report module: `report/text/structural.rs`
- All report formats updated with structural findings

#### New Quality Dimension: Test Quality (TQ)
- **TQ-001 No Assertion** — flags `#[test]` functions with no assertion macros (`assert!`, `assert_eq!`, `assert_ne!`, `debug_assert!*`). `#[should_panic]` + `panic!` counts as assertion.
- **TQ-002 No SUT Call** — flags `#[test]` functions that don't call any production function (only external/std calls)
- **TQ-003 Untested Function** — flags production functions called from prod code but never from any test
- **TQ-004 Uncovered Function** — flags production functions with 0 execution count in LCOV coverage data (requires `--coverage`)
- **TQ-005 Untested Logic** — flags production functions with logic occurrences (if/match/for/while) at lines uncovered in LCOV data. Combines rustqual's structural analysis with coverage data. One warning per function with details of uncovered logic lines. (requires `--coverage`)

#### LCOV Coverage Integration
- **`--coverage <LCOV_FILE>`** CLI flag — ingest LCOV coverage data for TQ-004 and TQ-005 checks
- **LCOV parser** — parses `SF:`, `FNDA:`, `DA:` records; graceful handling of malformed lines

#### Configuration
- **`[test]` config section** — `enabled` (default true), `coverage_file` (optional LCOV path)
- **6-field `[weights]` section** — new `test` weight field; default weights redistributed: `[0.25, 0.20, 0.15, 0.20, 0.10, 0.10]` for [IOSP, CX, DRY, SRP, CP, TQ]
- **`Dimension::Test`** — new dimension variant, parseable as `"test"` or `"tq"`, suppressible via `// qual:allow(test)`

#### Report Formats
- All report formats updated: text, JSON, GitHub annotations, HTML dashboard (6th card), SARIF (TQ-001..005 rules), baseline (TQ fields with backward compat)

### Changed
- **Breaking**: Default quality weights redistributed from 5 to 6 dimensions. Existing configs with explicit `[weights]` sections must add `test = 0.10` and adjust other weights to sum to 1.0.
- `ComplexityMetrics` now includes `logic_occurrences: Vec<LogicOccurrence>` for TQ-005 coverage analysis
- `extract_init_metrics()` moved from `lib.rs` to `config/init.rs`
- Version bump: 0.2.0 → 0.3.0
- Test count: 774 tests (767 unit + 4 integration + 3 showcase)
- Function count: 402

### Fixed
- **SDP violations not respecting `qual:allow(coupling)` suppressions** — `SdpViolation` now has a `suppressed: bool` field. `mark_sdp_suppressions()` in pipeline/metrics.rs sets it when either the `from_module` or `to_module` has a coupling suppression. `count_sdp_violations()` filters suppressed entries. All report formats (text, JSON, GitHub, SARIF, HTML) skip suppressed SDP violations.
- **Serde `deserialize_with`/`serialize_with` functions falsely flagged as dead code** — `CallTargetCollector` now implements `visit_field()` to extract function references from `#[serde(deserialize_with = "fn")]`, `#[serde(serialize_with = "fn")]`, `#[serde(default = "fn")]`, and `#[serde(with = "module")]` attributes. The new `extract_serde_fn_refs()` static method parses serde attribute metadata and registers both bare and qualified function names as call targets.
- **Trait method calls on parameters falsely classified as own calls** — Methods that only appear in trait definitions or `impl Trait for Struct` blocks (never in inherent `impl Struct` blocks) are now tracked as "trait-only" methods. Dot-syntax calls to these methods (e.g. `provider.fetch_daily_bars()`) are recognized as polymorphic dispatch, not own calls, preventing false IOSP Violations. Conservative: if a method name appears in both trait and inherent impl contexts, it is still counted as an own call.
- **Dead code false positives on `#[cfg(test)] mod` files** — Functions in files loaded via `#[cfg(test)] mod helpers;` (external module declarations) are no longer falsely flagged as "test-only" or "uncalled" dead code. The new `collect_cfg_test_file_paths()` scans parent files for `#[cfg(test)] mod name;` declarations and computes child file paths. `mark_cfg_test_declarations()` marks functions in those files as test code, and `collect_all_calls()` initializes `in_test = true` for cfg-test files so calls from them are classified as test calls. Supports both `name.rs` and `name/mod.rs` child layouts, and non-mod parent files (`foo.rs` → `foo/name.rs`).
- **Dead code false positives on `pub use` re-exports** — Functions exclusively accessed via `pub use` re-exports (with or without `as` rename, including grouped imports) are no longer falsely reported as uncalled dead code. The `CallTargetCollector` now implements `visit_item_use()` to record re-exported names. Private `use` imports are correctly skipped (calls captured via `visit_expr_call`). Glob re-exports (`pub use foo::*`) are conservatively skipped.
- **For-loop delegation false positives** — `for x in items { call(x); }` is no longer flagged as a Violation. For-loops with delegation-only bodies (calls, `let` bindings with calls, `?` on calls, `if let` with call scrutinee) are treated equivalently to `.for_each()` in lenient mode. Complexity metrics are still tracked. Detection uses `is_delegation_only_body()` with iterative stack-based AST analysis split into `extract_delegation_exprs` + `check_delegation_stack` for IOSP self-compliance.
- **Trivial self-getter false positives** — Methods like `fn count(&self) -> usize { self.items.len() }` are now detected as trivial accessors and excluded from own-call counting. This prevents Operations that call trivial getters from being misclassified as Violations. Detection supports field access, `&self.x`, stdlib accessor chains (`.len()`, `.clone()`, `.as_ref()`, etc.), casts, and unary operators. Name collisions across impl blocks are handled conservatively (non-trivial wins).
- **Type::new() false-positive own-call** — `Type::new()`, `Type::default()`, `Type::from()` and other universal methods called with a project-defined type prefix are no longer counted as own calls. Previously, `UNIVERSAL_METHODS` filtering was only applied to `Self::method` calls but not `Type::method` calls, causing false Violations when e.g. `Adx::new(14)` appeared alongside logic.
- **Trivial .get() accessor not recognized** — Methods like `fn current(&self) -> Option<&T> { self.items.get(self.index) }` are now detected as trivial accessors. The `.get()` method with a trivial argument (literal, self field access, or reference thereof) is recognized by the new `is_trivial_method_call()` helper, which was split from `is_trivial_accessor_body()` to keep cyclomatic complexity under threshold.
- **Match-dispatch false positives** — `match x { A => call_a(), B => call_b() }` is no longer flagged as a Violation. Match expressions where every arm is delegation-only (calls, method calls, `?`, blocks with delegation statements) and has no guard are treated as pure dispatch/routing — conceptually an Integration. Analogous to the for-loop delegation fix. Complexity metrics (cognitive, cyclomatic, hotspots) are still always tracked. Arms with guards (`x if x > 0 =>`) or logic (`a + b`) correctly remain Violations.

## [0.2.0] - 2026-02-26

### Added

#### New Complexity Checks
- **CX-004 Function Length** — warns when a function body exceeds `max_function_lines` (default 60)
- **CX-005 Nesting Depth** — warns when nesting depth exceeds `max_nesting_depth` (default 4)
- **CX-006 Unsafe Detection** — flags functions containing `unsafe` blocks (`detect_unsafe`, default true)
- **A20 Error Handling** — detects `.unwrap()`, `.expect()`, `panic!`, `todo!`, `unreachable!` usage (`detect_error_handling`, default true; `allow_expect`, default false)

#### New SRP Check
- **SRP-004 Parameter Count** — AST-based parameter counting replaces text-scanning `#[allow(clippy::too_many_arguments)]` detection; configurable `max_parameters` (default 5), excludes trait impls

#### New DRY Checks
- **A11 Wildcard Imports** — flags `use foo::*` imports (excludes `prelude::*`, `super::*` in test modules); configurable `detect_wildcard_imports`
- **A10 Boilerplate** — BP-009 (struct update syntax repetition) and BP-010 (format string repetition) pattern stubs

#### New Coupling Check
- **A16 Stable Dependencies Principle (SDP)** — flags when a stable module depends on a more unstable module; configurable `check_sdp`

#### New Tool Extensions
- **A2 Effort Score** — refactoring effort score for IOSP violations: `effort = logic*1.0 + calls*1.5 + nesting*2.0`; sort violations by effort with `--sort-by-effort`
- **E5 Configurable Quality Weights** — `[weights]` section in `rustqual.toml` with per-dimension weights (must sum to 1.0); validation on load
- **E6 Diff-Based Analysis** — `--diff [REF]` flag analyzes only files changed vs a git ref (default HEAD); graceful fallback for non-git repos
- **E9 Improved Init** — `--init` now runs a quick analysis to compute tailored thresholds (current max + 20% headroom) instead of using static defaults

#### Other
- `--fail-on-warnings` CLI flag — treats warnings (e.g. suppression ratio exceeded) as errors (exit code 1), analogous to clippy's `-Dwarnings`
- `fail_on_warnings` config field in `rustqual.toml` (default: `false`)
- Result-based error handling: all quality gate functions return `Result<(), i32>` instead of calling `process::exit()`, enabling unit tests for error paths
- `lib.rs` extraction: all logic moved to `src/lib.rs` with `pub fn run() -> Result<(), i32>`, binaries are thin wrappers
- New IOSP-compliant sub-functions: `determine_output_format()`, `check_default_fail()`, `setup_config()`, `apply_exit_gates()`
- `apply_file_suppressions()` in pipeline/warnings.rs for IOSP-safe suppression application
- `run_dry_detection()` in pipeline/metrics.rs for IOSP-safe DRY orchestration

### Changed
- Binary targets use Cargo auto-discovery (`src/main.rs` → `rustqual`, `src/bin/cargo-qual/main.rs` → `cargo-qual`) instead of explicit `[[bin]]` sections pointing to the same file — eliminates "found to be present in multiple build targets" warning
- Unit tests now run once (lib target) instead of twice (per binary target)
- `compute_severity()` now public (removed `#[cfg(test)]`), replacing inlined severity logic in `build_function_analysis` with a closure call
- HTML sections, text report, GitHub annotations, SARIF, and pipeline functions refactored to stay under 60-line function length threshold

### Fixed
- `count_all_suppressions()` attribute ordering bug: `#[allow(...)]` attributes directly before `#[cfg(test)]` were incorrectly counted as production code. Now uses backward walk to exclude test module attribute groups.
- CLI about string: "six dimensions" → "five dimensions"
- `cargo fmt` applied to `examples/sample.rs`

## [0.1.0] - 2026-02-22

### Added
- Five-dimension quality analysis: IOSP, Complexity, DRY, SRP, Coupling
- Weighted quality score (0-100%) with configurable dimension weights
- 6 output formats: text, json, github, dot, sarif, html
- Inline suppression: `// qual:allow`, `// qual:allow(dim)`, legacy `// iosp:allow`
- Default-fail behavior (exit 1 on findings, `--no-fail` for local use)
- Configuration via `rustqual.toml` with auto-discovery
- Watch mode (`--watch`): re-analyze on file changes
- Baseline comparison (`--save-baseline`, `--compare`, `--fail-on-regression`)
- Shell completions for bash, zsh, fish, elvish, powershell
- Dual binary: `rustqual` (direct) and `cargo qual` (cargo subcommand)
- Refactoring suggestions (`--suggestions`) for IOSP violations
- Quality gates (`--min-quality-score`)
- Complexity analysis: cognitive/cyclomatic metrics, magic number detection
- DRY analysis: duplicate functions, duplicate fragments, dead code, boilerplate (BP-001 through BP-010)
- SRP analysis: struct-level LCOM4 cohesion, module-level line length, function cohesion clusters
- Coupling analysis: afferent/efferent coupling, instability, circular dependency detection (Kosaraju SCC)
- Self-contained HTML report with dashboard and collapsible sections
- SARIF v2.1.0 output for GitHub Code Scanning integration
- GitHub Actions annotations format
- DOT/Graphviz call-graph visualization
- CI pipeline (GitHub Actions): fmt, clippy (`-Dwarnings`), test, self-analysis
- Release pipeline: cross-compiled binaries (6 targets), crates.io publish, GitHub Release

### Changed
- Replaced `#[allow(clippy::field_reassign_with_default)]` suppressions with struct literal syntax across 8 test modules
- Replaced `Box::new(T::default())` with `Box::default()` in analyzer visitor tests
- Added `#[derive(Default)]` to `ProjectScope` for cleaner test construction
- Clippy is now documented as running with `RUSTFLAGS="-Dwarnings"` (CI-equivalent)

[0.3.0]: https://github.com/SaschaOnTour/rustqual/releases/tag/v0.3.0
[0.2.0]: https://github.com/SaschaOnTour/rustqual/releases/tag/v0.2.0
[0.1.0]: https://github.com/SaschaOnTour/rustqual/releases/tag/v0.1.0