mirror of
https://github.com/stablyai/orca.git
synced 2026-09-28 08:02:43 +00:00
fix(ai-vault-search): restore the panel's reading of a quote that does not end a word
Unifying the two parsers changed panel behaviour on nine of twenty probed shapes, not the three previously pinned. Six of the nine were regressions, all from one rule: the shared parser refused a quoted span whose closing quote was not followed by a space, so `"a b"c` and `repo:"a"b` became single terms carrying their own quote characters, which match nothing. The rule was justified as what stops the apostrophes in `it's a repo:orca thing's` from swallowing the operator between them. It is not: a span only ever opens at a token start, and the quote in `it's` is not at one. Dropping the rule restores all six shapes to what the panel has always done and leaves that protection intact. Three changes remain and are kept because the old answer was worse in each: an operator with an empty quoted value is dropped rather than filtering on `""` and silently emptying the list, and a bare pair of quotes reads as an empty term rather than as the two characters. Each is pinned with a test that says which behaviour it is and why.
This commit is contained in:
@@ -50,22 +50,36 @@ describe('what counts as an operator', () => {
|
||||
// panel's old hand-rolled tokenizer answered differently, so the change of
|
||||
// behaviour is a decision on the record rather than a surprise.
|
||||
describe('the shapes where the panel parser used to answer differently', () => {
|
||||
it('drops an operator with an empty quoted value instead of searching for `""`', () => {
|
||||
// The old tokenizer kept the quotes as the value, which matched no repo at
|
||||
// all, so `repo:""` silently emptied the list.
|
||||
expect(splitAiVaultSearchQuery('repo:"" x').repoTerms).toEqual([])
|
||||
expect(parseVaultQuery('repo:"" x').repoTerms).toEqual([])
|
||||
it.each([
|
||||
['repo:"" x', 'repoTerms'],
|
||||
['path:"" x', 'pathTerms']
|
||||
] as const)('drops the empty operator value in %s instead of filtering on `""`', (query, key) => {
|
||||
// The old tokenizer kept the quote characters as the value, so `repo:""`
|
||||
// filtered on a label no session has and silently emptied the list. An
|
||||
// operator with nothing in it is not a narrowing.
|
||||
expect(splitAiVaultSearchQuery(query)[key]).toEqual([])
|
||||
expect(parseVaultQuery(query)[key]).toEqual([])
|
||||
})
|
||||
|
||||
it('reads a quote that does not end a word as ordinary text', () => {
|
||||
// `"foo"bar` was two tokens, `foo` and `bar`; it is one now. A closing quote
|
||||
// has to end a word, which is what keeps the apostrophes in `it's ... thing's`
|
||||
// from swallowing an operator between them.
|
||||
expect(parseVaultQuery('"foo"bar').terms).toEqual(['"foo"bar'])
|
||||
it.each(['"" empty', "'' empty"])('reads the empty quotes in %s as an empty term', (query) => {
|
||||
// Same reason one level up: the old parser searched for the two characters
|
||||
// and found nothing, where an empty term matches everything and leaves the
|
||||
// rest of the query to do the work.
|
||||
expect(parseVaultQuery(query).terms).toEqual(['', 'empty'])
|
||||
})
|
||||
|
||||
it('reads a bare pair of quotes as an empty term, not as the characters', () => {
|
||||
expect(parseVaultQuery('"" empty').terms).toEqual(['', 'empty'])
|
||||
it.each([
|
||||
['"foo"bar', { terms: ['foo', 'bar'], repoTerms: [], pathTerms: [] }],
|
||||
['"a b"c', { terms: ['a b', 'c'], repoTerms: [], pathTerms: [] }],
|
||||
['repo:"a"b', { terms: ['b'], repoTerms: ['a'], pathTerms: [] }],
|
||||
['path:"a"b', { terms: ['b'], repoTerms: [], pathTerms: ['a'] }],
|
||||
['repo:"a b"c d', { terms: ['c', 'd'], repoTerms: ['a b'], pathTerms: [] }]
|
||||
])('reads %s exactly as the panel always has', (query, expected) => {
|
||||
// A closing quote does not have to end a word. Requiring it turned each of
|
||||
// these into one term carrying its own quote characters, which matches
|
||||
// nothing; the apostrophe case below is protected by the token start, not
|
||||
// by that rule.
|
||||
expect(parseVaultQuery(query)).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -57,8 +57,13 @@ function isBoundary(char: string | undefined): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Why the closing quote must end a word: otherwise the apostrophes in
|
||||
* `it's a repo:orca thing's` open a span that swallows the operator between them.
|
||||
* A quoted span, or null when this is not one.
|
||||
*
|
||||
* What keeps the apostrophes in `it's a repo:orca thing's` from opening a span
|
||||
* that swallows the operator is the caller: this only ever runs at a token
|
||||
* start, and the quote in `it's` is not at one. The closing quote is then just
|
||||
* the next one, wherever it falls, so `"a b"c` reads as the panel has always
|
||||
* read it — the span, then the rest as its own token.
|
||||
*/
|
||||
function readQuoted(query: string, at: number): { value: string; end: number } | null {
|
||||
const quote = query[at]
|
||||
@@ -66,9 +71,7 @@ function readQuoted(query: string, at: number): { value: string; end: number } |
|
||||
return null
|
||||
}
|
||||
const close = query.indexOf(quote, at + 1)
|
||||
return close === -1 || !isBoundary(query[close + 1])
|
||||
? null
|
||||
: { value: query.slice(at + 1, close), end: close + 1 }
|
||||
return close === -1 ? null : { value: query.slice(at + 1, close), end: close + 1 }
|
||||
}
|
||||
|
||||
function readBare(query: string, at: number): string {
|
||||
|
||||
Reference in New Issue
Block a user