From 75e71ab9fc382a6e50adf75b5a3881bbb41fa719 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 15 Feb 2026 12:14:04 -0500 Subject: [PATCH 1/9] WIP: punctuation agnostic --- app/models/concerns/punctuation_strippable.rb | 23 ++++ app/models/workshop.rb | 11 +- app/services/workshop_search_service.rb | 36 ++++-- spec/services/workshop_search_service_spec.rb | 121 ++++++++++++++++++ 4 files changed, 179 insertions(+), 12 deletions(-) create mode 100644 app/models/concerns/punctuation_strippable.rb diff --git a/app/models/concerns/punctuation_strippable.rb b/app/models/concerns/punctuation_strippable.rb new file mode 100644 index 000000000..6dff99756 --- /dev/null +++ b/app/models/concerns/punctuation_strippable.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module PunctuationStrippable + extend ActiveSupport::Concern + + module ClassMethods + # Returns a SQL fragment that strips punctuation from a field + # Removes: hyphens, ampersands, periods, em/en dashes, and various quote types + def strip_punctuation_sql(field_name) + # 11 nested REPLACE calls for 11 punctuation characters + "REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(" \ + "#{field_name}, " \ + "'-', ''), '&', ''), '.', ''), '—', ''), '–', ''), " \ + "'\"', ''), \"'\", ''), ''', ''), ''', ''), '"', ''), '"', '')" + end + + # Strips punctuation from a string using Ruby + # Removes the same characters as strip_punctuation_sql + def strip_punctuation(text) + text.to_s.gsub(/[-&.—–'"''""]/, "") + end + end +end diff --git a/app/models/workshop.rb b/app/models/workshop.rb index 106eb5975..45a9a6fdd 100644 --- a/app/models/workshop.rb +++ b/app/models/workshop.rb @@ -1,5 +1,6 @@ class Workshop < ApplicationRecord include Featureable, Publishable, TagFilterable, Trendable, WindowsTypeFilterable, RichTextSearchable + include PunctuationStrippable include Rails.application.routes.url_helpers include ActionText::Attachable include ActiveModel::Dirty @@ -139,7 +140,14 @@ def self.mentionable_rich_text_fields # See Featureable, Publishable, TagFilterable, Trendable, WindowsTypeFilterable, RichTextSearchable scope :created_by_id, ->(created_by_id) { where(user_id: created_by_id) } scope :legacy, -> { where(legacy: true) } - scope :title, ->(title) { where("workshops.title like ?", "%#{ title }%") } + scope :title, ->(title) { + # Strip punctuation from input: hyphens, ampersands, periods, em/en dashes, quotes + sanitized_input = strip_punctuation(title) + sanitized_input = ActiveRecord::Base.sanitize_sql_like(sanitized_input) + + # Strip same punctuation from database field + where("#{strip_punctuation_sql('workshops.title')} LIKE ?", "%#{sanitized_input}%") + } scope :order_by_date, ->(sort_order = "asc") do order(Arel.sql(<<~SQL.squish)) COALESCE( @@ -151,7 +159,6 @@ def self.mentionable_rich_text_fields ) #{sort_order == "asc" ? "ASC" : "DESC"} SQL end - scope :title, ->(title) { where("workshops.title like ?", "%#{ title }%") } scope :windows_type_ids, ->(windows_type_ids) { where(windows_type_id: windows_type_ids) } scope :with_bookmarks_count, -> do left_joins(:bookmarks) diff --git a/app/services/workshop_search_service.rb b/app/services/workshop_search_service.rb index 86371c369..abdb5de3d 100644 --- a/app/services/workshop_search_service.rb +++ b/app/services/workshop_search_service.rb @@ -1,5 +1,6 @@ class WorkshopSearchService include ActionPolicy::Behaviour + include PunctuationStrippable authorize :user attr_reader :params, :user, :admin @@ -133,16 +134,31 @@ def filter_by_title def filter_by_query return unless params[:query].present? - results = @workshops.search(params[:query]) # Use the SearchCop search scope directly on the relation - - # If SearchCop returned an Array (e.g., because of scoring), convert back to Relation - if results.is_a?(Array) - ordered_ids = results.map(&:id) - @workshops = Workshop.where(id: ordered_ids) - .order(Arel.sql("FIELD(id, #{ordered_ids.join(',')})")) - else - @workshops = results - end + # Strip punctuation from query: hyphens, ampersands, periods, em/en dashes, quotes + sanitized_query = self.class.strip_punctuation(params[:query]).strip + sanitized_query = ActiveRecord::Base.sanitize_sql_like(sanitized_query) + return if sanitized_query.blank? + + # Build a custom SQL query that strips same punctuation from all searchable fields + searchable_fields = [ + :title, :full_name, + :objective, :materials, :setup, :introduction, + :demonstration, :opening_circle, :warm_up, + :creation, :closing, :notes, :tips, :misc1, :misc2, + :objective_spanish, :materials_spanish, :setup_spanish, :introduction_spanish, + :demonstration_spanish, :opening_circle_spanish, :warm_up_spanish, + :creation_spanish, :closing_spanish, :notes_spanish, :tips_spanish, :misc1_spanish, :misc2_spanish + ] + + # Create WHERE conditions that strip punctuation from both field and search term + conditions = searchable_fields.map do |field| + "#{self.class.strip_punctuation_sql("workshops.#{field}")} LIKE ?" + end.join(" OR ") + + # Use the same sanitized query for all fields + query_params = Array.new(searchable_fields.length, "%#{sanitized_query}%") + + @workshops = @workshops.where(conditions, *query_params) end # --- Search methods --- diff --git a/spec/services/workshop_search_service_spec.rb b/spec/services/workshop_search_service_spec.rb index 471503612..cdffc4ad1 100644 --- a/spec/services/workshop_search_service_spec.rb +++ b/spec/services/workshop_search_service_spec.rb @@ -130,5 +130,126 @@ expect(service.sort).to eq('created') end end + + context "punctuation-ignoring search" do + let!(:workshop_with_hyphen) do + create(:workshop, title: "Hello - Goodbye", year: 2025, month: 3) + end + let!(:workshop_without_hyphen) do + create(:workshop, title: "Hello Goodbye", year: 2025, month: 4) + end + let!(:workshop_with_ampersand) do + create(:workshop, title: "Arts & Crafts", year: 2025, month: 5) + end + let!(:workshop_with_period) do + create(:workshop, title: "Dr. Workshop", year: 2025, month: 6) + end + let!(:workshop_with_em_dash) do + create(:workshop, title: "Hello—Goodbye", year: 2025, month: 7) + end + let!(:workshop_with_en_dash) do + create(:workshop, title: "Hello–Goodbye", year: 2025, month: 8) + end + let!(:workshop_with_quotes) do + create(:workshop, title: "The 'Best' Workshop", year: 2025, month: 9) + end + + context "title search" do + it "finds workshops with hyphens when searching without hyphens" do + service = WorkshopSearchService.new({ title: 'Hello Goodbye' }).call + workshops = service.workshops + + expect(workshops).to include(workshop_with_hyphen) + expect(workshops).to include(workshop_without_hyphen) + end + + it "finds workshops without hyphens when searching with hyphens" do + service = WorkshopSearchService.new({ title: 'Hello - Goodbye' }).call + workshops = service.workshops + + expect(workshops).to include(workshop_with_hyphen) + expect(workshops).to include(workshop_without_hyphen) + end + + it "finds workshops with multiple hyphens when searching with different hyphen patterns" do + workshop_multi_hyphen = create(:workshop, title: "Hello -- Goodbye", year: 2025, month: 10) + + service = WorkshopSearchService.new({ title: 'Hello Goodbye' }).call + workshops = service.workshops + + expect(workshops).to include(workshop_multi_hyphen) + end + + it "finds workshops with ampersands when searching without ampersands" do + service = WorkshopSearchService.new({ title: 'Arts Crafts' }).call + workshops = service.workshops + + expect(workshops).to include(workshop_with_ampersand) + end + + it "finds workshops with periods when searching without periods" do + service = WorkshopSearchService.new({ title: 'Dr Workshop' }).call + workshops = service.workshops + + expect(workshops).to include(workshop_with_period) + end + + it "finds workshops with em dashes when searching without them" do + service = WorkshopSearchService.new({ title: 'HelloGoodbye' }).call + workshops = service.workshops + + expect(workshops).to include(workshop_with_em_dash) + end + + it "finds workshops with en dashes when searching without them" do + service = WorkshopSearchService.new({ title: 'HelloGoodbye' }).call + workshops = service.workshops + + expect(workshops).to include(workshop_with_en_dash) + end + + it "finds workshops with quotes when searching without quotes" do + service = WorkshopSearchService.new({ title: 'The Best Workshop' }).call + workshops = service.workshops + + expect(workshops).to include(workshop_with_quotes) + end + end + + context "query search" do + let!(:workshop_with_hyphen_content) do + create(:workshop, title: "Test Workshop", objective: "Learn about self-care", year: 2025, month: 11) + end + let!(:workshop_without_hyphen_content) do + create(:workshop, title: "Another Workshop", objective: "Learn about selfcare", year: 2025, month: 12) + end + let!(:workshop_with_ampersand_content) do + create(:workshop, title: "Third Workshop", objective: "Arts & crafts therapy", year: 2026, month: 1) + end + + it "finds workshops with hyphens in content when searching without hyphens" do + service = WorkshopSearchService.new({ query: 'selfcare' }).call + workshops = service.workshops + + expect(workshops).to include(workshop_with_hyphen_content) + expect(workshops).to include(workshop_without_hyphen_content) + end + + it "finds workshops without hyphens in content when searching with hyphens" do + service = WorkshopSearchService.new({ query: 'self-care' }).call + workshops = service.workshops + + expect(workshops).to include(workshop_with_hyphen_content) + expect(workshops).to include(workshop_without_hyphen_content) + end + + it "finds workshops with ampersands in content when searching without ampersands" do + service = WorkshopSearchService.new({ query: 'Arts crafts' }).call + workshops = service.workshops + + expect(workshops).to include(workshop_with_ampersand_content) + end + end + end end end From 64cbab037f468ed797019359cb1f4cc2013e7bf8 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 15 Feb 2026 12:14:13 -0500 Subject: [PATCH 2/9] update claude --- .claude/settings.local.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ea632f510..c00ff48b6 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -18,7 +18,9 @@ "Bash(git -C /Users/maebeale/programming/awbw branch --show-current)", "Bash(bundle exec rails routes:*)", "Bash(bundle exec rubocop:*)", - "Bash(git -C /Users/maebeale/programming/awbw diff --name-only main...HEAD)" + "Bash(git -C /Users/maebeale/programming/awbw diff --name-only main...HEAD)", + "Bash(git apply:*)", + "Bash(git checkout:*)" ] } } From 344cc72276f6e353df94c376435dbb4bed29822d Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 15 Feb 2026 12:38:50 -0500 Subject: [PATCH 3/9] Strip puncutation from searches and add index to bookmarks to improve speeds --- app/models/concerns/punctuation_strippable.rb | 41 +++++++++++--- app/services/workshop_search_service.rb | 2 +- ...2713_add_polymorphic_index_to_bookmarks.rb | 6 ++ db/schema.rb | 5 +- spec/services/workshop_search_service_spec.rb | 56 +++++++++++++++++++ 5 files changed, 101 insertions(+), 9 deletions(-) create mode 100644 db/migrate/20260215122713_add_polymorphic_index_to_bookmarks.rb diff --git a/app/models/concerns/punctuation_strippable.rb b/app/models/concerns/punctuation_strippable.rb index 6dff99756..ef79bf470 100644 --- a/app/models/concerns/punctuation_strippable.rb +++ b/app/models/concerns/punctuation_strippable.rb @@ -5,19 +5,46 @@ module PunctuationStrippable module ClassMethods # Returns a SQL fragment that strips punctuation from a field - # Removes: hyphens, ampersands, periods, em/en dashes, and various quote types + # Removes: hyphens, ampersands, periods, em/en dashes, quotes, slashes, + # colons, plus signs, exclamation/question marks, commas, parentheses, ellipsis def strip_punctuation_sql(field_name) - # 11 nested REPLACE calls for 11 punctuation characters - "REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(" \ - "#{field_name}, " \ - "'-', ''), '&', ''), '.', ''), '—', ''), '–', ''), " \ - "'\"', ''), \"'\", ''), ''', ''), ''', ''), '"', ''), '"', '')" + # Each pair is [SQL character expression, replacement] + # Use hex literals for Unicode and characters that conflict with SQL/AR syntax + chars = [ + "'-'", # hyphen + "'&'", # ampersand + "'.'", # period + "0xE28094", # em dash — + "0xE28093", # en dash – + "'\"'", # double quote " + "\"'\"", # single quote ' + "0xE28098", # left single curly quote ' + "0xE28099", # right single curly quote ' + "0xE2809C", # left double curly quote " + "0xE2809D", # right double curly quote " + "'/'", # slash + "':'", # colon + "'+'", # plus + "'!'", # exclamation + "CHAR(63)", # question mark ? + "','", # comma + "'('", # open paren + "')'", # close paren + "0xE280A6" # ellipsis … + ] + + result = field_name + chars.each { |c| result = "REPLACE(#{result}, #{c}, '')" } + + # Collapse multiple spaces into one (3 passes handles up to 8 consecutive spaces) + 3.times { result = "REPLACE(#{result}, ' ', ' ')" } + result end # Strips punctuation from a string using Ruby # Removes the same characters as strip_punctuation_sql def strip_punctuation(text) - text.to_s.gsub(/[-&.—–'"''""]/, "") + text.to_s.gsub(/[-&.—–'"''""\/:+!?,()…]/, "").gsub(/\s+/, " ") end end end diff --git a/app/services/workshop_search_service.rb b/app/services/workshop_search_service.rb index abdb5de3d..2126edc46 100644 --- a/app/services/workshop_search_service.rb +++ b/app/services/workshop_search_service.rb @@ -128,7 +128,7 @@ def filter_by_tag_names def filter_by_title return unless params[:title].present? - @workshops = @workshops.search("title:#{params[:title]}") + @workshops = @workshops.title(params[:title]) end def filter_by_query diff --git a/db/migrate/20260215122713_add_polymorphic_index_to_bookmarks.rb b/db/migrate/20260215122713_add_polymorphic_index_to_bookmarks.rb new file mode 100644 index 000000000..5d0ce0aae --- /dev/null +++ b/db/migrate/20260215122713_add_polymorphic_index_to_bookmarks.rb @@ -0,0 +1,6 @@ +class AddPolymorphicIndexToBookmarks < ActiveRecord::Migration[7.2] + def change + add_index :bookmarks, [ :bookmarkable_type, :bookmarkable_id ], + name: "index_bookmarks_on_bookmarkable" + end +end diff --git a/db/schema.rb b/db/schema.rb index 68b9fc3c4..ed01949ff 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_02_14_230901) do +ActiveRecord::Schema[8.1].define(version: 2026_02_15_122713) do create_table "action_text_mentions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.bigint "action_text_rich_text_id", null: false t.datetime "created_at", null: false @@ -216,6 +216,7 @@ t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.integer "user_id" + t.index ["bookmarkable_type", "bookmarkable_id"], name: "index_bookmarks_on_bookmarkable" t.index ["user_id"], name: "index_bookmarks_on_user_id" end @@ -245,9 +246,11 @@ create_table "category_types", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.datetime "created_at", precision: nil, null: false + t.string "display_text" t.string "legacy_id" t.string "name" t.boolean "published", default: false + t.boolean "story_specific", default: false t.datetime "updated_at", precision: nil, null: false end diff --git a/spec/services/workshop_search_service_spec.rb b/spec/services/workshop_search_service_spec.rb index cdffc4ad1..4c95f397f 100644 --- a/spec/services/workshop_search_service_spec.rb +++ b/spec/services/workshop_search_service_spec.rb @@ -214,6 +214,62 @@ expect(workshops).to include(workshop_with_quotes) end + + it "finds workshops with slashes when searching without slashes" do + workshop = create(:workshop, title: "Art/Music", year: 2025, month: 10) + + service = WorkshopSearchService.new({ title: 'Art Music' }).call + expect(service.workshops).to include(workshop) + end + + it "finds workshops with colons when searching without colons" do + workshop = create(:workshop, title: "Introduction: The Basics", year: 2025, month: 10) + + service = WorkshopSearchService.new({ title: 'Introduction The Basics' }).call + expect(service.workshops).to include(workshop) + end + + it "finds workshops with plus signs when searching without them" do + workshop = create(:workshop, title: "Arts + Crafts", year: 2025, month: 10) + + service = WorkshopSearchService.new({ title: 'Arts Crafts' }).call + expect(service.workshops).to include(workshop) + end + + it "finds workshops with exclamation marks when searching without them" do + workshop = create(:workshop, title: "Create!", year: 2025, month: 10) + + service = WorkshopSearchService.new({ title: 'Create' }).call + expect(service.workshops).to include(workshop) + end + + it "finds workshops with question marks when searching without them" do + workshop = create(:workshop, title: "What is Dance?", year: 2025, month: 10) + + service = WorkshopSearchService.new({ title: 'What is Dance' }).call + expect(service.workshops).to include(workshop) + end + + it "finds workshops with commas when searching without them" do + workshop = create(:workshop, title: "Draw, Paint, Create", year: 2025, month: 10) + + service = WorkshopSearchService.new({ title: 'Draw Paint Create' }).call + expect(service.workshops).to include(workshop) + end + + it "finds workshops with parentheses when searching without them" do + workshop = create(:workshop, title: "Weaving (Advanced)", year: 2025, month: 10) + + service = WorkshopSearchService.new({ title: 'Weaving Advanced' }).call + expect(service.workshops).to include(workshop) + end + + it "finds workshops with ellipsis when searching without it" do + workshop = create(:workshop, title: "Creating…Discovering", year: 2025, month: 10) + + service = WorkshopSearchService.new({ title: 'CreatingDiscovering' }).call + expect(service.workshops).to include(workshop) + end end context "query search" do From 2bf14250813da4c5e641e494b4ae9eaea907ed73 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 15 Feb 2026 13:07:20 -0500 Subject: [PATCH 4/9] Set up claude to autorun rubocop --- .claude/hooks/rubocop-autofix.sh | 8 ++++++++ .claude/settings.json | 15 +++++++++++++++ .claude/settings.local.json | 4 +++- 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100755 .claude/hooks/rubocop-autofix.sh create mode 100644 .claude/settings.json diff --git a/.claude/hooks/rubocop-autofix.sh b/.claude/hooks/rubocop-autofix.sh new file mode 100755 index 000000000..7e725cf46 --- /dev/null +++ b/.claude/hooks/rubocop-autofix.sh @@ -0,0 +1,8 @@ +#!/bin/bash +INPUT=$(cat) +FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') +if [[ ! "$FILE_PATH" =~ \.rb$ ]]; then + exit 0 +fi +bundle exec rubocop -A --format quiet "$FILE_PATH" 2>/dev/null +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..c63c6d37f --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/rubocop-autofix.sh" + } + ] + } + ] + } +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c00ff48b6..f3d053e7a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -20,7 +20,9 @@ "Bash(bundle exec rubocop:*)", "Bash(git -C /Users/maebeale/programming/awbw diff --name-only main...HEAD)", "Bash(git apply:*)", - "Bash(git checkout:*)" + "Bash(git checkout:*)", + "Bash(mysql -u root:*)", + "Bash(chmod:*)" ] } } From 771d9a3109f1d8b0a0596ff28738254b82c54212 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 15 Feb 2026 13:08:01 -0500 Subject: [PATCH 5/9] Update workshop searching to be punctuation agnostic --- app/models/concerns/punctuation_strippable.rb | 80 ++++++------ app/models/workshop.rb | 12 +- app/services/workshop_search_service.rb | 43 +++---- spec/services/workshop_search_service_spec.rb | 119 ++++++++++-------- 4 files changed, 140 insertions(+), 114 deletions(-) diff --git a/app/models/concerns/punctuation_strippable.rb b/app/models/concerns/punctuation_strippable.rb index ef79bf470..ce6899198 100644 --- a/app/models/concerns/punctuation_strippable.rb +++ b/app/models/concerns/punctuation_strippable.rb @@ -3,48 +3,56 @@ module PunctuationStrippable extend ActiveSupport::Concern - module ClassMethods - # Returns a SQL fragment that strips punctuation from a field - # Removes: hyphens, ampersands, periods, em/en dashes, quotes, slashes, - # colons, plus signs, exclamation/question marks, commas, parentheses, ellipsis - def strip_punctuation_sql(field_name) - # Each pair is [SQL character expression, replacement] - # Use hex literals for Unicode and characters that conflict with SQL/AR syntax - chars = [ - "'-'", # hyphen - "'&'", # ampersand - "'.'", # period - "0xE28094", # em dash — - "0xE28093", # en dash – - "'\"'", # double quote " - "\"'\"", # single quote ' - "0xE28098", # left single curly quote ' - "0xE28099", # right single curly quote ' - "0xE2809C", # left double curly quote " - "0xE2809D", # right double curly quote " - "'/'", # slash - "':'", # colon - "'+'", # plus - "'!'", # exclamation - "CHAR(63)", # question mark ? - "','", # comma - "'('", # open paren - "')'", # close paren - "0xE280A6" # ellipsis … - ] + PUNCTUATION_CHARS_SQL = [ + "'-'", # hyphen + "'&'", # ampersand + "'.'", # period + "0xE28094", # em dash — + "0xE28093", # en dash – + "'\"'", # double quote " + "\"'\"", # single quote ' + "0xE28098", # left single curly quote ' + "0xE28099", # right single curly quote ' + "0xE2809C", # left double curly quote " + "0xE2809D", # right double curly quote " + "'/'", # slash + "':'", # colon + "'+'", # plus + "'!'", # exclamation + "CHAR(63)", # question mark ? + "','", # comma + "'('", # open paren + "')'", # close paren + "0xE280A6" # ellipsis … + ].freeze - result = field_name - chars.each { |c| result = "REPLACE(#{result}, #{c}, '')" } + PUNCTUATION_REGEX = /[-&.—–'"''""\/:+!?,()…]/ - # Collapse multiple spaces into one (3 passes handles up to 8 consecutive spaces) + module ClassMethods + # "spaced" — punctuation → space, collapse multiple spaces. + # Matches "self care" to "self-care". + def strip_punctuation_sql_spaced(field_name) + result = field_name + PUNCTUATION_CHARS_SQL.each { |c| result = "REPLACE(#{result}, #{c}, ' ')" } 3.times { result = "REPLACE(#{result}, ' ', ' ')" } result end - # Strips punctuation from a string using Ruby - # Removes the same characters as strip_punctuation_sql - def strip_punctuation(text) - text.to_s.gsub(/[-&.—–'"''""\/:+!?,()…]/, "").gsub(/\s+/, " ") + # "spaceless" — punctuation AND spaces → removed entirely. + # Matches "selfcare" to "self-care" and to "self care". + def strip_punctuation_sql_spaceless(field_name) + result = field_name + PUNCTUATION_CHARS_SQL.each { |c| result = "REPLACE(#{result}, #{c}, '')" } + result = "REPLACE(#{result}, ' ', '')" + result + end + + def strip_punctuation_spaced(text) + text.to_s.gsub(PUNCTUATION_REGEX, " ").gsub(/\s+/, " ") + end + + def strip_punctuation_spaceless(text) + text.to_s.gsub(PUNCTUATION_REGEX, "").gsub(/\s+/, "") end end end diff --git a/app/models/workshop.rb b/app/models/workshop.rb index 45a9a6fdd..87d153003 100644 --- a/app/models/workshop.rb +++ b/app/models/workshop.rb @@ -141,12 +141,14 @@ def self.mentionable_rich_text_fields scope :created_by_id, ->(created_by_id) { where(user_id: created_by_id) } scope :legacy, -> { where(legacy: true) } scope :title, ->(title) { - # Strip punctuation from input: hyphens, ampersands, periods, em/en dashes, quotes - sanitized_input = strip_punctuation(title) - sanitized_input = ActiveRecord::Base.sanitize_sql_like(sanitized_input) + spaced = ActiveRecord::Base.sanitize_sql_like(strip_punctuation_spaced(title)) + spaceless = ActiveRecord::Base.sanitize_sql_like(strip_punctuation_spaceless(title)) - # Strip same punctuation from database field - where("#{strip_punctuation_sql('workshops.title')} LIKE ?", "%#{sanitized_input}%") + where( + "(#{strip_punctuation_sql_spaced('workshops.title')} LIKE :spaced)" \ + " OR (#{strip_punctuation_sql_spaceless('workshops.title')} LIKE :spaceless)", + spaced: "%#{spaced}%", spaceless: "%#{spaceless}%" + ) } scope :order_by_date, ->(sort_order = "asc") do order(Arel.sql(<<~SQL.squish)) diff --git a/app/services/workshop_search_service.rb b/app/services/workshop_search_service.rb index 2126edc46..eddf762e8 100644 --- a/app/services/workshop_search_service.rb +++ b/app/services/workshop_search_service.rb @@ -134,31 +134,28 @@ def filter_by_title def filter_by_query return unless params[:query].present? - # Strip punctuation from query: hyphens, ampersands, periods, em/en dashes, quotes - sanitized_query = self.class.strip_punctuation(params[:query]).strip - sanitized_query = ActiveRecord::Base.sanitize_sql_like(sanitized_query) - return if sanitized_query.blank? - - # Build a custom SQL query that strips same punctuation from all searchable fields - searchable_fields = [ - :title, :full_name, - :objective, :materials, :setup, :introduction, - :demonstration, :opening_circle, :warm_up, - :creation, :closing, :notes, :tips, :misc1, :misc2, - :objective_spanish, :materials_spanish, :setup_spanish, :introduction_spanish, - :demonstration_spanish, :opening_circle_spanish, :warm_up_spanish, - :creation_spanish, :closing_spanish, :notes_spanish, :tips_spanish, :misc1_spanish, :misc2_spanish - ] - - # Create WHERE conditions that strip punctuation from both field and search term - conditions = searchable_fields.map do |field| - "#{self.class.strip_punctuation_sql("workshops.#{field}")} LIKE ?" + spaced = ActiveRecord::Base.sanitize_sql_like( + self.class.strip_punctuation_spaced(params[:query]).strip + ) + spaceless = ActiveRecord::Base.sanitize_sql_like( + self.class.strip_punctuation_spaceless(params[:query]).strip + ) + return if spaced.blank? + + # Match against spaced variant (punctuation → space) and spaceless variant (punctuation + spaces removed) + fields = %w[workshops.title workshops.full_name action_text_rich_texts.plain_text_body] + conditions = fields.flat_map do |field| + [ + "#{self.class.strip_punctuation_sql_spaced(field)} LIKE :spaced", + "#{self.class.strip_punctuation_sql_spaceless(field)} LIKE :spaceless" + ] end.join(" OR ") - # Use the same sanitized query for all fields - query_params = Array.new(searchable_fields.length, "%#{sanitized_query}%") - - @workshops = @workshops.where(conditions, *query_params) + @workshops = @workshops + .joins("LEFT JOIN action_text_rich_texts ON action_text_rich_texts.record_id = workshops.id " \ + "AND action_text_rich_texts.record_type = 'Workshop'") + .where(conditions, spaced: "%#{spaced}%", spaceless: "%#{spaceless}%") + .distinct end # --- Search methods --- diff --git a/spec/services/workshop_search_service_spec.rb b/spec/services/workshop_search_service_spec.rb index 4c95f397f..a0195610e 100644 --- a/spec/services/workshop_search_service_spec.rb +++ b/spec/services/workshop_search_service_spec.rb @@ -133,30 +133,30 @@ context "punctuation-ignoring search" do let!(:workshop_with_hyphen) do - create(:workshop, title: "Hello - Goodbye", year: 2025, month: 3) + create(:workshop, :published, title: "Hello - Goodbye", year: 2025, month: 3) end let!(:workshop_without_hyphen) do - create(:workshop, title: "Hello Goodbye", year: 2025, month: 4) + create(:workshop, :published, title: "Hello Goodbye", year: 2025, month: 4) end let!(:workshop_with_ampersand) do - create(:workshop, title: "Arts & Crafts", year: 2025, month: 5) + create(:workshop, :published, title: "Arts & Crafts", year: 2025, month: 5) end let!(:workshop_with_period) do - create(:workshop, title: "Dr. Workshop", year: 2025, month: 6) + create(:workshop, :published, title: "Dr. Workshop", year: 2025, month: 6) end let!(:workshop_with_em_dash) do - create(:workshop, title: "Hello—Goodbye", year: 2025, month: 7) + create(:workshop, :published, title: "Hello—Goodbye", year: 2025, month: 7) end let!(:workshop_with_en_dash) do - create(:workshop, title: "Hello–Goodbye", year: 2025, month: 8) + create(:workshop, :published, title: "Hello–Goodbye", year: 2025, month: 8) end let!(:workshop_with_quotes) do - create(:workshop, title: "The 'Best' Workshop", year: 2025, month: 9) + create(:workshop, :published, title: "The 'Best' Workshop", year: 2025, month: 9) end context "title search" do it "finds workshops with hyphens when searching without hyphens" do - service = WorkshopSearchService.new({ title: 'Hello Goodbye' }).call + service = WorkshopSearchService.new({ title: 'Hello Goodbye' }, user: user).call workshops = service.workshops expect(workshops).to include(workshop_with_hyphen) @@ -164,7 +164,7 @@ end it "finds workshops without hyphens when searching with hyphens" do - service = WorkshopSearchService.new({ title: 'Hello - Goodbye' }).call + service = WorkshopSearchService.new({ title: 'Hello - Goodbye' }, user: user).call workshops = service.workshops expect(workshops).to include(workshop_with_hyphen) @@ -172,135 +172,154 @@ end it "finds workshops with multiple hyphens when searching with different hyphen patterns" do - workshop_multi_hyphen = create(:workshop, title: "Hello -- Goodbye", year: 2025, month: 10) + workshop_multi_hyphen = create(:workshop, :published, title: "Hello -- Goodbye", year: 2025, month: 10) - service = WorkshopSearchService.new({ title: 'Hello Goodbye' }).call + service = WorkshopSearchService.new({ title: 'Hello Goodbye' }, user: user).call workshops = service.workshops expect(workshops).to include(workshop_multi_hyphen) end it "finds workshops with ampersands when searching without ampersands" do - service = WorkshopSearchService.new({ title: 'Arts Crafts' }).call + service = WorkshopSearchService.new({ title: 'Arts Crafts' }, user: user).call workshops = service.workshops expect(workshops).to include(workshop_with_ampersand) end it "finds workshops with periods when searching without periods" do - service = WorkshopSearchService.new({ title: 'Dr Workshop' }).call + service = WorkshopSearchService.new({ title: 'Dr Workshop' }, user: user).call workshops = service.workshops expect(workshops).to include(workshop_with_period) end it "finds workshops with em dashes when searching without them" do - service = WorkshopSearchService.new({ title: 'HelloGoodbye' }).call + service = WorkshopSearchService.new({ title: 'HelloGoodbye' }, user: user).call workshops = service.workshops expect(workshops).to include(workshop_with_em_dash) end it "finds workshops with en dashes when searching without them" do - service = WorkshopSearchService.new({ title: 'HelloGoodbye' }).call + service = WorkshopSearchService.new({ title: 'HelloGoodbye' }, user: user).call workshops = service.workshops expect(workshops).to include(workshop_with_en_dash) end it "finds workshops with quotes when searching without quotes" do - service = WorkshopSearchService.new({ title: 'The Best Workshop' }).call + service = WorkshopSearchService.new({ title: 'The Best Workshop' }, user: user).call workshops = service.workshops expect(workshops).to include(workshop_with_quotes) end it "finds workshops with slashes when searching without slashes" do - workshop = create(:workshop, title: "Art/Music", year: 2025, month: 10) + workshop = create(:workshop, :published, title: "Art/Music", year: 2025, month: 10) - service = WorkshopSearchService.new({ title: 'Art Music' }).call + service = WorkshopSearchService.new({ title: 'Art Music' }, user: user).call expect(service.workshops).to include(workshop) end it "finds workshops with colons when searching without colons" do - workshop = create(:workshop, title: "Introduction: The Basics", year: 2025, month: 10) + workshop = create(:workshop, :published, title: "Introduction: The Basics", year: 2025, month: 10) - service = WorkshopSearchService.new({ title: 'Introduction The Basics' }).call + service = WorkshopSearchService.new({ title: 'Introduction The Basics' }, user: user).call expect(service.workshops).to include(workshop) end it "finds workshops with plus signs when searching without them" do - workshop = create(:workshop, title: "Arts + Crafts", year: 2025, month: 10) + workshop = create(:workshop, :published, title: "Arts + Crafts", year: 2025, month: 10) - service = WorkshopSearchService.new({ title: 'Arts Crafts' }).call + service = WorkshopSearchService.new({ title: 'Arts Crafts' }, user: user).call expect(service.workshops).to include(workshop) end it "finds workshops with exclamation marks when searching without them" do - workshop = create(:workshop, title: "Create!", year: 2025, month: 10) + workshop = create(:workshop, :published, title: "Create!", year: 2025, month: 10) - service = WorkshopSearchService.new({ title: 'Create' }).call + service = WorkshopSearchService.new({ title: 'Create' }, user: user).call expect(service.workshops).to include(workshop) end it "finds workshops with question marks when searching without them" do - workshop = create(:workshop, title: "What is Dance?", year: 2025, month: 10) + workshop = create(:workshop, :published, title: "What is Dance?", year: 2025, month: 10) - service = WorkshopSearchService.new({ title: 'What is Dance' }).call + service = WorkshopSearchService.new({ title: 'What is Dance' }, user: user).call expect(service.workshops).to include(workshop) end it "finds workshops with commas when searching without them" do - workshop = create(:workshop, title: "Draw, Paint, Create", year: 2025, month: 10) + workshop = create(:workshop, :published, title: "Draw, Paint, Create", year: 2025, month: 10) - service = WorkshopSearchService.new({ title: 'Draw Paint Create' }).call + service = WorkshopSearchService.new({ title: 'Draw Paint Create' }, user: user).call expect(service.workshops).to include(workshop) end it "finds workshops with parentheses when searching without them" do - workshop = create(:workshop, title: "Weaving (Advanced)", year: 2025, month: 10) + workshop = create(:workshop, :published, title: "Weaving (Advanced)", year: 2025, month: 10) - service = WorkshopSearchService.new({ title: 'Weaving Advanced' }).call + service = WorkshopSearchService.new({ title: 'Weaving Advanced' }, user: user).call expect(service.workshops).to include(workshop) end it "finds workshops with ellipsis when searching without it" do - workshop = create(:workshop, title: "Creating…Discovering", year: 2025, month: 10) + workshop = create(:workshop, :published, title: "Creating…Discovering", year: 2025, month: 10) - service = WorkshopSearchService.new({ title: 'CreatingDiscovering' }).call + service = WorkshopSearchService.new({ title: 'CreatingDiscovering' }, user: user).call expect(service.workshops).to include(workshop) end + + it "finds all variants of hyphenated/spaced/joined words" do + hyphenated = create(:workshop, :published, title: "Self-Care Workshop", year: 2025, month: 10) + spaced = create(:workshop, :published, title: "Self Care Workshop", year: 2025, month: 11) + joined = create(:workshop, :published, title: "Selfcare Workshop", year: 2025, month: 12) + + # Searching with spaces finds all three + service = WorkshopSearchService.new({ title: 'Self Care' }, user: user).call + expect(service.workshops).to include(hyphenated, spaced, joined) + + # Searching with hyphen finds all three + service = WorkshopSearchService.new({ title: 'Self-Care' }, user: user).call + expect(service.workshops).to include(hyphenated, spaced, joined) + + # Searching joined finds all three + service = WorkshopSearchService.new({ title: 'SelfCare' }, user: user).call + expect(service.workshops).to include(hyphenated, spaced, joined) + end end context "query search" do let!(:workshop_with_hyphen_content) do - create(:workshop, title: "Test Workshop", objective: "Learn about self-care", year: 2025, month: 11) + create(:workshop, :published, title: "Test Workshop", rhino_objective: "Learn about self-care", year: 2025, month: 11) end let!(:workshop_without_hyphen_content) do - create(:workshop, title: "Another Workshop", objective: "Learn about selfcare", year: 2025, month: 12) + create(:workshop, :published, title: "Another Workshop", rhino_objective: "Learn about selfcare", year: 2025, month: 12) end let!(:workshop_with_ampersand_content) do - create(:workshop, title: "Third Workshop", objective: "Arts & crafts therapy", year: 2026, month: 1) + create(:workshop, :published, title: "Third Workshop", rhino_objective: "Arts & crafts therapy", year: 2026, month: 1) end - it "finds workshops with hyphens in content when searching without hyphens" do - service = WorkshopSearchService.new({ query: 'selfcare' }).call - workshops = service.workshops - - expect(workshops).to include(workshop_with_hyphen_content) - expect(workshops).to include(workshop_without_hyphen_content) - end - - it "finds workshops without hyphens in content when searching with hyphens" do - service = WorkshopSearchService.new({ query: 'self-care' }).call - workshops = service.workshops - - expect(workshops).to include(workshop_with_hyphen_content) - expect(workshops).to include(workshop_without_hyphen_content) + it "finds all variants of hyphenated/spaced/joined content" do + # "selfcare" finds both hyphenated and joined + service = WorkshopSearchService.new({ query: 'selfcare' }, user: user).call + expect(service.workshops).to include(workshop_with_hyphen_content) + expect(service.workshops).to include(workshop_without_hyphen_content) + + # "self-care" finds both + service = WorkshopSearchService.new({ query: 'self-care' }, user: user).call + expect(service.workshops).to include(workshop_with_hyphen_content) + expect(service.workshops).to include(workshop_without_hyphen_content) + + # "self care" finds both + service = WorkshopSearchService.new({ query: 'self care' }, user: user).call + expect(service.workshops).to include(workshop_with_hyphen_content) + expect(service.workshops).to include(workshop_without_hyphen_content) end it "finds workshops with ampersands in content when searching without ampersands" do - service = WorkshopSearchService.new({ query: 'Arts crafts' }).call + service = WorkshopSearchService.new({ query: 'Arts crafts' }, user: user).call workshops = service.workshops expect(workshops).to include(workshop_with_ampersand_content) From 9ec7e3ff591b619878eee9b9f7c61ee26ff196ba Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 15 Feb 2026 13:17:29 -0500 Subject: [PATCH 6/9] Add extra searching to find more variations using the word and --- app/models/concerns/punctuation_strippable.rb | 22 +++++++++++++--- spec/services/workshop_search_service_spec.rb | 26 +++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/app/models/concerns/punctuation_strippable.rb b/app/models/concerns/punctuation_strippable.rb index ce6899198..4c48b8322 100644 --- a/app/models/concerns/punctuation_strippable.rb +++ b/app/models/concerns/punctuation_strippable.rb @@ -32,7 +32,7 @@ module ClassMethods # "spaced" — punctuation → space, collapse multiple spaces. # Matches "self care" to "self-care". def strip_punctuation_sql_spaced(field_name) - result = field_name + result = normalize_synonyms_sql(field_name) PUNCTUATION_CHARS_SQL.each { |c| result = "REPLACE(#{result}, #{c}, ' ')" } 3.times { result = "REPLACE(#{result}, ' ', ' ')" } result @@ -41,18 +41,32 @@ def strip_punctuation_sql_spaced(field_name) # "spaceless" — punctuation AND spaces → removed entirely. # Matches "selfcare" to "self-care" and to "self care". def strip_punctuation_sql_spaceless(field_name) - result = field_name + result = normalize_synonyms_sql(field_name) PUNCTUATION_CHARS_SQL.each { |c| result = "REPLACE(#{result}, #{c}, '')" } result = "REPLACE(#{result}, ' ', '')" result end def strip_punctuation_spaced(text) - text.to_s.gsub(PUNCTUATION_REGEX, " ").gsub(/\s+/, " ") + normalize_synonyms(text).gsub(PUNCTUATION_REGEX, " ").gsub(/\s+/, " ") end def strip_punctuation_spaceless(text) - text.to_s.gsub(PUNCTUATION_REGEX, "").gsub(/\s+/, "") + normalize_synonyms(text).gsub(PUNCTUATION_REGEX, "").gsub(/\s+/, "") + end + + private + + # Normalize synonyms so both sides strip the same way. + # " and " → " & " (then & gets stripped as punctuation) + # "w/" → "with" (then / gets stripped, leaving "with" on both sides) + def normalize_synonyms_sql(field_name) + result = "REPLACE(#{field_name}, ' and ', ' & ')" + "REPLACE(#{result}, 'w/', 'with')" + end + + def normalize_synonyms(text) + text.to_s.gsub(/ and /i, " & ").gsub(/w\//i, "with") end end end diff --git a/spec/services/workshop_search_service_spec.rb b/spec/services/workshop_search_service_spec.rb index a0195610e..80a9afade 100644 --- a/spec/services/workshop_search_service_spec.rb +++ b/spec/services/workshop_search_service_spec.rb @@ -187,6 +187,32 @@ expect(workshops).to include(workshop_with_ampersand) end + it "finds workshops with ampersands when searching with 'and'" do + service = WorkshopSearchService.new({ title: 'Arts and Crafts' }, user: user).call + expect(service.workshops).to include(workshop_with_ampersand) + end + + it "finds workshops with 'and' when searching with ampersand" do + workshop = create(:workshop, :published, title: "Arts and Crafts", year: 2025, month: 10) + + service = WorkshopSearchService.new({ title: 'Arts & Crafts' }, user: user).call + expect(service.workshops).to include(workshop) + end + + it "finds workshops with 'w/' when searching with 'with'" do + workshop = create(:workshop, :published, title: "Painting w/ Kids", year: 2025, month: 10) + + service = WorkshopSearchService.new({ title: 'Painting with Kids' }, user: user).call + expect(service.workshops).to include(workshop) + end + + it "finds workshops with 'with' when searching with 'w/'" do + workshop = create(:workshop, :published, title: "Painting with Kids", year: 2025, month: 10) + + service = WorkshopSearchService.new({ title: 'Painting w/ Kids' }, user: user).call + expect(service.workshops).to include(workshop) + end + it "finds workshops with periods when searching without periods" do service = WorkshopSearchService.new({ title: 'Dr Workshop' }, user: user).call workshops = service.workshops From 11e5981c9297475baccbbbf63bffeb78d5879e5f Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 15 Feb 2026 13:27:20 -0500 Subject: [PATCH 7/9] Correctly look for person name if user when doing author search --- app/services/workshop_search_service.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/services/workshop_search_service.rb b/app/services/workshop_search_service.rb index eddf762e8..be4a12f3b 100644 --- a/app/services/workshop_search_service.rb +++ b/app/services/workshop_search_service.rb @@ -164,13 +164,17 @@ def search_by_author_name(workshops, author_name) return workshops if author_name.blank? sanitized = author_name.strip.gsub(/\s+/, "") - workshops.left_outer_joins(:user) + workshops.left_outer_joins(user: :person) .where( "LOWER(REPLACE(workshops.full_name, ' ', '')) LIKE :name OR LOWER(REPLACE(CONCAT(users.first_name, users.last_name), ' ', '')) LIKE :name OR LOWER(REPLACE(CONCAT(users.last_name, users.first_name), ' ', '')) LIKE :name OR LOWER(REPLACE(users.first_name, ' ', '')) LIKE :name - OR LOWER(REPLACE(users.last_name, ' ', '')) LIKE :name", + OR LOWER(REPLACE(users.last_name, ' ', '')) LIKE :name + OR LOWER(REPLACE(CONCAT(people.first_name, people.last_name), ' ', '')) LIKE :name + OR LOWER(REPLACE(CONCAT(people.last_name, people.first_name), ' ', '')) LIKE :name + OR LOWER(REPLACE(people.first_name, ' ', '')) LIKE :name + OR LOWER(REPLACE(people.last_name, ' ', '')) LIKE :name", name: "%#{sanitized}%" ) end From d35fdff77514f04838b2e654310a099288f746c5 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 15 Feb 2026 13:40:33 -0500 Subject: [PATCH 8/9] Avoid brakeman error --- app/models/workshop.rb | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/app/models/workshop.rb b/app/models/workshop.rb index 87d153003..5efe579b3 100644 --- a/app/models/workshop.rb +++ b/app/models/workshop.rb @@ -141,14 +141,13 @@ def self.mentionable_rich_text_fields scope :created_by_id, ->(created_by_id) { where(user_id: created_by_id) } scope :legacy, -> { where(legacy: true) } scope :title, ->(title) { - spaced = ActiveRecord::Base.sanitize_sql_like(strip_punctuation_spaced(title)) - spaceless = ActiveRecord::Base.sanitize_sql_like(strip_punctuation_spaceless(title)) + spaced = "%#{ActiveRecord::Base.sanitize_sql_like(strip_punctuation_spaced(title))}%" + spaceless = "%#{ActiveRecord::Base.sanitize_sql_like(strip_punctuation_spaceless(title))}%" - where( - "(#{strip_punctuation_sql_spaced('workshops.title')} LIKE :spaced)" \ - " OR (#{strip_punctuation_sql_spaceless('workshops.title')} LIKE :spaceless)", - spaced: "%#{spaced}%", spaceless: "%#{spaceless}%" - ) + spaced_expr = Arel::Nodes::SqlLiteral.new(strip_punctuation_sql_spaced("workshops.title")) + spaceless_expr = Arel::Nodes::SqlLiteral.new(strip_punctuation_sql_spaceless("workshops.title")) + + where(spaced_expr.matches(spaced).or(spaceless_expr.matches(spaceless))) } scope :order_by_date, ->(sort_order = "asc") do order(Arel.sql(<<~SQL.squish)) From f836a7946a7768bce4089c3b83ce298f47e00f8b Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 15 Feb 2026 13:41:08 -0500 Subject: [PATCH 9/9] Claude can check brakeman --- .claude/hooks/brakeman-check.sh | 14 ++++++++++++++ .claude/settings.json | 4 ++++ .claude/settings.local.json | 3 ++- 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100755 .claude/hooks/brakeman-check.sh diff --git a/.claude/hooks/brakeman-check.sh b/.claude/hooks/brakeman-check.sh new file mode 100755 index 000000000..16ed99ecd --- /dev/null +++ b/.claude/hooks/brakeman-check.sh @@ -0,0 +1,14 @@ +#!/bin/bash +INPUT=$(cat) +FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') +if [[ ! "$FILE_PATH" =~ \.rb$|\.erb$ ]]; then + exit 0 +fi +# Get path relative to project root +REL_PATH="${FILE_PATH#$CLAUDE_PROJECT_DIR/}" +OUTPUT=$(bundle exec brakeman --only-files "$REL_PATH" --no-pager --format text --quiet --no-summary 2>/dev/null) +if [ -n "$OUTPUT" ] && ! echo "$OUTPUT" | grep -q "No warnings found"; then + echo "Brakeman warnings in $REL_PATH:" >&2 + echo "$OUTPUT" >&2 +fi +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json index c63c6d37f..bdc470d83 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -7,6 +7,10 @@ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/rubocop-autofix.sh" + }, + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/brakeman-check.sh" } ] } diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f3d053e7a..91208c351 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -22,7 +22,8 @@ "Bash(git apply:*)", "Bash(git checkout:*)", "Bash(mysql -u root:*)", - "Bash(chmod:*)" + "Bash(chmod:*)", + "Bash(bundle exec brakeman:*)" ] } }