fix(multi-get): support brace expansion patterns in glob matching (#424)

Brace expansion patterns like `{doc1,doc2}.md` or `collection/{a,b}.md`
were incorrectly parsed as comma-separated file lists instead of being
passed to the glob matcher (picomatch). This happened because the
comma-detection heuristic only checked for `*` and `?` but not `{`.

Also adds `collection/path` matching in `matchFilesByGlob` so patterns
like `my-collection/{file1,file2}.md` work — previously the glob only
matched against `qmd://collection/path` (virtual) and `path` (relative
to collection root), missing the `collection/path` form.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Antonio Mello 2026-04-05 17:45:33 -03:00 committed by GitHub
parent 09a4d19b31
commit ef062e1b54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 94 additions and 3 deletions

View File

@ -995,7 +995,7 @@ function multiGet(pattern: string, maxLines?: number, maxBytes: number = DEFAULT
const db = getDb();
// Check if it's a comma-separated list or a glob pattern
const isCommaSeparated = pattern.includes(',') && !pattern.includes('*') && !pattern.includes('?');
const isCommaSeparated = pattern.includes(',') && !pattern.includes('*') && !pattern.includes('?') && !pattern.includes('{');
let files: { filepath: string; displayPath: string; bodyLength: number; collection?: string; path?: string }[];

View File

@ -2362,7 +2362,7 @@ export function matchFilesByGlob(db: Database, pattern: string): { filepath: str
const isMatch = picomatch(pattern);
return allFiles
.filter(f => isMatch(f.virtual_path) || isMatch(f.path))
.filter(f => isMatch(f.virtual_path) || isMatch(f.path) || isMatch(f.collection + '/' + f.path))
.map(f => ({
filepath: f.virtual_path, // Virtual path for precise lookup
displayPath: f.path, // Relative path for display
@ -3540,7 +3540,7 @@ export function findDocuments(
pattern: string,
options: { includeBody?: boolean; maxBytes?: number } = {}
): { docs: MultiGetResult[]; errors: string[] } {
const isCommaSeparated = pattern.includes(',') && !pattern.includes('*') && !pattern.includes('?');
const isCommaSeparated = pattern.includes(',') && !pattern.includes('*') && !pattern.includes('?') && !pattern.includes('{');
const errors: string[] = [];
const maxBytes = options.maxBytes ?? DEFAULT_MULTI_GET_MAX_BYTES;

View File

@ -1909,6 +1909,55 @@ describe("Document Retrieval", () => {
await cleanupTestDb(store);
});
test("findDocuments supports brace expansion patterns", async () => {
const store = await createTestStore();
const collectionName = await createTestCollection();
await insertTestDocument(store.db, collectionName, {
name: "doc1",
filepath: "/path/doc1.md",
displayPath: "doc1.md",
});
await insertTestDocument(store.db, collectionName, {
name: "doc2",
filepath: "/path/doc2.md",
displayPath: "doc2.md",
});
await insertTestDocument(store.db, collectionName, {
name: "doc3",
filepath: "/path/doc3.md",
displayPath: "doc3.md",
});
const { docs, errors } = store.findDocuments("{doc1,doc2}.md");
expect(errors).toHaveLength(0);
expect(docs).toHaveLength(2);
await cleanupTestDb(store);
});
test("findDocuments supports brace expansion with collection prefix", async () => {
const store = await createTestStore();
const collectionName = await createTestCollection();
await insertTestDocument(store.db, collectionName, {
name: "readme",
filepath: "/path/readme.md",
displayPath: "readme.md",
});
await insertTestDocument(store.db, collectionName, {
name: "changelog",
filepath: "/path/changelog.md",
displayPath: "changelog.md",
});
const { docs, errors } = store.findDocuments(`${collectionName}/{readme,changelog}.md`);
expect(errors).toHaveLength(0);
expect(docs).toHaveLength(2);
await cleanupTestDb(store);
});
});
});
@ -2267,6 +2316,48 @@ describe("Fuzzy Matching", () => {
await cleanupTestDb(store);
});
test("matchFilesByGlob matches collection/path patterns", async () => {
const store = await createTestStore();
const collectionName = await createTestCollection();
await insertTestDocument(store.db, collectionName, {
filepath: "/p/readme.md",
displayPath: "readme.md",
});
await insertTestDocument(store.db, collectionName, {
filepath: "/p/changelog.md",
displayPath: "changelog.md",
});
const matches = store.matchFilesByGlob(`${collectionName}/*.md`);
expect(matches).toHaveLength(2);
await cleanupTestDb(store);
});
test("matchFilesByGlob matches brace expansion", async () => {
const store = await createTestStore();
const collectionName = await createTestCollection();
await insertTestDocument(store.db, collectionName, {
filepath: "/p/readme.md",
displayPath: "readme.md",
});
await insertTestDocument(store.db, collectionName, {
filepath: "/p/changelog.md",
displayPath: "changelog.md",
});
await insertTestDocument(store.db, collectionName, {
filepath: "/p/license.md",
displayPath: "license.md",
});
const matches = store.matchFilesByGlob(`${collectionName}/{readme,changelog}.md`);
expect(matches).toHaveLength(2);
await cleanupTestDb(store);
});
});
// =============================================================================