mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-18 13:37:23 +02:00
fix(gbrain): parse nested context query filters
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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 ───────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user