Skip to content

feat(query): --seed to start the traversal at given nodes - #4289

Open
devkancheti4-design wants to merge 3 commits into
Graphify-Labs:v8from
devkancheti4-design:query-seed
Open

devkancheti4-design wants to merge 3 commits into
Graphify-Labs:v8from
devkancheti4-design:query-seed

Conversation

@devkancheti4-design

@devkancheti4-design devkancheti4-design commented Oct 10, 2026 •

Copy link
Copy Markdown

What does this PR do?

Resolves #4293 (related: #445, #2381, #2384, #3810).

This PR adds --seed X (repeatable) to graphify query. The caller can choose where the traversal starts instead of
using the question's keyword matches. The use case: an external ranker picks where to start, and graphify's graph does
the expansion. The ranker could be embeddings, a hybrid or learned retriever, or an agent that already knows the
relevant file. This is the same retrieve-then-expand shape that BENCHMARKS.md describes for graph-expand, exposed on
the CLI.

graphify query "how are tokens refreshed?" --seed backend/app/core/security.py --seed refresh_token

Behaviour:

  • One interpretation of a node reference. Each value resolves exactly as affected resolves its seed
    (affected.resolve_seed): node id, unique label, callable name with or without (), or a repo-relative or absolute
    file path. An absolute path is anchored to the graph's own root, using the same derivation as affected (affected silently returns nothing when the path is spelled differently (./ or absolute) #2706), so
    it works from any cwd.
  • Fail closed. A value that does not resolve to exactly one node is listed in the header as
    Unresolved seeds (skipped). If none resolve, the command prints No unique node match for --seed: ... and does
    not fall back to keyword seeds.
  • Header. The header shows Start (given): [...]. Seeds are deduplicated, keeping their order. Context filters,
    --budget and BFS/DFS are unchanged.
  • No change without --seed. The output is unchanged: rendering moved into _render_query as is.

Not in this PR (happy to follow up if wanted):

  • the MCP query_graph parameter;
  • the skill fragments (which need skillgen --bless);
  • the docs-site query reference.

Context: I work on a code retrieval layer and wanted to hand graphify its top files as start points. The option is
generic and contains no external code.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Tests or CI
  • Refactor
  • Security fix

Verification & Invariants

Invariants:

  • No new way to interpret a node reference: it reuses resolve_seed.

  • Ambiguity fails closed, with no guessing and no silent fallback.

  • The default query path is unchanged.

  • No persisted or generated state is touched.

  • Read the CONTRIBUTING.md guide.

  • Reproduced the issue and identified the invariant. (n/a, this is a feature; the invariants are above)

  • Made the smallest fix necessary.

  • Added a regression test (if bug fix) or isolated boundary test.

  • Kept the PR description synchronized with the final implementation.

  • Documented any limitations / unsupported cases explicitly.

How was this tested?

uv run pytest -q tests/test_query_seeds.py                                         7 passed
uv run pytest -q tests/test_query*.py tests/test_affected*.py tests/test_mcp*.py   116 passed
uv run pytest -q -x tests                                                          5,960 passed, 151 skipped, then stopped at 1 failure
uv run ruff check graphify/serve.py graphify/cli.py graphify/__main__.py tests/test_query_seeds.py    All checks passed
uv run pyright graphify/serve.py graphify/cli.py                                   49 errors on v8 before and after this change (no new ones)

Complete pass (uv run pytest -q -rf tests --deselect tests/test_skillgen.py::test_audit_coverage_passes):
6,876 passed, 168 skipped, 8 failed. All 8 failures are in tests/test_skillgen.py. The deselected
test_audit_coverage_passes failed the same way in an earlier run, which makes 9 in all:

  • test_audit_coverage_passes
  • test_all_progressive_hosts_check_and_audit_clean
  • test_monolith_roundtrip_passes_for_aider_and_devin
  • test_monoliths_change_only_sanctioned_lines
  • test_always_on_roundtrip_is_byte_faithful
  • test_audit_catches_an_induced_per_host_drop
  • test_audit_catches_a_dropped_non_allowlisted_heading
  • test_amp_audit_coverage_passes_against_its_own_v8
  • test_agents_audit_baseline_is_amps_v8_body

Each of the 9 fails identically on unmodified v8 in my environment, because my clone is shallow and these tests read
older git refs (could not read <sha>:graphify/skill.md). The failures are not related to this change. No other test
fails.

New tests (tests/test_query_seeds.py):

  • a given seed replaces the keyword seeds;
  • file-path and id seeds are deduplicated in order;
  • an unresolved seed is reported and skipped;
  • when no seed resolves, the query fails closed with no fallback;
  • an absolute path resolves against the graph root;
  • the CLI flag is repeatable;
  • the CLI's absolute seed does not depend on the cwd.

Graphify-specific checklist

  • I updated generated skill artifacts (uv run python -m tools.skillgen --bless) when changing their source fragments. (n/a, no fragments changed)
  • I confirmed that AST/structural extraction remains deterministic (no ambient state dependencies like ENV variables).
  • I reviewed changes for security implications (no unsafe interpolation into shell/Python). Seed values only reach resolve_seed for comparison.
  • I confirmed no API keys or local-only graph data are included.
  • (If applicable) I disclosed AI authorship in my commit messages.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

Thanks for the pull request, @devkancheti4-design. A maintainer will review it soon.

Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions.

A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.

Not checked: tests were not run; formal verification proved 0 of 3 changed function(s) (2 sampled, not proven, 1 not verified).

Formal verification. PR-changed functions: 2/3 verified (0 proven, 2 may-equivalent, 0 distinguished) · 1 not verified (1 vacuous).

Not verified on this run: dispatch\_command (vacuous: never exercised).


Graphify review — findings

Adds a repeatable --seed X flag to graphify query so an external ranker can pick the traversal's start nodes instead of the question's keyword matches. Each seed resolves like affected does (node id, unique label, or file path anchored at the graph's repo root rather than CWD). Seeds that don't match exactly one node are listed as skipped in the header, and if none resolve the query returns an error rather than falling back to keyword seeds.

No blocking issues surfaced.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 763 functions depend on the 246 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: main() — 105 callers, 3 callees
  • worse: dispatch_command() — 2 callers, 131 callees
  • new: _render_query() — 1 callers, 7 callees

Verification — 763 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 763 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

40 of 362 test file(s) selected (11%) via static blast radius.

  • tests/test_affected_cli.py — impact
  • tests/test_agents_platform.py — impact
  • tests/test_benchmark.py — impact
  • tests/test_benchmark_raw_graph.py — impact
  • tests/test_cloud_cta.py — impact
  • tests/test_codebuddy.py — impact
  • tests/test_dedup_shrink_refuses_force_write.py — impact
  • tests/test_devin.py — impact
  • tests/test_explain_cli.py — impact
  • tests/test_extract_cli.py — impact
  • tests/test_file_label_disambiguation.py — impact
  • tests/test_global_add_tag_inference.py — impact
  • tests/test_god_nodes_cli.py — impact
  • tests/test_hollow_chunks_arm_shrink_guard.py — impact
  • tests/test_hook_guard_token_match.py — impact
  • tests/test_hook_out_of_project_paths.py — impact
  • tests/test_hook_strict.py — impact
  • tests/test_incomplete_build_guard.py — impact
  • tests/test_install.py — impact
  • tests/test_install_references.py — impact
  • tests/test_install_version_warning.py — impact
  • tests/test_mcp_graph_view_cache.py — impact
  • tests/test_merge_chunks_validation.py — impact
  • tests/test_multigraph_diagnostics.py — impact
  • tests/test_no_dedup_flag.py — impact
  • tests/test_opencode_plugin_refresh.py — impact
  • tests/test_partial_cache.py — impact
  • tests/test_path_cli.py — impact
  • tests/test_query_cli.py — impact
  • tests/test_query_induced_edges.py — impact
  • tests/test_query_mcp_direction.py — impact
  • tests/test_query_names_its_graph.py — impact
  • tests/test_query_seeds.py — impact, changed-test
  • tests/test_serve.py — impact
  • tests/test_serve_http.py — impact
  • tests/test_skill_auto_refresh.py — impact
  • tests/test_skill_version_warning.py — impact
  • tests/test_stale_prune.py — impact
  • tests/test_terraform.py — impact
  • tests/test_unverified_semantic_shrink.py — impact

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

No difference found (not proven): No behavior difference found in \_run\_cli (not a proof).

The verifier ran both versions of \_run\_cli on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify dispatch\_command.

The verifier did not have enough to check dispatch\_command, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 40 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in \_query\_graph\_text (not a proof).

The verifier ran both versions of \_query\_graph\_text on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

· 1 grounded finding(s) anchored inline below; 2 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/serve.py
context_filters=context_filters, graph_path=graph_path)


def _render_query(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — _render_query()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@devkancheti4-design

Copy link
Copy Markdown
Author

Thanks for the quick automated review.

Python 3.10 and 3.13: tests/test_query_seeds.py plus the existing query, affected and MCP tests pass on both,
116 passed each:

UV_PROJECT_ENVIRONMENT=.venv3.10 uv sync --python 3.10 && uv run pytest -q tests/test_query*.py tests/test_affected*.py tests/test_mcp*.py   -> 116 passed (Python 3.10.20)
UV_PROJECT_ENVIRONMENT=.venv3.13 uv sync --python 3.13 && uv run pytest -q tests/test_query*.py tests/test_affected*.py tests/test_mcp*.py   -> 116 passed (Python 3.13.13)

About the _render_query() coupling note: its 7 callees are the existing rendering steps of _query_graph_text,
moved as they were so that the seeded path and the keyword path share them. They are not new dependencies. If you
would rather not have the extra function, I can fold it back so that the seeded path flows into the same code with
no new function. Happy to do whichever you prefer.

@nothariharan

Copy link
Copy Markdown
Contributor

Thanks for the clean PR — this is a useful escape hatch for the recurring seed-selection issues, and reusing resolve_seed keeps a single interpretation of a node reference.

Verification

  • All 7 new tests in tests/test_query_seeds.py pass, plus 109 related tests (test_query*, test_affected*, test_mcp*) — no regressions.
  • Smoke-tested the CLI end-to-end: Start (given) / Unresolved seeds (skipped) render correctly, the all-unresolved case fails closed with no keyword fallback, and the default path is unchanged without --seed.
  • Confirmed _graph_root_for matches affected's root derivation (affected silently returns nothing when the path is spelled differently (./ or absolute) #2706), and the MCP query_graph path is unaffected (new params are keyword-only with defaults).

No blocking bugs. A few small things that would make it perfect:

  1. Duplicate unresolved seeds are reported twice — unresolved.append(value) has no dedup, unlike start_nodes.
  2. graphify query "q" --seed with no value silently runs a keyword query (falls into the else: i += 1 branch, same as --context). A one-line guard would fit the PR's no-silent-fallback stance.
  3. Minor test gaps: the callable-name form (refresh_token()) is documented but untested, as is the --seed + --context combination.

Optional nits: the quotes on seed_root: "Path | None" are redundant (Path is imported and from __future__ import annotations is present), and querylog.log_query doesn't record the seeds used.

Ship it after #1 and #2 — nice work.

@devkancheti4-design

Copy link
Copy Markdown
Author

Thanks for taking the time to review this, @nothariharan! The latest commit is up.

@safishamsi

Copy link
Copy Markdown
Member

Thanks @devkancheti4-design — the --seed design and logic are solid (seed resolution via resolve_seed, fail-closed on ambiguous/unresolved, deterministic, composes with context filters). One rebase hazard before it can land: serve._query_graph_text changed on v8 after this was written — the tail now (1) returns the _QUERY_NO_MATCH module constant and (2) passes seeds=start_nodes into the final _subgraph_to_text(...) so the queried symbol renders first and survives truncation (a prior branch merge had dropped this; it's the "#BUG2" fix). Your extracted _render_query is based on the pre-fix body, so a mechanical rebase would silently drop seeds=start_nodes and regress seed-first/truncation for both seeded and keyword queries.

Could you rebase onto current v8, carry seeds=start_nodes into _render_query's _subgraph_to_text call, use the _QUERY_NO_MATCH constant, and add one test asserting a seed renders first and survives truncation? Then it's good to land.

devkancheti4-design and others added 3 commits October 12, 2026 03:26
An external ranker (embeddings, a hybrid or learned retriever, or an agent
that already knows the file) can choose where graphify query starts instead
of the question's keyword matches; the graph then does the expansion.

Each --seed value resolves exactly as affected resolves its seed
(affected.resolve_seed: node id, unique label, callable name, repo-relative
or absolute file path anchored to the graph's root), so there is one
interpretation of a node reference. Fail closed: a value that does not
resolve to exactly one node is reported and skipped; if none resolve, the
query says so and does not fall back to keyword seeds. Without --seed the
output is unchanged (rendering moved into _render_query as is).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ved seed once; log seeds

- graphify query --seed with no value (trailing --seed or --seed=) now exits with an error instead of running a
  keyword query.
- An unresolved seed given twice is listed once.
- The query log records the seeds used.
- Tests: the callable form (name and name()), --seed with --context, both no-value forms, and the logged seeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cation

The seeded path hands the resolved nodes to _subgraph_to_text as seeds (seeds=start_nodes, the BUG2 fix), so the
given seed is the first NODE line and still shows under a small token budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@devkancheti4-design

Copy link
Copy Markdown
Author

Thanks @safishamsi! Rebased onto current v8 (0.9.86):

  • _render_query carries seeds=start_nodes into _subgraph_to_text. Its body now matches v8's tail line for line,
    apart from the Start (given) label and the unresolved-seeds line, so seed-first rendering and truncation hold for
    both seeded and keyword queries.
  • The keyword path returns _QUERY_NO_MATCH.
  • New test test_given_seed_renders_first_and_survives_truncation: a low-degree seed given with --seed is the first
    NODE line and survives a 40-token budget (TRUNCATED shown). The BUG2 tests in test_serve.py pass too.

Tests:

  • test_query_seeds.py, test_serve.py, and the query, affected and MCP tests: 297 passed on each of Python 3.10,
    3.13 and 3.14.
  • The full suite: 7,334 passed, 0 failed.

This branch has not been deployed

No deployments
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.

[Feature]: let graphify query start from caller-chosen nodes (--seed)

3 participants