From 01ee744757ac6b102a1a41c00134f2ee753fe583 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Tue, 7 Jul 2026 00:39:47 -0700 Subject: [PATCH] fix(commands): bound unknown verb suggestions --- src/github/command-suggest.ts | 75 +++++++++++++++++++++++-------- test/unit/command-suggest.test.ts | 11 ++++- 2 files changed, 67 insertions(+), 19 deletions(-) diff --git a/src/github/command-suggest.ts b/src/github/command-suggest.ts index 12d0047acf..0a72c119fd 100644 --- a/src/github/command-suggest.ts +++ b/src/github/command-suggest.ts @@ -9,33 +9,58 @@ export type CommandSuggestCatalog = { /** Max Levenshtein distance for a did-you-mean suggestion. */ export const COMMAND_SUGGEST_MAX_DISTANCE = 2; -export function levenshteinDistance(left: string, right: string): number { +/** Longest verb that can still be within the did-you-mean threshold of a catalog command. */ +export const COMMAND_SUGGEST_MAX_VERB_LENGTH = 64; + +function boundedLevenshteinDistance( + left: string, + right: string, + maxDistance: number, +): number { if (left === right) return 0; + if (Math.abs(left.length - right.length) > maxDistance) + return maxDistance + 1; if (left.length === 0) return right.length; if (right.length === 0) return left.length; - const rows = left.length + 1; - const cols = right.length + 1; - const matrix: number[][] = Array.from({ length: rows }, () => Array(cols).fill(0)); - for (let row = 0; row < rows; row++) matrix[row]![0] = row; - for (let col = 0; col < cols; col++) matrix[0]![col] = col; - for (let row = 1; row < rows; row++) { - for (let col = 1; col < cols; col++) { + + let previous = Array.from({ length: right.length + 1 }, (_, col) => col); + let current = Array(right.length + 1).fill(0); + + for (let row = 1; row <= left.length; row++) { + current[0] = row; + let rowMin = current[0]!; + for (let col = 1; col <= right.length; col++) { const cost = left[row - 1] === right[col - 1] ? 0 : 1; - matrix[row]![col] = Math.min( - matrix[row - 1]![col]! + 1, - matrix[row]![col - 1]! + 1, - matrix[row - 1]![col - 1]! + cost, + const distance = Math.min( + previous[col]! + 1, + current[col - 1]! + 1, + previous[col - 1]! + cost, ); + current[col] = distance; + rowMin = Math.min(rowMin, distance); } + if (rowMin > maxDistance) return maxDistance + 1; + [previous, current] = [current, previous]; } - return matrix[left.length]![right.length]!; + return previous[right.length]!; +} + +export function levenshteinDistance(left: string, right: string): number { + return boundedLevenshteinDistance(left, right, Number.MAX_SAFE_INTEGER); } function commandSuggestTargets(catalog: CommandSuggestCatalog): string[] { - return [...catalog.mentionCommands, ...catalog.actionCommands, ...Object.keys(catalog.actionAliases)]; + return [ + ...catalog.mentionCommands, + ...catalog.actionCommands, + ...Object.keys(catalog.actionAliases), + ]; } -export function isKnownGittensoryCommandVerb(rawVerb: string, catalog: CommandSuggestCatalog): boolean { +export function isKnownGittensoryCommandVerb( + rawVerb: string, + catalog: CommandSuggestCatalog, +): boolean { const verb = rawVerb.trim().toLowerCase(); if (!verb) return false; const canonical = catalog.actionAliases[verb] ?? verb; @@ -46,13 +71,27 @@ export function isKnownGittensoryCommandVerb(rawVerb: string, catalog: CommandSu } /** Return the closest catalog command within {@link COMMAND_SUGGEST_MAX_DISTANCE}, or null. */ -export function suggestCommand(rawVerb: string, catalog: CommandSuggestCatalog): string | null { +export function suggestCommand( + rawVerb: string, + catalog: CommandSuggestCatalog, +): string | null { const verb = rawVerb.trim().toLowerCase(); - if (!verb || isKnownGittensoryCommandVerb(verb, catalog)) return null; + if ( + !verb || + verb.length > COMMAND_SUGGEST_MAX_VERB_LENGTH || + isKnownGittensoryCommandVerb(verb, catalog) + ) + return null; const targets = commandSuggestTargets(catalog); let best: { name: string; distance: number } | null = null; for (const name of targets) { - const distance = levenshteinDistance(verb, name); + if (Math.abs(verb.length - name.length) > COMMAND_SUGGEST_MAX_DISTANCE) + continue; + const distance = boundedLevenshteinDistance( + verb, + name, + COMMAND_SUGGEST_MAX_DISTANCE, + ); if (best === null || distance < best.distance) { best = { name, distance }; } diff --git a/test/unit/command-suggest.test.ts b/test/unit/command-suggest.test.ts index f0bab7c543..78cb9895df 100644 --- a/test/unit/command-suggest.test.ts +++ b/test/unit/command-suggest.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { COMMAND_SUGGEST_MAX_DISTANCE, + COMMAND_SUGGEST_MAX_VERB_LENGTH, buildDidYouMeanSections, formatDidYouMeanLine, isKnownGittensoryCommandVerb, @@ -58,6 +59,12 @@ describe("suggestCommand", () => { expect(suggestCommand("xyzzyqwerty", catalog)).toBeNull(); }); + it("skips suggestions for oversized unknown verbs", () => { + const oversizedVerb = "a".repeat(COMMAND_SUGGEST_MAX_VERB_LENGTH + 1); + + expect(suggestCommand(oversizedVerb, catalog)).toBeNull(); + }); + it("keeps the closest catalog entry when multiple targets are within range", () => { expect(suggestCommand("hel", catalog)).toBe("help"); }); @@ -71,7 +78,9 @@ describe("suggestCommand", () => { describe("formatDidYouMeanLine", () => { it("renders a public-safe markdown hint", () => { - expect(formatDidYouMeanLine("preflight")).toBe("- Did you mean `@gittensory preflight`?"); + expect(formatDidYouMeanLine("preflight")).toBe( + "- Did you mean `@gittensory preflight`?", + ); }); });