fix(gbrain): parse nested context query filters

This commit is contained in:
t
2026-07-14 12:56:08 -07:00
parent 45d0fb2b54
commit 7e23aecfc5
2 changed files with 91 additions and 0 deletions
+35
View File
@@ -396,6 +396,7 @@ function extractGbrainBlock(frontmatter: string): GbrainManifest | null {
const globM = body.match(/(?:^|\n)\s*glob\s*:\s*"?([^"\n]+?)"?\s*$/m);
const sortM = body.match(/(?:^|\n)\s*sort\s*:\s*([^\n]+)/);
const tailM = body.match(/(?:^|\n)\s*tail\s*:\s*(\d+)/);
const filterMap = parseFilterMap(body);
if (idM) q.id = idM[1].trim();
if (kindM) {
@@ -408,6 +409,7 @@ function extractGbrainBlock(frontmatter: string): GbrainManifest | null {
if (globM) q.glob = globM[1].trim();
if (sortM) q.sort = sortM[1].trim();
if (tailM) q.tail = parseInt(tailM[1], 10);
if (filterMap) q.filter = filterMap;
if (q.id && q.kind && q.render_as) {
queries.push(q as GbrainManifestQuery);
@@ -418,6 +420,39 @@ function extractGbrainBlock(frontmatter: string): GbrainManifest | null {
return { schema, context_queries: queries };
}
/**
* Parse a nested `filter:` block map out of a single context_queries item body.
*
* The block is a YAML map nested under the `filter:` key:
*
* filter:
* type: timeline
* tags_contains: "repo:{repo_slug}"
*
* Each sub-key sits one indent level deeper than `filter:`. Surrounding quotes
* are stripped and template vars ({repo_slug}, now-7d, ...) are left intact for
* downstream substitution, matching how dispatchList stringifies each value
* into a `--filter k=v` argument. Returns undefined when there is no `filter:`
* block or it is empty.
*/
function parseFilterMap(body: string): Record<string, string> | undefined {
const lines = body.split("\n");
const filterIdx = lines.findIndex((l) => /^\s*filter\s*:\s*$/.test(l));
if (filterIdx === -1) return undefined;
const filterIndent = lines[filterIdx].match(/^\s*/)![0].length;
const filter: Record<string, string> = {};
for (let i = filterIdx + 1; i < lines.length; i++) {
const line = lines[i];
if (line.trim() === "") continue; // tolerate blank lines within the block
const indent = line.match(/^\s*/)![0].length;
if (indent <= filterIndent) break; // dedent to a sibling key ends the block
const kv = line.match(/^\s*([A-Za-z0-9_]+)\s*:\s*"?(.*?)"?\s*$/);
if (kv) filter[kv[1]] = kv[2].trim();
}
return Object.keys(filter).length > 0 ? filter : undefined;
}
// ── Public: withErrorContext ──────────────────────────────────────────────
const ERROR_LOG_PATH = join(gstackHome(), ".gbrain-errors.jsonl");
+56
View File
@@ -244,6 +244,62 @@ body
expect(m!.context_queries[0].id).toBe("complete");
rmSync(dir, { recursive: true, force: true });
});
it("parses a nested filter: block on a list query", () => {
const dir = mkdtempSync(join(tmpdir(), "gstack-test-"));
const file = join(dir, "filtered.md");
writeFileSync(
file,
`---
name: investigate
gbrain:
schema: 1
context_queries:
- id: prior-investigations
kind: list
filter:
type: timeline
tags_contains: "repo:{repo_slug}"
content_contains: "investigate"
sort: updated_at_desc
limit: 5
render_as: "## Prior investigations in this repo"
- id: recent-no-filter
kind: list
sort: created_at_desc
limit: 3
render_as: "## Recent (no filter)"
---
body
`
);
const m = parseSkillManifest(file);
expect(m).not.toBeNull();
expect(m!.context_queries).toHaveLength(2);
// The filter: sub-block is parsed into a key/value map, with quotes
// stripped and template vars left intact for downstream substitution.
const filtered = m!.context_queries[0];
expect(filtered.id).toBe("prior-investigations");
expect(filtered.filter).toEqual({
type: "timeline",
tags_contains: "repo:{repo_slug}",
content_contains: "investigate",
});
// Sibling fields on the same item still parse alongside the filter.
expect(filtered.sort).toBe("updated_at_desc");
expect(filtered.limit).toBe(5);
expect(filtered.render_as).toBe("## Prior investigations in this repo");
// A list query with no filter: leaves filter undefined (no regression).
expect(m!.context_queries[1].id).toBe("recent-no-filter");
expect(m!.context_queries[1].filter).toBeUndefined();
expect(m!.context_queries[1].sort).toBe("created_at_desc");
rmSync(dir, { recursive: true, force: true });
});
});
// ── withErrorContext ───────────────────────────────────────────────────────