Commit Graph

889 Commits

Author SHA1 Message Date
safishamsi 51fc00a30f fix(swift): type-qualified static calls resolve as EXTRACTED, not INFERRED (#1533)
A type-qualified Swift call (`Type.staticMethod()`, `Singleton.shared.method()`)
names the receiver type explicitly in source, so the resolved edge is an exact
reference — now emitted as EXTRACTED (1.0), matching the Python
qualified-class-method pass (_resolve_python_member_calls). Instance calls whose
receiver type comes from local inference (`obj.method()`) stay INFERRED (0.8).
Resolution and the single-definition god-node guard are unchanged.

This addresses the actionable part of #1533's "static calls" report: the edge
was always produced (graphify models calls as method->method), it was just
under-confident. Updated the confidence test to assert the instance/type-qualified
split.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 14:49:42 +01:00
safishamsi 1652dadd60 fix(objc): NS_ASSUME_NONNULL parse failure, dangling .m imports, alloc/init refs (#1475)
Three residual ObjC extractor bugs from the #1475 thread, each reproduced
against the real tree-sitter-objc grammar:

1. NS_ASSUME_NONNULL_BEGIN before @interface made the parser fail to emit
   a class_interface node at all (the whole interface was swallowed into
   ERROR nodes), so headers using the macro produced no class node. Blank
   the two argument-less annotation macros to equal-length spaces before
   parsing (offset-preserving; macro-free files are byte-identical). The
   reporter's "@class breaks it" hypothesis was wrong — only the macro does.

2. Quoted `#import "X.h"` edges dangled once a `.h`/`.m` pair existed: the
   target used the bare stem, which the post-pass canonicalizes and then
   _disambiguate_colliding_node_ids salts apart by path, so the import
   target no longer matched. Resolve the include to a real file (mirroring
   _import_c), and repoint imports/imports_from edges to the header variant
   in _disambiguate_colliding_node_ids — taking precedence over the
   same-source-file salt so a `.m` importing its own `.h` resolves to the
   header instead of self-looping. Also repairs the equivalent latent
   C-include dangling bug.

3. `[[Foo alloc] init]` produced no edge — walk_calls only reconstructed
   selectors and skipped the receiver. Emit a `references` edge from the
   allocating method to the class, resolved via the unique-class stub guard
   (ensure_named_node + _rewire_unique_stub_nodes) so unknown/ambiguous
   names produce no false edge. The calls-to-init edge is deliberately
   deferred (init selectors are ambiguous across classes).

Reported by JabberYQ with a precise repro and test repo. Adds regression
tests incl. a self-loop guard on the import edges. Still open on #1475:
dot-syntax property accesses (Bug 5) and @selector target-action (Bug 6b).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 14:41:22 +01:00
guy oron e8dabadeb0 feat(imports): resolve workspace subpath exports via package.json exports map (#1308)
Workspace imports with subpath exports (e.g.
`import { x } from "@scope/pkg/browser"`) now resolve through the
package's `exports` map instead of falling back to a bare path. Supports
string values, condition objects, nested conditions, and single-`*`
wildcard patterns (`"./*": "./src/*.js"`), falling back to the existing
bare-path/index resolution when there is no exports map or no match.

Adapted from #1541, taking only the exports-map resolver and not that
PR's competing import-node-ID normalization (current v8 already resolves
the node-ID mismatch via the #1529 id-remap post-pass, and the PR's
_file_stem approach regressed the relative-input alias case). Two
hardening changes over the original:
- `default` is consulted LAST in the condition priority (it is Node's
  catch-all), so a matching `import`/`module`/`svelte` condition wins.
- Export targets that escape the package directory are rejected
  (`_contained_in_package`), so a malicious `exports` value like
  `"./x": "../../../etc/..."` cannot resolve to a file outside the package.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 13:57:16 +01:00
safishamsi 7a9cda2452 fix(cache): prune orphan semantic-cache entries at end of extract (#1527)
The AST cache is version-swept but the semantic/LLM cache had no pruning,
so it grew unbounded: it is content-hash-keyed, so every content change
or file deletion leaves a permanent orphan entry (reporter saw 152
entries for 124 live docs). This matters for the committed-cache workflow
where the semantic cache is published for warm CI rebuilds.

Adds prune_semantic_cache(root, live_hashes) and wires it into the end of
the extract path, sweeping cache/semantic/*.json entries whose hash is not
in the live set. The live set is computed from the FULL detected document
set (not the incremental changed-subset, which would delete valid
entries), using the same file_hash recipe save_semantic_cache uses.
Best-effort (unlink guarded), only touches cache/semantic/ (.tmp and
cache/ast/** untouched), and keeps the semantic cache unversioned so
releases never re-bill LLM extraction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 13:14:37 +01:00
safishamsi 2133539930 fix(js): resolve alias/workspace import edges + honor tsconfig paths fallbacks (#1529, #1531)
#1529 (regression from the 0.9.0 full-repo-relative node-ID migration):
relative JS/TS imports resolve to repo-relative paths and ride the
extract() id-remap to canonical node IDs, but tsconfig path-alias and
workspace-package imports resolve to ABSOLUTE paths (their bases are
.resolve()'d), so the import-target ID baked in the on-disk prefix and
never matched the repo-relative definition node — the edge was dropped at
build (common on Next.js/SvelteKit `@/`-alias codebases). The id-remap
post-pass now also registers the absolute-resolved form of each input
path (file-level edges) and both the input-form and absolute-form symbol
prefixes (named-import edges), so alias/workspace import targets remap to
the canonical ID. Verified the built graph has no orphan nodes or
dangling edges.

#1531: tsconfig `paths` values are ordered fallback lists (tsc tries each
target until one resolves), but only targets[0] was kept. The alias map
now stores all targets in order, and a single _resolve_tsconfig_alias
helper (replacing six duplicated inline loops) returns the first target
whose candidate exists on disk, falling back to the first candidate when
none exist (no false edge). Wildcards, baseUrl, and array `extends` are
preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 13:14:36 +01:00
safishamsi 36b5e5cb29 chore(deps): drop unused safety dev dep (removes unfixable nltk HIGH)
`safety` was declared in the dev group but never invoked — the CI
security-scan job only runs bandit and pip-audit, and pip-audit already
provides the same dependency-CVE scanning. Its only practical effect was
pulling in nltk, which carries an unpatched HIGH path-traversal advisory
(GHSA-p4gq-832x-fm9v) with no fix available.

Removing safety drops nltk (and safety-schemas/typer/tenacity/tomlkit)
from the lockfile entirely, closing the alert with no loss of coverage.
Updated the stale CI comment that referenced safety. Full suite green
(2537 passed); pip-audit and bandit unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 11:29:45 +01:00
safishamsi 738c9cebcb chore(deps): bump msgpack 1.2.1 and pydantic-settings 2.14.2 (Dependabot)
Resolve two Dependabot alerts in transitive deps:

- msgpack 1.1.2 -> 1.2.1 (HIGH, GHSA-6v7p-g79w-8964): out-of-bounds
  read / crash on Unpacker reuse after a caught error. Pulled only via
  cachecontrol -> pip-audit (dev group), so not in the published wheel's
  closure, but a fix is available so we take it.
- pydantic-settings 2.14.1 -> 2.14.2 (MEDIUM, GHSA-4xgf-cpjx-pc3j):
  NestedSecretsSettingsSource follows symlinks outside secrets_dir.
  Pulled via mcp (the [mcp]/[all] extra); graphify does not use the
  affected secrets-dir source, but the fix is free.

Lockfile-only; both are transitive. Full suite green (2537 passed),
MCP/serve tests pass on the bumped versions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 11:26:29 +01:00
Ariadne Mitophane e3e4198038 fix(skillgen): host-generic /graphify install guidance (#1530)
Generated install/skill guidance told agents to invoke a literal `skill`
tool with `skill: "graphify"`, which is host-specific and not valid in
every environment. The always-on AGENTS fragment, packaged artifact,
expected snapshot, and _skill_registration() output now use host-generic
wording: "use the installed graphify skill or instructions". Also decodes
skillgen git blob reads as UTF-8 for Windows and replaces stale English
code-block examples in the translated READMEs.

The always-on roundtrip guard deliberately freezes the v8 baseline, so an
intentional wording change would otherwise fail it. Rather than only
patching the pytest mirror (which left the blocking CLI guard
--always-on-roundtrip red, as the original PR did), this adds an explicit,
reviewable ALWAYS_ON_SANCTIONED_EDITS registry: the guard applies the
approved old->new substitution to the baseline before the byte-for-byte
compare, so this exact sentence is allowed while any other drift still
fails. CLI guard and pytest test now agree and CI passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 10:01:06 +01:00
antonioscarinci 407a7f142d fix(export,cli): GraphML null-attr coercion + save-result --answer-file (#1502)
Two cross-platform fixes salvaged from #1502:

- to_graphml: nx.write_graphml raises ValueError on None attribute
  values, so a node/edge carrying a null field crashed the export.
  Coerce None -> "" for node and edge attributes before writing.

- save-result: add --answer-file as an alternative to --answer so long
  or multiline answers can be passed via a file instead of a fragile
  inline shell arg (notably Windows/PowerShell quoting). Exactly one of
  --answer / --answer-file is required.

The rest of #1502 (a version downgrade and a hand-edited generated
skill-windows.md that fails skillgen --check, plus duplicated
windows-scripts) is left for rework on the PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 09:54:08 +01:00
DhruvTilva 4e4935a64a fix(llm): enforce API timeout in the secondary LLM dispatch path (#1442)
_call_llm (used by the dedup LLM tiebreaker) built its Anthropic and
OpenAI-compatible clients with max_retries but no timeout, so requests
on this path silently ignored GRAPHIFY_API_TIMEOUT — unlike the primary
extraction paths (_call_openai_compat / _call_claude) which already pass
both. Add timeout=_resolve_api_timeout() to both constructors.

The PR branch self-neutralized: a v8 merge resolved the conflict in
favor of the max_retries-bearing line and dropped the original one-line
fix, so it is re-applied here on top of current v8 with max_retries
preserved. Adds regression coverage for both _call_llm branches, which
were previously untested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 09:45:03 +01:00
vamsi pavan mahesh gunturu 86ecb769b6 feat(ruby): type-aware member-call resolution via a resolver framework (#1499)
Resolve Ruby `obj.method()` calls by the inferred type of the receiver
instead of by globally-unique method name. `p = Processor.new; p.run`
now emits a `calls` edge to `Processor#run` and survives name collisions
with unrelated `Worker#run` definitions, where the old name-based match
either resolved by luck or dropped the edge as ambiguous.

Introduces graphify/resolver_registry.py, a behavior-identical
formalization of the existing tail-of-extract() language resolution
passes (Swift #1356, Python #1446 become registered entries), and
graphify/ruby_resolution.py, its first new consumer. Receiver type is
inferred only from unambiguous local `var = ClassName.new` bindings;
ambiguous or unknown receivers resolve to nothing (no false positives).

Note: Ruby member calls are now excluded from name-based cross-file
resolution and resolved by inferred type only. This is an intentional
precision-over-recall change scoped to Ruby: a cross-file `var.method`
whose receiver type cannot be proven from a local `X.new` binding no
longer resolves by name-luck (it produces no edge rather than a possibly
wrong one), matching the project's confidence model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 09:40:22 +01:00
safishamsi 12193a89f1 chore: untrack committed .DS_Store files
These were committed before .gitignore included the .DS_Store rule, so
gitignore never removed them from tracking. Untrack them (they remain
on local disk, just leave git) — the existing .gitignore rule keeps
them out going forward.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 09:24:02 +01:00
safishamsi 199061207f docs: add Discord badge to README
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:45:49 +01:00
safishamsi b7e256f779 release: 0.9.1
Patch over 0.9.0: completes the node-ID work (fully closes #1504 via injective
salt #1522), stops origin_file leaking into graph.json (#1516), extends cross-file
stub disambiguation to the six dedicated extractors (#1515), Java type-param skip
(#1518) + record component refs (#1519), prunes a deleted import's edge on update
(#1521), and retries rate-limited (429) requests instead of dropping chunks (#1523).
All non-breaking.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.9.1
2026-06-28 20:15:44 +01:00
safishamsi 0ca73bd00b docs: document GRAPHIFY_MAX_RETRIES env var (#1523)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:13:55 +01:00
safishamsi 64c1f21070 fix(llm): retry rate-limited (429) requests instead of dropping the chunk (#1523)
On strict per-org concurrency/RPM caps (notably Moonshot/kimi), a parallel
`graphify extract --force` 429'd, and because the provider SDKs default to only
max_retries=2, the chunk gave up after two attempts, logged `chunk N failed`, and
was silently dropped — an incomplete graph plus the screen full of
rate_limit_reached_error spam in the report. The SDKs already back off
exponentially and honor Retry-After; they just need more attempts to outlast the
rate window.

The OpenAI-compatible, Azure, and Anthropic clients are now built with
max_retries=_resolve_max_retries() (default 6, override via GRAPHIFY_MAX_RETRIES;
0 disables). For very tight accounts, --max-concurrency 1 further cuts the
concurrency that trips org-level limits.

Reported by @bercedev (#1523). Tests: _resolve_max_retries default/env/invalid,
and the OpenAI-compatible client is constructed with retries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:11:18 +01:00
safishamsi 22a5afb251 docs(changelog): add #1521, #1522, #1519 entries
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:57:33 +01:00
oleksii-tumanov 981e2b93cf fix(extract): emit Java record component references (#1519)
A Java record's component types produced no edges, so a record's data dependencies
were invisible. The record_declaration handler now emits `references` edges from
each component type (field context; generic_arg for type arguments), skipping
primitives and — via #1518's in-scope type-parameter filtering — the record's own
type parameters. Handles standard, generic, primitive, and varargs components.

Ported from PR #1520 by @oleksii-tumanov (clean 3-way onto v8; builds on #1518).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:55:51 +01:00
safishamsi 35fb437365 fix(ids): salt residual separator-collision node IDs injectively (#1522)
0.9.0 made the node-ID stem the full repo-relative path, but normalize_id collapses
every non-word run to "_", so the path separator is indistinguishable from inner
punctuation: foo/bar_baz.py and foo_bar/baz.py both normalized to foo_bar_baz and
still silently merged (the residual of #1504). The existing _disambiguate_colliding
_node_ids salt didn't help — it salted with _make_id(source_key, old_id), which
re-normalizes the path with the same lossy recipe, so the two colliders produced an
identical salted id.

When two distinct source paths' naive salts still collide, append a short stable
sha1(source_key)[:6] — injective over distinct paths — so they separate. Computed in
code from source_file (never trusted from the LLM), so AST<->semantic parity holds.

Blast radius: minimal/non-breaking — only the actual residual colliders get a hash
suffix. Non-colliding ids (the 99%, incl. the common #1504 case like two README.md
in different dirs) are byte-identical to 0.9.0 (verified: src/auth/session.py ->
src_auth_session, docs/v1/api/README.md -> docs_v1_api_readme unchanged). This is a
0.9.1 patch, not another migration.

Reported by @sub4biz (#1522). Regression tests cover both the collider-separation
and the non-collider-unchanged cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:54:48 +01:00
safishamsi 0080d8ac43 fix(update): prune edges owned by a re-extracted file (#1521)
`graphify update` preserved old edges keyed only on endpoint-node membership, so a
removed import's edge survived forever whenever both endpoint nodes still existed
(e.g. a.py no longer imports b, but both a and b are still present). The stale edge
drove phantom circular-dependency findings; `--force` didn't help (it only gates
the node-count shrink guard), only a clean rebuild did.

_rebuild_code (watch.py) now also drops preserved edges whose source_file was
re-extracted this run — an edge is owned by the file it was extracted from, and the
fresh extraction re-emits whichever ones still exist. Scoped to source_file
ownership (and the full-rebuild case, where evict_sources lists only deleted
files), so edges with no source_file or owned by a non-re-extracted file are kept —
cross-file edges that merely point at a re-extracted file (#1402 sourceless
stubs/rewire) are not over-pruned.

Reported by @UltronOfSpace (#1521). Regression test: a removed import's edge is
gone after update; verified no over-prune (still-present imports survive) and no
regression to deleted-file pruning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:29:51 +01:00
safishamsi faa9218907 chore: sync uv.lock to 0.9.0
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:06:28 +01:00
safishamsi 67d8c53ccc docs(changelog): add #1515 and #1518 entries to Unreleased
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:05:09 +01:00
oleksii-tumanov 8b9a99828a fix(extract): skip Java type-parameter references (#1518)
The generic-parent support (#1511) emitted a spurious references/generic_arg edge
(and a sourceless stub node) for a bare type parameter — the T in
`class Box<T> extends Container<T>` — because the Java extractor had no type-
parameter tracking. _java_collect_type_refs now collects the in-scope type-
parameter names (walking class/interface/record/method/constructor declarations,
including bounded and multiple params) and skips references to them, while keeping
every real type. Scoped to the declaring node, so a real class that shares a name
with a type parameter elsewhere is still referenced.

Ported from PR #1518 by @oleksii-tumanov. Closes the nit flagged when #1511 landed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:05:09 +01:00
Mohammed Ateeq d177f04270 fix(extract): add origin_file to cross-file stubs in the six dedicated extractors (#1515)
The #1462 same-label cross-file stub disambiguation (the origin_file key) only
existed in the generic extractor, so the six dedicated extractors — Julia,
Fortran, Go, Rust, PowerShell, ObjC — still collapsed same-named imported-type
stubs from different files into one conflated bare-id node (a false cross-package
link). Each now sets origin_file on its sourceless stub, identical to the generic
extractor; the generic _node_disambiguation_source_key consumes it, so two files
importing the same type stay distinct while source_file stays empty (the #1402
rewire onto a real definition is unchanged).

Ported from PR #1515 by @TPAteeq. Must ship with #1516: this widens origin_file to
six more languages, and #1516 is what strips it from graph.json. Verified: a 2-file
Go corpus now yields 2 distinct Widget stubs AND graph.json carries no origin_file.
Resolved a test-insertion conflict with #1516 by keeping both tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:01:55 +01:00
Mohammed Ateeq afa4aded2e fix(extract): drop internal origin_file so it stops leaking into graph.json (#1516)
The origin_file disambiguation field (#1462) is internal — it's consumed by the
colliding-id pass and must not be persisted. In 0.9.0 it shipped into graph.json
as an absolute, machine-specific path (the same portability bug class as #555,
#932), breaking on clone/move. It's now popped at the single extract() output
chokepoint, AFTER _disambiguate_colliding_node_ids has consumed it, so all persist
paths (clustered, --no-cluster, cluster-only) emit clean output. _origin is kept
(the incremental watcher relies on it, #1116).

Ported from PR #1516 by @TPAteeq. Verified: fresh extract (clustered and
--no-cluster) produces graph.json with zero origin_file fields; the #1462 same-
label disambiguation still works (it runs before the strip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:54:58 +01:00
safishamsi 94e5bafc89 docs: document the --timing flag in the extract examples (#1490)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:28:31 +01:00
safishamsi 92e682f1de release: 0.9.0
BREAKING: node IDs now include the full repo-relative path (#1504, #1509) — fixes
silent data loss when same-named files live in different directories, and aligns
the AST and LLM id rules. Existing graphs migrate automatically on next build;
`graphify extract --force` to recover previously-collided nodes; Neo4j persisted
stores need a re-import. Also ships the --timing flag (#1490).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.9.0
2026-06-28 18:20:10 +01:00
safishamsi 73710d33ce fix(ids): legacy-id detector only inspects file-level nodes (no Go false-positive)
The graph_has_legacy_ids nudge flagged fresh v9 graphs that contain Go code,
because the Go extractor scopes a type node by package directory
(_make_id(pkg_dir, name) -> "sub_thing"), which coincides with the OLD one-parent
file-stem form of pkg/sub/thing.go even though it's a current, by-design
package-scoped id (methods on a type across files in a package share one node).

The detector now inspects only file-level nodes (source_location "L1"), whose id is
unambiguously the file stem and which the migration actually re-keys. This still
catches a genuine pre-#1504 graph (its file nodes carry the old stem) while leaving
package/dir-scoped symbol ids alone. Verified end-to-end: a fresh Go graph no longer
triggers the rebuild nudge; a real legacy graph still does.

The migration itself was already correct — file nodes and Python/AST symbols
migrate to full-path ids; Go's package-scoped type ids are intentional and
unaffected. This only fixes the over-eager warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 17:52:34 +01:00
safishamsi 3999dbc67e feat(ids): warn on legacy-id graphs + harden re-key source_file contract (#1504)
Two pre-0.9.0 safety to-dos from the migration spike:

1. Auto-detect-and-warn: graph_has_legacy_ids() samples a loaded graph's node ids
   and detects the pre-#1504 (parent-dir/filename) scheme. Read-only consumers that
   don't re-extract — `graphify query` and the MCP serve loader — now print a
   one-line nudge to rebuild with `extract --force` when they load an old graph.
   Fires only on legacy graphs (verified: canonical graphs stay silent).

2. Re-key source_file contract: pinned with tests that a relative source_file is
   migrated to the full-path id while an absolute/unrelativizable one is skipped
   (its on-disk path must never leak into a persisted id). The is_absolute() guard
   already enforced this; the tests make the contract explicit.

Full suite 2507 passed. Lands on v9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 17:29:57 +01:00
safishamsi 388d1b64db docs(changelog): clarify --force is needed to recover previously-collided nodes (#1504)
Rigorous testing surfaced the nuance: the ID-format migration is automatic, but
same-named files that already collapsed into one node under the old rule are only
recovered as distinct nodes by a fresh extract --force (migrating an already-
collided graph/cache can't resurrect already-dropped nodes; edges referencing a
shared old id are ambiguous). Fresh extraction yields distinct nodes as expected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 17:12:30 +01:00
safishamsi b46634ef7a fix(ids): node IDs include the full repo-relative path (#1504, #1509)
BREAKING node-ID format change. The stem that prefixes every node id was the
immediate parent dir + filename, so same-named files in different directories
collided into one last-writer-wins node and silently dropped graph content
(docs/v1/api/README.md and docs/v2/api/README.md both -> api_readme). The stem is
now the full repo-relative path (docs_v1_api_readme vs docs_v2_api_readme);
top-level files are unchanged (setup.py -> setup).

- extractors/base.py::_file_stem -> full path (as_posix; make_id collapses
  separators). The two hand-copied stems (symbol_resolution, mcp_ingest) now
  import the canonical one, so they can't drift again.
- llm.py system prompt + extraction-spec fragments aligned to the same rule,
  fixing the #1509 AST<->LLM divergence (prompt used zero parent dirs -> ghosts).
  Regenerated + blessed the per-host specs.
- build_from_json deterministically re-keys every non-AST node id from its
  source_file via the new stem (and registers old-stem aliases), so a cached or
  pre-migration semantic fragment carrying an old short id reconciles with the
  AST node instead of spawning a ghost. The semantic cache is unversioned, so
  this code-side re-key (not LLM prose) is what makes it survive the format change
  with no re-bill. AST nodes are already canonical and skipped.
- Migration: existing graphs migrate automatically on the next build/update (the
  re-key runs in build_from_json, including the whole graph fed back through
  build_merge); `graphify extract --force` for a clean rebuild. Neo4j persisted
  stores need a re-import; GraphML layouts go stale.

Full suite green (2505 passed); #1504 collision fixed and old-id fragments
re-keyed verified by smoke. Lands on v9 for review before a 0.9.0 release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 16:57:00 +01:00
safishamsi 21a9f1f0b5 chore: sync uv.lock to 0.8.51
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 16:49:56 +01:00
safishamsi f7f89d7d84 feat(cli): --timing flag for per-stage timings on extract and cluster-only (#1490)
Adds an opt-in --timing flag that prints per-stage wall-clock timings to stderr so
slow stages are visible on large corpora. extract reports detect / AST extract /
semantic extract / build / cluster / analyze / export (and write on --no-cluster);
cluster-only reports load / cluster / analyze / label / report / export; both end
with a total. A small _StageTimer helper uses monotonic perf_counter.

Off by default and stderr-only, so default output and machine-read stdout/graph.json
are byte-identical (the extract arg loop already swallows unknown flags). Added a
regression test asserting the lines appear with --timing and are absent without it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 16:17:21 +01:00
safishamsi 7592244386 docs: document --obsidian-dir and its existing-vault safety (#1506)
The --obsidian-dir flag was undocumented in the README. Add it to the command
list with a note that exporting into an existing vault never overwrites the user's
own notes or .obsidian config (the guard shipped in 0.8.51).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:33:36 +01:00
safishamsi 8e98b916f4 release: 0.8.51
Dates the 0.8.51 CHANGELOG and bumps the version. Highlights: Obsidian export no
longer overwrites user notes / .obsidian config in an existing vault (#1506);
cross-file node-ID cluster fixes — C# resolver (#1466), Go sourceless stubs
(#1500), import-stub disambiguation (#1462); Java enum/annotation type nodes
(#1512) + generic parents (#1510); explain/affected source-file path lookup
(#1503); claude-cli Windows GBK fix (#1505); macOS install docs (#1471) and the
global-add docs fix (#1489).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.8.51
2026-06-28 11:20:23 +01:00
safishamsi 8b177cb33d fix(export): don't overwrite user notes or .obsidian config in an existing vault (#1506)
to_obsidian wrote one note per node straight into the target directory and
unconditionally replaced .obsidian/graph.json. Pointing --obsidian-dir at a real
vault could therefore clobber a user note whose name matched a graph node
(Database.md) and destroy the user's graph-view settings — silently, no backup,
irreversible.

graphify now records the files it owns in .graphify_obsidian_manifest.json and
refuses to overwrite any pre-existing file it didn't create: such a file is skipped
and reported in a single aggregated warning. A re-run still updates graphify's own
notes (they're in the manifest), and .obsidian/graph.json is only written when it
doesn't already exist or graphify owns it. The default graphify-out/obsidian output
and the flat note layout are unchanged.

Added regression tests: existing-vault preserves user note + .obsidian settings,
empty dir still gets the full vault, and a re-run updates own notes but not a
user-added file. The CHANGELOG also records the @oleksii-tumanov Java fixes
(#1512/#1510) and the @nuthalapativarun Windows GBK fix (#1505) committed just prior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 10:56:39 +01:00
nuthalapativarun 0e8d92cf5f fix(llm): tolerate non-UTF8 claude-cli output on Windows GBK systems (#1505)
On Windows where claude.cmd emits GBK/cp936 bytes, the claude-cli subprocess
decoding raised UnicodeDecodeError and crashed extraction. Both claude-cli
subprocess.run sites (_call_claude_cli and the claude-cli branch of _call_llm) now
pass errors="replace", so incidental non-UTF8 console chatter decodes to
replacement chars instead of crashing — the structured JSON payload (ASCII/UTF-8
on stdout) is unaffected. capture_output=True means the single errors= covers both
stdout and stderr.

Ported from PR #1507 by @nuthalapativarun (dropped an unrelated .gitignore change).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 10:32:57 +01:00
oleksii-tumanov 1f3f1c1ca6 fix(extract): emit Java generic parent relationships (#1510)
A generic parent — `class Foo extends Bar<T>` or `implements List<T>` — is a
`generic_type` node in the tree, but the inheritance extractor only fired on a
bare `type_identifier`, so those inherits/implements edges were silently dropped.
The parent type is now unwrapped to its base (`Bar<T>` -> `Bar`) for the
inherits/implements edge, and the type arguments are emitted as `generic_arg`
references.

Ported from PR #1511 by @oleksii-tumanov. Known pre-existing limitation (not
introduced here, worth a separate follow-up): the extractor has no type-parameter
tracking, so a bare parameter like `T` in `extends Container<T>` still produces a
`generic_arg` reference to `T`; the inherits/implements edge itself always targets
the real base type, never `T`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 10:31:57 +01:00
oleksii-tumanov 940cb53f4d fix(extract): emit Java enum and annotation declarations as type nodes (#1512)
Java `enum` and `@interface` (annotation) declarations weren't in _JAVA_CONFIG's
class_types, so they were never emitted as type nodes — a field typed as an enum or
a class annotated with a project annotation referenced a node that didn't exist
(dangling). enum_declaration and annotation_type_declaration are now extracted as
first-class (sourced) type nodes, so those references resolve onto them.

Ported from PR #1513 by @oleksii-tumanov.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 10:31:22 +01:00
behavio1 1b994965bb fix: resolve explain/affected when a source-file path matches multiple nodes (#1503)
A path query like `explain "app/api/route.ts"` tokenized to terms that matched no
node, so explain/affected returned "No node matching". Source-file paths are now
part of the search index and matched exactly (serve._find_node gains a leading
source-exact tier; affected.resolve_seed gains a source-file match). When several
nodes share a source_file (e.g. a file-level node plus a function node), the lookup
prefers the file-level node — the L1 node whose label basename matches the queried
filename, falling back to the unique L1 or unique basename match, else None.

Ported from PR #1503 by @behavio1. Maintainer fixes on top: aligned trailing-
separator handling between resolve_seed and _find_node (affected previously
returned None for a trailing-slash path that explain resolved), corrected the
stale "three-tier" _find_node docstring, and added regression tests for the
trailing-slash parity and the ambiguous-no-file-node -> None case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 23:57:39 +01:00
Mohammed Ateeq 11dc819c3f docs(install): uv tool PATH setup + uvx --from graphifyy for macOS (#1471)
Documents two expected uv behaviors that read as bugs on macOS: (1) uv tool
install puts `graphify` in ~/.local/bin which a fresh zsh shell may not have on
PATH (run `uv tool update-shell`); (2) `uvx graphify` fails because the package is
`graphifyy` and `graphify` is only its console script — use
`uvx --from graphifyy graphify install`. README install note + Troubleshooting.

Ported from PR #1474 by @TPAteeq. Maintainer fix on port: moved the CHANGELOG
entry from the already-released 0.8.50 block (where the stale base placed it) into
the current Unreleased section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 23:55:14 +01:00
Michael Katsoulakis 1225677b8a docs: fix graphify global add example (#1489)
The README showed `global add graph.json myrepo` (positional tag) writing to
`~/.graphify/global.json`. The real CLI takes the tag via `--as` (the positional
was silently ignored) and the file is `~/.graphify/global-graph.json`. Corrected
to `global add graph.json --as myrepo`.

Ported from PR #1489 by @MikeKatsoulakis.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 23:53:50 +01:00
safishamsi 6a45263bc0 chore: sync uv.lock to 0.8.50
The lockfile's own package version trailed the pyproject bump; uv regenerated it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 23:43:35 +01:00
jiangyq9 6509d0ca6f fix(extract): disambiguate imported type stubs across files without blocking rewire (#1462)
Two files that both import and use the same type as a bare name (e.g.
`from pathlib import Path` used as a type hint in a.py and b.py) collapsed into one
node, even with no project definition to anchor them. The referencing file is now
recorded in an internal `origin_file` field and used as the disambiguation key when
_disambiguate_colliding_node_ids splits same-id nodes — while source_file stays
empty, so the corpus-level rewire still collapses these stubs onto a real project
definition when one exists (the #1402 path is untouched). origin_file is read only
inside disambiguation; the rewire and the Java/C# type resolvers key off
source_file as before.

Ported from PR #1479 by @jiangyq9 (squash-merged onto current v8; the branch's
stale base made the raw diff appear to revert later features — the 3-way merge
applies only the surgical origin_file change). Resolved a test-adjacency conflict
with #1500 by keeping both cross-file tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 23:38:26 +01:00
TheFedaikin 76b6eabdb0 feat(extract): resolve C# cross-file type references + enum/struct/record (#1466)
Adds _resolve_csharp_type_references (graphify/extractors/csharp.py), the C#
counterpart to _resolve_java_type_references. It re-points dangling
inherits/implements/references edges from no-source shadow stubs onto the real
definitions, disambiguating same-named types across namespaces using the
referencing file's `using` directives + enclosing namespace; ambiguous matches
are refused (unique-hit guardrail), not guessed. Runs after id-disambiguation and
the sourceless-stub rewire, on the ambiguous remainder, behind a log-and-skip
try/except. _CSHARP_CONFIG is broadened to extract enum/struct/record as type
definitions so references to them resolve too.

Ported from PR #1466 by @TheFedaikin. Known follow-up: types declared in
nested/multiple namespaces in one file aren't registered as targets yet (fails
safe — under-resolves to a stub, never mis-resolves). Advances #1318 for C#.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 23:36:47 +01:00
Mohammed Ateeq 36b76ce8e0 fix(extract): Go cross-file type refs emit sourceless stubs (#1500)
Go's ensure_named_node emitted a SOURCED stub for a cross-file type reference,
unlike the generic/ObjC/Java extractors which emit sourceless ones. A sourced stub
bakes the referencing file's path into the node id at disambiguation time, which
blocks the corpus-level rewire from collapsing it onto the real definition — so a
type defined in one Go file and referenced from two others produced three nodes
(the real def plus two phantom per-file duplicates). Go now emits a sourceless
stub like the other languages, so the rewire collapses them to a single node.

Ported from PR #1501 by @TPAteeq. Closes the #1402 gap for Go.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 23:32:55 +01:00
safishamsi b17e88cc49 release: 0.8.50
Dates the 0.8.50 CHANGELOG and bumps the version. Highlights: WPF/XAML extraction
+ ViewModel/binding links (#1460/#1473), Objective-C relationship fixes (#1475),
.vue SFC grammar fix (#1468), Metal shader indexing (#1480), Java field/annotation
references (#1485/#1487), portable wiki links (#1444), *_BASE_URL backend overrides
(#1458), non-streaming OpenAI-compatible calls (#1223), reflect --if-stale sidecar
freshness (#1470), label --missing-only (#1481), canvas grid + case-fold dedup
(#1452/#1453), the Read|Glob hook extension fix (#1463), and the no-API-key skill
clarification (#1461).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.8.50
2026-06-27 10:26:04 +01:00
jiangyq9 a16b5bd151 feat(label): add --missing-only flag for incremental community naming (#1481)
`graphify label --missing-only` restricts LLM labeling to communities that are
unnamed or still hold a `Community N` placeholder, preserving existing
non-placeholder labels read from .graphify_labels.json and merging new labels over
them. Lets a large graph be relabeled incrementally without re-naming (and paying
for) communities that already have good names.

Ported from PR #1481 by @jiangyq9. This supersedes the earlier #1421 by
@matiasduartee, which proposed the same flag — credit to @matiasduartee for the
original; #1481 is written against the current label signature (post-#1390
max-concurrency/batch-size) and merges clean, where #1421 had drifted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:19:15 +01:00
jiangyq9 652ba42e61 feat(extract): index Metal (.metal) shader files (#1480)
Metal Shading Language is C++14, so .metal files are classified as code and routed
through the existing C++ extractor, mirroring the CUDA .cu/.cuh reuse. Also adds
.cu/.cuh/.metal to build.py's _LANG_FAMILY map (they were all missing), so the
cross-language phantom-`calls`-edge filter treats them as C++. README language
table updated.

Ported from PR #1480 by @jiangyq9. This supersedes the earlier #1450 by
@GoodOlClint (same .metal -> C++ approach and fixture) — credit to @GoodOlClint
for the original; #1480 additionally closes the CUDA family-map gap and updates
the docs, and merges clean against current v8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:19:15 +01:00
Mohammed Ateeq 7a94f72779 fix(wiki): portable relative markdown links so navigation works outside Obsidian (#1444)
to_wiki emitted Obsidian `[[Title]]` wikilinks. Outside Obsidian `[[Domain Data
Models]]` resolves to a literal "Domain Data Models.md", but the article file is
the slugged "Domain_Data_Models.md", so nearly every community/god-node link
opened an empty page; god-node articles also linked node-level neighbors that
never get an article file, so those were dead even inside Obsidian.

Links are now standard `[display](slug.md)` with the target URL-encoded (spaces,
&, parentheses, # survive in CommonMark and Obsidian alike); a link whose target
has no article is rendered as plain text instead of left dangling. A label->slug
resolver is built up front so a link points at the real on-disk filename incl. the
case-fold collision suffix.

Ported from PR #1465 by @TPAteeq. Maintainer edit: trimmed the CHANGELOG wording
that overstated the guarantee ("always matches ... collision suffix and all") —
two articles sharing a byte-identical label still keep the first slug (pre-existing
behavior, same as the old [[label]]), so the claim is scoped to the case-fold case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:19:15 +01:00