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>
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>
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>
`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>
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>
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>
Some OpenAI-compatible gateways default to SSE streaming when `stream` is omitted,
but graphify always reads the result as a single resp.choices[0]. The call would
then fail against those gateways. Pass `stream: False` explicitly.
Ported from PR #1482 by @jiangyq9 (covers the extraction dispatch path,
_call_openai_compat). Maintainer fix on top: applied the same `stream: False` to
the second OpenAI-compatible call site, _call_llm, which feeds the --dedup-llm
tiebreaker and had the identical bug.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Annotations on Java classes/interfaces/records (e.g. @Service, @Entity) produced
no edge, so type-level framework wiring was invisible even though method-level
annotations were already captured. The class/interface/record handler now emits
`references` edges with the existing `attribute` context, reusing the renamed
`_java_annotation_names` helper (was `_java_method_annotation_names`; logic
unchanged) at both the type and method call sites.
Ported from PR #1487 by @oleksii-tumanov. Resolved an additive test conflict with
#1485 by keeping both new test functions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Java field declarations produced no `references` edge for their type, so a class's
data dependencies (its field types) were missing from the graph even though
parameter and return types were already captured. The field handler now collects
the declared type via the same `_java_collect_type_refs` helper used elsewhere,
preserving the `field` and `generic_arg` contexts and skipping primitives
(int/boolean/etc.), matching the existing C#/PHP/Kotlin field handlers.
Ported from PR #1485 by @oleksii-tumanov.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A detailed report (#1475) showed the ObjC extractor silently dropping ~60% of
code-level relationships. Five fixes:
1. Dispatch: ObjC `.h` headers were parsed by extract_c (1 node, 0 edges, losing
every @interface/@protocol/@property/method). _get_extractor now routes a `.h`
to extract_objc when it contains an ObjC-only directive
(@interface/@protocol/@implementation/@import) — these are illegal in C/C++, so
the sniff never hijacks a genuine C/C++ header (verified: a plain C/C++ `.h`
stays on its existing extractor).
2. Calls: the method-body pass produced zero `calls` edges because it matched
child types `selector`/`keyword_argument_list`, but tree-sitter-objc tags
selector parts with the field name `method` (type `identifier`). The selector
is now reconstructed from every `method`-field child, explicitly skipping the
`receiver` field — so self/super/ClassName receivers are never mistaken for a
selector, and compound sends ([self a:x b:y]) resolve too. (Avoids the report's
suggested `"identifier"` fix, which would have matched receivers as selectors.)
3. Generic property types: NSArray<Product *> * wraps the type in a
generic_specifier, so the old direct-type_identifier scan saw nothing. The
element (Product) and container (NSArray) are now both referenced.
4. Class methods: `+ (…)shared` was labeled -shared; the +/- sigil is now read
from the method node's first child.
5. @import: `@import Foundation;` (a module_import node) now emits an imports edge.
Dot-syntax property `accesses` (Bug 5) and @selector(...) target-action edges
(Bug 6b) need type/name resolution policy and are left as follow-ups. Added six
regression tests; the reporter's claim that compound messages lack a
message_expression wrapper (Bug 6a) was checked against the real AST and refuted —
they share Bug 2's root cause, fixed by the same field-name change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds on the initial XAML support (#1460). Resolves a view to its ViewModel from
an explicit <Window.DataContext><vm:MainViewModel/>, a design-time
d:DataContext="{d:DesignInstance Type=...}", the View->ViewModel naming
convention, or Prism ViewModelLocator.AutoWireViewModel="True". Resolution is
always against an actually-extracted C# class node, so a name matching no class
(or an ambiguous 2+) emits no edge -- explicit DataContext is EXTRACTED,
convention/Prism are INFERRED. Also extracts binding paths ({Binding User.Name},
Path=Order.Total), commands (Command="{Binding SaveCommand}"), converters, and
CommunityToolkit [ObservableProperty]/[RelayCommand] generated members.
The #1460 event-handler hardening is preserved unchanged: events still resolve
only to methods with a .NET (object sender, ...EventArgs e) signature, and the
free-form-attribute denylist still prevents values like Content="Save" from
fabricating event edges (both regression tests still pass). ViewModel discovery is
bounded to the active extraction root.
Ported from PR #1473 by @MikeKatsoulakis (clean 3-way merge onto current v8).
Maintainer fix on top: the CommunityToolkit member reader now reads the
code-behind with errors="replace", so a non-UTF8 ViewModel .cs can't raise
UnicodeDecodeError and abort extract_xaml (matches every other reader in the
module). Added a regression test for that case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
.vue files were dispatched to extract_js, which picks a tree-sitter grammar by
suffix. .vue is neither .ts nor .tsx, so the whole SFC -- <template> markup,
<script>, and <style> -- was fed to the JavaScript grammar, producing a top-level
ERROR node and recovering no imports, symbols, or type references.
A dedicated extract_vue masks everything outside <script> (replacing it with
spaces so symbol line numbers stay accurate) and parses just the script with the
grammar named by `lang` (ts default; tsx/js/jsx honored). .vue also joins the
cross-file symbol-resolution pass now that it parses cleanly.
Ported from PR #1468 by @papinto. Maintainer fix on top: the <script> open-tag
scan now skips over quoted attribute values, so a `>` inside one (Vue 3.3+ generic
components, e.g. generic="T extends Record<string, unknown>") no longer ends the
tag early and swallow the body. Added a regression test for that case.
(CHANGELOG also records #1470, committed just prior.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
graphify reflect renders LESSONS.md from the memory docs and the
.graphify_analysis.json / .graphify_labels.json sidecars, but reflect --if-stale
only stat'd the memory docs and graph.json. So after community analysis or labels
changed (without the graph changing), --if-stale wrongly reported lessons fresh and
skipped the regen, leaving LESSONS.md stale. The freshness check now also stats the
analysis and labels sidecars, using the same custom --analysis / --labels paths the
run itself uses (so the check and the run can't disagree about which files matter),
and treats a missing sidecar as not-an-input. This makes the documented "no-op when
LESSONS.md is newer than every input" contract actually true.
Ported from PR #1470 by @oleksii-tumanov.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Read|Glob PreToolUse hook (the "run graphify first" nudge, shared by the
Claude Code and CodeBuddy installers via _READ_SETTINGS_HOOK) decided whether to
nudge by substring-scanning the joined file_path/pattern/path for known
extensions. That had two opposite failures: '.js' is a substring of '.json' so
package.json / tsconfig.json spuriously fired, and .astro/.vue/.svelte weren't in
the set so Astro/Vue/Svelte projects never nudged on their primary source type.
The hook now compares each value's real trailing extension (segment after the
last '/', then after the last '.') against the set, and adds .astro/.vue/.svelte.
package.json -> tail .json (silent); **/*.astro -> tail .astro (fires); an
extension on a directory component (my.ts/file) correctly stays silent. The
graphify-out/ suppression and fail-open behavior are unchanged.
Ported from PR #1464 by @marketechniks onto current v8. Added three regression
tests on top of the PR's (multi-dot a.test.tsx / foo.min.js, a Windows backslash
path, and the directory-extension trap) to pin the trickiest parts of the new
segment-split logic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hermes and the other AGENTS.md hosts (Codex, Aider, OpenClaw, Droid, Trae, ...)
run the graphify CLI directly and do not dispatch subagents. The Step 3 extraction
guidance only described the no-API-key path as "fall straight through to subagent
dispatch", so on `/graphify .` those agents had no path they could take, fixated on
the GEMINI/ANTHROPIC key language, and looped for minutes insisting on a missing key
before eventually proceeding (a pure-code corpus is AST-only and needs no key at all).
Step 3 now opens with a hoisted, host-agnostic statement -- graphify needs no API
key, never prompt for one, never block on one; code is AST-only; a code-only corpus
skips semantic extraction entirely -- and the no-key fallback now spells out a
terminal-only path (write the empty semantic file and continue, or extract content
inline) instead of assuming subagent dispatch.
Applied in the shared core fragment and in the aider/devin core variants, so all 16
skill bodies carry it; the aider/devin change is registered as a sanctioned
monolith-roundtrip diff. Regression test (test_extraction_states_no_api_key_required_
for_every_host) renders every host and pins the wording, ordering, and the
non-subagent fallback so this can't silently regress.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Makes .xaml a first-class code input. extract_xaml() uses stdlib XML (no new
parser dependency) behind the same DOCTYPE/ENTITY and size guards as the .csproj
extractor, and captures: the root element, named controls (x:Name/Name) and their
control types, {Binding ...} references, x:Class, and -- the useful part -- a
bridge from the view markup to its .xaml.cs code-behind by resolving event-handler
attributes to the matching methods on the partial class.
Ported from PR #1460 by @MikeKatsoulakis onto current v8.
Maintainer hardening on top of the original PR: event resolution is now gated so
it can't fabricate edges. The original matched any attribute value against
code-behind method names, so Content="Save" next to a business method Save(), or
Tag="<a-handler-name>", produced spurious "event" edges. Resolution now requires
(a) the attribute is not a known free-form/identity property (Content, Text, Tag,
Title, ToolTip, Header, ...), (b) the value is a bare identifier, and (c) the
matched method actually has the .NET event-handler signature
(object sender, <T>EventArgs e) -- read from the code-behind source since the C#
extractor does not record parameter lists on method nodes. Added regression tests
for both false-positive cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The kimi, gemini, and deepseek backends hardcoded their base_url, so users
behind an OpenAI-compatible proxy/gateway or running a self-hosted relay had no
way to redirect them (unlike ollama/openai, which already read *_BASE_URL). Each
backend now reads KIMI_BASE_URL / GEMINI_BASE_URL / DEEPSEEK_BASE_URL and falls
back to its official default when unset, so behavior is unchanged for anyone who
doesn't set the variable.
Ported from PR #1458 by @jc2shile onto current v8. The PR branch carried 624
unrelated files from a stale base; this lands just the clean 16-line llm.py
change. Added subprocess-based tests covering both the override and the default
for all three backends (BACKENDS reads the env at import time, so each case runs
in a fresh interpreter).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
to_canvas sized each community group box for a ceil(sqrt(n))-column grid but the
placement loop hardcoded 3 columns, so any community bigger than ~9 members
rendered as a cramped 3-wide strip in an over-wide, mostly-empty box (and the box
width/height didn't even agree — w used sqrt(n), h used /3). The column count is
now computed once per community (inner_cols) and reused for box width, box height,
and card placement, so the cards fill the box. Cosmetic, no data change.
Ported from PR #1459 by @TPAteeq onto current v8 (clean: only the grid math
changed, the #1457 dedup helper is untouched). Verified the geometry on a real
canvas: n=25 -> 5x5 grid with every card inside its box; n=10 -> 4 columns.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
to_obsidian / to_canvas / to_wiki keyed filename dedup on the exact-case name,
so two labels differing only by case (e.g. `References` vs `references`) counted
as non-colliding and the second write clobbered the first on case-insensitive
filesystems (macOS/APFS, Windows/NTFS) — silently, no suffix, no warning.
Dedup now folds case (keyed on the lowercased name) while emitting the
original-case filename, so any pair that would collide on disk gets a numeric
suffix. The obsidian/canvas dedup is one shared helper (`_dedup_node_filenames`)
so they can't drift; wiki's slug dedup gets the matching fix; the `_COMMUNITY_*`
overview notes (which had no dedup at all) are covered; and a generated `base_1`
is re-checked so it can't overwrite a node literally labelled `base_1`.
Ported from PR #1457 by @TPAteeq onto current v8. Verified with a rigorous
edge-case battery (case-only collision, base_1 literal re-check -> base_1_1,
community-label case fold, determinism) plus the PR's tests; full suite 2404
passed, ruff + skillgen clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>