Skip to content

feat(query): GraphRAG prompt compiler — cgis context <fqn> (#19)#219

Merged
zaebee merged 4 commits into
mainfrom
feat/issue-19-prompt-compiler
Jun 12, 2026
Merged

feat(query): GraphRAG prompt compiler — cgis context <fqn> (#19)#219
zaebee merged 4 commits into
mainfrom
feat/issue-19-prompt-compiler

Conversation

@zaebee

@zaebee zaebee commented Jun 12, 2026

Copy link
Copy Markdown
Owner

What

Implements #19 — a GraphRAG prompt compiler that feeds an LLM coding agent a compact, structured context package about the node it's about to edit, in place of raw file dumps.

New cgis context <fqn> CLI + cgis_context MCP tool emit an XML-tagged package:

<context focal="…build_context" type="FUNCTION" file="…:74-104">
<source>```python … focal source … ```</source>
<class>…enclosing class + siblings…</class>
<domain …>architectural boundary (L3)</domain>
<callers …>who calls this — upstream ripple</callers>
<callees …>what this calls — internal deps</callees>
</context>

Design decisions

  • Audience = AI agents, not humans. Humans already have trace/impact/structure (Rich trees) + Mermaid. So context optimises for LLM token-efficiency + parseability.
  • No Mermaid by default. Mermaid is a visual format an LLM parses at several times the token cost; adjacency is rendered as FQN+location bullet lists instead. (Optional --format mermaid is a possible follow-up.)
  • Source snippet, never whole files (linecache), with an adaptive code-fence guard against </source>/backtick prompt-injection.
  • INTERNAL-only neighbours. Builtins/third-party (len, ValueError, pathlib.Path) are dropped as noise; truly-unknown calls still surface via the unresolved raw_call: list.
  • L3 <domain> block (from @kehansama's two-layer-context insight) — surfaces the focal node's architectural boundary, gated on real domains tags (not the structural ontology_class fallback).

Architecture (Lane B, zero-conflict)

Mirrors the drift_service + graph_json split:

  • query/snippet.py — pure-ish I/O leaf (extract_snippet)
  • query/prompt.pypure compile_context (TDD'd in isolation)
  • query/context_service.py — orchestration (resolve → CALLS-filtered neighbours → class context → assemble)
  • cli.py / mcp_server.py — thin append-only wrappers

Dogfooding

Built and verified end-to-end via the cgis MCP + cgis context on its own code — which caught two noise bugs (junk <domain> block from the structural ontology_class fallback; builtin callees) that are fixed here.

Deferred (follow-up issue, crediting @kehansama)

Verification

make format lint type-check pytest doc-coverage all green — 898 unit + 48 self-parsing, mypy strict, doc 99.6%. No self-drift re-baseline (new modules fit the cgis.query pattern). MCP_REFERENCE intentionally left to the autodoc sync.

Closes #19

🤖 Generated with Claude Code

New agent-facing context compiler: feeds an LLM a compact, XML-tagged
package about a focal node (source snippet, enclosing class, domain
boundary, callers, callees) in place of raw file dumps.

- query/snippet.py: extract_snippet (linecache, graceful on missing/EOF)
- query/prompt.py: pure compile_context — Hybrid XML, adaptive code-fence
  anti-injection, graceful degradation, L3 <domain> boundary block
- query/context_service.py: orchestration — CALLS-filtered direct
  neighbours, INTERNAL-only (builtins/third-party dropped as noise),
  structural-parent class context, unresolved raw_call list
- cli.py `cgis context` (clean stdout payload, stderr resolve notes,
  --source-root to locate snippets after `ingest ./src`)
- mcp_server.py cgis_context tool (MCP_REFERENCE left to the autodoc sync)

Mermaid is a visual format an LLM parses at several times the token cost,
so adjacency is rendered as FQN+location bullet lists; only the focal
node's source is inlined, never whole files. Dogfooded via cgis MCP + CLI
(which caught two noise bugs: junk domain block from the structural
ontology_class fallback, and builtin callees — both fixed).

Incorporates @kehansama's two-layer-context insight: the <domain> block
surfaces the focal node's architectural boundary so agents respect it
during refactoring. Adaptive depth + centrality token-budgeting deferred
to a follow-up.

Closes #19

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@zaebee

zaebee commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

/guardian review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary: Found 3 real defects: (1) unresolved callees logic misses incoming unresolved calls, (2) structural parent lookup may return incorrect parent, (3) domain section may emit for structural ontology_class without domains. All other changes appear correct and well-tested.


🤖 mistral-medium-latest · 15,503 prompt + 711 completion = 16,214 tokens · graph 5/10 files (50%)

Comment thread src/cgis/query/context_service.py
Comment thread src/cgis/query/context_service.py
Comment thread src/cgis/query/prompt.py Outdated

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new feature to compile an agent-facing GraphRAG context package for a focal FQN, adding a new MCP tool, a CLI command, and dedicated services for context orchestration, prompt compilation, and source snippet extraction. The code review feedback suggests several key improvements: normalizing file paths to ensure cross-platform compatibility on POSIX systems, escaping source code to prevent XML injection vulnerabilities, and improving robustness when resolving structural parent nodes by continuing the search if a node is missing from the database.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/cgis/query/context_service.py Outdated
Comment thread src/cgis/query/prompt.py Outdated
Comment thread src/cgis/query/context_service.py Outdated
github-actions Bot added a commit that referenced this pull request Jun 12, 2026
… parent preference

- prompt.py: neutralize `</` in source so a file containing `</source>`/`</context>`
  can't close the prompt's own XML tags (gemini SEC-HIGH). Surgical `</`-escape
  keeps `->` return arrows, generics and comparisons readable, unlike a blanket
  `<`/`>` escape (verified by dogfood: `def f() -> Node` stays intact).
- context_service.py: normalize Windows backslash file_paths before locating the
  snippet so Windows-ingested graphs still read source on POSIX (gemini HIGH).
- context_service.py: `_structural_parent` now skips parents missing from the
  store and prefers the enclosing CLASS over a FILE/MODULE so a method's class
  context is never lost to an earlier file-level edge (gemini MEDIUM + guardian).

Rejected (guardian, empirically): the unresolved-callee `edge.target` "fix" —
callees come from get_flow_graph (outgoing), so `edge.source == focus` is
correct and a raw_call target is never the focus; and the domain-block claim —
already gated on `focus.domains`, the finder reviewed a pre-fix hunk.

5 new regression tests. 902 pass, mypy strict, doc 99.6%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@zaebee

zaebee commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new feature to compile and output an agent-facing GraphRAG context package for a focal FQN, adding a new CLI command, an MCP tool, and supporting orchestration, prompt compilation, and snippet extraction services along with comprehensive unit tests. The code review feedback is highly constructive, suggesting an optimization in the snippet extractor to break early on EOF to avoid performance bottlenecks, and recommending the escaping of special characters in XML attributes to prevent malformed XML generation.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/cgis/query/snippet.py Outdated
Comment thread src/cgis/query/prompt.py Outdated
Comment thread src/cgis/query/prompt.py
Comment thread src/cgis/query/prompt.py Outdated
Comment thread src/cgis/query/prompt.py Outdated
@zaebee

zaebee commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

Code review — cgis context prompt compiler

Clean Lane-B split (snippetprompt pure → context_service orchestration), good docstrings, injection guards thought-through, and the dogfooding catches are nice. A few correctness/accuracy issues on the agent-facing output below.

🔴 1. depth=2 default mislabels transitive neighbours as direct

build_context(..., depth: int = 2) passes that depth straight into get_impact_graph/get_flow_graph, whose BFS (engine.py _bfs_traverse) accumulates every node up to max_depth — i.e. 1-hop and 2-hop. But the results are surfaced through _direct_callers / _direct_callees, and rendered to the agent as:

  • <callees note="what this calls — its direct dependencies">
  • <callers note="who calls this …">

With the default depth, a 2-hop callee (something a callee calls) is listed as a direct dependency of the focal node. An LLM consuming this will build a wrong dependency model — e.g. conclude that focus calls X when in reality focus → mid → X, and "fix" the wrong edge during a refactor. Either:

  • render only depth-1 in the callers/callees lists (matching the _direct_* names + "direct dependencies" wording), or
  • keep depth-2 but drop the word "direct" and annotate hop distance so the agent can tell ripple from immediate deps.

(The PR body says depth-2 is intentional — the bug is the labeling/naming, not the traversal.)

🟠 2. Asymmetric unresolved-callee collection at depth ≥ 2

In _direct_callees, resolved callees come from the full-depth traversal, but unresolved names are filtered to edge.source == focus_fqn only:

unresolved = sorted({
    edge.target[len(RAW_CALL_PREFIX):]
    for edge in edges
    if edge.source == focus_fqn and edge.target.startswith(RAW_CALL_PREFIX)
})

So at the default depth-2 the <callees> block mixes 1-hop and 2-hop resolved deps, but only the focal node's own unresolved deps — a transitive callee's unresolved calls silently vanish. Pinning resolved callees to depth-1 too (finding #1) makes both halves consistent.

🟡 3. _neutralize_closing_tags mangles source that the agent reads

</&lt;/ is applied to the source inside the already-adaptive ```python fence. For Python </ is essentially never a token, but the repo also ships a TypeScript extractor — TS/JSX source (</div>, generics) would reach the agent visibly corrupted as &lt;/div>. The backtick fence already blocks fence-escape; consider scoping the XML-tag neutralisation to only the literal tag names you emit (</source>, </context>, …) rather than every </, so real code stays verbatim.


Nothing here blocks the I/O/architecture — #1 is the one I'd fix before merge since it directly degrades the product's core output. #2/#3 are cheap follow-ups.

🤖 via /code-review

…tr-escape

Addresses the colleague cross-review + gemini round 2 on #219.

- depth default 1 (was 2): callers/callees lists are now *direct* by default,
  so the `_collect_*` data and the section notes are honest. `--depth N` still
  pulls transitive neighbours, but the note states the hop bound ("callers
  within N hops …") instead of mislabeling them "direct" (colleague 🔴 #1).
- unresolved callees collected across the whole traversal, not just the focal
  node's own edges — symmetric with the resolved set at depth>1 (colleague 🟠 #2).
- `_neutralize_closing_tags` now escapes only the closing tags THIS module emits
  (regex over source/context/class/domain/callers/callees), so TS/JSX `</div>`
  and generics reach the agent verbatim while injection is still blocked
  (colleague 🟡 #3).
- `_escape_xml_attr` helper applied to every XML attribute (focal id/file, class
  name/file, domain ontology/domains) so `&`/`"`/`<`/`>` can't malform the tags
  (gemini MEDIUM ×4).
- snippet: break on linecache EOF ("" sentinel) so a corrupt/huge end_line can't
  spin millions of empty reads; a blank middle line ("\n") is still preserved
  (gemini MEDIUM).

CLI/MCP `cgis context` depth default → 1 to match. 8 new/updated tests, 908 pass,
mypy strict, doc 99.6%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@zaebee

zaebee commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

Thanks for the cross-review — all three addressed in d61be5c.

🔴 1. depth labeling. You nailed the real defect: the lists were honest only at depth 1, but the default was 2, so transitive neighbours rode under a "direct" note. Fixed by making depth default 1 (the _collect_* lists are now genuinely direct for the common focused-edit case), and the section notes are hop-honest above it — --depth 2 now renders note="callees within 2 hops downstream (transitive execution flow)" instead of claiming directness. Renamed _direct_*_collect_* to match. Adaptive depth (kehansama's log(out_degree+1)) stays on the #220 follow-up.

🟠 2. unresolved asymmetry. _collect_callees now gathers raw_call: targets across the whole traversal (dropped the edge.source == focus_fqn filter), so resolved and unresolved halves are symmetric at any depth. Regression test: a 2-hop callee's own unresolved call surfaces only at depth=2, not depth=1.

🟡 3. TS/JSX mangling. _neutralize_closing_tags now escapes only the closing tags this module emits (regex over source|context|class|domain|callers|callees), so </div>, generics and -> reach the agent verbatim while </source>/</context> injection is still blocked. Test asserts JSX stays intact.

Also took gemini's round-2 attribute-escaping (_escape_xml_attr on every XML attr) and snippet EOF early-break in the same commit. 908 tests green.

@zaebee

zaebee commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a GraphRAG context compiler feature that generates XML-tagged prompt contexts for focal FQNs, including their source snippets, enclosing classes, and call relationships. The changes introduce a new CLI command, an MCP tool, and supporting services for context orchestration, snippet extraction, and prompt rendering, backed by comprehensive unit tests. The review feedback suggests refactoring the structural parent resolution logic in context_service.py to avoid variable leakage caused by using an assignment expression inside a list comprehension.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/cgis/query/context_service.py Outdated
@zaebee

zaebee commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

Round 2 — fixes verified ✅

Re-reviewed d61be5c + 8770c86 against the three findings. All resolved, and at the right altitude:

# Finding Fix Verdict
🔴 1 depth-2 transitive neighbours labeled "direct" default depth 2→1, _direct_*_collect_*, notes now computed by _callers_note(depth)/_callees_note(depth) ("direct" only at depth 1, "within N hops" above), depth plumbed into compile_context. CLI + MCP defaults dropped to 1 too — consistent end-to-end Resolved — honest label instead of a hardcoded claim, the deeper fix
🟠 2 unresolved callees only from focal edges dropped the edge.source == focus_fqn filter — raw_call: targets now collected across the whole traversal, symmetric with the resolved set Resolved
🟡 3 </-escape mangled real source scoped to _OWN_CLOSING_TAG = only the 6 tags this module emits; TS/JSX </div>, generics, -> stay verbatim Resolved

Two unprompted hardenings I'll call out as good catches:

  • _escape_xml_attr on every attribute value — an FQN/domain tag containing " or < would previously have broken the XML envelope.
  • extract_snippet now breaks on the first empty (EOF) line — a corrupt/huge end_line can't spin millions of getline calls.

No regressions spotted in the round-2 changes.

Optional micro-nit (non-blocking): _OWN_CLOSING_TAG is exact + case-sensitive, so a source literally containing </source > (trailing space) or </SOURCE> slips through un-neutralised. An LLM isn't a strict XML parser and the source sits inside the backtick fence anyway, so the residual injection surface is negligible — fine to leave, just noting it.

LGTM to merge. 🚢

🤖 via /code-review

…alrus leak

- prompt.py: `_OWN_CLOSING_TAG` now allows whitespace before `>` (`</source >`),
  the only injection variant the exact match missed (colleague non-blocking nit).
  Case stays sensitive — tags are lowercase and XML end-tags are case-sensitive,
  so `</SOURCE>` can't close `<source>` anyway.
- context_service.py: `_structural_parent` rebuilt as a generator + filter
  comprehension instead of a `:=` walrus inside the list comp (which leaked
  `parent` to the function scope) — same behaviour, cleaner (gemini MEDIUM).

909 tests, mypy strict, doc 99.6%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@zaebee

zaebee commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

Thanks for the 🚢 — and good eye on the whitespace nit. Took it anyway in 8889a38: _OWN_CLOSING_TAG now allows whitespace before > (</source >), since XML end-tags permit it. Left case-sensitive on purpose — the tags are lowercase and XML end-tags are case-sensitive, so </SOURCE> can't close <source>. Also took gemini's walrus-scope cleanup in the same commit. PR now has zero open threads.

@sonarqubecloud

Copy link
Copy Markdown

@zaebee zaebee merged commit f1a79e5 into main Jun 12, 2026
3 checks passed
@zaebee zaebee deleted the feat/issue-19-prompt-compiler branch June 12, 2026 22:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: GraphRAG prompt compiler — subgraph context for LLM agents

1 participant