Fix complexity blind spots, exact churn, a meaningful hot flag, and 2× faster runs - #10
Merged
Merged
Conversation
… and blocks Only defs sitting directly in a class or module body were scored, so a method written as `private def`, inside `class << self`, inside `class_methods do`, behind an `if`, or in a `Data.define do` block was invisible to complexity — and to the ratchet, where turning `def` into `private def` made a regression disappear. The walk now descends through a type's whole body, stopping at nested classes and modules (which score their own methods). `class << self` and `class_methods do` name methods with `.`; a constant assigned a `Data.define`/`Struct.new` block owns the methods inside it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CdcWkAxkqwRCup8VkCPcFA
…ctory Churn matched a file by bare suffix against the whole repository's history, taking the maximum: `base.rb` in actionview was charged the 1218 commits of activerecord's `base.rb` (its own count is 352), a model took its spec's churn, and `base.rb` even matched `database.rb`. Only the first analyzed directory's repository was read. GitLog now asks git for the top level once, logs only the analyzed directories (literal pathspecs), and keys every path relative to the directory it sits in — the same display path the analyzers use — so a lookup is exact and constant-time instead of a scan over every path in history. A directory outside the repository no longer voids the others. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CdcWkAxkqwRCup8VkCPcFA
`Churn#hot?` counted sites sitting in any file with at least one commit, so in a git repository every clone was "hot": 2265 of 2265 across fifteen open-source codebases, same-file pairs included. The warning carried no information and lengthened every duplication finding. A clone is now hot when at least two distinct files it spans change more often than the typical analyzed file — above the median commit count of the history hashira read. A history where every file changed once has no hot clone, and a clone inside one file never is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CdcWkAxkqwRCup8VkCPcFA
Every sliding window re-walked the subtree of each statement it holds — up to twelve windows per statement, again at every nesting level, and once more for a method taken whole. A per-file Walks cache, shared by the file's sequences and whole-node fragments, walks each root once. Output is byte-identical; duplication alone on rubocop drops from about 24s to 15s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CdcWkAxkqwRCup8VkCPcFA
Lineage#assigned walked the whole ancestry and swept every ancestor's defs for each type context — and a namespace every file reopens (every rubocop cop opens `RuboCop::Cop`) is hundreds of contexts sharing one name, each redoing the same walk. The answer depends only on the name, so it is now remembered per name. The existing sweep memo shares the same `remember` helper. Output is byte-identical; smells alone on rubocop drop from about 14s to 7s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CdcWkAxkqwRCup8VkCPcFA
Restricting `git log` to the analyzed directories switched on git's default history simplification, which skips a side branch's commits when the merge leaves the path as the main line had it — fat_free_crm's contact.rb read 68 commits instead of 72. `--full-history` counts them as the unrestricted log always did; rails pays under a second for it. The empty-top-level guard in GitLog#roots changed nothing: outside a repository `git log` fails and reads as no history either way. Also records the field-test fixes in the changelog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CdcWkAxkqwRCup8VkCPcFA
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
These fixes come out of running hashira on 15 MIT-licensed Rails apps and gems: rails, rubocop, chatwoot, feedbin, rubygems.org, huginn, errbit, fat_free_crm, postal, devise, activeadmin, good_job, sinatra, faker and hanami.
Fixes
Complexity only scored methods that sit directly in a class body (
dc0b6ce)private def/protected def, defined insideclass << selforclass_methods do, behind anif, or in aConst = Data.define do … endblock were never scored.defintoprivate defmade a complexity regression disappear from--ratchet.Complexity::Bodywalk now covers the whole type body. It stops at nested classes and modules, so no method is counted twice. Singleton contexts name methodsClass.method.Faker::Internet.username(18).--ratchetwill report these newly visible methods as NEW findings. This is noted in the CHANGELOG.Churn matched files by bare suffix across the whole repo (
08d83cf,7dc824a)action_view/base.rbwas charged activerecord's 1218 commits; its own count is 352. A model picked up its spec's churn, andbase.rbeven matcheddatabase.rb. Only the first target's repo was read.GitLognow does onerev-parse --show-topleveland onegit log --full-history -- <targets>with literal pathspecs. It keys each path relative to its own target, sohitsis an exact hash lookup.--full-historyis needed because a path-limited log otherwise simplifies away side-branch commits."Both sites change often" was on 2265 of 2265 clones (
c2d79dd)Performance (byte-identical output)
16d5ebd): each statement's subtree is walked once per file through a sharedWalkscache. Before, it was walked again for every window of up to 12 statements that held it.cbbd6ad):Lineage#assignedis memoized per class name. A namespace every file reopens, such as rubocop'sRuboCop::Cop, used to redo the same ancestry walk hundreds of times.Across all 15 repos, every finding other than complexity and duplication is unchanged, and duplication clusters are identical apart from the
hotflag.Checks
bin/cipasses: rubocop, rspec at 100% line and branch coverage, the self-gate, and the self-ratchet.kimera ci --since origin/main: 71 mutants on the changed lines, 0 survivors.🤖 Generated with Claude Code
https://claude.ai/code/session_01CdcWkAxkqwRCup8VkCPcFA
Generated by Claude Code