Highlights
After a 5-week field study at hosting/muu (86 PRs scored, 18 flagged as Degraded), 67% of the Degradation comments were false positives caused by mechanical metrics (line count, public method count, single-method case/when, etc.). This release rebuilds the heuristics to reduce that rate to ~17% while keeping the genuine quality regressions in scope.
What's new
ISP — finer-grained public-method scoring
- #15 Replace the stepped
PUBLIC_METHOD_SCOREStable with a linear curve (5 → 100, 25 → 20, slope-4pt/method). Eliminates the 20pt cliffs from threshold-crossing. - #16 Detect symmetric method pairs (
enable_X/disable_X,start_X/stop_X,encrypt/decrypt, …). Each suffix-matched pair counts as one toward the public surface. - #17 Floor the score at 80 for Inspection / Diagnostic / Console / Debug / Tools classes (and matching file paths). Anchored regexes prevent mid-word false positives like
DiagnosticReport.
SRP — semantic statement counting & inspection mitigation
- #18 Replace the line-count penalty with
effective_statement_countfrom the AST (:send,:csend,:if,:case, …). Stylistic refactors such asraise X, msg→raise X.new(msg)no longer move the score. Nesteddef/defsboundaries are respected so inner-lambda statements don't leak into the parent count. - #17 Inspection-class minimum-score (shared with ISP via the new
Mitigationsmodule).
OCP — leniency for narrow conditionals
- #19 Per-method stepped
case/whenpenalty: 1 clause → 2pt, 2 → 5pt, 3+ →n*5pt. Small justified domain conditionals (payment methods, environment switches) are no longer treated as full OCP violations. Class-levelMAX_CASE_WHEN_PENALTYcap retained.
DIP — reward Rails-style memoised factories
- #20 Detect
@svc ||= ServiceClass.new(...)(and.create/.build/.call/.open) memoised factory bodies during parsing.DipAnalyzeradds up to 15pt bonus (5pt per qualifying method, complexity 1, non-stdlib receiver).Queue.new/Net::HTTP.newetc. are explicitly excluded via the standard-library whitelist used elsewhere.
Diff classification
- #21 New
DiffClassifier#classify(before, after)returnsdelta_total,delta_structural(OCP+LSP+DIP) anddelta_mechanical(SRP+ISP).ScoreResult#subscorescarries a per-principle breakdown (SRP exposeswmc_penalty,effective_statement_penalty, and named mitigations).JsonFormattersurfaces the breakdown so external review tooling can build structural/mechanical reports.
Configuration extension (parsed but not yet wired)
- #22 New
.solid-score.ymlknobs:inspection_classes,symmetric_method_pairs.custom,diff_thresholds.{ignore_below_delta, flag_only_structural_below}, andscoring.{isp_public_method_curve, case_when_2way_lenient}. Defaults preserve current behaviour. Malformed YAML values are tolerated (defaults stick), string scoring values are normalised case-insensitively, and a one-time warning is emitted when a key is parsed but not yet wired into the analyzers.
Notable behavioural changes
These are the internal API shifts intentional in this minor bump:
MethodInfoaddseffective_statement_count,memoized_factory_receiver,memoized_factory?.ClassInfoaddsinspection_class?. The unusedClassInfo#line_countwas removed.SrpAnalyzerexposesanalyze_with_breakdown(the legacy#analyzedelegates to it).- New shared
Analyzers::Mitigationsmodule providingmitigate_inspection.
Testing
- 161 → 223 examples, 0 failures
- Line coverage: 95.6% → 96.3%
Migration
No user-facing CLI flag changed. Projects that already use .solid-score.yml continue to work unchanged. Embedders depending on MethodInfo/ClassInfo/SrpAnalyzer should review the API additions above.
Closed issues
🤖 Generated with Claude Code