Commit Graph
2108 Commits
Author SHA1 Message Date
rajatnagda45 282e2bbc56 fix(powershell): link enum members with case_of instead of contains
A PowerShell enum member is a discriminant case, not a declared field, so it
should hang off its enum with a case_of edge like every other language that
has enums (Java #1719, C#, Swift, Rust, VB.NET). PowerShell was routing enum
members through the same contains path used for class members.

The relation is not cosmetic: case_of targets are excluded from constructor
binding, so an enum member named like a type could previously be mistaken for
one.

Update the existing enum test to expect case_of and add a focused regression
test asserting enum members get case_of (and not contains) while the file
still contains the enum type via contains.

(cherry picked from commit 011dda9b0f)
2026-10-05 19:40:03 +01:00
rajatnagda45 4f0eb679bf fix(solidity): link enum values with case_of instead of contains
A Solidity enum value is a discriminant case, not a declared member, so it
should hang off its enum with a case_of edge like every other language that
has enums (Java #1719, C#, Swift, Rust, VB.NET). Solidity was the lone
remaining outlier, routing enum values through the same contains path used
for struct fields.

The relation is not cosmetic: case_of targets are excluded from constructor
binding, so an enum value named like a type could previously be mistaken for
one. Struct fields keep contains, since a field is a real declaration.

Add a regression test asserting enum values get case_of (and not contains)
while a struct field keeps contains.

(cherry picked from commit fdd99502d7)
2026-10-05 19:40:03 +01:00
safishamsiandClaude Opus 4.8 b57cf9108e docs: add GraphGuild community-program banner
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-05 17:46:04 +01:00
safishamsiandClaude Opus 4.8 35adf432b9 docs(readme): shrink the Translations section to a one-liner
The top-of-README language switcher already lists every language; keep
the bottom section as a short pointer to docs/translations/ and the
PR-to-contribute call, instead of duplicating the full table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-04 22:28:19 +01:00
safishamsiandClaude Opus 4.8 61f610d550 docs(readme): add a Translations section
List all 32 README languages in a dedicated Translations section (a
#translations anchor), like other large OSS READMEs, with a pointer to
docs/translations/ for improving or adding one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-04 22:19:38 +01:00
safishamsiandClaude Opus 4.8 0010e25d0d release: 0.9.76
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.9.76
2026-10-04 21:02:57 +01:00
Syed Fahad c37be6169c chore(packaging): point the PyPI homepage at graphify.com
The PyPI page listed the GitHub repo as both Homepage and Repository, so
nothing on PyPI linked to the project's website or docs. Homepage is now
graphify.com, docs.graphify.com is listed as Documentation, and Repository
still points at GitHub.

(cherry picked from commit 25a73bcaa6)
2026-10-04 21:02:36 +01:00
safishamsiandClaude Opus 4.8 76d3b88aee docs: correct two stale resolver comments
Follow-ups on #3975 and #4043:
- the PHP language-construct set cited _GO_PREDECLARED_FUNCS, which does
  not exist; the real analog is _LANGUAGE_BUILTIN_GLOBALS.
- _EXTERNAL_STUB_RELATIONS said `calls` to an external target is always
  suppressed (#3156); after #4043 an imported module does receive a
  `calls` edge, so clarify the actual invariant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-04 20:41:11 +01:00
Alex Tumanov 140cb590e7 test(extract): preserve isolation with external module calls
Require exact coarse calls to unresolved absolute imports while retaining
package and cross-directory local target isolation.

AI-Assisted-By: OpenAI Codex
(cherry picked from commit e1eeede9b5)
2026-10-04 20:40:34 +01:00
Alex Tumanov 33edd89b43 fix(extract): preserve Python import bindings across sibling cases
Use existing plain-import provenance to distinguish separate and same-line from-imports. Reject type aliases and dynamic namespace references, and protect changed and unchanged external callers through incremental CLI extraction.

Fixes #3793

AI-Assisted-By: OpenAI Codex
(cherry picked from commit 5e240c8bd3)
2026-10-04 20:40:34 +01:00
Alex Tumanov 6868fff846 fix(extract): track calls to external Python modules
AI-Assisted-By: OpenAI Codex
(cherry picked from commit 3464399d4f)
2026-10-04 20:40:34 +01:00
berce ea1f158545 fix(path): resolve path::symbol and node-id endpoints before scoring (#3913)
`graphify path` and the MCP `shortest_path` tool picked endpoints by token
scoring only, so the forms `explain` already honours were misread:

- a bare label shared by symbols in several files was routed to one of them
  silently instead of being refused with the candidate ids;
- `src/b.ts::.nonce()` scored the path half and resolved to the file node;
- a full node id tokenized to its containing class.

Both entry points now share `_resolve_path_endpoint`: an exact
`_find_node` hit (path::symbol, node id, exact label or source path) is used
as-is and refused with the candidate list when it spans several files; only
queries without an exact hit fall through to `_score_nodes`.

Two same-named symbols in one file are refused as well, listing their
locations and ids, since `path::symbol` cannot tell them apart. A plain
file path still resolves to the file node. The ambiguity hint now suggests
`<path>::<label>` with the node's own label instead of echoing the query,
and `explain` prints the same shared message.

(cherry picked from commit c2d558aff7)
2026-10-04 20:40:34 +01:00
Agnik47 aa448fbb71 fix(extract): end a Svelte script block's trailing comment and report read errors
Review follow-ups on #3928:

- A `//` comment closing one `<script>` block swallowed a second block on
  the same line, since masking leaves them on one line. Terminate each
  block with U+2028 then `;`: the line terminator ends the comment and
  the `;` ends a statement that had none. Line numbers and byte offsets
  are unchanged.
- An unreadable file returns the `error` key again instead of an empty
  result, matching the behavior before the masking change.

(cherry picked from commit 5de47e3df3)
2026-10-04 20:40:34 +01:00
Agnik47 f9b42e8d23 fix(extract): parse only Svelte script blocks in the AST pass
extract_svelte fed the whole .svelte file to the JS grammar. The template
is not JS, so every component reported a syntax error at line 1 and no
symbol declared inside <script> was ever reached — only imports survived,
via the regex rescue pass.

Blank everything outside the <script> bodies (keeping newlines so
locations still match) and parse with the grammar the first block's lang
implies: js/jsx -> JS, ts or unset -> TS, a superset of JS and Svelte's
own default. <script module> and <script context="module"> are parsed
alongside the instance block. Scripts with a non-JS type
(application/ld+json, ...) are blanked too, and a script region is
terminated in place of the following `<` so two blocks on one line do not
run together. The existing regex import rescue is unchanged; an import
both passes resolve yields one edge after build.dedupe_edges, as on the
Astro path.

The masker mirrors _astro_mask_non_script and shares the script-tag and
non-JS-type patterns with it instead of duplicating them.

Fixes #3928. Covers the Svelte half of #3942 (markup is blanked, so a
template {#if} no longer mints a spurious if() node).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit b64806924e)
2026-10-04 20:40:34 +01:00
Cintu07 8629f02ff0 fix(php): stop language constructs binding to same-named methods (#3830)
empty(), isset(), eval() and die() parse as an ordinary
function_call_expression, so the call pass resolved them by bare name.
A reserved word has been a legal method name since PHP 7, so a class
that declares empty() collected every empty($x) in the corpus:
EXTRACTED from its sibling methods, INFERRED from every other file
through the case-insensitive fold.

Drop the callee for these constructs in the PHP branch, before the
in-file lookup and before raw_calls, the same way _GO_PREDECLARED_FUNCS
handles Go builtins. $bag->empty() is a member call and still resolves.

(cherry picked from commit 94574eb239)
2026-10-04 20:40:34 +01:00
nothariharan 762cfd33ad fix(hooks): refuse a core.hooksPath that leaves the repository
hook install followed git's hooks path even when core.hooksPath resolved
outside the checkout, then created that directory and wrote post-commit
there. The bytes are Graphify's own hook, not repository-chosen code, but
the write still escapes the repo. Keep in-repo paths such as .husky, and
git's own hooks directory (including a linked worktree's shared .git/hooks).
Anything else falls back to that default.

Fixes #3869

Co-Authored-By: Grok 4.7 <noreply@x.ai>
(cherry picked from commit ee35087aea)
2026-10-04 20:40:34 +01:00
azizur100389 bb20ef7234 fix(analyze): diversify suggested questions across signal types
Round-robin the existing question categories before truncating the result. Preserve within-category order, existing small-limit priority, candidate schema, and no-signal behavior. Fixes #3849.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
(cherry picked from commit 560b88d86f)
2026-10-04 20:40:34 +01:00
Deepanshu PalandClaude Opus 4.8 b8fcd29cb1 fix: snap cross-repo resolver confidence scores to the INFERRED rubric (#4045)
Snap the off-rubric INFERRED confidence scores emitted by the cross-repo
call/type resolvers and interface dispatch (0.8 and 0.9) to the nearest
canonical rubric tier (0.85), so every INFERRED edge carries one of the
documented discrete scores. Label-only change: no edge flips
EXTRACTED<->INFERRED and no edge is produced or dropped. Replaces the
substring-matching guard test with an AST-based one that scans every
module for off-rubric scores.

Squashed from #4045 (authorship preserved).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-04 20:23:44 +01:00
SrijanSriv 7e7de7d08f fix(dedup): keep unstamped document nodes apart across files
The exact pass joined equal labels without calling
_crossfile_fileanchored_blocked, so repeated headings collapsed
across source files. The fuzzy pass already had that block.

Fixes #3094

(cherry picked from commit 6a7409c06d)
2026-10-04 20:23:14 +01:00
Mahaboob pasha 7e8ad53e03 fix(detect): also skip graphify's single-file installs
The skill-folder rule from the previous commit left the files graphify
writes whole into a project still indexed: the always-on rules, steering
and workflow files (.agents/rules/graphify.md, .agents/workflows/graphify.md,
.cursor/rules/graphify.mdc, .kiro/steering/graphify.md,
.windsurf/rules/graphify.md) and the opencode/kilo hook plugins
(.opencode/plugins/graphify.js, .kilo/plugins/graphify.js). With every
platform installed they still added 12 nodes on top of the user's code.

List them in detect.py as a hidden holder dir plus the exact relative
path, the same paths install.py writes, and never match a bare
graphify.md/graphify.js name. detect(), ignored_predicate() and
collect_files() all check the same helper.

Files graphify only adds a section or entry to (AGENTS.md, CLAUDE.md,
.claude/CLAUDE.md, GEMINI.md, CODEBUDDY.md, .github/copilot-instructions.md
and the merged settings/hooks/plugin JSON configs) are still left alone.

The new test runs every project install in install.py plus the kilo,
codebuddy and vscode installers, so a new platform that writes a new file
fails it.

Refs #4057

Co-Authored-By: Grok Bot <noreply@x.ai>
(cherry picked from commit 102f4bca1e)
2026-10-04 20:23:14 +01:00
Mahaboob pasha c75bc16d2a fix(detect): skip graphify's own installed skill folder when scanning
`graphify install --project` copies graphify's SKILL.md and its
references/ bundle into the project (.claude/skills/graphify/,
.agents/skills/graphify/, ...). detect() treated those files as project
documents, so a small repo's graph was mostly graphify's own docs: in the
#4057 repro, 66 of 69 nodes came from the installed skill and the
always-on files, and only 3 from the user's code.

Prune the folder in _is_noise_dir, next to the .claude/worktrees rule, by
its full path shape as install.py writes it: <hidden dir>/skills/graphify/,
plus .pi/agent/skills/graphify/, .config/kilo/skills/graphify/ and
.aider/graphify/. The bare names "skills" and "graphify" are not matched on
their own (#2479), so graphify's own source tree (graphify/skills/<host>/),
a top-level skills/graphify/ and the user's other skills stay indexed.
Because the rule lives in _is_noise_dir, detect(), ignored_predicate() and
collect_files() all agree on it.

AGENTS.md, CLAUDE.md and .claude/CLAUDE.md are left as they are; those are
the user's files and how to treat graphify's section in them is still an
open question on the issue.

Fixes #4057

Co-Authored-By: Grok Bot <noreply@x.ai>
(cherry picked from commit 0d7087ea5c)
2026-10-04 20:23:14 +01:00
SrijanSriv ac6ab9f2bf fix(extract): keep an unresolved base off a class in another language
The unique-stub rewire copied a same-label class onto every edge, so a
Python base could inherit a TypeScript class. Refuse that write when the
language families differ. A same-language match still rewires.

Fixes #2207

(cherry picked from commit 48cdc8f484)
2026-10-04 20:23:14 +01:00
Faisal-Fayaz 856649270f fix(extract): stop walking JSON Schema files as config manifests (#2255)
`_is_config_json()` decides whether a `.json` file is worth AST-walking. It
matches by filename first, then falls back to a root-key probe. `$schema` was
in that probe's key set -- correctly, since configs legitimately carry it --
but it is also the *defining marker* of a JSON Schema, so every schema
document satisfied the probe and got walked in full. That re-opened the
keyword-node explosion #1224 closed for data JSON: one 160 KB contract
contributed 362 nodes labelled with bare schema keywords (`description` x113,
`type` x83, `$ref` x53, ...) plus ~12 schema-only communities, polluting
clustering, god-node analysis and GRAPH_REPORT.md.

Two changes, both ahead of the existing probe:

- Drop `$ref` from the probe keys. A root-level `$ref` is a schema construct
  and never a config signal.
- Return False when the root object carries `$schema` *plus* a
  schema-definition marker (`$defs`, `definitions`, or `$id`). This is what
  separates "this file IS a schema" from "this config POINTS AT a schema":
  the latter points `$schema` at its own tool's schema (biomejs.dev,
  docs.renovatebot.com) and never carries these markers.

The blast radius is smaller than it looks. The filename branches run first, so
biome.json / renovate.json / package.json / tsconfig.json never depended on the
probe at all; only arbitrarily-named files did. I confirmed this against a
control matrix before and after -- every config shape keeps byte-identical node
counts.

The root keys are now collected once into a set so the schema check and the
config probe agree on the same read of the file, rather than walking the root
twice.

Documented boundary, pinned by a test: a hand-written schema carrying only
`$schema` + `properties` (no `$defs`/`definitions`/`$id`) is still walked.
Closing that gap needs a keyword-density heuristic, which would risk skipping a
real config whose keys overlap schema vocabulary -- the worse error, since it
drops structure silently. Left for a deliberate change rather than guessed at.

Verified:
- uv run pytest tests/test_json_schema_config.py -q    (14 passed; 5 fail pre-fix)
- uv run pytest tests/ -q -> 28 failed, 6262 passed, 101 skipped
- baseline on a clean v0.9.75 checkout: 28 failed, 6248 passed, 101 skipped
  The 28 failures are byte-identical between the two (missing tree-sitter
  grammars for erlang/R/solidity/vbnet, plus ollama retry tests) and are diffed
  test-id-by-test-id, not compared by count. Passing delta is exactly +14.
- uv run ruff check . -> clean
- uv run pyright -> 636 errors, identical to the clean v0.9.75 baseline

Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit f45ea17288)
2026-10-04 20:23:14 +01:00
Ayushraj06-bit 48de61551a fix(elixir): scope import/use to the declaring module, not the file
The unqualified-call scope added for #4001 collected every `import` and
`use` in a file into one list, so an import in one module leaked into a
sibling module defined in the same file: `MyApp.Sibling.go/1` calling
`fmt(x)` still bound to `MyApp.Helpers.fmt/1` because `MyApp.Importer`
imported it. Elixir scopes `import`/`use` to the module body it appears
in and to the modules nested in it.

Record targets per enclosing module, with the file's top level as its
own scope, and build each call's scope from its own module, the modules
enclosing it, and the top level. Each raw call now gets its own list
rather than sharing one mutable list with every other call in the file.
A module nested in a same-named module shares its node id, so the walk
up the enclosing chain stops at a module it has already visited.

Follow-up to #4015. Refs #4001

(cherry picked from commit e508142dac)
2026-10-04 20:23:14 +01:00
rajatnagda45 b46263123a fix(pascal): extract enumerated types and their values
(cherry picked from commit 8cf55d37be)
2026-10-04 20:23:14 +01:00
rajatnagda45 67a96aa08f fix(vbnet): link enum members with case_of instead of contains
(cherry picked from commit 6ee9624c22)
2026-10-04 20:23:14 +01:00
rajatnagda45 9ca6298761 fix(cpp): extract union specifiers as class-like type nodes
(cherry picked from commit b6c9c34359)
2026-10-04 20:23:13 +01:00
rajatnagda45 11736158a4 fix(zig): emit a node per tagged-union variant with a case_of edge
(cherry picked from commit 1ed509f4d3)
2026-10-04 20:23:13 +01:00
safishamsiandClaude Opus 4.8 48d7c0e832 release: 0.9.75
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.9.75
2026-10-04 02:19:53 +01:00
ayushcodes10 97608ef573 Add regression tests for the symbolless file warning
Covers a plain data literal getting named and marked as likely data,
a real function staying silent, no overlap with the existing syntax
error warning on a genuinely broken file, and the total size count
across more than one file. Confirmed to fail against pre fix code.

(cherry picked from commit 32ad059ebc)
2026-10-04 02:11:27 +01:00
ayushcodes10 f81c4e5ec6 Warn when a cleanly parsed code file yields no symbols
A code file that parses with zero errors but still produces nothing
beyond its own bare file node is usually a plain data literal the AST
extractor has nothing to model (a const array, a module export of
plain data), not a code file that legitimately has no symbols. Those
two cases looked identical in the output: the file silently entered
the graph as an isolated, zero edge node, and nothing reported it. A
real corpus measured in the issue had 88 such nodes covering roughly
40 percent of the corpus, with not one edge among them.

Add a complementary check next to the existing syntax error warning,
gated on the opposite condition (no parse_errors and no extractor
error) so the two can never double report the same file: a file
already explained by a genuine parse error or a missing optional
dependency is excluded here.

Fixes #3946.

(cherry picked from commit 3b3ee82b1a)
2026-10-04 02:09:42 +01:00
SrijanSriv e8b5d276a4 fix(extract): keep shrink accounting out of graph.json
The prune counter included a stub created in this run, so it could
excuse a dedup shrink. Count only nodes that were already saved.
Strip both counters inside to_json so other writers cannot persist them.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
(cherry picked from commit 294946d4ce)
2026-10-04 02:09:34 +01:00
SrijanSriv daee93f782 fix(extract): refuse a dedup shrink unless the user accepts it
A finished extract force-writes, so the node-count check never sees a
graph that dedup made smaller. Refuse that overwrite and name
--allow-dedup-shrink. A deleted file still writes.

Fixes #3774

Co-Authored-By: Grok 4.7 <noreply@x.ai>
(cherry picked from commit 686bef5855)
2026-10-04 02:08:29 +01:00
safishamsiandClaude Opus 4.8 0110d962b9 harden(bash): don't treat a $VAR/path command word as an interpreter
Follow-up on #4007: is_interpreter_variable fired for any bare-$VAR
command word, so `"$DIR/run.sh" stage.py` could mint a spurious invokes
edge to the argument on top of the real .sh calls edge. A bare
interpreter variable ($PYTHON) has no path separator; gate on
`"/" not in cmd_word`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-04 02:08:21 +01:00
BrLumen e402b87b73 fix(dart): link every URI of a configurable import/export (#4002)
A configurable directive such as

    export 'service_stub.dart' if (dart.library.io) 'service_io.dart';

was matched only up to its first quoted string, so the `if` alternatives
were dropped. In the usual Flutter facade the stub is an empty placeholder
and the real implementation lives in the conditional branch, which left
the implementation unreachable from the facade and its importers.

Parse the whole directive and emit one edge per URI: the default URI keeps
its plain edge (unchanged), each alternative gets the same relation tagged
context="conditional_uri" with its own source line. A string literal inside
the condition (`if (app.flavor == 'prod')`) is part of the URI test and is
not mistaken for a URI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 2b965f23fc)
2026-10-04 02:07:23 +01:00
BrLumen 1dcac128c2 feat(dart): keep a file node for part files (#4008)
Since #1098 a `part of` file's declarations are redirected to the parent
library so one library has one id namespace. The redirect also dropped
the part's own file node, so a scanned source file could not be found by
name (`explain "<part>.dart"` -> no node).

Keep the redirect and keep the node: the part gets its regular file node,
`library --includes--> part` anchored on the `part of` line, and
`part --contains--> declaration` mirrors every `defines` edge the library
gets from this part. Declaration ids and the library's `defines` edges are
unchanged. test_roadmap_bug_fixes asserted the old "no part node"
behaviour and now asserts the part's own node instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e32f49dbad)
2026-10-04 02:07:00 +01:00
ayushcodes10 1dee8307f9 Add regression tests for the bash script invokes edge
Covers python3/python plus a bare interpreter variable, a non shell
interpreter running a dot sh target landing on its entrypoint node,
and two negative cases: a non interpreter command with a script
looking argument, and a missing target. Confirmed to fail against
pre fix code except for the two negative cases, which hold either way.

(cherry picked from commit 97d4c9207c)
2026-10-04 02:07:00 +01:00
ayushcodes10 0d2ade622d Record a bash script's invocation of another script as an edge
The bash extractor recorded functions, source imports, and calls
between functions, but nothing for the programs a script actually
runs. In a pipeline driven by a shell script that is most of what the
script does, so an orchestrator looked disconnected from every stage
it runs: path between the runner and a stage it invokes found
nothing.

Add an invokes edge when a command's first word is a known
interpreter (python, python3, python2, node, nodejs, ruby, perl) or a
bare variable standing in for one, and an argument resolves to an
existing file in the repo with a recognized script extension. Kept
entirely separate from the existing bash to bash runner handling
(bash/sh/zsh/ksh/dash plus direct dot slash invocation), which still
emits its own calls edge unchanged, so this only adds coverage rather
than risking the existing path.

Gated on the target actually existing on disk, same as every other
heuristic edge in this file, so a plain command that merely happens to
take a dot py argument (cp, a custom tool) is never mistaken for an
interpreter invocation.

Fixes #3802.

(cherry picked from commit 52fe75be93)
2026-10-04 02:07:00 +01:00
Ayushraj06-bit c96a743b0c fix(elixir): scope unqualified calls to imported and used modules
The shared cross-file call pass bound an unqualified call to any
same-named def in the corpus. In Elixir an unqualified call can only
reach the caller's own module, Kernel, or a module the file imports or
uses, so DSL calls landed on unrelated project functions: every
migration's `table(:users)` (Ecto.Migration, brought in by `use`) became
a calls edge to a Phoenix component's `table/1`, making it the top god
node.

The Elixir extractor now records, on each raw call, the modules its file
names in `import` or `use` (`alias` and `require` do not bring functions
into scope). The shared pass keeps only candidates whose file defines one
of those modules, the same way Go selectors are already filtered by
import path, and leaves the call unresolved otherwise. The module index
is read from the resolution nodes, so an unchanged file's modules still
count on an incremental rebuild.

Calls into a module that a `use` macro imports indirectly (for example
`use MyAppWeb, :live_view` importing CoreComponents) are no longer
resolved, since that import is not visible in the source.

Refs #4001

(cherry picked from commit 98d4ae9878)
2026-10-04 02:07:00 +01:00
rohit-jsfreaky 117e6b4bad fix(extract): keep recursive calls when merging C# partial classes
_merge_csharp_partial_class_nodes rewrote every edge in the corpus and
dropped every self-loop, so one partial class split across two files
erased recursive calls in every language of the repo. It also deduped
edges it never rewrote, on a key that ignores confidence and context.

Keep edges the merge does not remap verbatim (the Swift extension merge
rule, #2538) and drop a self-loop only when two distinct endpoints
collapse into one node (the deduplicate_entities rule, #3809), so a
pre-existing self-loop on a merged-away half is rewired, not lost.

Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 5992044f32)
2026-10-04 02:07:00 +01:00
Deepanshu Pal efa296e817 test(hook-guard): cover custom GRAPHIFY_OUT reminder text (#4040)
Regression tests for #4040: read, stale-read and search reminders name the effective graph.json under a custom GRAPHIFY_OUT, and the default output dir stays byte-identical.

Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 31cb3bb1c0)
2026-10-04 02:07:00 +01:00
Deepanshu Pal 7a1b12b6c5 fix(hook-guard): name the effective graph.json when GRAPHIFY_OUT is customized
The read, stale-read and search reminders always named graphify-out/graph.json, even when GRAPHIFY_OUT pointed elsewhere. Rewrite the path at emit time from out_path("graph.json"); the default output dir stays byte-identical.

Closes #4040

Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 654aefb49f)
2026-10-04 02:07:00 +01:00
GAURAV singh f1f363c9ad feat(swift): dispatch a protocol requirement to its single conformer
A call through an injected `Store` resolves to `Store.fetch()` — the
protocol requirement (#3673) — and the conforming `RemoteStore.fetch()`
is a separate node with nothing joining the two, so every directed walk
through an injected dependency stops at the protocol. Link the two with
the member-level `dispatches_to` edge C# already gets (#3003), under the
same single-conformer / single-owner guard: a protocol with two
conformers, a conformer with two same-named methods, or a default
implementation nobody overrides emits nothing.

The mechanism lives in graphify.interface_dispatch, lifted out of the
C# pass in the preceding refactor; swift_dispatch.py is the `.swift`
gate over it, registered right after the C# pass. Both ends of a pair
must be declared in a Swift file, so a Kotlin class naming the same
supertype never receives a Swift edge.

Known limit, pinned by test: a `class C: P` whose protocol is declared
in another file is classified `inherits` by the per-file classifier and
is not dispatched; struct/enum/actor conformers and `extension C: P`
are.

AI disclosure: Claude Code assisted with this change.

(cherry picked from commit 0ca5c71b1c)
2026-10-04 02:07:00 +01:00
GAURAV singh 0bd22d4bee refactor(csharp): lift interface dispatch into a language-agnostic core
Move the single-implementer dispatch mechanism from csharp_dispatch.py
into interface_dispatch.py with the file-suffix gate as a keyword
parameter. csharp_dispatch.py keeps its public names
(resolve_csharp_interface_dispatch, _method_label, DISPATCH_RELATION)
and becomes the `.cs` gate over the core. No behaviour change: the 15
C# dispatch tests pass untouched, and the extractor registration is
not modified.

Swift protocols have the same shape (#3673 resolves a call through an
injected protocol to the requirement, and the conformer's method sits
unreachable from it). With the core shared, that language can add a
gate instead of a second copy of the algorithm.

AI disclosure: Claude Code assisted with this change.

(cherry picked from commit 6aade32806)
2026-10-04 02:07:00 +01:00
Faisal01011 562dfd6db8 fix(extract): resolve Scala context bounds to references edges (#2046)
`def sort[A: Ordering]` desugars to an implicit `Ordering[A]`, so a context
bound is a real dependency the function has on that typeclass -- and the
idiomatic form is more common in modern Scala than the desugared
`(implicit ...)` it compiles to. `context_bound` was not referenced anywhere
in the extractor, so the dependency was invisible wherever it appeared.

Two grammar shapes drive the implementation, both verified against
tree-sitter-scala 0.26.0 before writing the fix:

- `context_bound` is a direct child of `type_parameters`, one per bound, so
  the walk belongs in the existing per-function Scala block and needs no new
  handler.
- A higher-kinded parameter nests a further `type_parameters` for its own arity
  brackets (`def program[F[_]: Async: Temporal]`), so the walk recurses rather
  than reading direct children only -- otherwise both bounds on `program` are
  missed.

`upper_bound` (`[T <: Comparable[T]]`) is a distinct node type and is
deliberately not matched: a subtype constraint is not a dependency, so an edge
there would assert something the source does not say. Bounds also get their
own `type_bound` context rather than reusing `parameter_type`, so a consumer
can tell a typeclass constraint from a parameter annotation.

One trap worth recording, because the neighbouring code invites it:
`type_parameters` and `parameters` carry the *same* field name on
`function_definition`, so `child_by_field_name("parameters")` returns whichever
comes first. The bounds are therefore located by node type, never by field
name -- a field lookup would read the function's real parameters as bounds. A
regression test pins that `xs: List[A]` still resolves under `parameter_type`
and never under `type_bound`.

Verified:
- uv run pytest tests/test_scala_context_bounds.py -q  (10 passed)
- 6 of the 10 new tests fail on the pre-fix tree
- uv run pytest tests/ -q -> 28 failed, 6164 passed, 101 skipped; the 28
  failures are byte-identical to the clean-tree baseline and none touch Scala
- uv run ruff check . -> clean
- uv run pyright -> 635 errors, identical to the clean-tree baseline

Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit e293b64f6e)
2026-10-04 02:07:00 +01:00
Faisal01011 cb973c0351 fix(extract): walk Scala type_definition right-hand sides (#2049)
A type alias (`type Alias = List[Int]`), an `opaque type`, and a `match type`
all declare a dependency on the types they name, but the type-reference
dispatch only fired for `val_definition`/`var_definition`, so these produced
zero `references` edges in any context -- in contrast to a `val`/`var`, whose
annotation is walked.

`match_type` needed a second, independent change: the type-reference walker's
recursion whitelist did not know `match_type`/`type_case_clause`, so even a
correctly dispatched call on a match type's right-hand side would have
resolved nothing. Both are now handled, recursing over named children exactly
as `compound_type` already is.

Verified field names against tree-sitter-scala 0.26.0 before writing the fix:
`type_definition` carries its RHS under the `type` field for all three forms
(`generic_type`, `type_identifier`, `match_type`), so the existing
`child_by_field_name("type")` walk applies verbatim and no new handler is
needed. File-scope aliases inherit the #2054 anchor fallback.

Scope: only the edges are added. The alias still gets no node of its own, which
is a separate already-accepted tradeoff shared with `val`/`given` bindings.

Verified:
- uv run pytest tests/test_scala_type_definition.py -q  (10 passed)
- 8 of the 10 new tests fail on the pre-fix tree
- uv run pytest tests/ -q -> 28 failed, 6154 passed, 101 skipped; the 28
  failures are byte-identical to the clean-tree baseline and none touch Scala
- uv run ruff check . -> clean
- uv run pyright -> 635 errors, identical to the clean-tree baseline

Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 879dfd330d)
2026-10-04 02:07:00 +01:00
Faisal01011 447d41dd0d fix(extract): keep Scala file-scope val/var type references (#2054)
Scala 3 allows `val`/`var` to be written directly at file scope with no
enclosing object/class/trait. The `val_definition`/`var_definition`
type-reference dispatch was gated on `and parent_class_nid`, so a file-scope
binding produced no node and no edge of any kind, while the byte-identical
definition nested one level inside any container resolved correctly. The
missing container was the only difference: both positions parse to the same
node type carrying the same `type` field.

This is the Scala instance of a generic defect in the walker's default
recursion path that had already caused and been fixed once for Python
(#1050) -- a position that legitimately has no container must fall back to
the file node rather than be skipped. The fix reuses the `owner_nid =
parent_class_nid or file_nid` idiom already present in the property branch
above, and moves the self-reference guard onto the same anchor so a
file-scope binding cannot mint an edge from the file node to itself.

Behaviour is unchanged for class-nested bindings; a regression test pins
that case explicitly.

Verified:
- uv run pytest tests/test_scala_top_level_binding.py -q  (8 passed)
- 6 of the 8 new tests fail on the pre-fix tree, 2 pass either way
  (the in-class control, and the self-edge guard which is trivially empty
  before the fix and meaningful after it)
- uv run pytest tests/ -q -> 28 failed, 6144 passed, 101 skipped; the 28
  failures are byte-identical to the clean-tree baseline (missing
  tree-sitter grammars for erlang/R/solidity/vbnet, plus ollama retry
  tests) and none touch Scala
- uv run ruff check . -> clean
- uv run pyright -> 635 errors, identical to the clean-tree baseline
  (pre-existing debt; this change adds none)

Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 2d61ef7e68)
2026-10-04 02:07:00 +01:00
safishamsiandClaude Opus 4.8 b523898eb9 harden: graceful merge-graphs --previous errors; JS/TS super fail-close
Two small follow-ups on the 0.9.75 batch:
- merge-graphs --previous now reports a clear error and exits 1 on a
  malformed/unreadable JSON file instead of crashing with an uncaught
  traceback (follow-up to #4006).
- JS/TS super.m() fails closed (no edge) instead of falling back to the
  file-wide name map, which bound it to the overriding method as a wrong
  self-loop. The extends chain is unknown at this pass, so super can
  never be resolved here; inherited this.m() still keeps the plain
  lookup (follow-up to #4012).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-04 01:52:03 +01:00
ayushcodes10 e3ac1689e5 Add regression tests for the parallel edge preservation fix
Covers the exact shape from the issue through the real CLI command
(an imports edge and a calls edge between the same pair surviving a
cluster only run), plus a direct unit test on the export function
confirming only a dangling endpoint is dropped, never a real pair.
Confirmed to fail against pre fix code.

(cherry picked from commit 5bde5b5a06)
2026-10-04 01:48:51 +01:00
ayushcodes10 7527e730b1 Let cluster only and label write back every original edge
cluster only and label reload an existing graph.json into a simple
Graph to re cluster, which keeps one edge per node pair. Writing that
same graph back out then silently dropped a second edge on any pair
that stayed connected by another, for example an imports edge and a
calls edge between the same two nodes. A real corpus measured in the
issue lost 322 edges this way across one run, every one of them a
second edge on a pair that stayed connected.

Community is purely a node attribute; re clustering never needs to
add or remove an edge. Add an original links parameter to to_json
that, when given, writes back every original link verbatim instead
of re deriving the link list from the simple graph, dropping only one
whose endpoint no longer exists in the graph. Wire cluster only and
label to pass the links already loaded from the input file. This also
sidesteps any endpoint order canonicalization undirected storage
would otherwise apply when deriving the link list from the graph,
since the original links keep their exact stored endpoint order.

The parameter is opt in and defaults to the previous behavior, so a
fresh extraction build (which has no prior links to preserve, since
the graph it just built is the only authoritative source) is
unaffected.

Fixes #3999.

(cherry picked from commit 576d03d89c)
2026-10-04 01:44:19 +01:00