[pull] v8 from Graphify-Labs:v8#112
Open
pull[bot] wants to merge 222 commits into
Open
Conversation
…nest benchmarks table) Rework the top of the README into a self-contained pitch: - New logo + Trendshift badge; collapse the 31-language row into a toggle so the tagline and hero land in the first screen. - Hero screenshot of the FastAPI graph, tagline with selective bold. - 'What it does' capability table and a 'See it in action' section with verbatim explain/path output (real relations + confidence tags). - Benchmarks as a scannable table, honest: shows where competitors lead, only bolds rows graphify wins or ties, names the tie. - Collapse platform picker + optional extras into <details>; typography and identifier-formatting polish; fix orphaned paragraphs; reword the confidence-tag gloss; drop the redundant callflow CTA from the hero. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…vent CLAUDE.md data loss (#1688) The updater located its managed block by substring (`marker in content` and `next(... if marker in line)`), so a heading that merely appeared as a substring of another line, or a duplicate heading, matched the wrong offset and the rewrite could truncate or drop unrelated content in CLAUDE.md / AGENTS.md. It now matches the section heading exactly (`line.strip() == marker`), appends when the section is absent, and prefers the last exact match when several exist. Thanks @bdfinst for the report. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_TOKENIZER.encode(content)` raises ValueError by default when the text contains a special token such as `<|endoftext|>`, so a doc or corpus that merely mentions these strings crashed the entire semantic pass. Both `encode` sites in `_estimate_file_tokens` now pass `disallowed_special=()` so such text is tokenized as ordinary bytes. Thanks @Kyzcreig for the report. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… the stall (#1686) A stalled local model wedged for `timeout * (max_retries + 1)`, which with the default 6 retries turned one long stall into a ~21-minute block with no progress. `_call_openai_compat` now defaults the Ollama backend to zero client-side retries (a local model that stalls will not un-stall on retry); set `GRAPHIFY_MAX_RETRIES` to opt back in. Other backends are unchanged. This bounds the wait; the underlying stall is driven by the model server and is non-deterministic, so it is not eliminated. Thanks @Kyzcreig for the report. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n cost (#1690, #1694) Two related fixes in the community-labeling path: #1690 (thanks @vdgbcrypto): a truncated or slightly malformed reply no longer discards the whole batch with "Expecting value: line 1 column 6". `_parse_label_response` now salvages the complete `"id": "name"` pairs from a reply that failed a strict `json.loads` (e.g. one truncated mid-object), raising only when no pairs can be recovered. The per-batch token budget was also raised (256 + 48*n, was 64 + 24*n) so models that prepend a short preamble have headroom to finish the JSON. The exact provider truncation could not be reproduced without a live key; the parser and budget address the mechanism. #1694 (thanks @sub4biz): cluster-only mode reported a hardcoded `0 input * 0 output` token cost because the labeling LLM calls were never accounted for. `_call_llm` now accumulates per-response usage into an optional accumulator threaded through the labeling path and surfaced in GRAPH_REPORT.md. Backends that do not return usage (the Claude Code CLI) contribute nothing, which is honest rather than estimated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the Hebrew translation under docs/translations/README.he-IL.md and links it from the language row (30 -> 31 languages). Merged from PR #1639 by @AdirBuskila; the language-row link was adapted to the current README structure by hand since the English README changed after the PR was opened. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An animated SVG that mirrors the real `graphify path` output: a terminal types the query on the left while the answer draws itself hop by hop across the knowledge graph on the right, ending on "3 hops. Zero files opened." Pure SMIL inside a self-contained dark card, so it renders inline on both GitHub themes with no JS and no external fonts. Palette is brand-only: muted emerald (sampled from the logo) as the single accent, no neon. Generated by scripts/gen_demo_path.py (re-run to regenerate; STATIC=1 bakes a still frame for QA). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r-broad filters (#1666) @krishnateja7 root-caused this precisely: the files were never reaching extraction, so the 0.9.7 no-cache-on-empty mitigation could not surface them. Two discovery-layer filters were the cause: (a) A bare `snapshots/` directory was pruned as a Jest/Vitest artifact, which killed legitimate code namespaces like a Rails `app/services/snapshots/`. It is now pruned only when it actually contains `.snap` files or sits directly under a JS test root (`__tests__`/`__test__`). `__snapshots__` stays unconditionally pruned. (b) `_is_sensitive` dropped files on a bare name-keyword hit (device_token.rb, passwords_controller.rb) even when `classify_file` had already resolved them to source code. A genuine programming-language source file is now exempt from the weak keyword heuristic, while real secret stores in data/config formats (credentials.json, secrets.yaml, .env, .pem, ...) are still caught — those route through the CODE path for manifest parsing but are deliberately not exempted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ault (#1621) @sub4biz verified against the live DeepSeek API that deepseek-v4-flash (and v4-pro) have thinking ENABLED by default, contradicting the built-in config's stale "non-thinking" comment (now corrected). The naive fix (mirror the kimi branch and force thinking off) is the wrong call: @sub4biz's production testing on real corpora found that disabling thinking removes a rare reasoning-leak failure — which the adaptive extraction/labeling retry already recovers from — but trades it for far more frequent benign truncation AND measurably lower extraction quality and file coverage, confirmed by a blind second reviewer. So thinking stays ON by default (quality/coverage), with a documented opt-in `GRAPHIFY_DISABLE_THINKING=1` for users who prefer run-to-run stability. Applies to reasoning-capable OpenAI-compatible backends at both extra_body sites (extraction + labeling). An explicit providers.json extra_body still wins, and the moonshot/kimi branch is unchanged (it must disable thinking or content is empty). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n Windows (#522) The hooks were inline POSIX bash (case/esac, [ -f ], single-quoted echo), which Windows cmd.exe/PowerShell cannot parse. On Windows the hook failed silently, so the "run `graphify query` before grepping/reading raw files" nudge was never injected and users fell back to manual /graphify. The detection logic (grep-command match; source/doc extension match; skip if the target is under the output dir; require graph.json to exist) moved into a shell-agnostic `graphify hook-guard <search|read>` subcommand, invoked via the absolute exe path resolved by _resolve_graphify_exe() — the exact pattern the codex hook already uses. A single console-script invocation has no shell syntax, so it parses identically under sh, cmd.exe and PowerShell. Behavior on macOS/Linux is unchanged: the nudge payload is byte-identical (compact JSON, same additionalContext text), matchers stay "Bash"/"Read|Glob" so install/uninstall still find and replace old hooks, and the command still contains "graphify". The graph-exists check now honors GRAPHIFY_OUT instead of the hardcoded graphify-out/ path. Codex stays a no-op there (hook-check) because Codex Desktop rejects additionalContext. Detection is fully unit-tested (ported test_read_hook.py + new test_search_hook.py, byte-identical output on POSIX); Windows execution itself is not testable in CI here, but the mechanism is now shell-independent by construction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…stic too (#522) The Gemini hook was a `python -c "..."` one-liner that (a) depended on a bare `python` being on PATH (frequently `python`/`py` or absent on Windows) and (b) embedded backticks + escaped quotes that Windows PowerShell mangles. Same fix as the Claude/Codebuddy hooks: it now invokes `graphify hook-guard gemini` via the absolute exe path. The gemini mode always returns {"decision":"allow"} (never blocks a tool) and appends the graph nudge as additionalContext only when graph.json exists — the BeforeTool contract Gemini expects, byte-identical to the old payload. It takes no stdin, honors GRAPHIFY_OUT, and the matcher stays "read_file|list_directory" so install/uninstall find and replace old hooks unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a wide matrix over _run_hook_guard covering the search/read detection
boundaries and the gemini BeforeTool contract:
- search: grep-family variants (grep/pgrep/egrep/fgrep/ripgrep), token matches
(rg/find/fd/ack/ag with trailing space), piped commands; and silence for
non-search commands, empty/missing/non-string command, 'find' without a
trailing space, 'ag' mid-word, top-level vs nested tool_input, non-dict
tool_input, and no-graph.
- read: source/framework extensions, uppercase and multi-dot names, Windows
backslash paths, glob patterns; and silence for .json (not .js), lockfiles,
images, extensionless files, an extension on a directory segment, targets
under the (default and custom-named) output dir, and no-graph.
- fail-open: malformed/empty/binary stdin and a throwing graph-existence check
never crash or block.
- gemini: always returns {"decision":"allow"}, nudges only with a graph, and
stays "allow" even if the check throws.
- dispatch/exit/encoding via real subprocess: missing/unknown mode exits 0
silently, every mode exits 0 (never blocks), and the read nudge's em dash
round-trips as valid UTF-8.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes: Windows PreToolUse/BeforeTool hooks for Claude Code, Codebuddy and Gemini CLI (#522); CLAUDE.md/AGENTS.md section-write data loss (#1688); tiktoken special-token crash (#1685); Ollama hang retry-multiplication (#1686); truncated community-label reply salvage (#1690); cluster-only labeling token/cost accounting (#1694); discovery-layer file drops from snapshots/ and name-keyword filters (#1666); deepseek thinking default + GRAPHIFY_DISABLE_THINKING opt-in (#1621). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sec quickstart - Badges reranked for click-through and split: lean top strip (PyPI, Downloads, Discord, LinkedIn, YC S26) with trust signals leading and social/company links at the tail; a "Community and links" footer (Discord, X, Sponsor, Book); CI badge dropped, book also linked from "Learn more". - Hero leads with three differentiator bullets, corrected to match the codebase: code is tree-sitter AST (deterministic, no LLM, local), while docs/PDFs/images/ video use your assistant's model or a configured API key for a semantic pass. Removed the inaccurate "local embedder" claim (there is no embedder; dedup is MinHash/LSH and there is no vector store) and qualified "zero LLM tokens" to the code path. "Not a vector index" is accurate. - Added a 30-second quickstart (install + graphify install + /graphify .) above the fold, moved "See it in action" above "What it does" so differentiation is shown before it is enumerated, and compressed the "works in" list to one line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… in _find_node (#1704) `_find_node` built its search term with `_search_tokens` (\w+ tokenization), so "blockStream.ts" became "blockstream ts" (space where the '.' was) while the node's stored `norm_label` keeps punctuation ("blockstream.ts"). The verbatim case is already rescued by the `term == label_tokens` tier (the node label tokenizes the same way), but that is a coincidence: if `label` and `norm_label` diverge, an exactly-typed punctuated label fails to resolve through `explain` even though `path`/`query` find it. Add a punctuation-preserving `norm_query` (`_strip_diacritics(label).lower()`) matched against `norm_label`/`bare_label` across the exact/prefix/substring tiers (and fed to the trigram prefilter so candidates are not missed). Purely additive, symmetric with how norm_label is stored. Regression tests cover the verbatim file-label case and the label/norm_label divergence case that only norm_query resolves. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ilently dropping them (#1689) Extensions like .r/.R (also .ejs, .ets) are in CODE_EXTENSIONS, so those files are classified as code and counted in the scan, but there is no entry for them in the extractor dispatch — so they produce zero nodes and are silently absent from the graph while the CLI still reports success. The #1666 zero-node warning deliberately skips them (it only fires when an extractor exists), so nothing surfaced the gap. extract() now emits a warning listing the offending extensions and counts ("N file(s) are classified as code but graphify has no AST extractor ...: .r (2)") so a primarily-R (or .ejs/.ets) corpus no longer looks fully mapped when it is not. Grouped by extension, fires only for files actually present with no extractor (today: .ejs, .ets, .r). Adding real grammars for these remains the follow-up; this removes the silent-data-loss now. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) Intermediate progress lines count against len(uncached_work) ("X/Y uncached files"), but the final line switched to total_files ("Y/Y files"), which includes cached hits and files with no extractor that never entered uncached_work. On a large corpus with unsupported-language files, the total jumped upward right after 99% with no explanation. Both the parallel and sequential final lines now report the same uncached_work denominator, so the count no longer appears to change mid-run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#1712) GRAPH_REPORT.md rendered the Community Hubs section as Obsidian wikilinks, but the `_COMMUNITY_*.md` notes they target are only created by the opt-in `--obsidian` export — and the report is written at build time, before any export runs. So on a default run every link dangled: inside an Obsidian vault they spawned phantom `_COMMUNITY_*` nodes in the graph view, and outside Obsidian they rendered as literal brackets that navigate nowhere. `generate()` now takes `obsidian: bool = False`; by default the hubs render as a plain list, and the wikilink form is emitted only when a caller opts in. The Obsidian export's own community notes already cross-link each other, so the vault stays navigable without the report's links. Mirrors the #1444/#1465 portability fix that was applied to `export wiki`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ently (#1692) When classify_file() returned None — an extensionless, non-shebang file (Dockerfile, Gemfile, Makefile, Rakefile, LICENSE, ...) or an unsupported extension — the file left no trace at all: not counted, not listed, nothing. A user had no way to tell from graphify's output that those files were even considered. detect() now collects these into an "unclassified" list in its result, and `graphify extract` prints a one-line summary after the scan counts: "N file(s) not classified (no supported extension or shebang), skipped: Dockerfile, Makefile, ...". Real code/docs are unaffected. This is the visibility half of the issue; wiring up extractors/manifest handling for Dockerfile/Makefile-style files remains a separate feature. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…an vault by default (#1681) The Usage block's bare `/graphify` comment read "full pipeline on current directory -> Obsidian vault", contradicting Step 6 (HTML viz always; Obsidian vault only when --obsidian is explicitly given). The comment predated the opt-in change and told agents a bare /graphify produces a vault. It now reads "full pipeline on current directory (HTML viz; add --obsidian for a vault)". Fixed at the skillgen source (fragments/core/core.md + the aider monolith override), added a sanctioned-diff predicate so the monolith round-trip guard allows the change, and re-blessed the expected/ snapshots. Every generated skill-*.md now carries the corrected comment; the only content change across all rendered files is this one line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mmar (#1702) `.m` is shared by Objective-C implementation files and MATLAB, but the extractor dispatch routed every `.m` to extract_objc unconditionally. Feeding real MATLAB to the Objective-C tree-sitter grammar yields root.has_error and garbage nodes/edges — worse than skipping, because it pollutes the graph with wrong data. _get_extractor now content-sniffs `.m` the same way `.h` already is: a genuine Objective-C `.m` carries an ObjC directive (@implementation/@interface/@import/ #import) and still routes to extract_objc; a `.m` without one (MATLAB, Octave) gets no extractor, so it is surfaced by the no-AST-extractor warning (#1689) instead of mis-parsed. `.mm` is unambiguously Objective-C++ and is left untouched. This stops the wrong-by-omission garbage; wiring a real tree-sitter-matlab extractor (the issue's primary ask) remains a follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes since 0.9.8: explain punctuated-label matching (#1704); surface code files with no AST extractor instead of dropping them silently (#1689); consistent AST-extraction progress denominator (#1693); no dangling Obsidian wikilinks in GRAPH_REPORT.md by default (#1712); MATLAB .m no longer force-parsed by the Objective-C grammar (#1702); corrected the /graphify usage comment in the skill files (#1681); surface unclassified files (Dockerfile/Makefile/...) instead of vanishing (#1692). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SECURITY.md said graphify never runs a network listener and only communicates over stdio. README.md documents an opt-in --transport http mode (with --host/--api-key flags) for sharing one server across a team, which does open a network listener. Update the claim to describe the default (stdio, no listener) and the documented opt-in exception.
…docstring _max_graph_file_bytes() resolves the graph-load memory-bomb cap and parses the GRAPHIFY_MAX_GRAPH_BYTES env var (plain bytes, MB/GB suffix, fallbacks), but had no behavioral tests — only the default constant was asserted. - Add tests for: unset/blank -> default, plain integer, MB/GB suffix (binary), case-insensitive and space-tolerant suffixes, unparseable -> default, and non-positive -> default. - Fix the docstring, which described the suffix as "decimal multipliers of 1024" (self-contradictory). MB/GB are treated as binary (MiB/GiB); the tests lock that behavior in. No behavior change.
…#2015) The --update runbook's Step 9 stamped the entire detected corpus into the manifest, so a semantic file (doc/paper/image) whose chunk failed or was omitted was recorded as done and never re-queued on the next update — losing its content permanently. The runbook now builds the manifest the way the library extract path does: cli._stamped_manifest_files stamps only files that actually produced nodes/edges/hyperedges, dispatched-but-empty files have their stale hash cleared, and scan_corpus drops newly-excluded in-root rows. Applied to the Claude, Aider, and Devin skill bodies and the shared update reference; all 134 artifacts regenerated. gen.py gains a sanctioned monolith- diff predicate for the new stamping lines.
…2051 follow-up) The #2051 disk-absence sweep guarded remote/virtual sources with a literal `"://"` check. But the write-side path normalization (Path.as_posix) collapses the double slash, so a stored `gdoc://x` reads back as `gdoc:/x` on the next update; the literal guard then missed it and the node fell into the disk-absence branch (`Path('gdoc:/x').exists()` is False), evicting it on the second `graphify update`. Match the scheme with a regex tolerant of the collapse, with a 2+ char scheme so a Windows drive letter (C:/) is not misread as remote. Regression test runs three consecutive updates and asserts the remote node survives every one.
0.9.22 carries the #2051 remote-source eviction follow-up.
…reachable json.JSONDecodeError subclasses ValueError, so the broader `except (ValueError, FileNotFoundError)` clause always matched first, making the intended "graph.json is corrupted (...). Re-run /graphify to rebuild." recovery hint dead code — users with a truncated graph.json got the bare json.JSONDecodeError message instead, contradicting the behavior SECURITY.md documents for this exact threat. Move the json.JSONDecodeError clause first so it actually catches. Audited the rest of serve.py's exception handlers for the same narrower-after-broader ordering bug; found no other instance. Fixes #2005.
…ession Per review on #2017 (thanks @HerenderKumar): add a guard asserting a non-decode ValueError (e.g. a non-.json path) still prints the plain "must be a .json file" message, not the corrupted-graph hint. Locks in the except-clause order from the parent fix so a future refactor can't silently collapse the two branches back together.
…path sidecars (#2058, #2059) #2058: `_is_noise_dir` treated any directory named `env`/`.env`/`*_env` as a Python virtualenv and pruned it during the walk — before `.graphifyignore` negation, with zero trace in any returned bucket. Real source dirs with those names (common in UVM/ASIC verification trees) were silently lost. The venv heuristic for those names is now gated on actual markers (`pyvenv.cfg`, `bin`/`Scripts/activate`, `lib/python*`, `conda-meta/`); `venv`/`.venv`/`*_venv` stay name-only. Pruned-as-noise dirs are recorded in a new `pruned_noise_dirs` bucket for traceability, and extract.py's walk call sites pass the parent so genuine venvs are still marker-checked and pruned. #2059: Office and Google-Workspace sidecars were named with a hash of the resolved ABSOLUTE source path, so the same tracked file in two clones/worktrees produced two differently-named byte-identical sidecars — unbounded duplicates when graphify-out/ is committed, each ingested as a distinct source doc. The hash is now over the scan-root-relative (NFC-normalized) path, stable across checkouts while still disambiguating same-stem files; out-of-root sources fall back to the old absolute form. Also fixes the same bug in google_workspace's `_sidecar_path` (which additionally never had the #1226 NFC fix).
#2004) Part 2: `god_nodes` was an analyzer, an MCP tool, and a README-advertised capability, but `graphify god_nodes` errored with "unknown command". Add a read-only `god-nodes`/`god_nodes` subcommand mirroring `affected` (--graph, --top, --json), routing labels through sanitize_label. Part 3: `--output DIR` on `extract` was silently dropped (output fell back to the default dir). It is now an alias of `--out` (both space and =forms), matching what `graphify tree` already documents. Help/usage text updated. Part 1 (affected/reverse-dep import-id mismatch) is deferred — a build-time id-resolution change, tracked separately.
#2040) In _extract_generic's class branch the `contains` edge was hard-coded to source from the file node, so a nested class/object/trait attached to the file instead of its enclosing type across ~19 languages (only C# even flagged the node with is_nested_type, and still emitted no edge to the parent). The edge now sources from parent_class_nid when set, else the file node — keeping the containment tree connected (file -> Outer -> Inner). A `!= class_nid` guard avoids a self-loop when same-name nesting collides ids (class ids omit the enclosing name). The C# is_nested_type flag is retained (load-bearing for cross-file resolution). Methods were already parent-sourced and are unaffected.
) In directory-per-entrypoint repos (Supabase Edge Functions, Next.js page.tsx, Rust mod.rs, Python __init__.py) many files share a basename, so basename-only file-node labels collided and `explain`/free-text discovery couldn't resolve them — exactly the highest-value files. build_from_json now runs a final pass that gives colliding-basename file nodes the shortest unique directory-qualified label (`process-order/index.ts`); unique basenames stay bare, and node ids/edges are never touched. The pass runs after the alias-competition (which still needs bare basenames), is idempotent (labels derive from source_file), and the downstream file-node predicates (analyze god-nodes, tree_html, serve lookup) recognize the qualified form via a shared _is_file_node_label helper.
…oo (#2032) The build_from_json label pass only covered the clustered/update paths; the raw `extract --no-cluster` path writes the merged node list directly, so colliding basenames stayed un-disambiguated there. Factor the logic into a shared _file_label_reassignments core with a list-based variant (disambiguate_file_labels_in_nodes) and apply it on the raw merged nodes. Caught by the clean-venv edge-case battery.
GitHub is the top organic source of waitlist signups, but the only path to the site was the logo and a CTA at the very bottom of the README. Add a short, plain-voice waitlist line right after the value proposition, and change the Enterprise-section CTA from "Learn more" to an explicit "Join the waitlist".
…ion (#2062) The uninstall strip used an unanchored regex `## graphify`, which matched inside a user's `### graphify` heading and deleted hand-written content; the `marker not in content` guard was a substring test that passed on the same mention. Add a shared `_remove_marker_section` helper that matches the heading only when a line is exactly the marker (mirroring the install-side #1688 hardening), running each section to the next same-level heading or EOF, and returning None (leave the file untouched) when no exact heading exists. Replace all six strip sites: GEMINI.md, copilot-instructions.md, AGENTS.md, CLAUDE.md (_strip_graphify_md_section), CODEBUDDY.md, and the H1 skill-registration (_remove_claude_skill_registration, which had the same bug with `# graphify`).
… calls (#2074) `graphify path` (and the MCP shortest_path tool) ran shortest_path over G.to_undirected(as_view=True), whose neighbor iteration is a hash-seeded set union, so among equal-length paths BFS returned a route that varied per process. Build a sorted, materialized undirected graph so the chosen path is canonical. The hop label also printed a relation read from an arbitrarily-collapsed parallel edge, so it could show `calls` on a pair that only carries `references`. Force multigraph on the cli path reload so parallel links survive, and render the ACTUAL stored relation(s) via edge_datas, falling back to an honest "related" when the edge has none. Serve's shared graph is left untouched (its degree feeds query-seed tie-breaks); the fix is applied locally in both path readers.
…al labels (#2073) A `cluster-only --no-label` run wrote "Community N" placeholders into .graphify_labels.json plus a matching .sig, and the reuse path treated them as fresh, so real labels were never regenerated on later runs. Two fixes: (a) don't persist the labels sidecar (or its .sig) on a placeholder-only run, so a later run generates real labels; (b) treat a stored "Community {cid}" as absent in the reuse path so an already-polluted sidecar self-heals via the hub labeler while genuine labels are still reused with no LLM call. The watch/update rebuild had the same placeholder-perpetuation twin — fixed alongside.
…2068) build_from_json's #1145 ghost-duplicate merge keyed on (Path(source_file).name, label), discarding the directory, so unrelated nodes from different files sharing a common basename (index.md, README.md, ...) and a generic label were silently merged onto one survivor with their edges rewired — corrupting multi-corpus doc graphs. The AST/LLM ghost twins the merge legitimately targets always share the same source_file, so keying on the full normalized source_file preserves #1145 while making cross-directory false merges impossible. This subsumes the #1753/#1257 cross-file ambiguity guard (now removed as dead code). Independent of the #2032 label pass. Updated the #1257 test to the now-correct precise merge.
…out) (#2072) Python absolute imports were resolved only against the scan root, and file-node ids are scan-root-relative, so a src-layout project (code under src/) lost most of its imports/imports_from edges when scanned from the repo root — the dangling edges were silently dropped, so the graph looked complete but wasn't. The chosen scan root thus silently changed the graph. Two fixes: (1) _resolve_python_module_path probes the scan root first, then walks up from the importing file toward the root so a nested package root (src/pkg) resolves (mirrors the Lua upward walk); (2) a Python post-pass detects each file's package root via its __init__.py chain and repoints absolute-import edge targets (dotted-module id -> real file-node id), guarded against shadowing an existing id and against ambiguous aliases claimed by >1 file. Result: byte- identical import edges whether scanned from the repo root or from src/.
…fixes) Three issues found reviewing #2072: - The import-edge repoint loop matched by target id regardless of the edge's language, so a non-Python dotted import (C# `using Pkg.Mod;`, Java/Go) whose dangling target coincided with a Python alias got repointed onto a Python file, fabricating a cross-language import. Gate the rewrite on the edge being Python-sourced. - The resolver ancestor walk probed package dirs too, resolving an absolute `from helpers import x` to a sibling in the current package (Python-2 implicit- relative semantics) even when `helpers` is external. Only probe sys.path-root candidates (ancestors without their own __init__.py). - Bound the __init__.py package-root chain walk by the path depth so a pathological `/__init__.py` can't loop. Added a cross-language-guard regression test; fixed the tautological (or->and) ambiguity assertion.
…n (benchmark) Two correctness defects found in a head-to-head benchmark of 0.9.22. Caller line numbers: explain/affected/get_neighbors/query printed the caller node's def line for an incoming call, presented as a precise citation, so click-through landed in the wrong place. The `calls` edge already carries the true call-site line (engine.py sets it); every caller/relation listing now reads the traversed edge's source_file:source_location, falling back to the node's own line only when the edge lacks one. Silent query truncation: rendered nodes were degree-ordered (a low-degree definition node ranked last, cut first), the queried symbol wasn't guaranteed to appear, and the truncation marker sat only at the end so silence read as absence. Nodes are now ranked by hop distance from the seeds (deterministic), the seed the question named is rendered first and never truncated, and a prominent TRUNCATED notice at the top states shown/total counts and how to widen the budget. Also rewires the seed-first ordering the renderer already supported — a branch merge had silently dropped the `seeds=` argument, leaving it dead code.
`uv run --with graphifyy python -m graphify` runs the system interpreter, so an older `graphifyy` in system site-packages loads before the uv-managed one — with no error. Env overrides like OPENAI_BASE_URL are then ignored and requests 401 against the default endpoint. The existing `skill is from graphify X, package is Y` warning is the real fingerprint, but users dismiss it as a stale skill. Add a troubleshooting entry next to the uvx-resolution one: name the symptom, show how to confirm which module actually loaded, and point at `uvx --from graphifyy` / uninstalling the stale copy.
The top-level `graphify --help` has listed --code-only since #1734, but the `graphify extract` usage string and the README never mentioned it. A user evaluating graphify on a "can this run with no network call" constraint sees the extract usage / README first and can conclude the flag doesn't exist. Add --code-only to the extract usage line, name it in the Privacy section and the command reference, and add a test asserting the usage advertises it.
get_neighbors and get_community render every edge/member line unbounded — on a god node or a large community that floods an MCP client's context window with 100KB+ of text in one tool result. query_graph already solves this with the ~3-chars/token cut in _subgraph_to_text; this applies the same budget rule to the two line-list tools via a shared helper (_cut_lines_to_budget): cut at a line boundary, report how many lines were dropped, and point at the narrowing path (relation_filter / get_node). Default 2000 like query_graph; output under budget is byte-identical to today.
…op + tests (follow-up to #2069)
`graphify query` builds an undirected nx.Graph (so BFS/DFS can explore both callers and callees of the seed node), but its text renderer assumed the BFS/DFS visit order (u, v) was always the edge's (source, target). On an undirected graph that assumption only holds when the seed happens to be the caller: seeding on the callee makes BFS/DFS visit the callee first, so a `caller --calls--> callee` edge was rendered backwards as `callee --calls--> caller`. graph.json's own source/target fields stay correct on disk; only the query rendering was wrong. `graphify path` and `graphify explain` don't have this problem because they force directed=True on load (#849, #853), and the MCP query_graph tool's _load_graph() does the same. Doing that for CLI `query` too was tried and reverted: forcing a DiGraph makes G.neighbors() return successors only, so a query seeded on a leaf/sink node (no outgoing edges) found zero neighbors instead of its callers — a recall regression, not just a display fix, and it would make the CLI and MCP query tools diverge in what they discover even though they'd render direction identically. Fix instead mirrors the _src/_tgt pattern graphify/build.py already uses for the same underlying problem (undirected storage loses direction): the CLI now stashes each link's true source/target on its edge data as _src/_tgt before constructing the (still undirected) graph, and _subgraph_to_text renders EDGE lines from _src/_tgt when present, falling back to (u, v) otherwise. Traversal itself is unchanged, so recall is unaffected — verified against the unpatched CLI, the node counts returned for the same seeds are identical before and after this fix, only the printed edge direction changes. Adds two regression tests in tests/test_query_cli.py seeding the same `calls` edge from both endpoints; the callee-seeded case fails on the prior code with the exact backwards-edge symptom above.
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )