]> git.mdlowis.com Git - proto/nlshell.git/commitdiff
added minor typo correction
authorMike Lowis <mike.lowis@gentex.com>
Thu, 1 Oct 2026 18:41:44 +0000 (14:41 -0400)
committerMike Lowis <mike.lowis@gentex.com>
Thu, 1 Oct 2026 18:41:44 +0000 (14:41 -0400)
nlshell.rb

index e0c533052b0c242809dfbe4fd420f3ea845807bc..172dd7fa18b3c1c7393bf8f708138ca6dc07c063 100755 (executable)
@@ -7,7 +7,6 @@ require "strscan"
 # Scan the environment for possible shell commands
 ShellCommands = Set.new
 ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).reverse.each do |dir|
-  pp dir
   next unless File.directory?(dir)
   Dir.each_child(dir) do |name|
     path = File.join(dir, name)
@@ -161,6 +160,34 @@ module Lexer
   end
 end
 
+# Shared fuzzy-equality helper: lets a comparison accept a small typo instead
+# of requiring an exact string match. Only ever used to compare a word against
+# ONE specific expected word (a pattern/phrase word), never to search a whole
+# vocabulary - so there's no ambiguity to resolve, and slot values (which
+# never go through this) can't be touched.
+module Fuzzy
+  MAX_DISTANCE = 2
+
+  def self.close?(a, b)
+    a == b || distance(a, b) <= MAX_DISTANCE
+  end
+
+  # Levenshtein edit distance.
+  def self.distance(a, b)
+    costs = (0..b.length).to_a
+    a.each_char.with_index do |ca, i|
+      prev = costs[0]
+      costs[0] = i + 1
+      b.each_char.with_index do |cb, j|
+        tmp = costs[j + 1]
+        costs[j + 1] = [costs[j + 1] + 1, costs[j] + 1, prev + (ca == cb ? 0 : 1)].min
+        prev = tmp
+      end
+    end
+    costs[b.length]
+  end
+end
+
 # Stage: CLS. Tags a clause as teach/conditional/loop/query/action by
 # matching its leading words against configured phrases, checked in the
 # configured order (first match wins).
@@ -185,7 +212,7 @@ module Classifier
   def self.leading_match?(clause, phrase)
     return false if clause.length < phrase.length
 
-    clause.first(phrase.length).map { |t| t.word? ? t.text : nil } == phrase
+    clause.first(phrase.length).each_with_index.all? { |t, i| t.word? && Fuzzy.close?(t.text, phrase[i]) }
   end
 end
 
@@ -224,13 +251,14 @@ module Tier1
 
         args[word.delete_prefix(":").to_sym] = token
       else
-        return nil unless token.word? && token.text == word
+        return nil unless token.word? && Fuzzy.close?(token.text, word)
       end
     end
     args
   end
 end
 
+
 # Stage 0 result (spec SS5.7). Everything downstream (Tier 1/2/3, executor)
 # operates on this, never on raw text or loose token/clause arrays.
 module Utterance
@@ -258,6 +286,9 @@ module Utterance
     clauses = sentences.flat_map { |sentence| Lexer.clauses(sentence) }
     raise Rejected, "that's too many clauses in one request, please split it up" if clauses.length > MAX_CLAUSES
 
+    # Classification/Tier 1 tolerate small typos on their own (Fuzzy.close?
+    # inside Classifier.leading_match? and Tier1.match), so no separate
+    # correction pass is needed here.
     Result.new(raw, clauses.map { |tokens| Clause.new(tokens, Classifier.classify(tokens)) })
   end
 end