# 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