mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-19 05:57:37 +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 globM = body.match(/(?:^|\n)\s*glob\s*:\s*"?([^"\n]+?)"?\s*$/m);
|
||||||
const sortM = body.match(/(?:^|\n)\s*sort\s*:\s*([^\n]+)/);
|
const sortM = body.match(/(?:^|\n)\s*sort\s*:\s*([^\n]+)/);
|
||||||
const tailM = body.match(/(?:^|\n)\s*tail\s*:\s*(\d+)/);
|
const tailM = body.match(/(?:^|\n)\s*tail\s*:\s*(\d+)/);
|
||||||
|
const filterMap = parseFilterMap(body);
|
||||||
|
|
||||||
if (idM) q.id = idM[1].trim();
|
if (idM) q.id = idM[1].trim();
|
||||||
if (kindM) {
|
if (kindM) {
|
||||||
@@ -408,6 +409,7 @@ function extractGbrainBlock(frontmatter: string): GbrainManifest | null {
|
|||||||
if (globM) q.glob = globM[1].trim();
|
if (globM) q.glob = globM[1].trim();
|
||||||
if (sortM) q.sort = sortM[1].trim();
|
if (sortM) q.sort = sortM[1].trim();
|
||||||
if (tailM) q.tail = parseInt(tailM[1], 10);
|
if (tailM) q.tail = parseInt(tailM[1], 10);
|
||||||
|
if (filterMap) q.filter = filterMap;
|
||||||
|
|
||||||
if (q.id && q.kind && q.render_as) {
|
if (q.id && q.kind && q.render_as) {
|
||||||
queries.push(q as GbrainManifestQuery);
|
queries.push(q as GbrainManifestQuery);
|
||||||
@@ -418,6 +420,39 @@ function extractGbrainBlock(frontmatter: string): GbrainManifest | null {
|
|||||||
return { schema, context_queries: queries };
|
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 ──────────────────────────────────────────────
|
// ── Public: withErrorContext ──────────────────────────────────────────────
|
||||||
|
|
||||||
const ERROR_LOG_PATH = join(gstackHome(), ".gbrain-errors.jsonl");
|
const ERROR_LOG_PATH = join(gstackHome(), ".gbrain-errors.jsonl");
|
||||||
|
|||||||
@@ -244,6 +244,62 @@ body
|
|||||||
expect(m!.context_queries[0].id).toBe("complete");
|
expect(m!.context_queries[0].id).toBe("complete");
|
||||||
rmSync(dir, { recursive: true, force: true });
|
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 ───────────────────────────────────────────────────────
|
// ── withErrorContext ───────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user