fix: TS/JS object-literal repository methods get their own graph node#2111
Open
nn-tmg wants to merge 1225 commits into
Open
fix: TS/JS object-literal repository methods get their own graph node#2111nn-tmg wants to merge 1225 commits into
nn-tmg wants to merge 1225 commits into
Conversation
…es (Graphify-Labs#1666) krishnateja7 reported that on a full-repo run a stable subset of Ruby files yields zero nodes (not even a file node), each fine in isolation, drop set byte-stable across runs. Root cause is a transient batch/parallel extraction that produces an empty result, which then gets cached and persists. Every extractable file yields at least a file node, so a zero-node result is anomalous. Both extraction paths (parallel worker and sequential fallback) now skip the cache write when a non-error result has no nodes, so a rerun re-extracts and self-heals instead of loading the stale empty. extract() also warns, listing the files that landed in the graph with zero nodes, so the previously-silent blindness in affected/explain is visible. This addresses the persistence and the silent blindness. The underlying trigger (why a valid file occasionally extracts empty when co-processed with certain others) was not reproducible with synthetic corpora; the warning now surfaces it for a concrete report if it recurs. Full suite: 2912 passed, 3 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y-Labs#1241) `_dynamic_import_js` emitted a deferred `import('./x')` as a plain `imports_from` edge, so `find_import_cycles` counted it as a static import. A file that statically imports another which dynamically imports it back was reported as a phantom circular dependency. Keep the edge as `imports_from` (the dependency stays visible in the graph) but mark it `deferred`, and skip deferred edges in `find_import_cycles`. Closes Graphify-Labs#1241
…d/mixed-case extensions (Graphify-Labs#1671)
…aphify-Labs#1241 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…od-bound callers in affected (Graphify-Labs#1668, Graphify-Labs#1669) Graphify-Labs#1668: Ruby `include`/`extend`/`prepend <Const>` in a class/module body now emits a `mixes_in` edge to the module. The mixin is captured during the node walk and resolved cross-file by resolve_ruby_member_calls (single-owner guard, reusing the Graphify-Labs#1640 module nodes as targets). The shared call pass skips these markers so they are not mislabeled as `calls`. `extend self` and non-constant args are skipped; ambiguous/undefined modules produce no edge. Rails concern composition is now visible to affected/explain. Graphify-Labs#1669: affected <Class> seeds the reverse walk with the root's own member nodes (one method/contains hop) so callers that bind at method granularity (e.g. Service.call -> the def self.call node, Graphify-Labs#1634) are reachable from the class. method/contains stay out of the general relation-filtered walk (no forward noise), and the seeded member nodes are not reported as hits. Full suite: 2924 passed, 3 skipped. Verified end-to-end (Rails-shaped repros) plus edge cases: extend self / undefined / ambiguous mixins emit nothing, mixins are not emitted as calls, member methods aren't reported, class-level callers still resolve, and one-hop seeding does not pull in downstream classes' methods. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the old v4-hosted SVG wordmark with the new brand logo (graph-cube icon + "Graphify" on the green brand gradient), tightly cropped from the source export (1384x645, ~2.15:1, even ~90px padding). Served from docs/logo.png on v8. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a hero screenshot of the interactive graph (FastAPI codebase, community coloring) right after the tagline so the value prop is shown, not just told. Converts the Benchmarks prose into a compact scannable table (LOCOMO recall/QA, LongMemEval, zero-credit build) while keeping the judge-validation credibility line and the link to BENCHMARKS.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ils> The 20+ row platform-install table and the optional-extras table (~60 lines combined) sat between Install and the value content. Wrapping both in collapsible <details> blocks keeps a skimmer moving to the report/commands sections while each platform's command stays one click away. No content changed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
detect.classify_file already labels extensionless files with a bash/python/ node/... shebang as CODE via _shebang_interpreter, but _get_extractor dispatched purely on path.suffix — so a CLI entry point like `devctl` or `manage` was detected as code and then silently contributed zero nodes to the graph (its doc-referenced symbols stayed dangling stubs). Resolve extensionless files through the same _shebang_interpreter and a new _SHEBANG_DISPATCH map. Only interpreters with a real extractor are mapped (python/bash-family/node/ruby/lua/php/julia); detect's wider set (perl, fish, tcsh, Rscript) stays unmapped and skipped rather than being mis-parsed by a wrong grammar. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parity with _extract_python_rationale: Python files get rationale nodes
from docstrings and '# NOTE:'-style comments, but JS/TS comments were
discarded entirely. This adds a post-pass to extract_js that:
1. extracts rationale comments ('// NOTE:', '// WHY:', block-comment
'* NOTE:' variants) as rationale nodes with rationale_for edges,
matching the Python behavior;
2. first-classes architecture-decision references (ADR-NNNN, RFC NNNN)
found in comments as doc_ref nodes with 'cites' edges from the file.
The doc_ref pass is the natural join point between code and design docs
in mixed corpora: teams conventionally cite ADR ids in file headers, but
today those citations produce no edges, so code<->ADR connections never
form even when the discipline exists. Spellings are normalized
(ADR-11 / ADR 0011 -> ADR-0011) so references to the same document
collapse to one node, and string literals are excluded (comment-shaped
lines only).
Tested on a real mixed corpus (Flutter/Supabase monorepo): router.ts
alone yields 10 ADR citations that previously produced zero edges.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
extract_pascal() already imports tree-sitter-pascal for AST-quality extraction and falls back to a regex extractor when it is absent (Graphify-Labs#781), but the grammar was not declared anywhere in the package metadata, so it was never installed and the AST path never ran out of the box. Declare a `pascal` extra (and add it to `all`) so users can opt into the AST extractor with `uv tool install "graphifyy[pascal]"`. tree-sitter-pascal publishes prebuilt wheels for every platform (win/macOS/Linux), so unlike the `dm` extra it needs no C toolchain. On a mid-size Delphi codebase the AST path yields notably more accurate relationship edges than the regex fallback (calls and inherits both up ~25%). README extras table and uv.lock updated accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y-Labs#1603) Adds a _JAVA_BUILTIN_TYPES skip list so ubiquitous java.lang/util/io/time/etc. type names (String, List, Map, Optional, ...) are not emitted as references edges (they never resolve to a project node). Mirrors _GO_PREDECLARED_TYPES / _PYTHON_ANNOTATION_NOISE. Nested user-type generic args still resolve. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…angelog for Graphify-Labs#1603 Applied the .claudeignore troubleshooting entry from Graphify-Labs#1539 manually (the PR branched from an older README). Closes Graphify-Labs#1387. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
17 fixes/features since 0.9.6. Highlights: - Ruby: mixes_in edges for include/extend/prepend (Graphify-Labs#1668) and affected <Class> reaching method-bound callers (Graphify-Labs#1669); constant-receiver call resolution hardening continues from Graphify-Labs#1634. - Extensionless shebang CLIs are now extracted (Graphify-Labs#1683); JS/TS rationale + ADR/RFC doc refs (Graphify-Labs#1599); Java stdlib types dropped from references noise (Graphify-Labs#1603); pascal optional extra (Graphify-Labs#1616). - Incremental/detect correctness: Office source edits re-enter --update (Graphify-Labs#1649), Windows long-path hashing (Graphify-Labs#1655), word-count caching (Graphify-Labs#1656), zero-node results no longer cached + warned (Graphify-Labs#1666). - JS/TS phantom cross-package calls edge killed (Graphify-Labs#1659); Windows skill name + OpenCode plugin separator + doc-corpus report noise (Graphify-Labs#1635/Graphify-Labs#1646/Graphify-Labs#1657); case-insensitive suffix dispatch (Graphify-Labs#1671); postgres URI on Windows (Graphify-Labs#1672); deferred import() not a cycle (Graphify-Labs#1241). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…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 (Graphify-Labs#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>
…fy-Labs#1685) `_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 (Graphify-Labs#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 (Graphify-Labs#1690, Graphify-Labs#1694) Two related fixes in the community-labeling path: Graphify-Labs#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. Graphify-Labs#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 Graphify-Labs#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 (Graphify-Labs#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 (Graphify-Labs#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 (Graphify-Labs#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 (Graphify-Labs#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>
…s#522) 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>
…oo (Graphify-Labs#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 (Graphify-Labs#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 Graphify-Labs#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 (Graphify-Labs#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 (Graphify-Labs#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.
…raphify-Labs#2068) build_from_json's Graphify-Labs#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 Graphify-Labs#1145 while making cross-directory false merges impossible. This subsumes the Graphify-Labs#1753/Graphify-Labs#1257 cross-file ambiguity guard (now removed as dead code). Independent of the Graphify-Labs#2032 label pass. Updated the Graphify-Labs#1257 test to the now-correct precise merge.
…out) (Graphify-Labs#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/.
…arial-review fixes) Three issues found reviewing Graphify-Labs#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.
…phify-Labs#1540) `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.
…ify-Labs#2071) The top-level `graphify --help` has listed --code-only since Graphify-Labs#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 Graphify-Labs#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 (Graphify-Labs#849, Graphify-Labs#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.
`graphify explain "<node>"` sorts a node's connections by neighbor degree and shows only the top 20, appending a bare "... and N more" for the rest. On a high-degree node (a logging/error helper called from dozens of places is typical) that leaves the exact question explain is meant to answer - "who calls this, what's the impact?" - unanswered for 97% of the callers, with nothing pointing at where they live (Graphify-Labs#2009). The top-20 list and its ordering are unchanged (no behavior change for nodes at or under the cutoff). Past it, the cut connections are now grouped by (direction, source_file) with counts and printed under a "Grouped by file:" section, sorted by count descending, so the caller/callee distribution is visible without falling back to a repo-wide grep. The aggregation itself is capped at 20 files with its own "... and N more files" line for the pathological case where the remainder is spread across more files than that. Full per-caller detail behind a flag (--callers --all / --group-by=dir as proposed in the issue) is left for a follow-up: it's a larger, more opinionated surface (flag naming, pagination semantics) than the literal complaint needs, and the default output no longer hides the answer either way. Four regression tests in tests/test_explain_cli.py: the pre-existing truncation notice on a 30-connection node is unchanged, the grouped- by-file output carries real counts (3 files, one at 4 and two at 3) that sum back to exactly the cut total (no silent loss), a byte-for-byte no-op check for nodes at/under the cutoff, and a boundary check pinning the cutoff itself at exactly 20 connections (no section) vs exactly 21 (one grouped entry) — the earlier tests sit well clear of the edge and wouldn't catch a future off-by-one.
…raphify-Labs#2082) `from pkg import mod as alias` correctly emits the file-level `imports_from` edge, but every downstream `alias.func()` call was dropped: the module arm of the cross-file member-call resolver (Graphify-Labs#1883, _resolve_python_member_calls in extract.py) matches a call receiver against the imported module's own file stem, with no awareness that the local binding in the importing file can be a different name. `from pkg import mod` / `mod.func()` resolves because the receiver ("mod") equals the stem; aliasing breaks the match because the receiver ("alias") never does, and the calls edge silently disappears while imports_from stays present -- the graph looks connected, only the symbol-level reverse query comes back empty. `import pkg.mod as alias` regresses the same way through the same resolver. Root cause is a missing propagation, not a missing feature: the local alias is already parsed correctly in two places (_python_imported_names in extractors/resolution.py, and the aliased_import branch of _import_python in extract.py) but discarded before it reaches the edges the module arm reads. Fix threads the alias through as a `local_alias` field on the `imports`/`imports_from` edge (mirroring the existing `target_file` transient-hint pattern, Graphify-Labs#1814, including its pop-once-consumed hygiene so the hint never reaches graph.json): - _import_python now splits the alias off `import pkg.mod as alias` and stamps it on the edge instead of only using it to compute the bare module_name. - _SymbolResolutionFacts.module_imports gains a 4th `local_name` slot so the `from pkg import submod [as alias]` submodule path (Graphify-Labs#1146) carries the binding through to _apply_symbol_resolution_facts, which now stamps `local_alias` on the edge whenever it differs from the submodule's own stem. - The module arm's receiver match now accepts the tracked alias in addition to the module's real stem, keyed per (importing file, target module) so two files aliasing the same module differently each match their own binding. - extract() pops `local_alias` off every edge right after run_language_resolvers runs, and build_from_json drops it from edge attrs too -- the same two spots target_file is dropped at (Graphify-Labs#1814), except the extract()-side pop has to happen AFTER the resolver reads the field, not at the earlier point _disambiguate_colliding_ node_ids already pops target_file: that function runs before run_language_resolvers, so popping local_alias there would strip it before the resolver ever sees it and silently undo the fix above. Known limitation, left out of scope: two different aliases bound to the same module in the same file only resolve the last one registered, since the match is one alias slot keyed per (importing file, target module) -- not a regression, since the parent resolved neither. Adds five regression tests in tests/test_extract.py: the issue's own shape (`from pkg import gate as m_gate`), its try/except-guarded variant (the issue's literal repro, confirming the drop is independent of try: nesting), the adjacent `import mathlib as m` and dotted `import pkg.gate as g_alias` forms, and the relative `from . import gate as r_gate` form -- plus an assertion that `local_alias` never survives into the returned edges. All fail on the parent commit with the exact missing-edge symptom and pass after the fix. Full suite: 3554 passed vs. 3549 on the unfixed parent, a delta of exactly these five new tests; ruff clean. Graphify-Labs#2080's calls-edge-direction regression tests (test_serve.py) still pass unchanged.
…hify-Labs#2076) The claude-cli backend delivers the extraction schema in the user turn and trusts the model to emit raw JSON. Newer Claude Code releases treat that prompt as an agentic task and report the result in prose instead ("Knowledge graph extracted — 21 nodes, 20 edges…"), so the graph parses empty, reads as truncation, and adaptive-retry bisects without ever converging. Pass --json-schema (structured output) when the CLI advertises it — probed once via `claude --help` and cached — so the object shape is constrained regardless of prompt framing. Older CLIs that predate the flag keep the user-turn prompt as a fallback. The `result` envelope still carries the JSON string, so the parse path is unchanged.
…e handling (follow-up to Graphify-Labs#2102)
const repo = { findX(...) {...}, y: async () => {...} } at module scope
produced exactly one node, for repo itself. The module-level-const branch
of _js_extra_walk creates that node then unconditionally returns True,
short-circuiting the caller's default recursive descent, so the object's
own pair/method_definition children were never visited by any code path
and never became nodes. Every call into or out of such a method was
invisible to affected/explain/the call graph.
This is distinct from Graphify-Labs#2023/Graphify-Labs#1985 (edge resolution to nodes that already
exist) -- here the target node never existed to resolve to in the first
place.
Fix walks the object literal's direct children when the const's value is
type=="object": method_definition children (shorthand methods) and pair
children whose value is a function/arrow-function get a member node
scoped under the const's id, with a method edge back to it -- mirroring
the already-correct class-field-with-arrow-value handling just above.
New nodes are wired into callable_def_nids/local_bound_names/
function_bodies the same way every other callable node is, so downstream
call resolution and indirect-dispatch guards treat them consistently.
7 new tests. Full JS/TS suite (675 tests) and full suite (3344 tests,
excluding pre-existing environment-only gaps unrelated to this change)
pass clean.
Fixes Graphify-Labs#2110
Co-Authored-By: Claude Sonnet 5 <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.
Fixes #2110.
What's broken
const repo = { findX(...) {...}, y: async () => {...} }at module scope produces exactly one graph node — forrepoitself — and nothing for its methods. Every call into or out of such a method is invisible toaffected/explain/the call graph.This hits the common "repository object" pattern in TypeScript (
export const fooRepo = { findById() {...}, create() {...}, ... }), used instead of a class when there's no instance state.Root cause
In
graphify/extractors/engine.py, the module-level-const branch of_js_extra_walk(handlinglexical_declarationwith a value type ofobject/array/as_expression/call_expression/new_expression) creates a single node for the const and then unconditionally returnsTrue. That return short-circuits the caller's default recursive descent into the const's children, so the object literal's ownpair/method_definitionchildren — the actual methods — are never visited by any code path.The already-correct handling just above it, for class fields with function values (
class C { handler = () => {} }), walks each member and gives it its own node + amethodedge back to the class. This fix mirrors that pattern for the object-literal case.Fix
Walks the object literal's direct children when the const's value is
type == "object". Formethod_definitionchildren (shorthand methods) andpairchildren whose value is a function/arrow-function, creates a member node scoped under the const's id, with amethodedge from the const. New nodes are wired intocallable_def_nids/local_bound_names/function_bodiesthe same way every other callable node is, so downstream call resolution and the indirect-dispatch guard treat them consistently with any other method.Testing
tests/test_js_object_literal_repository_methods.py, 7 tests: shorthand + async shorthand + arrow-valued members all get nodes, the container still gets its own node, amethodedge exists from container to each member, an in-body call resolves to a free-standing helper, and a same-file external call torepo.findX()resolves to the member node. (Cross-file resolution through an imported plain-object binding — as opposed to anew Class()instance, whichtest_ts_receiver_member_calls.pyalready covers — is a separate, broader concern out of scope here, matching JS/TS extractor: instance-method calls (this.x.y() / obj.method()) never produce calls/indirect_call edges to the method node #2023/fix: indirect JS/TS call resolution for dispatch tables and callbacks #1985.)openai/tree-sitter-hcldeps, a wheel-build toolchain nuance) — none touch JS/TS.🤖 Generated with Claude Code