Skip to content

Falkordb backend - #1

Open
galshubeli wants to merge 579 commits into
v8from
falkordb-backend
Open

Falkordb backend#1
galshubeli wants to merge 579 commits into
v8from
falkordb-backend

Conversation

@galshubeli

Copy link
Copy Markdown
Owner

No description provided.

Copilot AI 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.

Pull request overview

This PR migrates graphify’s runtime graph backend from NetworkX-in-memory graphs to a FalkorDB-backed GraphStore, and updates exports/docs/tests accordingly so graph operations (merge/update/query/path/explain/cluster/benchmark) work against the store while still emitting graph.json artifacts for compatibility.

Changes:

  • Replace NetworkX usage with FalkorDB GraphStore/MemGraph plumbing and node-link JSON helpers.
  • Add FalkorDB export docs/flags and integration tests; update CLI export tests to seed/use the store.
  • Refactor tests to remove the NetworkX dependency via a lightweight tests/nxcompat.py shim and FalkorDB-backed fixtures.

Reviewed changes

Copilot reviewed 152 out of 152 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tools/skillgen/fragments/references/shared/update.md Update reference flow to merge/update/diff via FalkorDB store.
tools/skillgen/fragments/references/shared/exports.md Document FalkorDB export/push flags and usage.
tools/skillgen/fragments/core/core.md Add FalkorDB flags to core skill usage and step list.
tools/skillgen/expected/graphify__skills__windows__references__update.md Regenerated expected skill reference for FalkorDB-based update flow.
tools/skillgen/expected/graphify__skills__windows__references__exports.md Regenerated expected skill reference for FalkorDB exports.
tools/skillgen/expected/graphify__skills__vscode__references__update.md Regenerated expected skill reference for FalkorDB-based update flow.
tools/skillgen/expected/graphify__skills__vscode__references__exports.md Regenerated expected skill reference for FalkorDB exports.
tools/skillgen/expected/graphify__skills__trae__references__update.md Regenerated expected skill reference for FalkorDB-based update flow.
tools/skillgen/expected/graphify__skills__trae__references__exports.md Regenerated expected skill reference for FalkorDB exports.
tools/skillgen/expected/graphify__skills__pi__references__update.md Regenerated expected skill reference for FalkorDB-based update flow.
tools/skillgen/expected/graphify__skills__pi__references__exports.md Regenerated expected skill reference for FalkorDB exports.
tools/skillgen/expected/graphify__skills__opencode__references__update.md Regenerated expected skill reference for FalkorDB-based update flow.
tools/skillgen/expected/graphify__skills__opencode__references__exports.md Regenerated expected skill reference for FalkorDB exports.
tools/skillgen/expected/graphify__skills__kiro__references__update.md Regenerated expected skill reference for FalkorDB-based update flow.
tools/skillgen/expected/graphify__skills__kiro__references__exports.md Regenerated expected skill reference for FalkorDB exports.
tools/skillgen/expected/graphify__skills__kilo__references__update.md Regenerated expected skill reference for FalkorDB-based update flow.
tools/skillgen/expected/graphify__skills__kilo__references__exports.md Regenerated expected skill reference for FalkorDB exports.
tools/skillgen/expected/graphify__skills__droid__references__update.md Regenerated expected skill reference for FalkorDB-based update flow.
tools/skillgen/expected/graphify__skills__droid__references__exports.md Regenerated expected skill reference for FalkorDB exports.
tools/skillgen/expected/graphify__skills__copilot__references__update.md Regenerated expected skill reference for FalkorDB-based update flow.
tools/skillgen/expected/graphify__skills__copilot__references__exports.md Regenerated expected skill reference for FalkorDB exports.
tools/skillgen/expected/graphify__skills__codex__references__update.md Regenerated expected skill reference for FalkorDB-based update flow.
tools/skillgen/expected/graphify__skills__codex__references__exports.md Regenerated expected skill reference for FalkorDB exports.
tools/skillgen/expected/graphify__skills__claw__references__update.md Regenerated expected skill reference for FalkorDB-based update flow.
tools/skillgen/expected/graphify__skills__claw__references__exports.md Regenerated expected skill reference for FalkorDB exports.
tools/skillgen/expected/graphify__skills__claude__references__update.md Regenerated expected skill reference for FalkorDB-based update flow.
tools/skillgen/expected/graphify__skills__claude__references__exports.md Regenerated expected skill reference for FalkorDB exports.
tools/skillgen/expected/graphify__skills__amp__references__update.md Regenerated expected skill reference for FalkorDB-based update flow.
tools/skillgen/expected/graphify__skills__amp__references__exports.md Regenerated expected skill reference for FalkorDB exports.
tools/skillgen/expected/graphify__skill.md Regenerated expected core skill with FalkorDB flags.
tools/skillgen/expected/graphify__skill-windows.md Regenerated expected Windows skill with FalkorDB flags.
tools/skillgen/expected/graphify__skill-vscode.md Regenerated expected VS Code skill with FalkorDB flags.
tools/skillgen/expected/graphify__skill-trae.md Regenerated expected Trae skill with FalkorDB flags.
tools/skillgen/expected/graphify__skill-pi.md Regenerated expected Pi skill with FalkorDB flags.
tools/skillgen/expected/graphify__skill-opencode.md Regenerated expected OpenCode skill with FalkorDB flags.
tools/skillgen/expected/graphify__skill-kiro.md Regenerated expected Kiro skill with FalkorDB flags.
tools/skillgen/expected/graphify__skill-kilo.md Regenerated expected Kilo skill with FalkorDB flags.
tools/skillgen/expected/graphify__skill-droid.md Regenerated expected Droid skill with FalkorDB flags.
tools/skillgen/expected/graphify__skill-copilot.md Regenerated expected Copilot skill with FalkorDB flags.
tools/skillgen/expected/graphify__skill-codex.md Regenerated expected Codex skill with FalkorDB flags.
tools/skillgen/expected/graphify__skill-claw.md Regenerated expected Claw skill with FalkorDB flags.
tools/skillgen/expected/graphify__skill-amp.md Regenerated expected AMP skill with FalkorDB flags.
tests/test_wiki.py Switch tests to nxcompat shim instead of NetworkX.
tests/test_serve_http.py Seed FalkorDB store for HTTP server tests.
tests/test_semantic_similarity.py Switch tests to nxcompat shim instead of NetworkX.
tests/test_query_cli.py Use seed_graph fixture instead of writing NetworkX node-link JSON.
tests/test_prs.py Switch tests to nxcompat shim instead of NetworkX.
tests/test_path_cli.py Use seed_graph fixture for path CLI regression tests.
tests/test_obsidian_filename_cap.py Switch tests to nxcompat shim instead of NetworkX.
tests/test_multigraph_diagnostics.py Update diagnostics expectations for GraphStore behavior.
tests/test_multigraph_compat.py Remove NetworkX multigraph capability tests.
tests/test_labeling.py Switch tests to nxcompat shim instead of NetworkX.
tests/test_hypergraph.py Switch tests to nxcompat shim instead of NetworkX.
tests/test_falkordb_integration.py Add real FalkorDB push integration tests (auto-skip when unavailable).
tests/test_explain_cli.py Seed FalkorDB store for explain CLI tests.
tests/test_confidence.py Switch tests to nxcompat shim instead of NetworkX.
tests/test_cluster.py Switch tests to nxcompat shim instead of NetworkX.
tests/test_cli_export.py Ensure subprocess CLI can import repo + add FalkorDB export test + seed store for exports.
tests/test_benchmark.py Seed FalkorDB store for benchmark tests; remove oversized graph.json cap test.
tests/test_affected_cli.py Use seed_graph fixture for affected CLI tests.
tests/nxcompat.py Add NetworkX-like shim backed by MemGraph for tests.
tests/conftest.py Add FalkorDB-backed fixtures (store, seed_graph, make_store) and session reachability check.
README.md Document FalkorDB extra and add FalkorDB flags to examples.
pyproject.toml Replace NetworkX dependency with FalkorDB; add lite extra; update all extra.
graphify/wiki.py Remove NetworkX import (GraphStore-based runtime).
graphify/watch.py Replace node-link NetworkX serialization with graphjson.to_node_link; build into store during rebuild.
graphify/skills/windows/references/update.md Update Windows skill reference for FalkorDB merge/diff flow.
graphify/skills/windows/references/query.md Update Windows skill reference to traverse GraphStore instead of NetworkX.
graphify/skills/windows/references/exports.md Update Windows skill reference for FalkorDB export steps.
graphify/skills/vscode/references/update.md Update VS Code skill reference for FalkorDB merge/diff flow.
graphify/skills/vscode/references/query.md Update VS Code skill reference to traverse GraphStore instead of NetworkX.
graphify/skills/vscode/references/exports.md Update VS Code skill reference for FalkorDB export steps.
graphify/skills/trae/references/update.md Update Trae skill reference for FalkorDB merge/diff flow.
graphify/skills/trae/references/exports.md Update Trae skill reference for FalkorDB export steps.
graphify/skills/pi/references/update.md Update Pi skill reference for FalkorDB merge/diff flow.
graphify/skills/pi/references/query.md Update Pi skill reference to traverse GraphStore instead of NetworkX.
graphify/skills/pi/references/exports.md Update Pi skill reference for FalkorDB export steps.
graphify/skills/opencode/references/update.md Update OpenCode skill reference for FalkorDB merge/diff flow.
graphify/skills/opencode/references/query.md Update OpenCode skill reference to traverse GraphStore instead of NetworkX.
graphify/skills/opencode/references/exports.md Update OpenCode skill reference for FalkorDB export steps.
graphify/skills/kiro/references/update.md Update Kiro skill reference for FalkorDB merge/diff flow.
graphify/skills/kiro/references/exports.md Update Kiro skill reference for FalkorDB export steps.
graphify/skills/kilo/references/update.md Update Kilo skill reference for FalkorDB merge/diff flow.
graphify/skills/kilo/references/exports.md Update Kilo skill reference for FalkorDB export steps.
graphify/skills/droid/references/update.md Update Droid skill reference for FalkorDB merge/diff flow.
graphify/skills/droid/references/exports.md Update Droid skill reference for FalkorDB export steps.
graphify/skills/copilot/references/update.md Update Copilot skill reference for FalkorDB merge/diff flow.
graphify/skills/copilot/references/exports.md Update Copilot skill reference for FalkorDB export steps.
graphify/skills/codex/references/update.md Update Codex skill reference for FalkorDB merge/diff flow.
graphify/skills/codex/references/exports.md Update Codex skill reference for FalkorDB export steps.
graphify/skills/claw/references/update.md Update Claw skill reference for FalkorDB merge/diff flow.
graphify/skills/claw/references/exports.md Update Claw skill reference for FalkorDB export steps.
graphify/skills/claude/references/update.md Update Claude skill reference for FalkorDB merge/diff flow.
graphify/skills/claude/references/exports.md Update Claude skill reference for FalkorDB export steps.
graphify/skills/amp/references/update.md Update AMP skill reference for FalkorDB merge/diff flow.
graphify/skills/amp/references/exports.md Update AMP skill reference for FalkorDB export steps.
graphify/skill.md Add FalkorDB flags and step list updates in core skill.
graphify/skill-windows.md Add FalkorDB flags and step list updates in Windows skill.
graphify/skill-vscode.md Add FalkorDB flags and step list updates in VS Code skill.
graphify/skill-trae.md Add FalkorDB flags and step list updates in Trae skill.
graphify/skill-pi.md Add FalkorDB flags and step list updates in Pi skill.
graphify/skill-opencode.md Add FalkorDB flags and step list updates in OpenCode skill.
graphify/skill-kiro.md Add FalkorDB flags and step list updates in Kiro skill.
graphify/skill-kilo.md Add FalkorDB flags and step list updates in Kilo skill.
graphify/skill-droid.md Add FalkorDB flags and step list updates in Droid skill.
graphify/skill-copilot.md Add FalkorDB flags and step list updates in Copilot skill.
graphify/skill-codex.md Add FalkorDB flags and step list updates in Codex skill.
graphify/skill-claw.md Add FalkorDB flags and step list updates in Claw skill.
graphify/skill-amp.md Add FalkorDB flags and step list updates in AMP skill.
graphify/report.py Remove NetworkX import (GraphStore-based runtime).
graphify/hooks.py Update hook comments to be backend-agnostic (no NetworkX mention).
graphify/graphjson.py Add node-link JSON helpers to avoid NetworkX round-trips.
graphify/diagnostics.py Build diagnostics against GraphStore instead of NetworkX.
graphify/cluster.py Replace NetworkX/Leiden/Louvain pipeline with GraphStore server-side Louvain UDF.
graphify/callflow_html.py Parse node-link JSON directly (no NetworkX) for HTML callflow visualization.
graphify/benchmark.py Run benchmark against the store (open_store) rather than parsing graph.json.
CHANGELOG.md Add release note for FalkorDB export backend.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pyproject.toml
Comment thread tests/conftest.py Outdated
Comment thread tools/skillgen/fragments/references/shared/update.md Outdated
Comment thread graphify/skills/windows/references/update.md Outdated
galshubeli added a commit that referenced this pull request Jun 17, 2026
Addresses the gaps found reviewing PR #1:

- affected.connect_graph now guards the empty-graph case (raises so the CLI
  prints "Re-run /graphify to build" + exits non-zero) instead of handing back
  an empty store and silently reporting "no unique node match". Parity with
  serve._connect_graph and the query/path/explain commands.

- tests: validate the hand-rolled GraphML writer emits well-formed XML
  (xml.etree parse), add a to_svg export test (skipped without matplotlib), and
  add an affected unbuilt-graph test that asserts a non-zero exit (covers the
  guard above).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
galshubeli added a commit that referenced this pull request Jun 17, 2026
- pyproject: add `falkordb` extra so the documented `graphifyy[falkordb]`
  install resolves (falkordb is a core dep; the extra just re-lists it).
- conftest: the FalkorDB reachability skip is no longer session-autouse — the
  store/seed_graph/make_store fixtures depend on it, so only DB-backed tests
  skip when no engine is reachable (DB-free tests still run; CI can't silently
  pass with 0 tests).
- update skill doc: the graph-diff step used a global `graphify_diff_scratch`
  graph name (data-loss if a user has that graph; collisions on concurrent
  runs). Derive a per-project + pid scratch name from the current graph name.
  Regenerated skill artifacts + blessed goldens.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@galshubeli
galshubeli requested a review from Copilot June 17, 2026 12:56

Copilot AI 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.

Pull request overview

Copilot reviewed 98 out of 99 changed files in this pull request and generated 3 comments.

Comment thread pyproject.toml
Comment on lines +61 to +64
# FalkorDB Lite: embedded in-process engine (redislite + bundled falkordb module),
# no external server. Requires Python >= 3.12. Select with a falkordb-lite:// URI
# or the GRAPHIFY_FALKORDB_LITE env var.
lite = ["falkordblite; python_version >= '3.12'"]
Comment on lines 219 to 223
if i < len(path) - 1:
_raw = G[nid][path[i+1]]; edge = next(iter(_raw.values()), {}) if isinstance(G, nx.MultiGraph) else _raw
edge = G[nid][path[i+1]]
rel = edge.get('relation', '')
conf = edge.get('confidence', '')
print(f' {label} --{rel}--> [{conf}]')
Comment on lines 219 to 223
if i < len(path) - 1:
_raw = G[nid][path[i+1]]; edge = next(iter(_raw.values()), {}) if isinstance(G, nx.MultiGraph) else _raw
edge = G[nid][path[i+1]]
rel = edge.get('relation', '')
conf = edge.get('confidence', '')
print(f' {label} --{rel}--> [{conf}]')
krishnateja7 and others added 22 commits July 11, 2026 11:31
…raphify-Labs#1784)

.rake files are plain Ruby (Rake's task DSL is ordinary method calls), but the
extension was gated out everywhere, so rake tasks were classified as
unsupported, skipped, and their calls invisible. Add `.rake` to all seven `.rb`
gates the reporter mapped:
  - detect.CODE_EXTENSIONS (classification)
  - extract._DISPATCH (extractor dispatch)
  - extract._LANG_FAMILY_BY_EXT-adjacent language-name map (.rake -> ruby)
  - the ruby_member_calls LanguageResolver suffix set
  - both `.rb`-suffix filters in ruby_resolution.py (raw-call gather + class-def index)
  - analyze language-stats map
  - build repo-tag map

The extractor already parsed the content; this is purely extension routing.
Regression test: a `.rake` task's `Widget.tally` resolves cross-file to the
`.rb` definition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
extract_bash only created a cross-file edge for `source x.sh` / `. x.sh`. The
two most common ways one script runs another — `bash x.sh` and `./x.sh` —
produced no edge, so in any repo where scripts invoke each other by execution
the call topology was missing (each script left an isolated file+entry pair).

Emit a `calls` edge (context `script_invocation`) from the caller's entry (or
enclosing function) to the invoked script's entry node, for script-runner
commands (bash/sh/zsh/ksh/dash <path>) and bare `./x.sh`, but only when the
target resolves to a real .sh file on disk — so no phantom edges to missing or
function-shadowed names. Verified end-to-end: the edges land on real target
nodes (no dangling drop at build).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt (Graphify-Labs#1768)

suggest_questions()'s "isolated/weakly-connected nodes" filter was missing the
`file_type != "rationale"` exclusion that report.py's Knowledge Gaps section
already applies, so the same GRAPH_REPORT.md reported two different counts for
the same concept (757 vs 245 on a real graph) — an internal inconsistency that
made a healthy graph look like a documentation problem. Add the same filter so
both computations agree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nts (Graphify-Labs#1785)

`graphify path` committed each endpoint to _score_nodes()[0]. The full-query
bonus tier only fires when the query equals/prefixes a label, so a query that is
a token subset of the intended label ("Reject-everything judge" vs "Degenerate
Reject-Everything Judge") got no bonus and a node prefix-matching one rare token
("Rejection Summary") could out-score it on IDF alone — anchoring the path on an
unrelated, often disconnected node and returning a false "No path found".

_pick_scored_endpoint() scans the score-ordered list and takes the first
candidate whose label contains EVERY query token, falling back to scored[0] when
none does — so when the head already full-matches (the common case) resolution
is unchanged. Wired into both the `path` CLI and the MCP _tool_shortest_path.
The close-runner-up ambiguity warning now fires only when the picked endpoint is
the raw score head (a full-token override was chosen on coverage, not score, so
the head's margin is irrelevant).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aphify-Labs#1789)

The absolute-path-in-node-ids leak reported on 0.8.19 is already fixed on v8:
detect() returns paths relative to the scan root, so the CLI-produced graph.json
uses relative structural node ids (portable, no username/home leak). Lock it with
a regression test that extracts the same corpus from two different absolute
checkout dirs and asserts identical, leak-free node ids.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y-Labs#1796)

build_merge already drops a re-extracted file's stale base nodes before merging
(replace-per-source), so on current code an EDITED file passed only in
new_chunks is handled correctly. But the prune step still removed every node
whose source_file was in prune_sources, with no guard for re-extracted files —
so a caller following the old edit-workflow (pass the changed file in BOTH
new_chunks and prune_sources) had its freshly-built nodes deleted after the
merge, silently losing a concept whose label survived the edit.

Exclude new_sources (files present in new_chunks) from prune_set: a re-extracted
file is being replaced, never deleted, so "replace" wins over a contradictory
"delete" of the same source. Genuine deletions (in prune_sources but not
new_chunks) still prune. Regression tests for both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Switch every website URL from graphifylabs.ai to graphify.com — the hero logo,
the Penpax section, and the waitlist link — across the main README and the
translated READMEs. The contact email stays on graphifylabs.ai (mail is hosted
there); no mailto links were changed. graphify.com is the official graphify
product site; graphifylabs.ai remains the company site (org profile + email).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Graphify-Labs#1795)

_reconcile_existing_graph treated "source identity absent from the collected
corpus" as deletion and evicted its nodes/edges/hyperedges. But corpus absence
is ambiguous: it's also what you see when a file still exists and merely stopped
being collected (ignore rules or filters changed). Upgrading into the merged-
.gitignore scan semantics (Graphify-Labs#1363) mass-evicted 655 nodes from a deliberately-
built, .gitignore'd docs dir whose files were present the whole time — reported
as a successful rebuild.

Fail-closed: before evicting a corpus-absent identity, require Path(identity)
.exists() is False (identity is an absolute path). Alive-but-excluded sources
are preserved (nodes, edges, hyperedges) and a loud line reports how many were
kept and why. True deletions and renames still evict (old path gone from disk);
a full extract --force still purges deliberate exclusions via the AST ownership
rule. Existence is memoized (one stat per file that left the corpus).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… twin (Graphify-Labs#1799)

The semantic pass mints a document node <slug>_doc; the markdown quick-scan
(extract_markdown) mints the bare <slug>. After a semantic build, a `graphify
update` (AST path) re-runs the quick-scan and the graph ends up with BOTH — one
document as two disconnected nodes, the file's edges split between them (semantic
`references`/hyperedges on the _doc twin, quick-scan cross-links on the bare
one). path/query traversals dead-end on the wrong twin; degree and communities
split.

build_from_json now reconciles the pair: when <slug> and <slug>_doc both exist
with the same source_file and both are file_type=document, remap the bare node
into the semantic _doc node (canonical, richer edges) and repoint its edges and
hyperedges. Remap-induced self-loops are dropped; pre-existing ones are left
alone. Gated to document twins for the same file, so a code symbol `foo` and an
unrelated `foo_doc` never merge. Regression tests for both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bs#1797)

querylog wrote every query/path/explain question + corpus path (and full
responses under GRAPHIFY_QUERY_LOG_RESPONSES) to a default-on, unbounded,
fail-silent plaintext file at ~/.cache/graphify-queries.log — outside any repo's
.gitignore/retention, and undocumented. A default-on plaintext record of
proprietary queries contradicts graphify's on-device / no-telemetry posture.

Flip to opt-in: _log_path() returns None unless GRAPHIFY_QUERY_LOG_ENABLE=1
(default path) or GRAPHIFY_QUERY_LOG=<path> is set; GRAPHIFY_QUERY_LOG_DISABLE=1
still forces it off (back-compat, wins). Document all four env vars in the
README (the old entries implied default-on). Regression tests cover
default-off, both enable paths, disable-wins, and that log_query writes nothing
without opt-in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ify-Labs#1789)

A solution folder is a virtual grouping, not a file: VS writes its name
as both the display name and the "path" (name == path, no real file).
extract_sln resolved it to an absolute filesystem path anyway and keyed
the node id off that. The CLI id-relativization pass only remaps ids of
real files in the scan set, so a virtual folder never matched and its
absolute id (with the local username) survived into a committed
graph.json.

Detect solution folders (name == path) and key their id/source_file off
the folder name only; real project files still resolve as before. Adds a
regression test asserting the folder node id is relative.

The earlier fix (0.9.13) covered .csproj/.sln file nodes but missed the
virtual folders, so Graphify-Labs#1789 was closed prematurely; this completes it.

Reported and diagnosed by @fremat79.

Co-Authored-By: fremat79 <fremat79@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nvs (Graphify-Labs#1804)

Graphify-Labs#1807 — piping graphify into a reader that stops early (head,
Select-Object -First N, sed q) disconnected stdout mid-write, raising an
unhandled BrokenPipeError (OSError(EINVAL) on Windows) and exiting 255,
so CI wrappers and agent harnesses read a successful query as a failure.
The console entry point now wraps the CLI body: a closed-pipe reader is
treated as success — stdout is redirected to devnull so shutdown flush
can't raise again, and the process exits 0. Adds a subprocess regression
test.

Graphify-Labs#1804 — .nox/ (nox virtualenvs, tox's successor, same .nox/ tree shape)
was missing from _SKIP_DIRS while .tox was present, so nox site-packages
got fully indexed (one repo came out 91% venv noise). Added next to .tox
with a regression test.

Reported by @varuntej07 (Graphify-Labs#1807) and @igorregoir-lgtm (Graphify-Labs#1804).

Co-Authored-By: varuntej07 <varuntej07@users.noreply.github.com>
Co-Authored-By: igorregoir-lgtm <igorregoir-lgtm@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-Labs#1810, Graphify-Labs#1809)

Graphify-Labs#1810 — detection read only .gitignore/.graphifyignore, never
.git/info/exclude, which is where git records local-only excludes and
where `git worktree add` writes nested worktree paths. graphify walked
into those worktree copies and the graph exploded (one 5-worktree repo:
9.4k nodes/10MB -> 210k nodes/311MB, ~77% duplicate). detect now loads
info/exclude at lowest precedence (below every per-dir .gitignore, per
git, so a nearer `!` still wins) and resolves the linked-worktree /
submodule case where `.git` is a file to the shared common git dir.

Graphify-Labs#1809 — two git-hook gaps: (a) post-checkout never honored
GRAPHIFY_SKIP_HOOK, so the var stopped commit rebuilds but not
branch-switch ones; now checked in both. (b) with core.hooksPath shared
across worktrees, a commit in any linked worktree fired post-commit,
which wrote a rogue delta-only graph.json into it and raced deploy/CI
`git clean` against the detached rebuild. Both hooks now short-circuit
in a linked worktree (git-dir != git-common-dir), comparing ABSOLUTE
paths so the primary checkout (where --git-common-dir is the relative
".git") is never false-positived and skipped.

Adds regression tests: info/exclude honored + negation precedence;
both hooks honor the skip env and carry the worktree guard; and an
end-to-end guard check against a real `git worktree`.

Reported by @cdahl86-cyber (Graphify-Labs#1810, Graphify-Labs#1809); the worktree guard was
co-developed with @Claude-Madera's PR Graphify-Labs#1806.

Co-Authored-By: cdahl86-cyber <cdahl86-cyber@users.noreply.github.com>
Co-Authored-By: Claude-Madera <Claude-Madera@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Graphify-Labs#1831 — `graphify export graphml` crashed on any dict/list-valued
attribute (per-node metadata dict, graph-level hyperedges list) because
nx.write_graphml only accepts scalars; a real ~2,300-node graph failed
every export and left a 0-byte .graphml behind. to_graphml now coerces
None->"" and JSON-serializes non-scalars across graph/node/edge scopes
(int/float/bool/str pass through), and writes atomically via a temp file
so a failed export can't leave a partial file. Closes Graphify-Labs#1830.

Graphify-Labs#1807 followup — adopt @varuntej07's explicit in-guard sys.stdout.flush()
from Graphify-Labs#1811: piped stdout is block-buffered, so a small fully-buffered
output would only flush at interpreter shutdown (outside the guard),
where a closed-pipe reader escapes as a noisy shutdown error and nonzero
exit. Flushing inside the try closes that gap. Closes Graphify-Labs#1811.

Reported by @hofmockel (Graphify-Labs#1831) and @varuntej07 (Graphify-Labs#1807/Graphify-Labs#1811).

Co-Authored-By: hofmockel <hofmockel@users.noreply.github.com>
Co-Authored-By: varuntej07 <varuntej07@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#1757 followup)

The Graphify-Labs#1835 fix scoped save_semantic_cache's final CLI write to an
allowed_source_files allowlist, but the per-chunk incremental checkpoint
in llm.py `_checkpoint_chunk` — the write that actually runs on every
`graphify extract`/`update` via extract_corpus_parallel — still called
save_semantic_cache with no allowlist. A chunk whose model result
mis-attributes a node's source_file to another corpus file would merge
that stray fragment into the victim's cache entry (merge_existing=True).

Scope the checkpoint write to the chunk's own dispatched files (FileSlice
-> .rel, bare Path -> the relative source_file). Also hoist the
`import warnings` in cache.py to module level.

Adds an extract_corpus_parallel integration test: a chunk dispatching
only A.py that returns a node attributed to already-cached B.py must
leave B.py's cache entry untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…phify-Labs#1766)

Many nodes sharing one generic label (framework route handlers all
labelled GET/POST, a repeated handler) consumed every BFS seed slot, so
query traversal explored near-identical neighborhoods and buried the
actual target. Seed selection now dedups by normalized label (GET/Get/get
collapse together), keeping one representative per label, and the per-term
guarantee loop honors the same cap so it can't reintroduce a dupe.

Adopts @devcool20's seed-dedup from Graphify-Labs#1832 but drops that PR's second
mechanism — a per-label multiplicity penalty applied inside the shared
_score_nodes. That scorer also resolves shortest_path/explain endpoints,
so dividing scores there silently reweighted path/explain (out of scope
for Graphify-Labs#1766 and able to flip endpoint selection); the dedup alone bounds
the flood. Also normalizes the dedup key (the PR keyed on the raw label).
Adds the tests the PR was missing: dedup of homonymous labels,
case/diacritic normalization, per-term-guarantee cap, and a guard that
identical-label nodes still score equally in _score_nodes.

Co-Authored-By: devcool20 <devcool20@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…raphify-Labs#1774)

With no explicit cache_root, extract() wrote graphify-out/cache/ under the
inferred common parent of the inputs — the analyzed source tree — so
scanning a read-only/foreign corpus silently polluted it.

The naive fix (point the root at CWD) breaks two other things that shared
the same parameter: file_hash keys become absolute/non-portable for an
out-of-CWD corpus, and the XAML/C# project-scan boundary would scan CWD
instead of the corpus. So split cache LOCATION from key/id ANCHOR:
load_cached/save_cached gain a cache_root arg for where the dir lives,
while `root` (inferred common parent) still anchors file_hash keys,
source_file relativization, node ids, and the XAML boundary. extract()
now locates the cache at CWD (or cache_root) but anchors on `root`; the
parallel worker tuple carries both. Existing callers passing cache_root
(CLI, watcher) are unchanged.

Adopts @SimiSips's Graphify-Labs#1802 (the CWD default + the two location tests) and
adds the decoupling plus a regression test that keys stay relative for a
corpus outside CWD — the property the one-line version would have lost.

Co-Authored-By: SimiSips <SimiSips@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…chor

Completes Graphify-Labs#1774. The prior fix redirected the AST cache dir to CWD but
file_hash still called _ensure_stat_index(root) without the cache
location, so the hash fastpath's stat-index.json kept anchoring on the
key-root (the analyzed corpus) — leaving a stray graphify-out/cache/
stat-index.json inside a writable foreign corpus even though the AST
cache itself had moved to CWD.

Thread cache_root through file_hash -> _ensure_stat_index (which already
accepts it, Graphify-Labs#1747). Surfaced by an out-of-CWD parallel-extract edge case:
the leak was masked in the in-process test suite because _stat_index_root
is a set-once module global that an earlier test had already pinned. The
regression test resets that global to simulate a fresh process and
asserts the corpus stays clean while the stat index lands under CWD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…raphify-Labs#1838)

The HTML report's neighbor "focus" links dropped an unescaped
JSON.stringify(nid) into a double-quoted inline onclick. The stringified
value carries its own quotes, so the attribute was truncated on every
node (links never worked), and a node id/label containing a double-quote
broke out of the attribute and injected live event handlers. AST ids are
[a-z0-9_]-safe, but ids/labels from documents or titles scraped via
`graphify add <url>` are not, so a hostile source could plant an
executable handler into a locally-opened report.

Carry the id in an HTML-escaped data-nid attribute and dispatch via one
delegated listener bound to document (survives the innerHTML rebuild that
recreates #neighbors-list). Closes the injection and repairs the links.

Reported by @edgestack-ai.

Co-Authored-By: edgestack-ai <edgestack-ai@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…scan root (Graphify-Labs#1206)

detect() only read .gitignore/.graphifyignore in the scan root and its
ancestor directories (up to the nearest VCS root), loaded once before the
walk began. A .gitignore sitting in a descendant directory — e.g.
vendor/sub/.gitignore — was never read, so files/dirs it excluded leaked
into the graph. Real git (and every other gitignore-aware tool) honors
.gitignore at every directory level, not just the ancestor chain.

Extracts the per-directory read+parse logic into a shared
_load_dir_own_ignore() helper (used by both the existing ancestor-chain
loader and the new call site) and invokes it live inside detect()'s
os.walk loop for every directory visited, before that directory's
children are pruned — so a nested ignore file governs its own subtree
with the same closer-file-wins precedence git uses.

Adds three regression tests: nested file exclude, nested directory prune
(the walk never descends into it), and nested negation overriding a
broader root-level rule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
hopstreax and others added 15 commits July 28, 2026 09:37
…Graphify-Labs#2206)

_extract_python_rationale / _extract_js_rationale sliced the raw
docstring/comment text to 80 characters before collapsing whitespace,
so the cut could land mid-word, leave a run of literal spaces where a
newline + indentation used to be, and, when the cut landed on a ".",
produce an Obsidian export filename ending in "..md".

Both _add_rationale sites now share _shorten_rationale_label, which
normalizes whitespace first via textwrap.shorten (word-boundary safe,
adds a placeholder only when it actually truncates) and falls back to
a plain character truncation when shorten collapses to a bare
placeholder -- which it does when the first word alone is already
>= 80 chars (e.g. a comment opening with one long URL), a case that
would otherwise regress to a content-free label.
…l args (Graphify-Labs#2241)

walk_calls flattens an inline/untracked arrow or function-expression argument
(one not separately tracked in function_bodies) onto the enclosing named
function's caller_nid, so its calls resolve as if made directly by that
function (Graphify-Labs#1630). But the closure's own parameters and locals were never
folded into the shadow set used to guard argument-based indirect_call
resolution, so a call argument inside the closure that happened to share a
name with an unrelated callable elsewhere in the corpus produced a fabricated
indirect_call edge, confidence 0.8 — even though the identifier was, in fact,
a local binding one lexical scope down:

  rows.map((r) => c.get(r))   // `r` is the arrow's own param, not a
                               // reference to some other same-named function

Single-letter names make this common, since they collide with same-named
symbols anywhere else in the repo (loop vars, test helpers).

Fix: thread an extra_locals set through walk_calls's recursion. Entering an
untracked closure folds that closure's own bindings (computed the same way as
a tracked function's, via _js_local_bound_names) into extra_locals for its
subtree only; deeper untracked closures compound the same way on their own
recursion. All six call sites that build the caller's shadow set now union in
extra_locals, so the fix applies uniformly to the argument, collection, and
assignment/return capture paths already sharing that guard, not just the
argument one that surfaced it. Tracked closures (const-assigned arrows,
methods) are unaffected — they already get their own caller_nid and their own
correctly-scoped shadow set.

Scope: this fixes the shadow-set gap for closures. A `for (const x of xs)`
loop variable not wrapped in a variable_declarator is a separate, pre-existing
gap in the same shadow computation, already addressed by Graphify-Labs#1985 — not
duplicated here.
…y-Labs#2052)

self_type (`self: Logging with Database =>`, `this: T =>`) was never
dispatched on anywhere in the Scala extractor, so a trait/class's
structural precondition on its enclosing type produced zero edges, in
any context. The type node sits at a fixed position among self_type's
unnamed-field children (binder identifier first, type second when
present), and _scala_collect_type_refs already handles every shape
that position can take (type_identifier, compound_type for `with`,
refinement bodies) -- reused unchanged, one new dispatch branch.

Also add the new `requires` relation to DEFAULT_AFFECTED_RELATIONS,
mirroring how `indirect_call` was wired into blast-radius traversal
when it was introduced, so `graphify affected` follows it like the
existing inherits/mixes_in/embeds structural relations.

Covers: single type, `with`-compound, structural refinement (base
type only, matching how refinement bodies are already unscanned
elsewhere), the binder-only `self =>` shape (no requires edge),
coexistence with an unrelated `extends`, and a plain class without a
self-type (no spurious edge).
safe_name left stems like .env intact, so the vault wrote .env.md which
Obsidian treats as a hidden file — invisible in the explorer and as
unresolved wikilinks. Prefix with dot- (shared _obsidian_safe_stem for
vault + canvas). True label stays in the note body.

Fixes Graphify-Labs#2205
Assert .env / .gitignore become dot-env / dot-gitignore on disk and in
the canvas file nodes, so Obsidian cannot hide them again (Graphify-Labs#2205).
…ge 2

Stage 2's .env regex treated .env.example / .sample / .template / .dist
like live secret files and dropped them from the graph. Carve out those
suffixes for .env / .envrc basenames only — real .env.local etc. stay blocked.

Fixes Graphify-Labs#2184
…raphify-Labs#2243)

Follow-up to Graphify-Labs#1899. That fix taught the relativization pass to catch a NODE
whose id was minted from an absolute out-of-root path and give it a portable
"ext_"-namespaced id, by matching the node's own id against
_make_id(str(its source_file)). But several cross-file resolvers (Python
relative imports, C/C++/ObjC quoted #include) only ever emit an EDGE for an
import target, no node -- so when that target lives outside the scan root,
the belt-and-braces pass has nothing to learn the old->new id from, and the
edge keeps the raw _make_id(str(absolute_path)) slug forever. The scan path,
including the OS username, ends up in links[].source/target, and differs
between machines/checkouts even though the node id sets are identical.

_import_c also never stamped the transient `target_file` hint (Graphify-Labs#1814/Graphify-Labs#2169)
its Python/JS siblings already use for exactly this kind of cross-file
target canonicalization, so it could not benefit from that machinery either.

Fix, in the two places this root cause actually lives:

- _import_c now stamps target_file on a resolved #include, mirroring
  _import_python/_import_js.
- The id_remap pass that already walks target_file-stamped edges to
  canonicalize in-root-but-unscanned targets now also handles the
  out-of-root branch it previously skipped ("leave its ids alone"): an
  existing out-of-root target gets the same portable ext_-namespaced id an
  out-of-root NODE already gets, so an edge with no node of its own is
  covered too. A target that does not exist on disk still stays dangling,
  unchanged from before.

_portable_out_of_root_sf moved next to id_remap so both the new edge-target
branch and the existing node-level pass share one implementation.

Four tests in tests/test_extract.py: the out-of-root #include gets a
portable id instead of the raw slug, and its transient target_file hint
never leaks into the returned edge; the same corpus built from two
differently-nested checkout paths produces a byte-identical target (the
reported non-determinism, made explicit); an in-root, same-batch include
still resolves to the real node's id (negative/regression guard); and the
equivalent out-of-root Python relative import is fixed too, since the gap
was in the shared remap path, not language-specific.

Known limitation: this covers every current target_file-stamping resolver
(Python relative imports, C/C++/ObjC #include, JS/TS/Svelte/Astro/Vue
rescued imports). A resolver that mints a path-derived edge target WITHOUT
stamping target_file at all -- none do today -- would still leak; the fix
closes the gap in the shared mechanism, not a per-language allowlist.
…bs#2231, Graphify-Labs#2243)

Absolute/machine-slug ids still leaked into edge endpoints from producers
the target_file-stamp loop didn't reach. Three fixes: apply id_remap to
raw_calls caller_nid so module-top-level indirect_call sources canonicalize
(Graphify-Labs#2231); a general backstop in the final relativization pass that learns
_make_id(abs source_file) -> canonical id for every node and rewrites all
node ids and edge endpoints (in-root -> _file_node_id, out-of-root -> ext_),
suffix-aware for __entry; and target_file stamps on bash source/entry edges
so they ride the same canonicalization. No node id or edge endpoint now
carries the scan-root slug for any file in the batch. Builds on Graphify-Labs#2250.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ook rebuild (Graphify-Labs#2251)

_reconcile_existing_graph loaded graph.json inside a swallowing try, so a
graph that was merely unreadable (over the size cap or unparseable) was
silently replaced by the code-only extraction, in both the clustered and
--no-cluster hook paths (force made it worse). It now loads through the
fail-closed build._load_existing_graph and _rebuild_code refuses the write
(prints and returns False) on a load failure, matching the CLI path; the
--no-cluster write is now atomic with a protected-graph backup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
….example regression test (Graphify-Labs#2205, Graphify-Labs#2184)

Follow-ups on the cherry-picked Graphify-Labs#2242/Graphify-Labs#2232: an all-dots label ('...') no
longer produces an empty 'dot-' Obsidian stem (falls back to 'unnamed'),
and the .env.example carve-out gets the regression test it shipped without
(templates graphable, real .env still sensitive, secrets/.env.example still dropped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v8 split the monolithic __main__.py into a thin shim + graphify/cli.py.
The FalkorDB backend was written against the monolith, so cli.py merged in
clean but entirely un-ported: 84 NetworkX/graph.json references and zero
open_store calls. Since this branch drops networkx as a dependency, that
would have merged green and ImportError'd on a default install.

Resolution:
- __main__.py: take v8's shim; replay our 19 hunks onto cli.py's
  dispatch_command (8 applied, 11 hand-ported).
- serve/build/store/affected/export/benchmark: combine both sides, keeping
  v8's fixes (Graphify-Labs#2074 honest relations, #BUG1 call sites, #BUG2 seed-first
  truncation, Graphify-Labs#1504 re-key, Graphify-Labs#1749 cross-language guard, Graphify-Labs#1831 GraphML
  coercion, Graphify-Labs#2032 label disambiguation) on the store-backed paths.
- 70 generated skill artifacts regenerated from the two conflicted
  fragments; `skillgen --check` passes on all 134.
- uv.lock regenerated from the merged pyproject.

Store extended so upstream fixes survive the backend swap:
- edge_attrs_all(): _edge_attrs used LIMIT 1, collapsing parallel edges and
  defeating Graphify-Labs#2074's honest relation reporting.
- node_connections()/subgraph_render_data(): return the edge's own
  source_file/source_location and true orientation via startNode(r), so
  #BUG1 call sites and edge direction survive.
- incoming_edges()/member_nodes(): give affected's native path the #BUG1
  locations and Graphify-Labs#1669 member seeding the in-memory path already had.

Bugs found and fixed while porting:
- open_store derived the graph name from the PARENT of the output dir, so
  two graph dirs sharing a parent silently shared one FalkorDB graph.
- A relation-less edge was stored with relation="RELATED_TO", so readers
  could not distinguish "no relation" from a real one.
- god-nodes imported the removed affected.load_graph; affected/god-nodes
  also could not read pre-FalkorDB graphs that query/explain could.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
galshubeli pushed a commit that referenced this pull request Jul 28, 2026
…languages (Graphify-Labs#1581)

Cross-file name resolution folded case for every language, so `from pathlib
import Path` resolved to a shell script's `export PATH=...` node — one variable
becoming the corpus's #1 god-node (266 false incoming edges on a real repo),
polluting god-node rankings, affected blast-radius, and clustering. Reported
with a precise diagnosis by @sheik-hiiobd.

Case is semantic in Python/Rust/Go/Java/C#/Kotlin/Swift/Ruby/C/C++/JS/TS: `Path`
(class), `PATH` (env var), `path` (variable) are distinct. Fix gates folding by
language at the two resolution sites the repro exercised:

- global cross-file CALL resolver: index by exact case; a folded index is built
  only for case-insensitive-language nodes (PHP/SQL/Nim) and consulted only when
  the calling file is such a language.
- type-reference STUB rewire (_rewire_unique_stub_nodes): match stubs to real
  defs by exact case, with a folded fallback restricted to case-insensitive-
  language definitions — so a case-sensitive `PATH` can never absorb a `Path`.

For case-sensitive languages this only ever removes false edges. Concept/doc
dedup (dedup.py, guarded to non-code nodes) is intentionally left folding.
Regression tests: Python `Path` no longer hits shell `PATH`; a case-differing
cross-file ref doesn't resolve; exact-case resolution still works; PHP fold
preserved. Full suite 2777.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
galshubeli pushed a commit that referenced this pull request Jul 28, 2026
…raphify-Labs#1749)

The extraction spec forbids cross-language `calls` edges, and build already
dropped cross-language INFERRED `calls`. But `imports`/`references` had no such
guard: an unresolved Python `import time` resolved by bare stem (the Graphify-Labs#1504
old-stem alias) onto a `src/time.ts` file node, welding a polyglot repo's two
language halves together. In the reporter's repo three such edges were the only
bridge between 2409 Python and 1403 TS nodes, so every backend<->frontend
shortest path routed through time.ts, inflating its betweenness ~90x and making
it the #1 reported god node.

Hoist the interop-family map to a module constant and extend the edge-loop
guard to `imports`/`imports_from`/`references`. For these relations the edge is
dropped only when BOTH endpoints are known code languages of different families,
so a config/manifest -> code reference (unknown ext) is never mistaken for a
phantom. `calls` behavior is unchanged (still INFERRED-only, still drops when
either family differs). Regression tests: py->ts import dropped, ts->ts import
kept, config->code reference kept.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
galshubeli and others added 12 commits July 29, 2026 10:14
Clears the 20 failures CI reported on the merge commit.

Behavioral fixes (not test-only):
- build_from_json now records the scan root on the graph (`scan_root` meta) and
  build_merge reads it back when the caller omits root. This is the FalkorDB
  replacement for the `graphify-out/.graphify_root` marker; without it the
  skill's root-less --update never relativized absolute prune_sources and a
  deleted file's nodes survived as ghosts (Graphify-Labs#1571).
- to_graphml dropped graph-level attributes entirely, losing the hyperedge
  layer from every export; they are now emitted as for="graph" keys with the
  same Graphify-Labs#1831 scalar coercion as nodes/edges.
- find_import_cycles required the graph object to expose .simple_cycles(),
  so it crashed on a plain NetworkX graph passed in by a library caller
  (networkx exposes simple_cycles as a module function, not a method). The
  enumeration is a pure function of the edge list, so it moved to
  store.simple_cycles_from_edges with the FalkorDB UDF kept as the fast path.
- merge-graphs emitted directed: true; the combined cross-repo view is
  undirected, as the old nx.compose path produced (Graphify-Labs#1606). merge_node_link
  now takes directed=, defaulting to the first input's flag so the
  same-graph merge-driver still round-trips.
- benchmark could not read a pre-FalkorDB / --no-cluster graph.json; it now
  falls back to the same back-compat import serve and affected use.
- The MCP multi-project loader reported "could not connect to graph"; it is a
  not-found condition and now says so.

Test ports (v8 tests written against file-based loading):
- test_build_merge_hyperedges_and_prune: seed the store, pass graph_name; the
  two Graphify-Labs#1571 cases now pin the recorded scan_root instead of the marker file.
- test_extract: read edge direction from (u, v) — the _src/_tgt markers the
  undirected NetworkX storage needed no longer exist.
- test_export: build the dict/list-attribute graph as a MemGraph, the only
  shape that can carry non-scalar attrs (the engine stores scalars only).
- test_extract_cli: inject the committed semantic layer into the store, which
  is the incremental baseline now, not graph.json.
- test_src_layout_import_resolution: give each build its own store — without
  one, both builds target the default graph name and the second silently
  overwrites the first, comparing a graph with itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…fy-Labs#2277, Graphify-Labs#2279, Graphify-Labs#2291)

mcp 2.0.0 dropped mcp.types.AnyUrl and the Server decorator-registration
API graphify/serve.py relies on, so an unpinned resolve broke graphify-mcp
on every fresh graphifyy[mcp]/[all] install. Cap mcp>=1,<2 (resolves 1.29.0)
and starlette>=1.3.1,<2 in both extras; re-lock. Bump to 0.9.30. Porting to
the mcp 2.x API is a tracked follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oot (Graphify-Labs#2257)

Extractors mint node ids from the path STRING they are handed
(_make_id(str(path)), _file_node_id(path)), so an AST cache entry written
under root A embeds A's slug in every id and edge endpoint. save_cached
relativized only source_file, never the ids. Because extract()'s
id-remap / final-canonicalization passes key their rewrites off the
CURRENT run's paths, an A-derived id matches no key on a warm hit under
root B (a clone, a moved checkout, a second mount) and the stale
machine slug survives into graph.json. Distinct from Graphify-Labs#2231/Graphify-Labs#2243, which
fix producers on a cold run, and from Graphify-Labs#2199 (stat-index portability).

Entries are now stored root-anchored and re-anchored on read, the same
store-portable/re-anchor-on-load contract source_file (Graphify-Labs#777) and the
stat index (Graphify-Labs#2199) already use: _relativize_ids_in replaces the root's
contribution with a $graphify-root$ marker on write, _absolutize_ids_in
restores what the current run's extractor would mint on read. That is
the pre-remap form every downstream pass in extract() expects, so a
replay reproduces a cold run exactly and no other pass changed.

The anchor is derived per entry rather than assumed equal to the scan
root (normalize_id distributes over path joins), so a symlinked root or
relative inputs decompose exactly; only absolute root spellings may
anchor, or a relative root ("src") would rewrite an already-canonical
src_utils_foo into an absolute-derived id on the semantic path. The
walk covers the whole payload rather than a bucket list, since the id
form is self-identifying: that also reaches raw_calls[].caller_nid,
swift_extensions[].nid, edges[].target_file, bash_sources[].source_file
and *_type_table.path, the last three being resolution inputs that
would otherwise still point at root A. save_cached's deepcopy is now
unconditional; the old truthiness gate skipped it for a payload whose
only content lives outside nodes/edges, which would have let the
transform mutate the caller's dict and break cold-run remapping.

Pre-fix entries carry no marker and their content hash never changes,
so they cannot self-heal; they are swept when the release bumps the
version, since AST entries live under cache/ast/v{version}/.

Tests: extract a python/C/bash/markdown corpus under root A, copy the
tree and graphify-out to root B, extract under B on the warm cache, and
assert the run is genuinely warm (zero extractor calls), that no node id
or edge endpoint carries A's slug, that the on-disk entries hold neither
A's slug nor an absolute path, that a cold run still yields canonical
ids, and that warm and cold match exactly. The fixture avoids JS/TS on
purpose: those suffixes bypass the cache, which would make the warm
assertions vacuous.
Converse returns output.message.content as a list of blocks and does not
promise a text block is first. Reasoning-capable models emit a
reasoningContent block ahead of the answer, and toolUse or future block
types can precede it too, but both bedrock call sites indexed position 0:

    content", [{}])[0].get("text", "{}")

For those models the default was returned on every call, so _parse_llm_json
saw an empty object, _response_is_hollow reported a hollow result,
finish_reason was rewritten to "length", and the adaptive retry bisected the
chunk. Splitting could not converge because the position assumption fails
identically at every chunk size, and raising GRAPHIFY_MAX_OUTPUT_TOKENS did
nothing because output length was never the constraint. stopReason on those
responses was end_turn, i.e. the model had answered correctly.

Selection now keys on the block's shape rather than its position, at both
_call_bedrock and the bedrock branch of _call_llm. A response whose first
block is already text -- every non-reasoning model today -- is unaffected.

On a 48-document corpus the hollow warnings and the bisection to the
recursion cap disappear, the 17 files previously reported as producing no
nodes are extracted, and output tokens drop from 217,538 to 53,274 as the
wasted retries stop.

Fixes Graphify-Labs#2287
The two bedrock-runtime clients (primary extraction in _call_bedrock and
the secondary dispatch path in _call_llm) were built with no botocore
config, so Converse used botocore's 60s default read timeout and ignored
GRAPHIFY_API_TIMEOUT / --api-timeout entirely. A long opus-class
generation then died with "Read timeout on endpoint URL" no matter how
high the timeout was set.

Both client constructions now pass a botocore.config.Config wiring
read_timeout to _resolve_api_timeout() (default 600s), a 10s
connect_timeout, and retries from _resolve_max_retries() in adaptive
mode. This mirrors the fixes that closed the same gap for the claude-cli
subprocess (Graphify-Labs#1112/Graphify-Labs#1111) and the secondary LLM dispatch path (Graphify-Labs#1442) --
bedrock was the last cloud backend still ignoring the knob.

Also updates the README env-var row, which listed the timeout as
applying to HTTP/claude-cli/Anthropic only, and the _fake_boto3 test
fixture to register botocore.config and capture the client config so the
new coverage can assert the timeout is wired.
…ops leaking absolute-slug ids (Graphify-Labs#2262)

_parse_js_tree parsed .tsx with language_typescript(), so JSX misparsed and
error-recovery floated nested handlers to top level; the symbol-resolution
pass then emitted calls edges whose SOURCE was an absolute-stem id for a
caller that owns no node — a leak the 0.9.29 backstop (learns only from
nodes) can't see. Fix: use language_tsx() for .tsx; never emit a calls
use-edge from an unowned source (reattribute to the file node); and teach
the backstop stem-form prefixes so any node-less absolute-derived endpoint
canonicalizes. No node id or edge endpoint now carries the scan-root slug.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ig in the reasoning test (follow-up to Graphify-Labs#2283/Graphify-Labs#2288)

botocore max_attempts counts the initial call, so GRAPHIFY_MAX_RETRIES must
map to _resolve_max_retries() + 1 (a value of 6 -> 7 total attempts; 0 ->
1, i.e. no retry). Also stub botocore.config in the Graphify-Labs#2288 reasoning-model
test, which broke once Graphify-Labs#2283 added the botocore.config import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second merge: v8 moved 24 commits (0.9.28 -> 0.9.30) while the first was in
flight. Four files conflicted; three of the upstream changes land in code this
branch had already re-ported.

- serve: adopt v8's bounded context cache (Graphify-Labs#2268 — an unbounded dict retained
  every project a shared server ever served), adapted to the store model. The
  LRU + pinned-default structure is kept verbatim; the entry key becomes the
  graph's (nodes, edges) rather than graph.json's (mtime, size), since the
  store is a live connection with no file to stat and a FalkorDB-only project
  may have no graph.json at all. Only the derived `communities` map can go
  stale, which is what the key guards.
- serve: split _connect_graph into a CLI flavor (print + exit) and
  _connect_graph_or_raise. The MCP path needs the exception, not an exit code,
  to tell "no graph built here" from "the graph.json here is corrupt" — and to
  keep one bad project_path from killing a server serving other projects.
  A corrupt artifact now says so instead of reporting it as not-found, which
  would have sent the user to rebuild a graph they already have.
- build/cli: v8's Graphify-Labs#2261 _src/_tgt rewrites in prefix_graph_for_global and
  merge-graphs have no analogue here. Those markers existed only to recover
  direction from undirected NetworkX storage; edges are stored in their native
  source->target orientation, so prefixing both endpoints keeps direction
  correct by construction. Documented at both sites so the next merge does not
  re-litigate it.

Tests ported to the same reasoning: the Graphify-Labs#2261 case now asserts direction
survives prefixing directly (has_directed_edge) rather than via the markers,
and the LRU test counts _connect_graph_or_raise, the load path the cache
actually calls on a miss.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.