Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions lib/chorus/router.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,18 @@ def route(message)

private

# Whole-word matching, not substring: a naive `include?` check would
# match "class" inside "classical" or "error" inside "errors", routing
# unrelated messages to :coder. Splitting into words first avoids that
# class of false positive.
#
# This doesn't make keyword matching perfect — a word like "function" is
# genuinely ambiguous ("a function" vs. "how it functions") and will
# still misroute some messages either way. That's an inherent limitation
# of keyword matching, not a bug; see the LLM-based router follow-up.
def coding_task?(message)
normalized = message.downcase
# rubocop:disable Style/ArrayIntersect -- `normalized` is a String, not
# an Array; Array#intersect? would raise a TypeError here. This is a
# substring check on each keyword, not an array-element intersection.
CODER_KEYWORDS.any? { |keyword| normalized.include?(keyword) }
# rubocop:enable Style/ArrayIntersect
words = message.downcase.scan(/\w+/)
CODER_KEYWORDS.intersect?(words)
Comment on lines +34 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Ruby version:"
ruby -v

echo
echo "Locate router.rb:"
fd -a 'router\.rb$' . | sed 's#^\./##'

echo
echo "Relevant file excerpt:"
sed -n '1,120p' lib/chorus/router.rb 2>/dev/null || true

echo
echo "Unicode tokenization behavior:"
ruby - <<'RB'
messages = [
  "What does classé mean?",
  "Check the function.",
  "classé",
  "café résumé naïve",
]
messages.each do |message|
  puts message.inspect
  puts "  \\w+ => #{message.downcase.scan(/\w+/).inspect}"
  puts "  Unicode \\p{Word}+ => #{message.downcase.scan(/\p{Word}+/).inspect}"
end
RB

Repository: Amayyas/Chorus

Length of output: 2335


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ruby -E utf-8 -v - <<'RB'
messages = [
  "What does classé mean?",
  "Check the function.",
  "classé",
  "café résumé naïve",
  "I use the classical approach.",
]
messages.each do |message|
  puts message.inspect
  puts "  UTF8\\w+ => #{message.downcase.scan(/\w+/).inspect}"
  puts "  UTF8\\p{Word}+ => #{message.downcase.scan(/\p{Word}+/).inspect}"
end
RB

python3 - <<'PY'
import unicodedata
samples = ["what does classé mean?", "café résumé naïve", "I use the classical approach."]
for s in samples:
    words_w = []
    cur = []
    for ch in s:
        cat = unicodedata.category(ch)
        if cat.startswith('L') or cat.startswith('N') or ch in '_':
            cur.append(ch)
        else:
            if cur:
                words_w.append("".join(cur))
                cur = []
    if cur:
        words_w.append("".join(cur))
    print(s)
    print("  Unicode word segments:", words_w)
PY

Repository: Amayyas/Chorus

Length of output: 710


🌐 Web query:

Ruby Regexp /w/ Unicode property \p{Word} documentation

💡 Result:

In Ruby regular expressions, \p{Word} is a Unicode property that matches characters defined as word characters according to Unicode standards [1][2]. As of recent Ruby versions, \p{Word} (and the equivalent POSIX-style bracket expression [[:word:]]) matches a character if it belongs to one of the following Unicode categories or has one of the specified Unicode properties [1][2]: Unicode Categories: - Mark (M) [2] - Decimal Number (Nd) [2] - Connector Punctuation (Pc) [2] Unicode Properties: - Alpha (Alphabetic characters) [2] - Join_Control (specifically U+200C and U+200D) [2] Documentation Note: Historically, documentation for \p{Word} was less precise or incomplete regarding the specific Unicode categories and properties included [3][2][4]. Recent updates (e.g., in Ruby 3.3+) have aligned the documentation with current implementations and Unicode Technical Standard #18, which defines word characters to include alphabetic values, decimal numbers, and join control characters [2][5]. Be aware that bugs or inconsistencies between the documented behavior and the actual implementation of the underlying Unicode character database can occasionally occur across different Ruby versions [3][4]. Comparison with \w: - \w: In Ruby, \w is typically equivalent to [a-zA-Z0-9_] (when not using the /u flag for full Unicode support or when relying on standard ASCII behavior), though its exact behavior can vary depending on the regex engine's configuration and the version of Ruby [6]. - \p{Word}: Unlike \w, \p{Word} is specifically designed for Unicode-aware matching, providing a much broader set of characters that conform to global linguistic standards for what constitutes a word character [1][6].

Citations:


🌐 Web query:

site:ruby-doc.org/core Regexp \w Unicode \p{Word}

💡 Result:

In regular expression engines, the \w token and \p{Word} property serve different purposes regarding character matching, particularly concerning Unicode support [1][2]. \w (Word Character) The \w shorthand character class typically matches a limited set of ASCII characters. In most standard regex implementations—including Ruby's—it is equivalent to the character set [a-zA-Z0-9_] [1][3]. While some modern regex engines (such as those in PCRE or certain language-specific implementations) may be configured to treat \w as Unicode-aware, it is historically and primarily defined as matching ASCII letters, digits, and underscores [1][4]. \p{Word} (Unicode Property) The \p{Word} construct is a Unicode property escape [1][2]. It matches any character that is considered a "word" character according to the Unicode standard [1][4]. This is significantly broader than \w because it includes characters from various scripts, symbols, and marks [1][3]. Specifically, it matches any character belonging to the following Unicode General Categories [1][2]: - Letter (L) - Mark (M) - Number (N) - Connector_Punctuation (Pc) While \w is useful for matching technical identifiers constrained to ASCII, \p{Word} is the correct choice for internationalized applications requiring support for a wide range of global languages and character sets [1][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp)"
cat > "$tmp" <<'RB'
messages = [
  "What does classé mean?",
  "Check the function.",
  "classé",
  "café résumé naïve",
  "I use the classical approach.",
]
messages.each do |message|
  puts message.inspect
  puts "  UTF8\\w+ => #{message.downcase.scan(/\w+/).inspect}"
  puts "  UTF8\\p{Word}+ => #{message.downcase.scan(/\p{Word}+/).inspect}"
end
RB

ruby -E utf-8 -v "$tmp"

Repository: Amayyas/Chorus

Length of output: 777


Make tokenization Unicode-aware.

Ruby’s \w tokenizes classé as ["class", "mean"], causing accented words to still match :coder keywords. Use a Unicode word property such as /\p{Word}+/ and add a regression case for Unicode input.

Proposed fix
-      words = message.downcase.scan(/\w+/)
+      words = message.downcase.scan(/\p{Word}+/)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
words = message.downcase.scan(/\w+/)
CODER_KEYWORDS.intersect?(words)
words = message.downcase.scan(/\p{Word}+/)
CODER_KEYWORDS.intersect?(words)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/chorus/router.rb` around lines 34 - 35, Update the tokenization in the
router logic around CODER_KEYWORDS to use a Unicode-aware word pattern such as
\p{Word}+ so accented words remain single tokens, and add a regression case
covering Unicode input like “classé” to verify coder keyword matching is not
triggered by partial ASCII tokenization.

end
end
end
26 changes: 26 additions & 0 deletions spec/chorus/router_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,30 @@
it "is case-insensitive when matching keywords" do
expect(router.route("THERE IS A BUG HERE")).to eq(:coder)
end

# Regression cases for a substring-matching bug: a naive `include?` check
# against the raw message matched "class" inside "classical"/"classic" and
# "error" inside "errors", misrouting these to :coder.
describe "whole-word matching (regression)" do
cases = {
"What is the classical explanation for gravity?" => :research,
"Can you summarize this classic novel?" => :research,
"What errors did historians identify in this account?" => :research
}

cases.each do |message, expected_agent|
it "does not misroute #{message.inspect} on a keyword substring" do
expect(router.route(message)).to eq(expected_agent)
end
end

it "documents a known residual limitation: standalone ambiguous keywords still misroute" do
# "function" is a whole word here, not a substring match, but it's
# inherently ambiguous ("a function" vs. "how it functions"). Keyword
# matching can't disambiguate that — only a semantic/LLM-based router
# can. This spec exists so the behavior is a documented, intentional
# limitation rather than a silent surprise.
expect(router.route("How does a democracy function in practice?")).to eq(:coder)
end
end
end