This file captures project-specific conventions Claude should follow when working in this repo.
-
bin/validate-patternsvalidates every detection pattern YAML file underrails-upgrade/detection-scripts/patterns/. Run it before committing any change to a pattern file. Pure-stdlib Ruby, no Bundler or Gemfile required.bin/validate-patternsvalidates every filebin/validate-patterns path/to/file.ymlvalidates one or more specific filesbin/validate-patterns --self-testruns built-in fixture assertions covering the four positivekind:values and the four rejection paths (missing top-level key, missing required pattern key, broken regex, unknownkind:value). CI runs this alongside the file-validation step- Checks: YAML parses, required top-level keys present, the eight required pattern keys present on each entry, every
pattern/excluderegex compiles, and thekind:value is one of the allowed enum values (breaking,deprecation,migration,optional) - Exits 0 on success, 1 on any failure with a per-file error report
-
bin/test-patternsruns fixture tests for a pattern file against its sibling*.expectations.yml(e.g.rails-40-patterns.yml+rails-40-patterns.expectations.yml). It confirms a pattern's regex actually matches what its explanation claims and skips what it shouldn't —validate-patternsonly confirms the regex compiles, not that it's correct. Pure-stdlib Ruby, same shape asbin/validate-patterns.bin/test-patternstests every pattern file that has an expectations sibling; files without one are reportedSKIP, not a failurebin/test-patterns path/to/file.ymltests one or more specific filesbin/test-patterns --self-testruns built-in fixture assertions. CI runs this alongside the fixture-test step- Expectations are keyed by
variable_nameand listmatch(lines the pattern MUST flag) andno_match(lines it MUST NOT flag) — see the worked example inrails-40-patterns.expectations.yml - Exits 0 on success, 1 on any failure with a per-pattern error report
- Do NOT include "Difficulty" or "Estimated Time" in the header. These are subjective, application-dependent, and drift out of date. Keep the header minimal: title, Ruby requirement, and the attribution line.
- Base content on primary sources: the official Rails upgrade guide, the FastRuby.io blog, the OmbuLabs ebook chapter, and RailsDiff for the matching versions.
- Organize breaking changes under 🔴 HIGH / 🟡 MEDIUM / 🟢 LOW priority sections.
- Each breaking change entry should include: "What Changed", a detection pattern, and a BEFORE/AFTER fix.
- Use
NextRails.next?(neverrespond_to?orGem::Versioncomparisons) in dual-boot code examples.
- File naming:
rails-{VERSION}-patterns.ymlwhere{VERSION}is the major+minor without a dot (e.g.,rails-42-patterns.ymlfor Rails 4.2). - Organize patterns under
high_priority,medium_priority, andlow_priority. - Each pattern needs:
name,kind(one ofbreaking/deprecation/migration/optional— see "Assigning kind" below),pattern(regex),exclude(regex, empty string if none),search_paths,explanation,fix,variable_name. Placekind:immediately aftername:for visual scannability. - Include a
dependenciessection for any bridge/compatibility gems mentioned in the guide. - Required before committing any change to a detection pattern file: run
bin/validate-patterns(orbin/validate-patterns path/to/file.ymlfor the file you touched). Do not commit a pattern change without a clean run; broken YAML or schema drift in this directory breaks the skill at runtime. See the## Repository toolingsection above for what the script checks. - New pattern? Add a fixture expectation. Every new
variable_nameneeds amatch/no_matchentry in the sibling*.expectations.ymlfile (create the file if the version doesn't have one yet), then runbin/test-patterns path/to/file.ymlclean before committing. Exception: a pattern whosepatternis""(pure path-based detection, e.g.VENDOR_PLUGINS) can never flag a line and should stay uncovered — note why in a comment instead of forcing a fixture.
Priority applies to both version guide sections and detection pattern entries. Assign based on blast radius and reversibility, not on how much code typically needs to change.
A change belongs in HIGH when at least one of these is true:
- Prevents boot, bundle, or test-suite startup (e.g., a gem removed from Rails core that the app still requires; a renamed config key that raises on load).
- Causes runtime errors in typical code paths (e.g., a removed method that most apps call —
update_attributes,deliver,find_all_by_*). - Required to upgrade at all (Ruby version bump, mandatory base-class change like
ApplicationRecord, removed DSL options that raise). - Silently wrong behavior with production impact — data loss, security regression, broken auth/CSRF, or cache key mismatches that invalidate stored data.
If the user cannot complete the upgrade without addressing it, it is HIGH.
A change belongs in MEDIUM when:
- Affects many apps but not all (gem extractions like
responders, test-helper changes likeassigns/assert_template). - Behavioral change in a commonly-used API that usually works fine but has known edge-case breakage (HTML sanitizer output, serialized attribute nil handling, per-request CSRF tokens).
- Config rename or relocation that doesn't raise but should be updated for forward compatibility.
If the upgrade completes without it but a noticeable class of apps will see problems, it is MEDIUM.
A change belongs in LOW when:
- Opt-in or optional improvement (Timecop →
travel_to, Foreigner → native FKs, adopting new Gemfile defaults). - Environment-specific config tweak (
rails serverbind host, dev-only settings). - Cosmetic/tooling changes (schema.rb column ordering, .gitignore recommendations, new
bin/setupscript).
If most apps will ignore it without consequence, it is LOW.
- Would a typical Rails app's test suite fail to run after the version bump without this fix? → HIGH.
- Would the app boot and tests run, but produce a future blocker for a noticeable class of apps? → MEDIUM.
- Would the app be unaffected unless the user opts into a new feature or runs in a specific environment? → LOW.
Priority is about urgency during an upgrade, not editorial weight.
kind: describes what the change is; priority describes how urgent it is. The two are orthogonal. A HIGH deprecation (silently wrong, like DIRTY_TRACKING_AFTER_SAVE) and a HIGH breaking (won't boot) are both "fix first" but for different reasons.
Judge kind at the target hop, not the API's historical timeline. Each rails-XY-patterns.yml file is a statement about that hop — what changes when the user upgrades INTO that version. A removal that was first deprecated in an earlier Rails minor is breaking in the file for the version where it actually raises, not deprecation because of its history. The same API can legitimately be deprecation in rails-31-patterns.yml and breaking in rails-40-patterns.yml. Apply the rule to all four kinds: kind reflects what the change is at this hop, not what it was earlier or will become later.
Concrete example: SCOPE_WITHOUT_LAMBDA was deprecated in Rails 3.1 and raises in 4.0 — it is breaking in rails-40-patterns.yml.
The four values:
breaking— Raises, removed, or prevents the app from booting / bundling / running its test suite. The user cannot complete the upgrade without addressing it. Example:update_attributesremoved in 6.1,redirect_to :backremoved in 5.1.deprecation— Works at this hop but emits a deprecation warning. Removal is scheduled for a later Rails version. Example: dynamic:controllerroute segments in 5.2, stringif:conditions on callbacks.migration— Works today, no warning, but a recommended migration target. Adopting it now avoids rework on the next hop or an entirely new approach. Example:Rails.application.secrets→credentials.yml.encin 5.2.optional— Opt-in feature or improvement. The user can ignore it without consequence. Example:bootsnap,webpackerin 5.1,propshaftadoption ahead of 8.0.
How to decide:
- Will the upgrade fail (boot, bundle, tests) without this fix? →
breaking. - Does Rails emit a deprecation warning when this code runs at the target version? →
deprecation. - Is this a recommended path forward (e.g.
secrets.yml→credentials.yml.enc) that does not yet warn? →migration. - Is this purely opt-in / cosmetic / a new feature? →
optional.
If kind and priority seem to conflict, trust both. They answer different questions.
The top-level dependencies: block in each rails-*-patterns.yml file is not bound to a single kind value. It serves two distinct purposes:
-
Bridge / compatibility gems for
breakingpatterns — gems that rescue functionality removed from Rails core, so the user can keep shipping while migrating call sites. Abreakingpattern with a corresponding bridge entry is a softenable break: install the gem to keep the upgrade landing while migration happens separately. Examples:protected_attributesrescuesattr_accessible/attr_protected(4.0)activerecord-deprecated_findersrescues removed dynamic finders (4.0)rails-observersrescuesActiveRecord::ObserverandActionController::Caching::Sweeper(4.0)respondersrescuesrespond_withand class-levelrespond_to(4.2)rails-controller-testingrescuesassigns/assert_template(5.0)
-
New gems Rails introduces or recommends at this version — gems that are not in the previous version's Gemfile. These pair with
optionalpatterns (the user can ignore them) or with no pattern at all. Examples:bootsnap(5.2),web-console(4.2),webpacker(5.1)propshaft(8.0),solid_cache/solid_queue/solid_cable(8.0)kamal(8.0),bundler-audit(8.1)
The check: true / check: false flag on each dependencies: entry is editorial advice about whether the gem applies broadly, not a per-app determination. The actual applicability depends on whether the user's app triggers the breaking pattern the bridge rescues:
check: true— the gem rescues abreakingthat most apps will trigger (e.g.,respondersrescuesrespond_with, which most controller-heavy apps use). Default to recommending it.check: false— either a bridge for abreakingthat not all apps will trigger (e.g.,actionpack-action_cachingonly matters if the app usescaches_page/caches_action), or a new-default gem the user can adopt at their own pace (e.g.,bootsnap,solid_cache). The user's actual code drives whether they need it.
A check: false bridge gem still becomes effectively required for any specific app that triggers its rescued breaking pattern. Treat the flag as a starting recommendation; the per-app fix: field on each pattern entry is what tells the user whether they actually hit it.
kind: deprecation, migration, and optional patterns are resolved in code via the per-pattern fix: field, not via dependencies:.
bin/validate-patterns does not enforce the rules in this section — they are editorial guidance for authors and reviewers. The validator only checks schema (required keys, regex compilation, kind: enum membership), not the semantic relationship between dependencies: entries and kind: values.