2324 Commits
Author SHA1 Message Date
safishamsiandClaude Opus 4.8 a9860f70dc chore(release): 0.9.88
Twenty community pull requests: Java/C-C++/Python/Swift/Go/PowerShell/C#
/JS extraction and call-resolution fixes, EJS extractor, dedup and
shrink-guard hardening, and query/graph.html ergonomics. The AST cache
schema is bumped (now 12), so the first build after upgrading
re-extracts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.9.88
2026-10-12 23:05:07 +01:00
safishamsiandClaude Opus 4.8 3d44deb84f chore(cache): bump AST schema to 12 for the Java/C-C++/JS batch
Java inherited-supertype and chained/new/cast receiver typing (#4305,
#4307), C/C++ cross-directory quoted-include provenance (#4392), and
JS external imports anchored to the package rather than the path leaf
(#3092) all change AST extraction output, so warm caches must
re-extract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 23:03:06 +01:00
rohit-jsfreaky e90bd328d9 fix(js): anchor rescued external imports to the package, not the path leaf (#3084)
(cherry picked from commit 794e0c391f)
2026-10-12 23:02:50 +01:00
rohit-jsfreaky ba8b4c863c fix(js): keep import edges to declared npm packages (#3084)
(cherry picked from commit 70bdd8a6dc)
2026-10-12 23:02:50 +01:00
himanshupatro-334 a4028f3c8d fix: resolve cross-directory C/C++ quoted includes (#4354)
(cherry picked from commit 97b8c8dbf2)
2026-10-12 23:02:50 +01:00
rohit-jsfreaky 2531ab7dfc feat(java): resolve calls on chained, new and cast receivers through declared return types
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
(cherry picked from commit 23f3604948)
2026-10-12 23:02:44 +01:00
rohit-jsfreaky 8297745215 fix(java): a superclass method wins over an interface default for a bare call
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
(cherry picked from commit 85fab922d2)
2026-10-12 22:58:51 +01:00
rohit-jsfreaky 44a7d50aec fix(java): resolve bare calls and field receivers inherited from a supertype in another file
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
(cherry picked from commit 724626659a)
2026-10-12 22:58:51 +01:00
rohit-jsfreaky 06b8d71245 fix(wiki): link index rows to their own article when labels collide (#3032, #3033)
(cherry picked from commit 2816f9365b)
2026-10-12 22:58:40 +01:00
AndresDiagoM 61ce767dab test(build): cover AST-twin shrink-guard fix end to end; sync alias docstrings
- Add to_json() and watch._check_shrink() regression tests that build a real
  graph where build_from_json folds a semantic ghost into its AST twin (#4389),
  plus a negative control showing a genuine semantic loss is still refused.
  Both positive tests fail on v8 and pass with this branch.
- Update _fold_node_aliases / _fold_edge_aliases docstrings and the build()
  dedup comment to list the new file/filepath and relationship aliases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 946333b2ab)
2026-10-12 22:58:40 +01:00
AndresDiagoM 3c5b1687fb fix(build): tighten AST-twin exemption to strict canonical AST origin and exclude collisions
(cherry picked from commit b2965c9dd5)
2026-10-12 22:58:40 +01:00
AndresDiagoM bbddf831ba fix(build): exempt deduplicated AST-twin semantic nodes from shrink guard and fold LLM node/edge field aliases
(cherry picked from commit 73a76fcd5d)
2026-10-12 22:58:40 +01:00
devkancheti4-design 3ba70ce9ac test(query): a seed given with --seed renders first and survives truncation
The seeded path hands the resolved nodes to _subgraph_to_text as seeds (seeds=start_nodes, the BUG2 fix), so the
given seed is the first NODE line and still shows under a small token budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 67f65c44b0)
2026-10-12 22:58:40 +01:00
devkancheti4-design f2d4bd49d3 feat(query): --seed fails closed without a value; report each unresolved seed once; log seeds
- graphify query --seed with no value (trailing --seed or --seed=) now exits with an error instead of running a
  keyword query.
- An unresolved seed given twice is listed once.
- The query log records the seeds used.
- Tests: the callable form (name and name()), --seed with --context, both no-value forms, and the logged seeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit c46b0e78d0)
2026-10-12 22:58:40 +01:00
devkancheti4-design 4545e73124 feat(query): --seed to start the traversal at given nodes
An external ranker (embeddings, a hybrid or learned retriever, or an agent
that already knows the file) can choose where graphify query starts instead
of the question's keyword matches; the graph then does the expansion.

Each --seed value resolves exactly as affected resolves its seed
(affected.resolve_seed: node id, unique label, callable name, repo-relative
or absolute file path anchored to the graph's root), so there is one
interpretation of a node reference. Fail closed: a value that does not
resolve to exactly one node is reported and skipped; if none resolve, the
query says so and does not fall back to keyword seeds. Without --seed the
output is unchanged (rendering moved into _render_query as is).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit c8af4e1b93)
2026-10-12 22:58:40 +01:00
Hassan Zidan 5399548ad8 fix(export): show legend tooltip from the whole row and clear it on hide
Follow-up to the review on #4291:

- Hovering anywhere on a legend row except its count (checkbox, dot,
  gaps, padding) now shows the truncated name. The tooltip tracks the
  area the pointer must leave (the whole row for a name, the count
  itself for a count), so moving inside a row doesn't hide or restart it.
- hideSidebarTip() also clears textContent, so a hidden tooltip doesn't
  keep the last name in the DOM.
- Tests: the mock elements now have offsetWidth/offsetHeight, so the
  positioning math is checked (no more NaNpx). New tests cover the
  cursor offset, clamping and flipping near the window edges, and
  hovering every part of a legend row on a parent-linked mock DOM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 1000de9b46)
2026-10-12 22:58:40 +01:00
Hassan Zidan 8036e0903a feat(export): show truncated sidebar names and legend node counts on hover in graph.html
The 280px sidebar truncates community names in the legend, search results
and Node Info neighbor links with a CSS ellipsis and gives no way to read
the full text, and the legend count has no label. On graphify's own graph
126 of 283 legend labels (45%) are truncated.

Add one shared tooltip that reuses vis-network's .vis-tooltip style and
100ms tooltipDelay:

- Names show it only when the text is truncated (scrollWidth > clientWidth).
- The legend count always shows it, as "N nodes" / "1 node".
- Text is set with textContent only, keeping the #1838 / #3686 escaping
  invariant.
- One delegated listener covers rows rebuilt later.
- No title attributes are added, so the native tooltip does not stack.

Node-driven regression tests cover truncated and non-truncated rows, the
count wording, literal rendering of & " ' < >, and single escaping of the
legend label.

Fixes #4286

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit def404e576)
2026-10-12 22:58:40 +01:00
nothariharan 28938a4732 feat(extract): add EJS (.ejs) template extractor (#3811)
EJS views sat in CODE_EXTENSIONS with no extractor, so an Express/Node app's templates were counted as code and contributed nothing (the #1689 warning). A .ejs file is HTML with <% %> scriptlets and inline <script> blocks, so feeding the whole file to the JS grammar made it one top-level ERROR.

Add extract_ejs: mask the template down to its JS regions (code-tag and <script> bodies), parse them with the TS grammar, and add the two relationships no JS grammar can see - dynamic import('./X') and the EJS <%- include('partial') %> composition edge, resolved to the real partial file. Wire .ejs into the dispatch, the jsts language family, the JS resolve/cache sets, and the hook source extensions.

(cherry picked from commit 3824cda537)
2026-10-12 22:58:40 +01:00
safishamsiandClaude Opus 4.8 4de6ed1eb1 chore(cache): bump AST schema to 11 for the extractor batch
Python property-accessor decorators and `Self` (#4385, #4386), Swift
`Self` (#4387), Go generic type params (#4388), the Python forward
type-hint file-node fix (#4343), PowerShell dot-source paths (#4369),
and the C# verbatim-interpolated-string recovery (#3612) all change AST
extraction output, so warm caches must re-extract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 22:40:06 +01:00
Deepjyoti-Sarmah b3f9af4e74 fix(dedup): keep a same-file reference as a mention (#4281)
A semantic fragment sometimes mints the bare id of a file it only
points at: docs/firmware/protocol.md emits `protocol` for
docs/protocol.md. Path-qualifying that id per file renamed it onto the
mentioning file's own page, so the reference became a link from the file
to itself. A collapse that picked the survivor by label length could
instead drop the real document.

Rule: a node is read as a mention when another node of its own file
points at it with a `references` edge from that same file AND its id is
itself a path slice of that file (it names a document). A mention is
not a rename candidate, and it ranks as a reference in _collision_rank,
so the real document outranks it whatever the label lengths are. One
helper, _mentions, decides both places.

The path-slice gate matters: entity ids such as `x_intro` are the target
of ordinary intra-file `references` edges. Without the gate the rename
skips them and two real documents collapse, and the ranking changes the
survivor of unrelated collisions. A test pins the survivor.

Fixes R (reference to another document) and R_labelswap: no self-link,
and no dropped document decided by label length. Two real documents
(D), a reference from a third file (F) and one entity described in two
documents (G) are unchanged.

Limits:
- The evidence is inferred from the edges an LLM chose to emit. A
  missing or reversed edge gives no evidence, so both files are renamed
  as before.
- A real document whose own node is linked to its file-level id the
  same way reads as a mention and collapses as on v8.
- A top-level protocol.md that also mints `protocol` is a separate
  collision and still drops one document; pinned by a test.
- If two or more other files also mint the mention's id, it stays on
  the bare id and build folds it into its own page.

(cherry picked from commit 2d69f5e541)
2026-10-12 22:38:15 +01:00
Deepjyoti-Sarmah 27c57c7815 fix(dedup): path-qualify semantic ids that dropped the directory (#4281)
Semantic extraction sometimes mints an id from the bare filename stem, so
a/x.md and b/x.md both emit `x`. Dedup then collapsed them into one node
and one file's node was lost. _semantic_id_remap in build.py knows how to
rebuild the full-path id, but it runs after dedup and is keyed by id alone,
so it cannot send one id to two files.

Add _path_qualify_dropped_collisions next to the extension pass. When two
or more files with different full path stems mint the same shorter-stem
id, rename each node to its own full-path id (keyed by id and source
file) and move edges by their source_file. References from other files
cannot be attributed, so they are left unresolved with a warning.

Boundaries: a group is skipped when the file the bare id names is in the
input (a genuine reference); absolute source_files are never qualified,
so checkout paths cannot leak into ids; AST-tier nodes are untouched.

This reverses the collapse pinned in #1504/#4282 for path-dropped ids:
the #1504 test now uses a non-shorter-stem id and the protocol test now
expects both files to be path-qualified.

(cherry picked from commit 58f844e57f)
2026-10-12 22:38:15 +01:00
Fabio Rueda 364b258749 fix(powershell): dot-source quoted/$PSScriptRoot paths and script-scope calls (#4356)
Two gaps in the PowerShell extractor dropped edges from real-world
scripts:

- Dot-source only read the path when tree-sitter parsed it as
  command_name. `. "$PSScriptRoot\lib.ps1"` and `. '.\lib.ps1'` parse as
  string_literal, and unquoted `. $PSScriptRoot\lib.ps1` as
  path_command_name, so no imports_from edge was emitted. Accept all
  three and strip surrounding quotes; the existing basename logic
  already handles the rest.
- Calls were collected only from function/method bodies, so commands at
  script scope (the main body of most .ps1 scripts) produced no calls
  edge or raw_call. Walk the file root too, with the file node as the
  caller. walk_calls stops at function_statement/class_statement, so
  function bodies are not walked twice (covered by a control test).

`. (Join-Path $PSScriptRoot 'lib.ps1')` stays unresolved: the path is
only known at runtime.

AST cache schema 8 -> 9 so cached results for unchanged .ps1 files are
re-extracted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LcXGsdFdpLjYVHemLogkE
(cherry picked from commit 69e20c70e3)
2026-10-12 22:38:11 +01:00
Mpasha17 75e9f102bf fix(extract): only redirect a file-stem forward ref to a local class (#4334)
Keep v8's sourceless stub and resolve after the walk: rewrite to the local
id only when that id is a class defined in this file. An imported name (e.g.
pkg/widget.py doing `from .base import Widget`) or a same-id function like
`def widget()` now falls through to the cross-file rewire exactly as on v8.

Co-Authored-By: Grok Bot
(cherry picked from commit 1511543938)
2026-10-12 22:37:55 +01:00
Mpasha17 672441393e fix(extract): keep Python file node when a same-named class is forward-referenced (#4334)
A type hint like `w: Widget` before `class Widget` in widget.py minted a
bare 'widget' stub, which collides with the file node id once paths are made
relative, so the file node vanished and Widget ended up containing itself.
Point that reference at the local class id instead.

Co-Authored-By: Grok Bot
(cherry picked from commit c25d1626e9)
2026-10-12 22:37:55 +01:00
rohit-jsfreaky 1704777e39 perf(csharp): resume past the \$/@ prefix run so malformed files stay linear (#3376)
(cherry picked from commit 7f0fd6ddfe)
2026-10-12 22:37:55 +01:00
rohit-jsfreaky 709bba316b fix(csharp): extract past a verbatim interpolated string ending in an escaped quote (#3376)
(cherry picked from commit 1524a6370d)
2026-10-12 22:37:54 +01:00
N. Hariharan d2c1592eab fix(install): preserve line endings of CLAUDE.md, AGENTS.md and siblings (#4360)
#3741 stopped the always-on and Gemini writers from translating line endings
on write (newline=""), but every one of these paths still READ through
Path.read_text, which opens in universal-newline mode and folds CRLF to LF
before the block is merged. A later write then re-emitted the platform
default, so a read-modify-write normalised the WHOLE file: on Windows an LF
file gained CRLF on every line, on Linux a CRLF file lost its CR.

Install and uninstall now go through one shared read/write pair
(_read_keep_newline / _write_keep_newline): the file's own dominant ending is
detected on read, the section helpers keep working on bare-LF text, and the
ending is restored on write with newline="". A file with no prior convention
is written LF, matching the other writers. This covers claude_install,
_agents_install, codebuddy_install, gemini_install/_uninstall,
vscode_install/_uninstall, _strip_graphify_md_section, _agents_uninstall,
codebuddy_uninstall and _remove_claude_skill_registration, plus the two
#3741 paths so the read side can no longer drift from the write side.

Adds a round-trip test per writer: an LF file and a CRLF file in, same
endings out, and install-then-uninstall returns the exact original bytes.

(cherry picked from commit df5f032a62)
2026-10-12 22:37:54 +01:00
N. Hariharan c3d537ae4a fix(install): remove only graphify's own hook entries, keep others (#4359)
Hook install/uninstall identified graphify's hooks with
`"graphify" in str(group)` over the whole group dict. That removed a user's
own command sharing graphify's `hooks` array (with the whole group), and any
unrelated group whose command path merely contained the word "graphify" (e.g.
a script under `graphify-notes/`). Uninstall rewrote the settings file with no
backup, and `_strip_graphify_hook` raised AttributeError on a non-dict entry.

Match the individual `hooks[]` entries instead. A graphify entry is a command
hook whose command invokes graphify's own `hook-guard`/`hook-check` subcommands
(current installs) or references `graphify-out/` (the pre-#522 inline bash
guard). Only those entries are removed; a group is dropped only when nothing is
left in it, and non-dict/non-list shapes are passed through. Applies to the
Claude, CodeBuddy, Codex and Gemini install and uninstall filters, keeping them
in step. Uninstall now goes through `_write_settings_with_backup`, so it keeps
a rolling `.graphify-bak` like install already did.

Adds tests covering a user command sharing graphify's group, an unrelated group
whose path contains "graphify", a non-dict entry, and the legacy-bash replacement
plus idempotency for each host.

(cherry picked from commit 41310c256d)
2026-10-12 22:37:54 +01:00
Mpasha17 804b7933cd fix(serve): refuse the dotted-symbol same-file tie in _tier_refusal (#4366)
Moves the check out of _resolve_single_node so the path endpoint
(CLI path / MCP shortest_path) refuses pkg.mod.run the same way
get_node and explain do, instead of taking the first hit.

Co-Authored-By: Grok Bot <grok-bot@x.ai>
(cherry picked from commit 77f473437b)
2026-10-12 22:37:54 +01:00
Mpasha17 a3d7165df5 fix(serve): resolve dotted Python symbols by module path (#4366)
pkg.mod.func, pkg.mod.func() and the pkg.mod:func entry-point form now
map onto the path::symbol lookup (pkg/mod.py or pkg/mod/__init__.py,
suffix-matched so src/ layouts work). A function and same-named method
in that module are refused as a tie instead of picking one.

Co-Authored-By: Grok Bot <grok-bot@x.ai>
(cherry picked from commit 8f6451af9f)
2026-10-12 22:37:54 +01:00
rajatnagda45 149f55979a fix(go): drop generic type parameters from signature type references
A `[T any]` type parameter is a local binding, not a corpus symbol, but
`_go_collect_type_refs` appended the bare `T`/`R` identifiers used in a
function signature, struct field, or interface method as type references and
ran them through the sourceless-stub path. Every generic function/type in the
corpus then collapsed its `T` onto one bare `t` node — a god node that
pollutes god-node rankings, since single-letter type parameters are not in the
analyzer's builtin-noise filter. TS, C#, and Java already exclude their type
parameters.

A new `_go_type_param_names` helper collects the names a declaration binds
(its own `[T, R]` list and, for a method, the receiver's `Box[T]` arguments —
Go methods reuse the receiver's parameters), and `_go_collect_type_refs` skips
them. Genuine user types and the generic container head (the `Box` in a
`Box[T]` return) still reference.

(cherry picked from commit 434eb689c8)
2026-10-12 22:37:54 +01:00
rajatnagda45 6dbd10a311 fix(extract): drop Swift Self type annotation from references
A `-> Self` / `: Self` annotation names the enclosing type, not a corpus
symbol, but `_swift_collect_type_refs` appended the `Self` type_identifier
as a type reference and ran it through the sourceless-stub path. That
fabricated a node with the bare id `self` that every `Self`-returning
method in the corpus collapses onto — a god node, the same keyword-type
pitfall Rust's and Python's `Self` were fixed for.

`Self` is now skipped in both the `user_type` and bare `type_identifier`
arms. Genuine user types and the `Int`/`String` builtin type hubs are
unaffected.

(cherry picked from commit c4fb250b3b)
2026-10-12 22:37:54 +01:00
rajatnagda45 dafdbec33b fix(extract): drop Python Self type annotation from references
A `-> Self` / `: Self` / `typing.Self` annotation (PEP 673) names the
enclosing class, not a corpus symbol, but the Python type-ref pass treated
the bare identifier `Self` as a type reference and ran it through the
sourceless-stub path. That fabricated a node with the bare id `self` that
every `Self`-annotated method in the corpus collapses onto — a god node,
the same keyword-type pitfall Rust's `Self` was fixed for.

`Self` now sits with the other typing special forms in the type-container
set, so it is skipped the way `None`/`Ellipsis`/`ClassVar` already are;
this also covers the `typing.Self` attribute spelling. Dropping it matches
how the `-> "ClassName"` self forward-ref is already left unresolved. A
genuine user return type is unaffected.

(cherry picked from commit b67f05408e)
2026-10-12 22:37:54 +01:00
rajatnagda45 c36516988e fix(extract): stop Python property-accessor decorators minting stub nodes
`@x.setter` / `@x.getter` / `@x.deleter` are the property-descriptor
protocol, not references to corpus symbols named setter/getter/deleter.
The Python decorator-edge pass (#2154) ran `ensure_named_node` on the
decorator's head symbol, so every property with a setter/deleter/getter
redefinition fabricated a sourceless stub node and a false `references`
(context=decorator) edge onto it — the same provenance pollution the
builtin-decorator noise list already guards against for `@property`.

`_python_is_property` already recognizes the `.getter` form for the
read-semantics; the decorator-edge path now mirrors it and skips any
`.setter`/`.getter`/`.deleter` decorator. Imported decorators reached
through an attribute (`@app.route`) are unaffected.

Regression test asserts no decorator edge and no sourceless stub for a
property with setter and deleter accessors.

(cherry picked from commit 2dc8972c67)
2026-10-12 22:37:54 +01:00
safishamsiandClaude Opus 4.8 7536dc0665 chore(release): 0.9.87
Eighteen community pull requests: PHP/C#/Python/TypeScript/Julia
extraction fixes, retrieval and watch hardening, skill and benchmark
correctness. The AST cache schema is bumped (C# type_path, Python
property reads), so the first build after upgrading re-extracts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.9.87
2026-10-12 14:55:03 +01:00
JensD-git d85cb85654 fix(csharp): keep id-sharing and qualified-base partials out of the merge (#4363)
Follow-ups from the automated review of the first commit:

- Two same-named nested types under different outers in one file share one
  node id. That node took the first declaration's type_path and could join
  the other outer's halves; it now gets no type_path and is not merged.
- The shadowed-base step drops only a placeholder spelled with the bare
  name. A base written qualified (Other.IFoo), or through a using alias of
  that name, can be a different type that only shares the simple name, and
  keeps its edge.
- Pin that receiver-qualified calls (base.M(), this.M(), Type.M(), obj.M())
  keep their targets: they leave the cross-file pass as member calls before
  the own-type binding.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 37ec971f16)
2026-10-12 14:50:32 +01:00
JensD-git 371cb47e83 fix(csharp): merge nested and generic partial types by their full type path (#4363)
The partial merge grouped halves by (namespace, label) and skipped nested
types, because nested ids omit the enclosing type. A nested partial type split
across files stayed split, and `Box` / `Box<T>` partials were folded into one
node although they are distinct types (#4249).

- The C# extractor records each partial type's path inside its namespace,
  enclosing types first, each with its arity (`Outer`1.Inner`), as
  metadata.type_path; the merge keys on (assembly, namespace, type_path).
- The cross-file call pass binds a C# unqualified call to the one candidate
  that is a member of the caller's own type, so a call into another half of a
  partial type is not lost to a same-named method elsewhere (#4252 does this
  in-file only). Ties fall through unchanged.
- After C# type resolution, a merged partial type drops a placeholder base
  that sits next to a real base of the same name; one half resolving a sibling
  nested base and the other not left two bases, and base-chain lookups failed.
- _AST_CACHE_SCHEMA 8 -> 9, so cached C# results without type_path are
  re-extracted.

Fixes #4363.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 92d9197787)
2026-10-12 14:50:32 +01:00
rikurunico c7bc5fe4c0 fix(php): route ALL scoped calls through the PHP resolver, not just uppercase receivers
engine.py's defer guard only deferred a scoped_call_expression into raw_calls
when member_receiver[:1].isupper() (or is_this_field_call/_csharp_defer), so
self::, static::, parent::, $var::, and lowercase-first class receivers fell
through to the pre-existing per-file bare-name path instead of reaching
_resolve_php_member_calls. That path is last-write-wins across the whole file
and can mis-bind a same-named method on an unrelated class.

Tag scoped_call_expression with is_scoped_call and add it to the defer
condition unconditionally, so every `::` call is deferred to raw_calls and
resolved by the dedicated resolver regardless of receiver casing or sigil.

Also tighten the self::/static:: and $var:: fixtures with a same-named decoy
method on another class, so they actually prove enclosing-class scoping
and no-fabrication instead of passing on a bare-name coincidence.

Addresses review feedback on PR #3874.

(cherry picked from commit 801c2a2dfd)
2026-10-12 14:50:04 +01:00
rikurunico b0fd76a61c fix(php): handle case-insensitivity and namespace qualification in static-call resolution
Addresses 3 findings from the automated review on PR #3874:

- Fully-qualified scopes (\App\Models\User::method() or
  App\Models\User::method()) were left parked as unresolved: the class
  lookup keyed on the full scope text, which never matches a class
  node's own unqualified label. Now normalizes to the last namespace
  segment before lookup.
- Method names were matched case-sensitively even though PHP method
  calls are case-insensitive; a call spelled with different case than
  the declaration silently produced no edge. The lookup key is now
  lowercased on both sides.
- A lowercase-first class name (legal PHP, just non-PSR) fell through
  the `receiver[:1].isupper()` gate into "dynamic scope" and was
  dropped entirely. The gate is now `not receiver.startswith("$")`,
  since a `$var::` scope is the only case actually requiring the
  declared-type table this resolver doesn't have.

(cherry picked from commit 7ed0e1d70a)
2026-10-12 14:49:34 +01:00
rikurunico 2d37da18c9 fix(php): resolve Class::method() static calls to the actual method
scoped_call_expression (`Helper::format()`) used the scope text as the
callee name instead of the method name, so the shared cross-file
bare-name pass bound the call to whatever node shared the class's
label - almost always the class definition - never the actual method.
It also skipped member-call handling entirely, so the edge went
through the import-evidence confidence gate: same-namespace PHP
classes never need a `use` import for each other, so every
same-namespace static call was stuck at INFERRED 0.85 even though the
receiver class is named explicitly in source.

Capture the real method name as the callee and the scope as the
receiver, mark it a member call, and add a dedicated PHP resolver
(mirroring the existing Java/Swift/C++ member-call resolvers) that
binds self::/static:: to the enclosing class, parent:: through a
single inherits edge, and Class:: to the class's own method - all
EXTRACTED, since the receiver is named explicitly in source. A
dynamic scope ($var::method()) has no declared-type table to resolve
against and is left unresolved rather than guessed.

Fixes #3872

(cherry picked from commit 2ec460168b)
2026-10-12 14:49:34 +01:00
smngvlkz 169e470ce0 fix(watch): don't drop full rebuilds when the lock is busy (#4274)
(cherry picked from commit 6ad54959bd)
2026-10-12 14:49:29 +01:00
jnrod03-rgb 2aad748f37 fix(skillgen): pass root= to extract() in Step 3 Part A (#3727)
extract() uses cache_root as the fallback anchor for source_file and node ids
when root is not given (#1941). Part A called it with cache_root alone. That is
harmless while the cache sits in the scan root, but the two parameters look
alike and the block gets adapted: pointing only the cache somewhere else
re-anchored every code node. Same node count, every id renamed, every cached
edge into code left dangling - the node-count shrink guard cannot see it.

Pass root=Path('INPUT_PATH') next to cache_root, as the CLI already does, and
say in the block which of the two anchors ids. The rendered call is behaviour
preserving as written; it only stops being a trap when the cache is relocated.

The aider/devin monoliths call extract(code_files) with neither argument and
are left untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit efe15d792c)
2026-10-12 14:49:29 +01:00
rohit-jsfreaky a5e71809f9 fix(benchmark): count the tokens of the answer query_graph returns
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
(cherry picked from commit 687cedd407)
2026-10-12 14:49:28 +01:00
oleksii-tumanov 7ff507b1c6 fix: honor Claude CLI model env during labeling
Use GRAPHIFY_CLAUDE_CLI_MODEL for plain Claude CLI completions when callers do not pass an explicit model. Preserve explicit-model precedence and the CLI default when the setting is absent.

AI assistance: OpenAI Codex was used to help implement and validate this change.

(cherry picked from commit 109107e407)
2026-10-12 14:49:28 +01:00
Mahaboob pasha 8435b91c0d feat(html): add a light/dark/system theme toggle to graph.html (#3894)
Co-Authored-By: Grok Bot <noreply@x.ai>
(cherry picked from commit 95dd5397cd)
2026-10-12 14:49:28 +01:00
SrijanSriv aa7837960b fix(update): let a removed import stub go when the total grows
An auto-minted external node is rebuilt from import edges. Dropping one
while code grows is not a lost rationale node, so the update writes
without --force.
2026-10-12 13:02:57 +01:00
SrijanSriv be04997e9d fix(export): keep an integer node id that is still in the graph
The semantic check only treated a string id as present, so a second
to_json of nodes 1, 2, and 3 refused the same graph.
2026-10-12 13:02:57 +01:00
SrijanSriv 7bfd59ce2b fix(update): refuse a saved semantic id when the total grows
Compare saved semantic ids with the new graph instead of the node total.
A sourceless stub cannot fill a missing id, and a code rebuild is not a
deletion. A legacy id that build_from_json rewrites to its canonical
stem is the same node, so cluster-only still writes.
2026-10-12 13:02:57 +01:00
SrijanSriv 3050270639 test(update): refuse a lost rationale id when the node total grows
graphify update accepts any new node list that is at least as long as
the saved one, and a rebuilt code file was treated as a reason to drop
the rationale node on that file. A deleted code file must still write.

The #1116 fixture nodes are marked AST so a removed code symbol stays a
code deletion.
2026-10-12 13:02:35 +01:00
SrijanSriv 355c2b538b test(export): refuse a lost rationale id when the node total grows
A larger total used to make to_json accept a write that dropped a saved
rationale id. The new stub keeps the semantic count flat, so a count
check would miss the same loss.
2026-10-12 13:02:35 +01:00