`self.product.name` dot-syntax now emits an `accesses` edge and
`@selector(method)` emits a `calls` edge, both resolved only to an
unambiguous in-scope definition (a sibling method of the same class for
dot-syntax; exactly one method by exact selector name for @selector) so
no false-edge fan-out occurs when multiple classes share a name.
Hardened over the original PR: resolution now matches the method node id
EXACTLY (a method id is _make_id(container, name)) rather than by
`endswith` suffix. The substring match would mis-resolve `self.name` to a
sibling `-surname` (false positive) and, when a substring-colliding
sibling existed, suppress the correct edge (false negative); exact
matching fixes both. Adds substring-collision regression tests
(`-name`/`-surname`, `-doThing`/`-reallyDoThing`).
Completes the #1475 ObjC follow-ups (Bug 5 dot-syntax accesses, Bug 6b
@selector target-action).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A member call through a constructor-injected dependency
(`constructor(private db: Database)` ... `this.db.query()`) now produces
a calls edge to the field type's method. The field->type map is captured
from constructor parameter-properties, and resolution reuses the existing
single-definition god-node guard (like the Swift/Python/Ruby member-call
resolvers): the edge is emitted only when the field's type name resolves
to exactly one class definition that owns the method, so an ambiguous or
unknown/untyped field produces no edge — no global name-match fan-out.
Edges are EXTRACTED (the type is explicit from the annotation). TS/JS-only
and additive; scope is constructor parameter-property injection.
Adds the decisive regression tests the implementation needed: two classes
defining the same method name where the injected field is typed to one of
them (must resolve to that one only), and an ambiguous type-name case
(must emit no edge).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`export * as ns from './mod'` now creates a real symbol node for the
namespace binding `ns`, registers it as a named export (so a downstream
`import { ns }` resolves to it), and emits a file-level `re_exports` edge
to the target module. The binding is treated as a single opaque symbol —
`ns.member` accesses are deliberately NOT expanded into per-member
name-matching, avoiding the over-linking that would fan false edges.
Includes re-export cycle and deep-chain recursion guards.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the tsconfig path-alias resolver (#1531) with single-`*` wildcard
capture and substitution: a pattern like `@app/*` or `@*/interfaces`
captures the matched segment and substitutes it into each target in
declared order, honoring baseUrl and tsc's longest-prefix / exact-wins
specificity rules, and preserving #1531's first-existing-target-wins
fallback (no false edge when nothing resolves). Builds on the
_resolve_tsconfig_alias helper rather than reintroducing inline loops;
multi-star patterns remain out of scope.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ruby type-aware member-call resolution and workspace exports-map
resolution, the #1529 alias/workspace import-edge regression fix, tsconfig
paths fallbacks, semantic-cache pruning, three ObjC extractor fixes, Swift
static-call confidence, the secondary LLM timeout, GraphML null coercion,
host-generic install wording, and Dependabot dep bumps. See CHANGELOG.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
#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>
`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>
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>
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>
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>
_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>
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>
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>
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>
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>
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>
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>
`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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>