Falkordb backend - #1
Open
galshubeli wants to merge 579 commits into
Open
Conversation
There was a problem hiding this comment.
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/MemGraphplumbing 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.pyshim 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.
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>
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}]') |
…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>
…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>
…#2243/Graphify-Labs#2251/Graphify-Labs#2241/Graphify-Labs#2253/Graphify-Labs#2206/Graphify-Labs#2184/Graphify-Labs#2205/Graphify-Labs#2052 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>
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>
…phify-Labs#2262/Graphify-Labs#2257/Graphify-Labs#2287/Graphify-Labs#2284/Graphify-Labs#2261/Graphify-Labs#2268 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.