Skip to content

feat(matchable): add whitelisted partial matching - #21

Merged
jplot merged 3 commits into
mainfrom
feat/matchable
Jul 3, 2026
Merged

feat(matchable): add whitelisted partial matching#21
jplot merged 3 commits into
mainfrom
feat/matchable

Conversation

@jplot

@jplot jplot commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

The last idea from the gem-landscape review, with every precaution it flagged: matchable :reference exposes partial matching as three distinct keys under filters[<attribute>], so the sargable prefix variant is distinguishable from the full-scan ones — never a catch-all like.

Changes

  • starts_with (LIKE 'term%', index-friendly), ends_with (LIKE '%term'), contains (LIKE '%term%'); ninth declaration DSL with aliasing, inheritance, association targets, applied_filters and validator support all inherited from the existing machinery (Target.narrow, accepted pattern, KINDS)
  • security: the term goes through sanitize_sql_like — a user %/_ matches literally (spec-locked with '%', '0%', 'A_B'), and the wildcard placement is fixed by the declarative key: no filterameter-style partial: :dynamic
  • arel matches, case insensitive by default (ILIKE on PostgreSQL, spec-proven via the PostgreSQL visitor); matchable :code, case_sensitive: true opts a declaration into sensitivity — the option is peeled from the declaration hash so it coexists with aliases without the Ruby kwargs-capture pitfall (case_sensitive is a reserved word, documented); end-to-end sensitive spec via SQLite's PRAGMA case_sensitive_like
  • the request may override either way with the reserved nested key filters[<attribute>][case_sensitive] — strict true or bare key switches on, explicit false switches off (even on a declared-sensitive attribute), anything else falls back to the declaration; the boolean-param vocabulary (TRUE_VALUES/FALSE_VALUES) now lives in one home, Togglable
  • blank/non-string terms and non-hash values narrow nothing, per the never-raise contract

How to Test

  1. bundle exec rake
  2. MovementDetail.filterable(filters: { reference: { starts_with: 'INV' } })LIKE 'INV%' (échappé) ; { contains: '%' } → seules les lignes contenant un % littéral ; { starts_with: 'INV', case_sensitive: true }LIKE au lieu d'ILIKE sur PostgreSQL ; case_sensitive: 'false' sur un attribut déclaré sensible → ILIKE

jplot added 2 commits July 3, 2026 18:15
- `matchable :reference` exposes starts_with/ends_with/contains under
  `filters[<attribute>]` — three distinct keys so the sargable prefix
  variant is distinguishable from the full-scan ones, never a catch-all
  like
- the term is LIKE-escaped through sanitize_sql_like (user % and _
  match literally) and the wildcard placement is fixed by the key: the
  caller never controls the pattern
- arel matches, case insensitive by default (ILIKE on PostgreSQL);
  `matchable :code, case_sensitive: true` opts a declaration into
  sensitivity — the option is peeled from the declaration hash, so it
  coexists with aliases without the kwargs capture pitfall;
  case_sensitive is a reserved word, documented
- association targets, aliasing, inheritance, applied_filters and the
  declarations validator all come from the existing machinery
Copilot AI review requested due to automatic review settings July 3, 2026 16:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@fluence-ci

fluence-ci Bot commented Jul 3, 2026

Copy link
Copy Markdown

🛡️ Documentation Quality: 100.0% (0.0%)

████████████████████ 100.0% — 1570/1570 checks passing

Base PR Δ
Coverage 100.0% 100.0% ±0%
Failing 0 0 ±0

Documentation quality above threshold.

Changed files

File Failing Δ vs base
🟢 lib/filterable.rb 0 ±0
🟢 lib/filterable/attachable.rb 0 ±0
🟢 lib/filterable/concerns/matchable.rb 0 ±0
🟢 lib/filterable/declarations_validator.rb 0 ±0
🟢 lib/filterable/matchable.rb 0 ±0
🟢 lib/filterable/matchable/contains.rb 0 ±0
🟢 lib/filterable/matchable/ends_with.rb 0 ±0
🟢 lib/filterable/matchable/starts_with.rb 0 ±0
🟢 lib/filterable/railtie.rb 0 ±0
🟢 lib/filterable/togglable.rb 0 ±0

@fluence-ci

fluence-ci Bot commented Jul 3, 2026

Copy link
Copy Markdown

📊 Code Quality Report

Tool Base PR Δ
Reek 78 85 smells 🔴 +7
Flog total 887.4 1028.9 🔴 +141.5
Flay total 572 705 🔴 +133

🔍 7 new reek smells introduced — review below.

🔍 7 new reek smells introduced by this PR
Smell File Context Lines Message
LongParameterList lib/filterable/matchable.rb Filterable::Matchable#narrow 98 has 4 parameters
ManualDispatch lib/filterable/concerns/matchable.rb Filterable::Concerns::Matchable#matchable_attribute_names 29 manually dispatches method call
ManualDispatch lib/filterable/concerns/matchable.rb Filterable::Concerns::Matchable#matchable_case_sensitive 43 manually dispatches method call
ManualDispatch lib/filterable/matchable.rb Filterable::Matchable#bounds 29 manually dispatches method call
NilCheck lib/filterable/matchable.rb Filterable::Matchable#sensitive? 64 performs a nil-check
TooManyStatements lib/filterable/concerns/matchable.rb Filterable::Concerns::Matchable#split_matchable_options 73 has approx 7 statements
UtilityFunction lib/filterable/concerns/matchable.rb Filterable::Concerns::Matchable#split_matchable_options 73 doesn't depend on instance state (maybe move it to another class?)
🔍 All 85 reek smells on this PR
Smell File Context Lines Message
ControlParameter lib/filterable/attachable.rb Filterable::Attachable#apply 135 is controlled by argument 'key'
ControlParameter lib/filterable/attachable.rb Filterable::Attachable#presence 153 is controlled by argument 'value'
DataClump lib/filterable/attachable.rb Filterable::Attachable 205, 220, 235 takes parameters ['attachment', 'scope'] to 3 methods
DataClump lib/filterable/attachable.rb Filterable::Attachable 134, 151, 167, 178, 191 takes parameters ['attachment', 'sub_scope', 'value'] to 5 methods
DataClump lib/filterable/attachable.rb Filterable::Attachable 31, 44, 59 takes parameters ['params', 'scope'] to 3 methods
DataClump lib/filterable/declarations_validator.rb Filterable::DeclarationsValidator 79, 93, 113 takes parameters ['kind', 'public_name'] to 3 methods
DataClump lib/filterable/equatable.rb Filterable::Equatable 18, 31, 43, 57 takes parameters ['params', 'scope'] to 4 methods
DataClump lib/filterable/scopable.rb Filterable::Scopable 17, 30, 44 takes parameters ['params', 'scope'] to 3 methods
DataClump lib/filterable/sortable.rb Filterable::Sortable 19, 33, 44 takes parameters ['params', 'scope'] to 3 methods
DataClump lib/filterable/target.rb Filterable::Target 81, 102, 116 takes parameters ['model', 'path'] to 3 methods
DataClump lib/filterable/target.rb Filterable::Target 29, 47, 157 takes parameters ['sub_scope', 'target'] to 3 methods
DataClump lib/filterable/togglable.rb Filterable::Togglable 29, 42, 56 takes parameters ['params', 'scope'] to 3 methods
DuplicateMethodCall lib/filterable/attachable.rb Filterable::Attachable#accepted 64, 66 calls 'declared[name]' 2 times
DuplicateMethodCall lib/filterable/attachable.rb Filterable::Attachable#presence 153, 155 calls 'sub_scope.where' 2 times
DuplicateMethodCall lib/filterable/concern.rb Filterable::Concern#effective_filters 178, 178 calls 'params[:filters]' 2 times
DuplicateMethodCall lib/filterable/declarations_validator.rb Filterable::DeclarationsValidator#path_error 96, 102 calls '@model.name' 2 times
DuplicateMethodCall lib/filterable/sortable.rb Filterable::Sortable#terms 45, 47 calls 'params[:sort]' 2 times
DuplicateMethodCall lib/filterable/target.rb Filterable::Target#apply 174, 174 calls 'resolution[:condition]' 2 times
DuplicateMethodCall lib/filterable/togglable.rb Filterable::Togglable#toggled_on? 79, 81 calls 'TRUE_VALUES.include?(value)' 2 times
FeatureEnvy lib/filterable/concern.rb Filterable::Concern#apply_filter 133, 135 refers to 'filter' more than self (maybe move it to another class?)
FeatureEnvy lib/filterable/concern.rb Filterable::Concern#effective_filters 177, 177, 178, 178 refers to 'params' more than self (maybe move it to another class?)
FeatureEnvy lib/filterable/concern.rb Filterable::Concern#evaluated_defaults 189, 189 refers to 'value' more than self (maybe move it to another class?)
LongParameterList lib/filterable/attachable.rb Filterable::Attachable#apply 134 has 4 parameters
LongParameterList lib/filterable/concern.rb Filterable::Concern#apply_filter 131 has 4 parameters
LongParameterList lib/filterable/declarations_validator.rb Filterable::DeclarationsValidator#column_error 113 has 4 parameters
LongParameterList lib/filterable/matchable.rb Filterable::Matchable#narrow 98 has 4 parameters
ManualDispatch lib/filterable/attachable.rb Filterable::Attachable#accepted 62 manually dispatches method call
ManualDispatch lib/filterable/attachable.rb Filterable::Attachable#attachment_reflection 237 manually dispatches method call
ManualDispatch lib/filterable/concern.rb Filterable::Concern#add_filter 33 manually dispatches method call
ManualDispatch lib/filterable/concern.rb Filterable::Concern#applied_filters 107 manually dispatches method call
ManualDispatch lib/filterable/concern.rb Filterable::Concern#default_filter_params 48 manually dispatches method call
ManualDispatch lib/filterable/concern.rb Filterable::Concern#effective_filters 179 manually dispatches method call
ManualDispatch lib/filterable/concern.rb Filterable::Concern#evaluated_defaults 189 manually dispatches method call
ManualDispatch lib/filterable/concern.rb Filterable::Concern#filters 21 manually dispatches method call
ManualDispatch lib/filterable/concern.rb Filterable::Concern#indifferent? 201 manually dispatches method call
ManualDispatch lib/filterable/concerns/attachable.rb Filterable::Concerns::Attachable#attachable_attachment_names 25 manually dispatches method call
ManualDispatch lib/filterable/concerns/datable.rb Filterable::Concerns::Datable#datable_attribute_names 27 manually dispatches method call
ManualDispatch lib/filterable/concerns/equatable.rb Filterable::Concerns::Equatable#equatable_attribute_names 25 manually dispatches method call
ManualDispatch lib/filterable/concerns/matchable.rb Filterable::Concerns::Matchable#matchable_attribute_names 29 manually dispatches method call
ManualDispatch lib/filterable/concerns/matchable.rb Filterable::Concerns::Matchable#matchable_case_sensitive 43 manually dispatches method call
ManualDispatch lib/filterable/concerns/rangeable.rb Filterable::Concerns::Rangeable#rangeable_attribute_names 26 manually dispatches method call
ManualDispatch lib/filterable/concerns/scopable.rb Filterable::Concerns::Scopable#scopable_scope_names 24 manually dispatches method call
ManualDispatch lib/filterable/concerns/sortable.rb Filterable::Concerns::Sortable#sortable_attribute_names 24 manually dispatches method call
ManualDispatch lib/filterable/concerns/togglable.rb Filterable::Concerns::Togglable#togglable_scope_names 25 manually dispatches method call
ManualDispatch lib/filterable/datable.rb Filterable::Datable#bounds 38 manually dispatches method call
ManualDispatch lib/filterable/declarations_validator.rb Filterable::DeclarationsValidator#attachment_errors 144, 147 manually dispatches method call
ManualDispatch lib/filterable/declarations_validator.rb Filterable::DeclarationsValidator#column_errors 65 manually dispatches method call
ManualDispatch lib/filterable/declarations_validator.rb Filterable::DeclarationsValidator#scope_errors 127, 130 manually dispatches method call
ManualDispatch lib/filterable/equatable.rb Filterable::Equatable#values 59 manually dispatches method call
ManualDispatch lib/filterable/matchable.rb Filterable::Matchable#bounds 29 manually dispatches method call
ManualDispatch lib/filterable/rangeable.rb Filterable::Rangeable#bounds 36 manually dispatches method call
ManualDispatch lib/filterable/scopable.rb Filterable::Scopable#accepted 47, 51 manually dispatches method call
ManualDispatch lib/filterable/togglable.rb Filterable::Togglable#accepted 59, 62 manually dispatches method call
NestedIterators lib/filterable/datable.rb Filterable::Datable#accepted 55 contains iterators nested 2 deep
NestedIterators lib/filterable/datable/after.rb Filterable::Datable::After#call 19 contains iterators nested 2 deep
NestedIterators lib/filterable/datable/before.rb Filterable::Datable::Before#call 19 contains iterators nested 2 deep
NestedIterators lib/filterable/datable/range.rb Filterable::Datable::Range#call 20 contains iterators nested 2 deep
NestedIterators lib/filterable/datable/since.rb Filterable::Datable::Since#call 19 contains iterators nested 2 deep
NestedIterators lib/filterable/rangeable.rb Filterable::Rangeable#accepted 53 contains iterators nested 2 deep
NestedIterators lib/filterable/rangeable/maximum.rb Filterable::Rangeable::Maximum#call 19 contains iterators nested 2 deep
NestedIterators lib/filterable/rangeable/minimum.rb Filterable::Rangeable::Minimum#call 19 contains iterators nested 2 deep
NilCheck lib/filterable/attachable.rb Filterable::Attachable#entries 83 performs a nil-check
NilCheck lib/filterable/concern.rb Filterable::Concern#evaluated_defaults 190 performs a nil-check
NilCheck lib/filterable/equatable.rb Filterable::Equatable#accepted 46 performs a nil-check
NilCheck lib/filterable/matchable.rb Filterable::Matchable#sensitive? 64 performs a nil-check
NilCheck lib/filterable/scopable.rb Filterable::Scopable#accepted 51 performs a nil-check
NilCheck lib/filterable/target.rb Filterable::Target#polymorphic_hop? 106 performs a nil-check
NilCheck lib/filterable/target.rb Filterable::Target#resolve 87 performs a nil-check
NilCheck lib/filterable/togglable.rb Filterable::Togglable#toggled_on? 76 performs a nil-check
RepeatedConditional lib/filterable/declarations_validator.rb Filterable::DeclarationsValidator 65, 127, 144 tests '@model.respond_to?(reader)' at least 3 times
TooManyStatements lib/filterable/attachable.rb Filterable::Attachable#accepted 59 has approx 6 statements
TooManyStatements lib/filterable/attachable.rb Filterable::Attachable#presence 151 has approx 6 statements
TooManyStatements lib/filterable/concern.rb Filterable::Concern#applied_filters 102 has approx 6 statements
TooManyStatements lib/filterable/concerns/matchable.rb Filterable::Concerns::Matchable#split_matchable_options 73 has approx 7 statements
TooManyStatements lib/filterable/datable.rb Filterable::Datable#accepted 52 has approx 6 statements
TooManyStatements lib/filterable/rangeable.rb Filterable::Rangeable#accepted 50 has approx 6 statements
TooManyStatements lib/filterable/scopable.rb Filterable::Scopable#accepted 44 has approx 8 statements
TooManyStatements lib/filterable/sortable.rb Filterable::Sortable#terms 44 has approx 6 statements
TooManyStatements lib/filterable/target.rb Filterable::Target#resolve 81 has approx 8 statements
TooManyStatements lib/filterable/togglable.rb Filterable::Togglable#accepted 56 has approx 7 statements
TooManyStatements lib/filterable/value_normalization.rb Filterable::ValueNormalization#normalize 15 has approx 6 statements
UtilityFunction lib/filterable/concern.rb Filterable::Concern#context_aware? 163 doesn't depend on instance state (maybe move it to another class?)
UtilityFunction lib/filterable/concern.rb Filterable::Concern#indifferent? 200 doesn't depend on instance state (maybe move it to another class?)
UtilityFunction lib/filterable/concerns/matchable.rb Filterable::Concerns::Matchable#split_matchable_options 73 doesn't depend on instance state (maybe move it to another class?)
UtilityFunction lib/filterable/declarations_validator.rb Filterable::DeclarationsValidator#column_error 113 doesn't depend on instance state (maybe move it to another class?)
📊 Flog complexity breakdown
1028.9: flog total
     7.0: flog/method average

    37.5: main#none
    23.6: Filterable::Railtie::initializer#filterable.active_record lib/filterable/railtie.rb:19-31
    16.6: Filterable::Target#narrow        lib/filterable/target.rb:29-37
    16.4: Filterable::Scopable#accepted    lib/filterable/scopable.rb:44-55
    15.5: Filterable::Sortable#terms       lib/filterable/sortable.rb:44-53
    15.0: Filterable::Togglable#accepted   lib/filterable/togglable.rb:56-66
    14.4: Filterable::Concern#effective_filters lib/filterable/concern.rb:176-181
    14.1: Filterable::Attachable#accepted  lib/filterable/attachable.rb:59-68
    13.9: Filterable::Target#polymorphic_join lib/filterable/target.rb:133-137
    13.6: Filterable::Concern#applied_filters lib/filterable/concern.rb:102-109
    12.5: Filterable::Target#resolve       lib/filterable/target.rb:81-93
    12.5: Filterable::Attachable#entries   lib/filterable/attachable.rb:77-84
    12.4: Filterable::Datable::Range#call  lib/filterable/datable/range.rb:17-24
    11.9: Filterable::Attachable#presence  lib/filterable/attachable.rb:151-158
    11.8: Filterable::Target#polymorphic_resolve lib/filterable/target.rb:116-124
    11.5: Filterable::Concern#evaluated_defaults lib/filterable/concern.rb:187-192
    11.3: Filterable::DeclarationsValidator#errors lib/filterable/declarations_validator.rb:49-53
    10.9: Filterable::DeclarationsValidator#path_error lib/filterable/declarations_validator.rb:93-103
    10.9: Filterable::Concern#context_aware? lib/filterable/concern.rb:163-166
    10.5: Filterable::Concern#filterable   lib/filterable/concern.rb:83-88
    10.4: Filterable::Datable#accepted     lib/filterable/datable.rb:52-60
    10.4: Filterable::Rangeable#accepted   lib/filterable/rangeable.rb:50-58
    10.3: Filterable::Concerns::Matchable#split_matchable_options lib/filterable/concerns/matchable.rb:73-82
    10.3: Filterable::ValueNormalization#normalize lib/filterable/value_normalization.rb:15-25
    10.2: Filterable::Matchable#accepted   lib/filterable/matchable.rb:43-51
     9.9: Filterable::Target#unpack        lib/filterable/target.rb:62-70
     9.6: Filterable::DeclarationsValidator#scope_errors lib/filterable/declarations_validator.rb:125-134
     9.6: Filterable::Target#apply         lib/filterable/target.rb:172-176
     9.0: Filterable::Concerns::Datable#none
     9.0: Filterable::Concerns::Matchable#none
     8.9: Filterable::Matchable::StartsWith#call lib/filterable/matchable/starts_with.rb:16-22
     8.9: Filterable::Matchable::EndsWith#call lib/filterable/matchable/ends_with.rb:16-22
     8.9: Filterable::Matchable::Contains#call lib/filterable/matchable/contains.rb:16-22
     8.9: Filterable::Rangeable::Maximum#call lib/filterable/rangeable/maximum.rb:16-21
     8.9: Filterable::Rangeable::Minimum#call lib/filterable/rangeable/minimum.rb:16-21
     8.9: Filterable::Datable::Since#call  lib/filterable/datable/since.rb:16-21
     8.9: Filterable::Datable::Before#call lib/filterable/datable/before.rb:16-21
     8.9: Filterable::Datable::After#call  lib/filterable/datable/after.rb:16-21
     8.8: Filterable::Concern#add_filter   lib/filterable/concern.rb:30-38
     8.6: Filterable::Concerns::Matchable#matchable lib/filterable/concerns/matchable.rb:57-63
     8.6: Filterable::Concerns::Togglable#togglable_scope_names lib/filterable/concerns/togglable.rb:23-30
     8.6: Filterable::Concerns::Sortable#sortable_attribute_names lib/filterable/concerns/sortable.rb:22-29
     8.6: Filterable::Concerns::Scopable#scopable_scope_names lib/filterable/concerns/scopable.rb:22-29
     8.6: Filterable::Concerns::Rangeable#rangeable_attribute_names lib/filterable/concerns/rangeable.rb:24-31
     8.6: Filterable::Concerns::Matchable#matchable_case_sensitive lib/filterable/concerns/matchable.rb:41-48
     8.6: Filterable::Concerns::Matchable#matchable_attribute_names lib/filterable/concerns/matchable.rb:27-34
     8.6: Filterable::Concerns::Datable#datable_attribute_names lib/filterable/concerns/datable.rb:25-32
     8.6: Filterable::Concern#default_filter_params lib/filterable/concern.rb:46-53
     8.6: Filterable::Concerns::Equatable#equatable_attribute_names lib/filterable/concerns/equatable.rb:23-30
     8.6: Filterable::Concerns::Attachable#attachable_attachment_names lib/filterable/concerns/attachable.rb:23-30
     8.5: Filterable::Target#polymorphic_hop? lib/filterable/target.rb:102-107
     8.4: Filterable::Target#narrow_equal  lib/filterable/target.rb:47-54
     8.3: Filterable::Sortable#call        lib/filterable/sortable.rb:19-25
     8.3: Filterable::DeclarationsValidator#attachment_errors lib/filterable/declarations_validator.rb:142-151
     8.3: Filterable::Datable#bounds       lib/filterable/datable.rb:36-40
     8.3: Filterable::Rangeable#bounds     lib/filterable/rangeable.rb:34-38
📊 Flay duplication breakdown
Total score (lower is better) = 705

1) Similar code found in :module (mass = 255)
  lib/filterable/datable/after.rb:4
  lib/filterable/datable/before.rb:4
  lib/filterable/datable/since.rb:4
  lib/filterable/rangeable/maximum.rb:4
  lib/filterable/rangeable/minimum.rb:4

2) Similar code found in :module (mass = 205)
  lib/filterable/concerns/attachable.rb:4
  lib/filterable/concerns/equatable.rb:4
  lib/filterable/concerns/scopable.rb:4
  lib/filterable/concerns/sortable.rb:4
  lib/filterable/concerns/togglable.rb:4

3) Similar code found in :module (mass = 110)
  lib/filterable/matchable/contains.rb:4
  lib/filterable/matchable/starts_with.rb:4

4) Similar code found in :defn (mass = 69)
  lib/filterable/datable.rb:36
  lib/filterable/matchable.rb:27
  lib/filterable/rangeable.rb:34

5) Similar code found in :defn (mass = 66)
  lib/filterable/datable.rb:52
  lib/filterable/rangeable.rb:50

@fluence-ci

fluence-ci Bot commented Jul 3, 2026

Copy link
Copy Markdown

🛡️ Coverage Report: 100.0% (0.0%)

████████████████████ 100.0% — 690/690 lines covered

Base PR Δ
Total 100.0% 100.0% ±0%

All changes are tested.

Changed files

File Coverage Δ vs base Lines
🟢 lib/filterable.rb 100.0% ±0% 36/36
🟢 lib/filterable/attachable.rb 100.0% ±0% 62/62
🟢 lib/filterable/concerns/matchable.rb 100.0% 33/33
🟢 lib/filterable/declarations_validator.rb 100.0% ±0% 45/45
🟢 lib/filterable/matchable.rb 100.0% 26/26
🟢 lib/filterable/matchable/contains.rb 100.0% 13/13
🟢 lib/filterable/matchable/ends_with.rb 100.0% 13/13
🟢 lib/filterable/matchable/starts_with.rb 100.0% 13/13
🟢 lib/filterable/railtie.rb 100.0% ±0% 13/13
🟢 lib/filterable/togglable.rb 100.0% ±0% 27/27

@fluence-ci

fluence-ci Bot commented Jul 3, 2026

Copy link
Copy Markdown

🛡️ RuboCop: 0 offenses (0)

Base PR Δ
Offenses 0 0 ±0

No RuboCop offenses.

- the reserved `filters[<attribute>][case_sensitive]` key overrides
  the declaration either way: strict true or bare key switches on,
  explicit false switches off — even on an attribute declared
  sensitive — and anything else falls back to the declaration
- the boolean-param vocabulary moves to one home: Togglable gains
  FALSE_VALUES and toggled_off?, Attachable's local copy is gone
@jplot
jplot merged commit 6b8b33a into main Jul 3, 2026
16 checks passed
@jplot
jplot deleted the feat/matchable branch July 3, 2026 17:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants