export type SkipReason = "too_short" | "too_long" | "code_like"; export interface SkipRuleOptions { minWords: number; maxChars: number; } const CODE_FENCE_RE = /```/; const SHELL_PROMPT_RE = /^\s*[$#>]\s/m; const STACK_TRACE_RE = /\bat\s+\S+\s*\(.*:\d+:\d+\)/; const PATH_HEAVY_RE = /(\/[\w.-]+){3,}|([A-Za-z]:\\[\w.-]+){2,}/; const CODE_SYMBOL_RE = /[{};()<>=]/g; export function countWords(text: string): number { return text.trim().split(/\s+/).filter(Boolean).length; } /** Fence markers, prompts, stack traces, dense paths, or a high symbol density all read as code, not prose. */ function isCodeLike(text: string): boolean { if (CODE_FENCE_RE.test(text)) return true; if (SHELL_PROMPT_RE.test(text)) return true; if (STACK_TRACE_RE.test(text)) return true; if (PATH_HEAVY_RE.test(text)) return true; const symbolCount = text.match(CODE_SYMBOL_RE)?.length ?? 0; return text.length > 40 && symbolCount / text.length > 0.05; } /** * Both a cost and a privacy control: anything skipped here is never transmitted * to the correction provider at all. */ export function shouldSkip(text: string, options: SkipRuleOptions): SkipReason | null { const trimmed = text.trim(); if (trimmed.length > options.maxChars) return "too_long"; if (isCodeLike(trimmed)) return "code_like"; if (countWords(trimmed) < options.minWords) return "too_short"; return null; }