Skip to content

Commit 2a721c8

Browse files
committed
improvement(logs): only flag genuine regex intent, document the truncated invariant
Review follow-ups on the literal log-grep. The `patternNotice` fired on any regex metacharacter, which includes `.` — so ordinary literal searches (`example.com`, `file.pdf`, `v1.2.3`, `block_1.output`) were told their pattern "was not interpreted as a regex", inviting the agent to retry a search that had in fact worked. Measured 10 false notices across 19 realistic literal searches. `REGEX_INTENT` now keys on actual regex intent — escape classes, character class, group, alternation, a leading/trailing anchor, or a repetition quantifier — which drops that to 0/19 while still flagging all 10 regex attempts tested. `truncated` gains a TSDoc block stating its invariant: it reports that trace was left unread, not that a budget was reached. Every point that skips work goes through `done()`, which sets it, so a budget exhausted by the final match correctly reports `false`. Raised by Cursor Bugbot; behavior is right, the contract was just implicit. Also drops `snippetAround`'s `maxChars` parameter, which every caller passed from the state object it already receives.
1 parent cbce1e5 commit 2a721c8

2 files changed

Lines changed: 31 additions & 5 deletions

File tree

apps/sim/lib/logs/log-views.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,16 @@ describe('grepSpans', () => {
177177
expect(result.patternNotice).toContain('literal')
178178
})
179179

180+
it.each(['example.com', 'file.pdf', 'v1.2.3', 'block_1.output', '$0.42', 'why?'])(
181+
'does not warn about regex on the ordinary literal %s',
182+
async (pattern) => {
183+
const spans = [span({ output: { v: `saw ${pattern} here` } })]
184+
const result = await grepSpans(spans, pattern, ctx)
185+
expect(result.matches.some((m) => m.field === 'output')).toBe(true)
186+
expect(result.patternNotice).toBeUndefined()
187+
}
188+
)
189+
180190
it('does not interpret a regex pattern, and says so', async () => {
181191
const spans = [span({ output: { v: 'status=503' } })]
182192

apps/sim/lib/logs/log-views.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,12 @@ export interface GrepSpanMatch {
188188

189189
export interface GrepSpansResult {
190190
matches: GrepSpanMatch[]
191+
/**
192+
* Whether the scan stopped with trace left unread — not whether a budget was
193+
* reached. Every point that skips work goes through `done`, which sets this,
194+
* so a budget exhausted by the very last match reports `false`: the caller's
195+
* results are complete, and nothing was withheld.
196+
*/
191197
truncated: boolean
192198
/**
193199
* Present when the pattern contained regex syntax, which is matched literally.
@@ -224,8 +230,17 @@ function escapeRegExp(input: string): string {
224230
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
225231
}
226232

227-
/** Regex syntax in a pattern signals the caller expected regex semantics. */
228-
const REGEX_METACHARACTERS = /[.*+?^${}()|[\]\\]/
233+
/**
234+
* Signals that the caller wrote a pattern *intending* regex semantics: escape
235+
* classes, a character class, a group, alternation, a leading/trailing anchor,
236+
* or a repetition quantifier.
237+
*
238+
* Deliberately narrower than the set `escapeRegExp` escapes. A bare `.` or `?`
239+
* is ordinary text in the things people actually grep for — `example.com`,
240+
* `file.pdf`, `v1.2.3`, `block_1.output` — and flagging those would tell the
241+
* caller its correct search was misinterpreted, prompting a pointless retry.
242+
*/
243+
const REGEX_INTENT = /\\[dwsbDWSB]|[[\]()|*+]|^\^|\$$|\{\d+,?\d*\}/
229244

230245
/**
231246
* Compile a caller-supplied grep pattern as a case-insensitive literal.
@@ -244,7 +259,7 @@ const REGEX_METACHARACTERS = /[.*+?^${}()|[\]\\]/
244259
*/
245260
function compilePattern(pattern: string): { regex: RegExp; notice?: string } {
246261
const regex = new RegExp(escapeRegExp(pattern), 'i')
247-
if (!REGEX_METACHARACTERS.test(pattern)) return { regex }
262+
if (!REGEX_INTENT.test(pattern)) return { regex }
248263
return {
249264
regex,
250265
notice:
@@ -266,9 +281,10 @@ function runTimed<T>(state: GrepState, match: (regex: RegExp) => T): T {
266281
}
267282
}
268283

269-
function snippetAround(text: string, state: GrepState, maxChars: number): string {
284+
function snippetAround(text: string, state: GrepState): string {
270285
const m = runTimed(state, (regex) => regex.exec(text))
271286
const index = m ? m.index : 0
287+
const maxChars = state.maxSnippetChars
272288
const half = Math.floor(maxChars / 2)
273289
const start = Math.max(0, index - half)
274290
const end = Math.min(text.length, start + maxChars)
@@ -304,7 +320,7 @@ function recordIfMatch(
304320
blockId: span.blockId,
305321
name: span.name,
306322
field,
307-
snippet: snippetAround(text, state, state.maxSnippetChars),
323+
snippet: snippetAround(text, state),
308324
})
309325
if (state.matches.length >= state.maxMatches) state.truncated = true
310326
}

0 commit comments

Comments
 (0)