NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
PyPI · #3097 most downloaded on PyPI
AI coding assistant skill (Claude Code, CodeBuddy, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Devin CLI, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph
Last release today
04 Oct 2026
Ships on a steady schedule
a new release about every 8 days
Nearly every release is documented
notes for 58 of the last 60 stable releases
1 version withdrawn
withdrawn after publishing
6 months old
244 releases · first in 2026
One column per month.
Nothing published for this version
Nothing published for this version
Feature: four more language extractors gained structural depth. Ruby paren-less self-sends ( do_thing with no receiver or parens) are now captured as
do_thing with no receiver or parens) are now captured as calls, with a local-variable/parameter/block-parameter in scope correctly suppressing the call (fail-closed, no fabrication) (#3960, thanks @rajatnagda45). TypeScript abstract method signatures in an abstract class are now extracted as callable method members and resolve as call targets (#3961, thanks @rajatnagda45). Scala deferred (abstract) method declarations in traits and abstract classes (def foo: Int, no body) are now extracted as methods (#3962, thanks @rajatnagda45). C++ a member-function declaration inside a class (void foo();, defined out-of-line) is now classified as a method rather than a field, including pure-virtual, const, operator, and destructor forms; real data members stay fields (#3963, thanks @rajatnagda45).Option, Result, Vec, String, Box, Rc, Arc, HashMap, and friends) no longer accumulate spurious in-degree and surface as god nodes that distort community detection and ranking; they are filtered at extraction time, and a type the file defines itself is kept. Tuple-struct and unit-struct construction (ClientId(id)) is reclassified from a calls edge to a references/constructor edge (#3973, thanks @liam-mcelhaney122).[[v1.2 release]], [[note.en]]) now resolves instead of being dropped by a premature extension strip; plain, explicit-extension, anchor, and alias wikilinks are unchanged, a literal indexed file beside the link keeps precedence, and a genuinely missing target is still not fabricated (#3905, #3904, thanks @Yyunozor).artifacts/graphify-notes vs artifacts/graphify) is no longer wrongly treated as inside the output dir; symlinks, .., relative paths, and case-insensitive filesystems are handled (#3964, #3959, thanks @shobhitagnihotri69).exports path to the legacy main/module fields (#4000, #3834, thanks @Adityakk9031)..vh) are now detected and extracted through the same tree-sitter-verilog grammar as .v/.sv, so macros, parameters, and modules declared in headers are captured (#3983, thanks @oleksii-tumanov).(id, source_file) so a same-id edge from a different, unchanged file is preserved while a genuine same-file re-emit still replaces (#3997, #3981, thanks @Ayushraj06-bit).@ManyToOne(targetEntity = Customer::class)) are now extracted as attribute references to the referenced type (#3965, #3835, thanks @hopstreax).obj:method(), self:method()) now resolve to the table-qualified method definition; a non-self receiver fails closed with no cross-file member fabrication (#3994, thanks @rajatnagda45).fun Name/Arity, as in lists:map(fun helper/1, L)) are now recorded as indirect_call edges to the arity-matched local function; remote fun Mod:Name/Arity and anonymous funs are left as-is, and an undefined or arity-mismatched local fails closed (#3996, thanks @rajatnagda45).receiver?.Method()) now resolve through the same typed-receiver path as receiver.Method(), including the call-site generic-argument walk; chained and element-access forms fall through to bare-name resolution with no fabrication (#3976, #3797, thanks @Cintu07).Feature: enum members are now extracted as nodes with case_of edges in four more languages — Rust enum variants ( #3938 ), Zig enum members ( #3940 ),
case_of edges in four more languages — Rust enum variants (#3938), Zig enum members (#3940), C++ enum/enum class enumerators including nested enums (#3939), and Scala 3 enum cases plus their methods (#3937) — all thanks @rajatnagda45.super.method() now resolve to the declaring ancestor (walking the inherits chain, nearest declaration wins), instead of dangling; an unknown/external or ambiguous ancestor fails closed (#3932, thanks @janwaleed09).MyModule.DoThing(), MyClass.SharedMethod()) resolve to the target method; value-receiver and MyBase. calls fail closed (#3909, thanks @rajatnagda45).<script> blocks are fed to the AST pass (the HTML template no longer produces parse errors), with line numbers preserved (#3902, thanks @Bosken85).--exclude-hubs, a node whose only neighbours are excluded hubs is kept with its hub's community instead of being severed into a singleton; the default path is unchanged (#3933, thanks @neo1777).--push path creates a per-label id index before the node upsert loop, fixing the throughput collapse on large graphs (#3957, thanks @Yi-111-a).graphify hook status reports hooks written by an older release as out of date (run graphify hook install to refresh) (#3951, #3771, thanks @bercedev).Cargo.toml (only [workspace], no [package]) is treated as skipped-by-design rather than warned as zero-node (#3930, #3910, thanks @Adityakk9031).CLAUDE.md etc.) is a symlink, install reports the real target it wrote to, and uninstall keeps the symlink (strips only the graphify section) instead of deleting the link (#3950, #3953, #3805, thanks @bercedev).Feature: after a package upgrade, graphify refreshes stale installed skills automatically (the SKILL.md + references sidecar it manages) so the versio
graphify refreshes stale installed skills automatically (the SKILL.md + references sidecar it manages) so the version-mismatch warning no longer requires a manual graphify install. It runs on any non-install CLI command when a skill is stale, backs up local edits to SKILL.md.bak, never touches your marker-bounded CLAUDE.md/AGENTS.md/GEMINI.md sections, and can be disabled with GRAPHIFY_NO_AUTO_REFRESH=1 (#3895, #1805, thanks @bercedev).graph.html's Node Info panel now shows the real Type/Source/Community for each node instead of "Type: unknown / Source: -" (the panel read field names that did not match the emitted node schema); aggregated community nodes show a member count (#3918, #3914, thanks @hopstreax).@Volatile var x = 0) no longer crashes extraction with an UnboundLocalError that dropped the whole file (#3915, thanks @nothariharan; #3899, thanks @harshaygadekar; #3884).DO $$ ... $$ block is now extracted — the block node the parser produces for that span is walked instead of skipped (#3900, thanks @bercedev).@functions { } blocks (classic Razor Pages/MVC), not only Blazor @code { } blocks (#3908, thanks @rajatnagda45).@extends (#3907, thanks @rajatnagda45).docx sidecar conversion now keeps tables in their document position (instead of dumping them after all prose) and reads all text, including tracked insertions, content controls, and text boxes, by walking the document body in order (#3833, thanks @L4XB)..graphifyignore/.gitignore/--exclude and resolves an article-named index without overwriting the generated index.md hub — two independent wiki/markdown fixes (#3818, thanks @breken-ai; escaped-alias parsing [[target\|alias]] #3772, thanks @zagushka).GRAPH_REPORT.md reuses the shared real-node filter, so rationale/concept nodes no longer inflate a community's node count or leak into the listing (#3836, #3794, thanks @ayushcodes10).pdf extra (pypdf) is not installed, instead of silently producing no text (#3710, #3702, thanks @shobhitagnihotri69).graphify / graphify --help now shows the logo banner and a link to the hosted platform at app.graphify.com.Feature: SQL CREATE TRIGGER statements are now extracted and linked to their table ( ON <table> ), including OR REPLACE / OR ALTER , INSTEAD OF , and
CREATE TRIGGER statements are now extracted and linked to their table (ON <table>), including OR REPLACE/OR ALTER, INSTEAD OF, and procedural BEGIN…END bodies that previously landed in a parser-error node and were dropped (#3863, thanks @rajatnagda45).enum declarations and their constants are extracted, with members linked to the enum via case_of (#3861, thanks @rajatnagda45).R6::R6Class, methods::setRefClass) are now recognised, so the class body and its methods are no longer dropped (#3864, thanks @rajatnagda45).self$method() and private$method() now resolve to the sibling method instead of dangling; super$ is left unresolved (single-file dispatch is not visible) (#3865, thanks @rajatnagda45)..graphifyignore/.gitignore/--exclude — it no longer descends huge ignored trees when building the [[link]] index, and a wikilink can no longer resolve into an ignored file (#3826, #3822, thanks @Abhirup0).C:\/UNC key on POSIX) using posixpath/ntpath rather than the host's rules, fixing separator corruption introduced by the 0.9.69 portability work (#3879, thanks @Dakshcore).x.com/twitter.com appearing in the path or query is no longer corrupted (#3880, thanks @Dakshcore).graphify install shows the refreshed graphify logo banner (#3892, thanks @rajarshidattapy).Security: the Fortran capital-F cpp step no longer allows an untrusted .F / .F90 source to read arbitrary host files. -nostdinc -I /dev/null did not s
.F/.F90 source to read arbitrary host files. -nostdinc -I /dev/null did not stop cpp from resolving absolute (#include "/etc/passwd") or traversing (#include "../../../secret") includes, which inlined host-file contents into graph.json/GRAPH_REPORT.md and the LLM context on the default offline path. Every #include directive is now stripped before preprocessing and the source is fed to cpp on stdin; macro expansion is preserved (GHSA-pcc4-rvhr-2pr8, CWE-22/73/200).--watch snippet no longer interpolates the agent-substituted INPUT_PATH into a shell command — it now reads the trusted graphify-out/.graphify_root written in Step 1, closing the last instance of the shell-injection class from #3642 (#3852, #3844, thanks @hopstreax).value paired with a secret-named name in name/value pair lists (environment = [{ name = "DB_PASSWORD", value = "…" }], ECS valueFrom included), where the sensitive signal is the sibling name literal rather than a key (#3870, #3787, thanks @breken-ai).graphify watch now serializes concurrent rebuilds on Windows via msvcrt byte-range locking instead of a no-op lock, closing a WinError 32 race between overlapping rebuilds; the POSIX fcntl path is unchanged (#3883, #3881, thanks @harshaygadekar).(int Count, string Name) recorded a bogus ref to Count/Name); only the element types are referenced (#3877, #3796, thanks @KaiyiQuan).calls edges — <MyButton/> and <_Row/> link to the component, while lowercase DOM tags (<div>) and member tags (<Nav.Item>) are conservatively skipped, so React component graphs capture render relationships (#3855, #3854, thanks @sinangumuskabak-sys).from Company.Apps.Team.lib import x when the scan root is Team/) now resolve to the local module by projecting the namespace prefix onto the scan-root layout (#3867, #3843, thanks @nikhilsaxena04).Feature: five language extractors gained structural depth — OCaml classes now emit their methods (via the method relation) and instance variables ( #3
method relation) and instance variables (#3838, thanks @rajatnagda45); Elixir defprotocol/defimpl are extracted as containers holding their functions, with a same-file implements link (#3839, thanks @rajatnagda45); Fortran derived-type contains blocks link type-bound procedures to the type, resolving the => impl target (#3840, thanks @rajatnagda45); Julia macro definitions and @enum types are extracted, including valued (red = 1) and typed (Color::UInt8) enum forms, with members using the case_of relation (#3841, thanks @rajatnagda45); Kotlin annotations (class/function/property, use-site targets) and val/var primary-constructor properties now produce edges (#3848, #3842, thanks @nikhilsaxena04).tsconfig.json that only carries references (no paths of its own) now resolves path aliases declared in the referenced project configs, so alias imports in a tsc -b layout no longer dangle (#3753, #3745, thanks @abhay-codes07).spawn start method can't re-import __main__ (stdin, python -c, REPL, embedded callers), falling back to a correct sequential run instead of a wall of BrokenProcessPool tracebacks (#3754, #3669, thanks @abhay-codes07).ingest classifies a URL by its parsed host, not by text anywhere in the URL, so example.com/article-about-youtube is no longer mistaken for a YouTube link; subdomains and youtu.be still match, path-based extension detection is unchanged (#3831, thanks @L4XB).graphify update on a destination outside the scan root no longer leaks a stat-index cache into the corpus — the incremental detection path now forwards cache_root like the fresh-scan path (#3850, #3847, thanks @ayushcodes10).../. segments so more duplicates actually collapse (#3781, #1964, thanks @ayushcodes10).CONTRIBUTING.md, CODE_OF_CONDUCT.md, and RELEASING.md, refreshed issue/PR templates, and corrected factual drift in SECURITY.md (supported version, network/XSS boundaries), ARCHITECTURE.md (shared-state), and AGENTS.md (scoped-query workflow) (#3845, thanks @nikhilsaxena04).Security: the /graphify add ... --watch reference no longer passes the raw, agent-substituted INPUT_PATH placeholder unquoted into a shell command ( …
/graphify add ... --watch reference no longer passes the raw, agent-substituted INPUT_PATH placeholder unquoted into a shell command (… -m graphify.watch INPUT_PATH), where a scan root containing $(…), backticks, or ; could execute — a follow-on to the Step 1 fix. The watcher now reads the trusted graphify-out/.graphify_root that Step 1 resolves, so there is no path to substitute (#3742, #3642, thanks @ayushcodes10). The identical placeholder still appears in the Aider/Devin monolith --watch snippet and is tracked separately.update/extract --code-only/watch) now preserve a cross-file imports/calls/uses edge whose target symbol lives in an unchanged file — the symbol-resolution facts pass widens its target index with the read-only resolution context, so re-extracting one file no longer silently drops its edges into the rest of the graph; a genuinely removed edge is still pruned (#3812, #3776, thanks @hopstreax).inherits/implements/parameter/return/base types) no longer resolve to a same-named non-type node — an enum member, property, field, or method sharing a type's name is excluded from the type-definition index, so a class inherits from the real base rather than a stray member; resolution of partial classes without a contains edge is restored (#3815, #3795, thanks @hopstreax).pom.xml ingestion resolves a dependency's inherited groupId/version from the local <parent> block and substitutes ${property} / ${project.*} placeholders (offline, one level), so previously-dangling depends_on edges now connect (#3823, #3806, thanks @chiliec).os.execvpe is a true process replacement only on POSIX; on Windows it spawned a new process and let the parent race ahead unpinned, crashing update/extract/cluster-only/label. The Windows path now spawns via subprocess.run and exits with the child's status; the POSIX path is unchanged (#3816, #3799, thanks @sinangumuskabak-sys).variable default and output value — the secret's name lives in the block label, so the literal sat under the generic default/value key that the key-name check never flagged (variable "db_password" { default = "…" } reached graph.json verbatim). Redacted when the label names a secret or the block sets sensitive = true (#3817, #3644/#3762 follow-up, thanks @breken-ai).PERFORM edge was silently dropped, leaving only the file and program nodes (#3813, thanks @abhay-codes07).PERFORM A THRU/THROUGH Z now links both endpoints of the range, not just the entry paragraph, so the range-end no longer lacks an inbound calls edge; a dangling THRU target is skipped rather than fabricated (#3814, thanks @abhay-codes07).GRAPH_REPORT.md's Knowledge Gaps section only offers "undocumented components" as an explanation for an isolated node when the graph actually has a semantic layer (a document/paper/image node an LLM extracted meaning from) — a code-only graph no longer lists a possibility it can never have (#3828, #3801, thanks @ayushcodes10).Fix: the PYTHONHASHSEED determinism pin (0.9.66) now re-execs via python -m graphify instead of replaying argv[0] , fixing a Windows regression where
python -m graphify instead of replaying argv[0], fixing a Windows regression where update/extract/cluster-only/label failed to re-launch through the console-script .exe launcher (#3780, thanks @ayushcodes10).function(){…}, an arrow fn()=>…, or a closure passed as an argument now produces a node and its inner calls are captured, in all positions including file scope. Route-definition closures get a semantic VERB /path name (composing nested group() prefixes); other closures get a stable per-scope ordinal (#3461, #3409, thanks @nikhilsaxena04).import pkg.sub, from pkg.sub import x) now resolve to the local package/module node within the scan root, reusing the canonical resolver (bounded walk, PEP 420 namespace handling); an ambiguous module name across scanned trees fails closed rather than binding arbitrarily (#3729, thanks @Ha1baraA11).pkg/ package alongside a pkg.py module) no longer produces a phantom import cycle — the spurious provisional edge is retracted while genuine package-init and submodule edges are preserved (#3784, #3777, thanks @hopstreax).configs = [{ password = "…" }]), not just maps (#3762, #3644 follow-up, thanks @abhay-codes07).export no longer rewrites unchanged wiki/Obsidian pages on every run — a page whose content is identical is left untouched (stable mtimes, clean git/Obsidian sync), while changed and new pages still write and orphaned pages are still swept (#3760, #3060, thanks @abhay-codes07).Feature: five new language extractors — COBOL ( .cbl / .cob / .cobol / .cpy ; programs, paragraphs, PERFORM / CALL / COPY , pure-regex, no new depende
.cbl/.cob/.cobol/.cpy; programs, paragraphs, PERFORM/CALL/COPY, pure-regex, no new dependency) (#3713, thanks @Abdul535), VB.NET (.vb; case-insensitive types/methods, Inherits/Implements/Handles) (#3717, thanks @Abdul535), R (.r/.R; assignment-form function defs, library/source, S4/R6 classes) (#3715, thanks @Abdul535), Solidity (.sol; contracts/interfaces/libraries, is inheritance, imports, modifiers) (#3716, thanks @Abdul535), and Erlang (.erl/.hrl/.escript; modules, functions by arity, behaviours, local + remote foo:bar() calls) (#3714, thanks @Abdul535). The R and Erlang grammars ship via the r/erlang extras (they have no standalone PyPI wheel); Solidity and VB.NET have their own extras.references edges (#3672, #3737, thanks @rajatnagda45, @oleksii-tumanov).self.method() call resolves across files for simple generic impls (impl<T> Foo<T>) (#3653, thanks @oleksii-tumanov).graph.json is now deterministic across runs — update/extract/cluster-only/label pin PYTHONHASHSEED via a one-time re-exec, so hash-seed-sensitive community detection (Leiden/Louvain) produces identical output run-to-run and matches hook-triggered rebuilds (#3743, #3641, thanks @ayushcodes10).imports_from edges now repoint onto the imported package's real file nodes instead of dangling at a go_pkg_ sink; external/stdlib imports stay external (#3748, thanks @carterko23).Cargo.toml in a subdirectory when none sits at the scan root (e.g. Tauri's src-tauri/), degrades gracefully instead of aborting the whole extraction when no manifest is found, and resolves workspace = true inherited dependency identity from [workspace.dependencies] (#3740, #3739, thanks @ayushcodes10; #3734, thanks @oleksii-tumanov).get_node/get_neighbors now accept the node identifier under node_id/id, not only label, so agents that spell the argument differently no longer get a missing-argument error (#3725, thanks @ahm3dwasim).graphify install no longer corrupts line endings (writes preserve the file's existing EOLs instead of rewriting to CRLF on Windows) and re-install is properly idempotent (a marker-bounded replace instead of a bare substring check) (#3741, #3668, thanks @ayushcodes10)..graphify_root marker (moved/deleted/symlink-loop target) is now ignored with a fall-back to the graph's directory, and the marker value resolves to an absolute path when GRAPHIFY_OUT is a shared absolute directory (#3707, thanks @Ha1baraA11; #3735, #3375, thanks @ayushcodes10).Security: the svg / all extras now floor Pillow at >=12.3.0 for CVE-2026-54058 (Pillow was pulled in transitively via matplotlib). Note: Pillow 12.3.0…
svg/all extras now floor Pillow at >=12.3.0 for CVE-2026-54058 (Pillow was pulled in transitively via matplotlib). Note: Pillow 12.3.0 dropped its glibc<2.27 cp310 Linux wheel, so a very old-glibc Python 3.10 host with the svg/all extra builds Pillow from sdist (#3698, thanks @viral-antuit).type Foo interface { Bar() }) now attach to the interface node, so calls resolve to them (#3672, thanks @rajatnagda45).protocol P { func f() }) now attach to the protocol node (#3673, thanks @rajatnagda45).let/const bindings are now scoped to their own block rather than the whole function, so a block-local binding no longer suppresses a genuine indirect_call edge elsewhere in the function; var stays function-scoped (#3688, thanks @ayushcodes10).graph.html no longer crashes vis-network with a stack overflow on large graphs — nodes are seeded on a spiral before physics runs so overlap-avoidance can't blow the layout recursion (#3699, thanks @sanjaiyan-dev).vector<int>, generics, &, quotes) instead of raw HTML entities, while the HTML sinks that need escaping keep it (#3686, #3664, thanks @hopstreax).Graphify-Labs/graphify instead of the old account (including in generated wiki output), translated READMEs use the current logo, GitHub issue/PR templates were added, and the Enterprise link was corrected (#3692, #3694, #3693, thanks @Abdul535).Feature: Terraform block attributes ( ami , instance_type , cidr_block , tags, and the like) are now preserved on the resource/data/module node and ar
ami, instance_type, cidr_block, tags, and the like) are now preserved on the resource/data/module node and are queryable and searchable, with typed values (bool/number/list/map) and nested blocks kept separate from direct attributes. Secret-named attribute values (password, *secret*, *token*, *_key, connection strings) are redacted before they reach graph.json or the model, so a hardcoded credential in a .tf file does not leak (#3644, thanks @hopstreax).<script> block of a PHP file is now indexed as JS (functions and calls) under the PHP file node, mirroring the Vue/Svelte embedded-script handling, with source lines mapped back to the real file positions (#3627, #2320, thanks @ayushcodes10).self.method() call now resolves across files for simple generic impls (impl<T> Foo<T>), extending the split-impl resolution to generic types while staying fail-closed on bounded, where, trait, and concrete-instantiation shapes (#3653, thanks @oleksii-tumanov).Receiver.method() call now resolves across files when the receiver's object/class (or its companion object) is declared in another file (#3598, #1698, thanks @ayushcodes10).Foo::bar() now resolves to a definition in another translation unit even when it survived extraction only as a qualified-label node (#3613, #2348, thanks @ayushcodes10).import x from "pkg/sub") now resolves to the same node as the bare package import, so a dependency no longer fragments into separate external nodes (#3601, thanks @ayushcodes10).export now detects a stale .graphify_analysis.json sidecar and reconstructs communities from graph.json, comparing partition structure rather than just the node-id set, so a stale sidecar can no longer override fresh update data (#3557, #2386, thanks @ayushcodes10).update now reconciles Markdown-family links across .md/.mdx/.qmd/.skill, and a document whose parse fails no longer has its authored links pruned (#3655, thanks @oleksii-tumanov).$ref/import targets) from genuinely dangling edges, so an expected external reference is no longer reported as a broken endpoint (#3590, thanks @DevChiniwala).Feature: Elixir alias / import / require / use targets now resolve onto the module's defmodule node across files, so the internal module dependency gr
alias/import/require/use targets now resolve onto the module's defmodule node across files, so the internal module dependency graph is no longer dropped as dangling. Only top-level modules are indexed (a nested defmodule, labeled with its bare inner name, cannot capture an unrelated use <Name> from another file), and a same-file reference is left unresolved so it cannot clobber the structural contains edge (#3603, thanks @ayushcodes10).self.method() call now resolves to a method defined on the same type in another file (the common split-impl-block layout), pooling methods across every impl of one type and refusing to link when two unrelated types share a bare name (#3602, thanks @ayushcodes10).obj.foo on a known-type receiver now resolves to a method foo inherited from a superclass, including across files, using the same conservative promotion as the implicit-self resolver — a single owning class, matching method kind, and one unambiguous ancestry chain, or it stays dangling (#3585, thanks @oleksii-tumanov).graph.json is now a declared node. An imports/imports_from/re_exports edge to an external module (stdlib, a third-party dependency) mints a typed external stub node instead of leaving a dangling endpoint that loaders materialise as an attribute-less phantom, and a cross-repo merge unifies the same external module into one global node instead of fragmenting it per repo; sourceless external call targets stay suppressed (#2873, #2878, thanks @AromalBiju1).Foo.bar) resolves using its qualifiers as evidence, rejecting misleading matches such as time.sleep or pyproject.toml (#3587, thanks @AstroMined).Feature: Terraform module calls with a literal local source ( ./… or ../… ) now resolve to a directory-scoped module node, exposing the caller→impleme
source (./… or ../…) now resolve to a directory-scoped module node, exposing the caller→implementation topology (e.g. environment → application → base); remote and registry sources and source expressions are left unresolved and never fabricate a target. After upgrading an existing graph, run graphify update . once to regenerate Terraform ids and topology (#3571, thanks @vstepko).`Widget.render`) now emits a references edge to the symbol it names, but only when exactly one corpus callable matches — generic words and ambiguous names link to nothing, so docs join the graph without a false-edge firehose (#3562, thanks @AstroMined).TOKENS.md) is no longer excluded as a credential dump; singular token.md and other keywords' bare plurals still exclude, and content-based secret detection is unchanged (#3527, thanks @HARSHAVARDHAN-RAJU5).calls edge, so an unresolved external name stops accruing a phantom god-node (#3156, thanks @DevChiniwala).using scoping — a parked member call binds to a same-named type in another repo only when the caller's namespace or an in-scope using resolves it, so a third-party receiver no longer binds to an unrelated repo-local type (#3360, thanks @DevChiniwala).require("mod") statement (no assignment) now emits an imports edge, including the chained require("lazy").setup({}) LazyVim idiom, so Neovim configs graph correctly instead of coming out as disconnected files (#3320, thanks @A-Levin).cohesion_score now excludes self-loops (e.g. a recursive call) from the edge count, so a community's score stays within 0..1 instead of being inflated above 1 (#3558, thanks @jordanalexanderp18-rgb).graph.json replace through the shared atomic-write fallback at both write sites, so a cross-drive or locked-file replace can no longer corrupt the graph mid-update (#3555, thanks @ayushcodes10).source_file is malformed is now recovered by basename instead of being dropped, with the recovered group's source_file corrected before it is written back; an ambiguous basename stays skipped (#3553, thanks @ayushcodes10).export const { a, b } = obj) now emits one node per exported name instead of a single combined node, so each name resolves and cross-file imports of a split export link correctly (#3552, #2604, thanks @ayushcodes10).GRAPHIFY_OUT instead of a hardcoded graphify-out/, so a renamed output directory no longer makes the hooks silently no-op (#3546, thanks @breken-ai).Fix: graphify.serve now imports cleanly on Python 3.12 and 3.13. The chinese extra pins jieba-py from 3.12 onward (0.9.60 mistakenly kept the old jieb
graphify.serve now imports cleanly on Python 3.12 and 3.13. The chinese extra pins jieba-py from 3.12 onward (0.9.60 mistakenly kept the old jieba until 3.14, and its invalid regex escapes are a hard error on 3.12+), and the jieba import now suppresses the tokenizer's SyntaxWarning regardless of message or line so it never escalates under -W error..graphify_root on Python 3.13, whose Path.resolve() no longer raises on a loop — the saved root must resolve to a real directory inside the repo before it is adopted.…graspologic-native ) by interpreter version, and vulnerable dependency floors (setuptools, pypdf, yt-dlp) were raised ( #3490 , thanks @taazbro ).
WinError 17 (cannot move to a different drive), not just PermissionError — a shared os_replace_with_fallback copies through a temp in the target directory and restores the original if the swap fails, keeping install/export/cache writes crash-safe (#3508, thanks @ayushcodes10).graphify-out/converted/ are no longer dropped when graphify-out/ is gitignored — the tool stops ignoring its own output (#3504, thanks @ayushcodes10).export function f(){ g() }, export const f = () => g()) now resolves, including calls to aliased imports (import { x as y }; y()) (#3346, thanks @abhay-codes07).exports map is now resolved by importer platform with the runtime condition preferred over types, so cross-package edges no longer drop to a non-existent .d.ts (#3487, thanks @dajiaohuang).jieba to jieba-py, graspologic to graspologic-native) by interpreter version, and vulnerable dependency floors (setuptools, pypdf, yt-dlp) were raised (#3490, thanks @taazbro).Fix: an incremental rebuild no longer drops cross-file concept nodes from files it didn't touch — global dedup during a merge now protects existing no
concept nodes from files it didn't touch — global dedup during a merge now protects existing nodes from untouched files instead of collapsing same-labeled ones across them (#3477, thanks @hopstreax).graphify explain now accepts a path::Symbol form to disambiguate a symbol that shares its name with its file, and the ambiguity hint now shows a form the resolver actually accepts (#3485, thanks @ayushcodes10)..graphify_root pointing outside the worktree is ignored and falls back to the repo top, so a checked-in marker can't steer the hook to scan or write outside the tree (#3265, thanks @ayushcodes10).Fix: a call to a Python function defined nested inside another function now resolves to that inner definition per lexical scope, instead of leaking to
__init__.py) now resolve to the target module instead of being dropped (#3429, thanks @flaukowski).sys.path[0] behavior — without over-resolving a genuine third-party name (#3430, thanks @hopstreax).use Foo\Bar as Baz; import keys its local binding on the alias, so later Baz references resolve to the real class, and distinct aliased same-named classes stay separate (#3421, thanks @ayushcodes10).out is skipped as build output only when there is build-output evidence, so a real source out/ is no longer silently dropped (#3347, thanks @abhay-codes07)."$SCRIPT_DIR/foo.sh") now resolves to the target when the variable holds a constant, matching the existing source handling (#3416, thanks @edwardselby).Iterable import in build.py so its type annotations resolve (they were an undefined name) (#3462, thanks @xiongjianxu).graphify global add no longer infers an empty repo tag for a path like /tmp/graph.json; it degrades to a sensible non-empty tag via the same helper merge-graphs uses (#3464, thanks @xiongjianxu).CREATE [UNIQUE] INDEX, linked to the table they index (#3467, thanks @L4XB).export * re-export no longer resolves a name to a same-named interface method — only module-level exports are candidates (#3436, thanks @L4XB).static and const declarations are now extracted as nodes (#3471, thanks @L4XB).rationale/summary when a semantic node's absolute source_file collides with its AST twin's relative path during dedup — source paths are normalized first (#3472, thanks @hopstreax).graphify install no longer aborts when the always-on registration target is unwritable (read-only or symlinked config); it skips that step with an actionable warning and still installs the skill (#3474, thanks @dajiaohuang).claude CLI resolved at run time instead of pinning a path that expires (e.g. under snap/nvm), so labelling keeps working across updates (#3475, thanks @ktsang622).all extra now includes psycopg[binary], so pip install 'graphifyy[all]' provides the postgres driver (#3482, thanks @L4XB).Fix: an incremental rebuild no longer wipes cross-file project AST nodes — re-extracting one .csproj / .sln was dropping package/framework nodes of a
.csproj/.sln was dropping package/framework nodes of a referenced project (whose stub carried the referenced file's source_file); the AST-replacement set is now derived from the files actually extracted (#3411, thanks @hopstreax).Get<int>(...), unqualified or through this — now resolves to the method definition instead of capturing Get<int> as the callee and failing to match (#3406, thanks @abhay-codes07).this.X = function / this.X = () => … members are now captured in every enclosing-function form (function expressions, arrows, IIFEs, callbacks), not just function declarations (#3408, thanks @abhay-codes07).static and const declarations (and associated consts inside an impl) are now extracted as nodes with a contains edge and a reference to their declared type — neither node type had a branch, so a constant only ever reached the graph through files that referenced it (#3471, thanks @sortakool)..csproj/.sln was dropping package/framework nodes of a referenced project (whose stub carried the referenced file's source_file); the AST-replacement set is now derived from the files actually extracted (#3411, thanks @hopstreax).Get<int>(...), unqualified or through this — now resolves to the method definition instead of capturing Get<int> as the callee and failing to match (#3406, thanks @abhay-codes07).this.X = function / this.X = () => … members are now captured in every enclosing-function form (function expressions, arrows, IIFEs, callbacks), not just function declarations (#3408, thanks @abhay-codes07).Fix: Rust trait method declarations (signature-only, and default-bodied) are now extracted as nodes — trait bodies were never walked, so both were sil
FileNotFoundError (#3351, thanks @hopstreax).GRAPHIFY_SKIP_HOOK), it no longer terminates the whole hook — the block runs in a subshell so chained hooks and later steps still execute (#2986, thanks @abhay-codes07).@/ project-root alias imports now resolve when no tsconfig paths mapping exists — an explicit mapping still wins, @scope/pkg packages are untouched, and only existing files are linked (#3357, thanks @hopstreax).query_graph seeded on a node with only incoming edges (e.g. a leaf function that is called but calls nothing) now traverses undirected, so it reaches that node's callers — matching the CLI's behavior (#3373, thanks @kuchtgpt-svg)..open, .get, .map) is now handed to cross-file resolution instead of being short-circuited, so a genuine user-defined method with that name links — without fabricating an edge to an actual builtin (#3381, thanks @ayushcodes10).os._exit fallback paths) instead of being orphaned (#3396, thanks @ayushcodes10).#services/foo via a package.json imports map, including * wildcards and condition objects) now resolve to the mapped file — previously every #-specifier resolved to nothing (#3382, thanks @julien-e).source_location (1-based L{line}) on nodes and edges, matching every other extractor, instead of leaving it null (#3365, thanks @ayushcodes10).Fix: a module docstring preceded by a leading comment (shebang, # -*- coding -*- , or a license header) is now extracted instead of silently dropped —
# -*- coding -*-, or a license header) is now extracted instead of silently dropped — comments are skipped when locating the first statement, across module/class/function bodies (#3312, thanks @ayushcodes10)._get_connection and get_connection) no longer collide and silently drop one — private/dunder members are salted while a unique public member keeps its plain id, so existing graphs are unaffected (#3302, thanks @ayushcodes10).source_file was set to the doc that merely mentions a file is now merged onto the real file node via a conservative unique-label fallback, so it stops surviving every rebuild (#3344, thanks @leninherrera94).inherits/implements/references) are no longer fabricated from a symbol that owns no node — e.g. a class nested in a named function, or an abstract method signature — closing a path that leaked phantom ext_*-sourced edges (#3356, thanks @VasuBansal7576).graphify watch/update in no-cluster mode now disambiguates same-basename file labels (e.g. two errors.ts) the same way the clustered build does, instead of collapsing them to a bare basename (#3363, thanks @VasuBansal7576).import { x } from './a'; export { x }, export { x } from './a' barrels, alias renames, and export * chains — now resolves to the original definition instead of dangling on a fabricated barrel symbol; ambiguous or unresolved origins are left untouched (#3358, thanks @VasuBansal7576).concept nodes, gated by an entropy + provenance guard and an own-file-node exclusion so distinct entities stay separate (partially addresses #296, thanks @yotamleo).import(...) is no longer blanked by the TS type-argument normalizer — masking parses first and only rewrites import(...) inside genuine call type-argument positions adjacent to a grammar error, and the whitespace form import (...) is recovered, so real module dependencies survive (#3210, thanks @zfaustk).extends (inheritance) edge or emits a label-level self-loop dependency — only a real top-level extends yields an inheritance edge, and dependency edges are sourced from the manifest node with a namespaced target (#3330, thanks @pranavshipit).graphify watch rebuild no longer clears the pending semantic-update flag, so a queued re-extraction of changed docs/papers/images is not silently dropped (#3294, thanks @theSatvik).prs tools no longer hang the stdio transport on Windows — the PR subprocesses run with stdin detached, and an explicit --repo is passed positionally to gh repo view (#3318, thanks @hopstreax).Fix: a Python type reference to a name imported from another module now resolves to that module's definition — the sourceless stub is repointed onto t
graphify merge-graphs now resolves a member call whose receiver type is defined in another repo — the call is parked at extraction and linked at merge time only on a single unambiguous cross-repo type + method match, composing with the cross-repo type link (#3152, thanks @xiongjianxu).definition_file node attribute (the merged decl/def implementation site) is now stored portably — relative to the scan root with canonical separators, matching source_file — across graph.json, the AST cache, the watch/incremental path, and direct extract, so a graph is byte-stable across machines (#3223, thanks @abhay-codes07 and @hopstreax).god_nodes now honours exclude_hubs_percentile, so --exclude-hubs actually affects god-node ranking (not just clustering); the default output is unchanged (#3205, thanks @abhay-codes07).hyperedges slot that node_link_graph otherwise drops, so hand-edited or externally written hyperedges survive re-export (#3321, thanks @yiheng-kkk).rationale attribute as its own tier, between an exact label match and a source-path match — adding recall for rationale-only hits without inflating the exact-match coverage score (#2293, thanks @andytsai821201-spec).graphify install now writes the .graphify_version stamp atomically (temp file + os.replace), so a crash mid-write cannot leave a truncated version file (#3286, thanks @drmikecrypto).graph.canvas and vault notes atomically, preventing a concurrent reader (or a git mmap hash) from seeing a half-written file (#3282, thanks @drmikecrypto).tomli no longer silently drops every pyproject.toml/Cargo.toml from the graph — tomli is now a runtime dependency for pre-3.11, and the manifest parser surfaces a visible per-file error if it is ever absent (#3283, thanks @drmikecrypto).Fix: a batch of cross-language inheritance-edge corrections (thanks @Synvoya ): JavaScript class X extends Y now emits an inherits edge ( #1790 ); PHP
class X extends Y now emits an inherits edge (#1790); PHP interfaces, enums, and traits are captured as class-like nodes with their heritage (#1791); Scala trait declarations become class-like nodes (#1792) and qualified extends/with bases resolve to the tail type (#1794); a qualified Kotlin supertype resolves to its tail type instead of the package head (#1793); a C# interface extending an interface is classified as inherits, not implements (#1817); and a Go interface type-set constraint no longer emits a spurious embeds edge (#1818)..robot/.resource files are now extracted (optional [robot] extra) — suites, test cases, user keywords, keyword-call edges, and resource/library imports, with case/space/underscore-insensitive keyword resolution (#3192, thanks @nshiveg).<|…|>, [INST]/[SYSTEM]) rather than an enumerated few, closing a prompt-injection gap for attacker-chosen tokens (e.g. <|eot_id|>); legitimate content is untouched (#3183, thanks @abhay-codes07).graphify watch/update now preserves an authored markdown link whose target node is still live under a different spelling, extending the #3190 reconcile without loosening the deleted-target gate (#3190, thanks @logan683).graphify install now backs up a diverged SKILL.md before overwriting and prints an actionable warning, instead of silently clobbering user edits (#3144, thanks @abhay-codes07).GRAPH_REPORT headline community counts now reconcile with what is actually rendered, and the knowledge-gaps threshold matches its own label (#3148, thanks @abhay-codes07).Fix: graphify watch / update no longer silently drops a Markdown link to a semantic-backed document during a code-only rebuild — authored [[wikilink]]
graphify watch/update no longer silently drops a Markdown link to a semantic-backed document during a code-only rebuild — authored [[wikilink]] references are repointed onto the target file's representative node, preserving links to the semantic tier without resurrecting a deleted target (#3190, thanks @logan683).CREATE OR ALTER, the PROC shorthand, and bracket-delimited names like [dbo].[Get Widgets] — are now recovered by name over a comment/string-masked copy, so they appear in the graph without commented-out or dynamic SQL fabricating nodes (#3164, thanks @egarcia74).graphify --help now lists the prs and provider commands and the export formats it actually supports (thanks @SyedFahad7).deepseek-v4-flash pricing entry and the build_merge docstring (which wrongly claimed it saved the graph; callers persist) (thanks @adrianengkh).@inject/@using in .razor/.cshtml files now flow through the scope-aware C# type resolver, so an injected service type resolves to its cross-file definition; an external/undeclared type fabricates nothing (partially addresses #3187 — a bare inject of a file-scoped-namespace type still dangles, follow-up) (thanks @hopstreax).prs tools (list_prs, triage_prs, get_pr_impact) now surface a genuine failure as an MCP error (isError) instead of success text, while an empty-but-successful result is unaffected (thanks @noQbot).get_node tool now resolves a node through the same tiered resolver as get_neighbors, so the two agree deterministically instead of get_node returning an iteration-order substring match (thanks @noQbot).graphify install --project (committed/shared) install now emits a bare graphify hook command resolved at run time instead of pinning the absolute interpreter path, so the committed hook no longer churns across machines; the global install still pins the absolute path (thanks @davidbhoward).new Foo() now emits a calls edge to the constructed class (namespaced names resolve to the last segment), completing the object-creation modeling across C# (#2998), TypeScript (#3135), and now PHP; dynamic (new $var()), self/static/parent, and unknown external constructions fabricate nothing (#3115, thanks @abhay-codes07).self.field / obj.field method calls still resolve after graphify update normalizes ids (#3150, thanks @abhay-codes07).import type { T } from './m') no longer manufacture false import cycles; the type-only edge is marked and excluded from cycle detection while a mixed import { type A, B } keeps its real value-import edge (#3123, thanks @abhay-codes07).import(...) used as a call type-argument (f<typeof import('mod')>()) no longer causes declarations after it to be dropped; the construct is normalized before parsing with source locations preserved (#3185, thanks @hopstreax).Fix: the incomplete-build shrink guard now stays armed when a chunk came back hollow, unparseable, or omitting files, so a run that silently lost cont
--allow-partial still overrides (#3105, thanks @abhay-codes07).graphify extract --force --code-only now fully rescans code (instead of skipping unchanged files and keeping stale import/alias resolution) while still carrying the existing document/semantic tier forward (#3125, thanks @hopstreax).graph.json now has its members routed through the dedup survivor remap, so it no longer dangles when one of its members is merged away; an unresolvable member is dropped gracefully (#3102, thanks @abhay-codes07).graphify-out/ tree that was deleted during the run, so a removed corpus stays removed (#2974, thanks @abhay-codes07).new Foo() now emits a calls edge to the constructed class (member, chained, and generic forms), so constructor usage is visible; built-in globals like new Map() / new Promise() are not fabricated (#3116, thanks @hopstreax).when guard (def foo(x) when is_integer(x), do: ...) is now extracted, not dropped; multi-clause, multi-condition guards, and defp are handled (#3111, thanks @santhiprakash)..lisp files in different directories no longer collide on merge (thanks @guitelesc).graspologic_native binding directly instead of importing the full graspologic package, avoiding its heavy import chain (umap / pynndescent / numba JIT); clustering output is unchanged, and it falls back to the graspologic wrapper and then NetworkX Louvain when the native binding is absent (#3104, thanks @Mohammad-Palla).git pull / git merge need a manual graphify update . (thanks @Mohammad-Palla).Fix: Ruby methods whose names end in ! , ? , or = now keep distinct node ids, so save and save! (or foo and foo= ) no longer collide into one node; th
!, ?, or = now keep distinct node ids, so save and save! (or foo and foo=) no longer collide into one node; the label keeps the raw spelling and member-call resolution still matches (#3077, thanks @hopstreax).ActiveRecord::Base.transaction) now matches the receiver's full constant path, so it no longer binds to an unrelated lone class named Base; an edge is emitted only on a single unambiguous match (#3078, thanks @rohit-jsfreaky).exports.x = wrap(fn), module.exports.y = onCall({...}, handler)) is now captured, reaching through the wrapper to the function it wraps without fabricating the wrapper as the export's identity (#3035, thanks @hopstreax).graphify merge-graphs now offsets each input's community ids so community 0 of one repo no longer fuses with community 0 of another; within-input structure is preserved and the original id is kept in local_community (#3014, thanks @santhiprakash)..graphify_root marker written by Windows PowerShell (which prepends a UTF-8 BOM) no longer breaks hook rebuilds or silently mis-roots a scan; PowerShell now writes the marker BOM-less and every reader decodes BOM-tolerantly (#3028, thanks @rohit-jsfreaky).Path and calls relative_to per pattern per file — it computes the relative path lexically in string space, parses each pattern once into a bounded process cache, and memoizes per-entry work; ignore decisions are unchanged (differential-fuzz verified) and a pattern-heavy monorepo scans dramatically faster (#2226, thanks @Azeem1985). The **-aware matcher was also lifted out of a per-call cache closure that leaked a reference cycle each call.case_of edge to their enum (matching the existing Java/Kotlin/Swift enum handling), so an enum case is visible as a member; explicit and implicit values, const enum, and quoted TypeScript member names are all handled and no built-in types are fabricated (#3063, #3064, thanks @durmazoguzhan).graphify watch no longer re-triggers on its own reads — read-only inotify events (opened, closed_no_write, emitted by the watcher's own AST rebuild) are dropped, while close-after-write and create/modify/move/delete still trigger; a no-op on the macOS/Windows backends that never emit them (thanks @Azeem1985).pip install graphifyy[postgres] now carries the tree-sitter-sql grammar the introspection path needs, and a missing or ABI-incompatible grammar raises an actionable error instead of silently returning zero nodes (thanks @Azeem1985).Feature: graphify merge-graphs now links a type declaration that two repos share — same fully-qualified namespace and name, from different repos — wit
graphify merge-graphs now links a type declaration that two repos share — same fully-qualified namespace and name, from different repos — with a same_type_as edge, so a shared contract type is navigable across the repo boundary; two unrelated types that merely share a short name are not linked (#3007, thanks @durmazoguzhan).x=$(fn)) now emits a calls edge like a bare $(fn), while argument-position and process substitutions stay suppressed (#2978, thanks @akshitj11).CREATE TABLE wrapped in a BEGIN/COMMIT transaction is now extracted, not just top-level DDL (#2953, thanks @akshitj11).new Foo()) now emit a calls edge to the constructed type, so constructor usage is visible in the graph; built-in and out-of-corpus types are not fabricated and a qualified construction resolves against declared namespaces (#2997, thanks @durmazoguzhan).dispatches_to edge, so a call through an injected dependency reaches the implementation; guarded against false links (single implementer, single case-sensitively same-named method, both ends C#) (#3003, thanks @durmazoguzhan).abstract type Dog <: Animal end) is now extracted with its inherits edge instead of being dropped (#3000, thanks @rajatnagda45).defclass whose superclass lives in another file now keeps its inherits edge — the cross-file base is a sourceless stub that the corpus rewire collapses onto the real definition (#3001, thanks @rajatnagda45).enum definitions and their members are now extracted as a real sourced node, so a [Color] type reference resolves to the enum instead of a phantom stub (#3002, thanks @rajatnagda45).graphify extract --code-only --force over an existing graph now preserves the document/paper/image semantic layer instead of dropping it; files deleted from disk are still pruned (#2923, thanks @santhiprakash).extends \Exception) no longer collapses onto a same-named class in another language, so a PHP+TypeScript monorepo stops growing a phantom cross-language inheritance edge / god node; same-language inheritance and user classes named like a built-in still link (#2812, thanks @ousamabenyounes).uv tool, by scanning the uv tool locations after the pin and launcher probes miss and verifying graphify is importable before adopting an interpreter, so the hook stops silently no-opping under that install (#2852, thanks @santhiprakash).graphify/ frontmatter tag or community markers) instead of orphaning them beside a fresh generation (#2863, thanks @abhay-codes07).baseUrl caches are now cleared per extraction run, so an edit to compilerOptions.paths or baseUrl is picked up on the next graphify watch / MCP rebuild instead of resolving imports through a stale alias map for the life of the process (#2917, thanks @sashankh).references edges for types used as generic arguments — both at call sites (repo.Get<User>(), services.AddSingleton<IFoo, Foo>()) and in field declarations (private List<Order> _orders) — so generic calls and field-injected dependencies are no longer invisible; built-in and type-parameter types are not fabricated (#2911, thanks @santhiprakash and @brobl2008)..qmd / .skill / .html / .yaml / .yml is no longer silently truncated at the character cap (#2900, thanks @abhay-codes07).Fix: a control character in a node label or id no longer aborts the whole export; the GraphML and Obsidian exporters scrub only the characters those f
graph.json and its byte-identity round-trip are untouched (#2897, thanks @abhay-codes07).graphify update / label / cluster-only no longer leave a large graph without a graph.html; the aggregated community view renders instead of raising, a failed render preserves the previous file, and a missing graph.html is regenerated on the no-change fast path without reclustering (#2853, thanks @oleksii-tumanov).graphify extract --no-dedup skips the fuzzy near-duplicate merge on build and incremental merge, for operators who would rather keep distinct symbols that fuzzy-matched; exact-id uniqueness is unaffected and the flag arms the shrink guard so a surprising node drop is refused loudly (#2881, thanks @rajarshidattapy).GRAPHIFY_MAX_RETRY_DEPTH=0 disables the hollow retry too so a chunk costs exactly one call (#2880, thanks @rajarshidattapy).ref class, gcnew, ^/% handles) are normalized to plain C++ before parsing so tree-sitter no longer fabricates phantom symbols from the ERROR nodes it would otherwise produce (#2876, thanks @rajarshidattapy).[[wikilinks]] now resolve vault-wide by basename when sibling resolution misses, so cross-folder links are no longer silently lost; resolution stays deterministic on ambiguous basenames (shallowest then lexicographic path) and sibling matches keep priority (#2875, thanks @BaeHyunJae).Fix: extraction now bisects a file chunk on timeout instead of failing the whole chunk, so one slow file no longer drops its chunk-mates from the grap
graphify extract --out <dir> no longer writes converted Office/Google-Workspace sidecars back into the scanned source tree; sidecar writes go through the cache root while the content hash stays anchored to the scan root, so a read-only or pinned checkout stays clean (#2787, thanks @hopstreax).\foo\bar) as cwd-relative on Windows, where such a path resolves against the current drive root and is actually outside the project; POSIX behaviour is unchanged (#2795, thanks @abhay-codes07).const api = {}; api.foo = fn) now keep those members in the graph — the API object is modeled beneath its factory and the assigned functions attach as methods, including arrow-function assignments and their intra-factory call edges. The owner is minted only for identifiers proven to be object-literal bindings in the enclosing function, so arbitrary receivers do not reintroduce the phantom-owner flood, and the factory's contains edge is emitted once no matter how many methods hang off the object (#2745, thanks @rajanpanth).graph.json field order is now stable across a read-rebuild round-trip, so re-running graphify update on an unchanged graph produces a byte-identical file instead of reshuffling node and link keys (#2582, thanks @C0KERNEL).graphify query now names the graph it opened and its node count at the head of the answer (relative to the CWD when the graph is underneath it, absolute otherwise), so a query run from a parent project no longer silently answers from the wrong corpus with no indication which graph was used (#2789, thanks @abhay-codes07).claude backend no longer crashes with AttributeError: 'ThinkingBlock' when an extended-thinking response leads with a thinking block; the first text block is read instead (#2697, thanks @mdshzb04).graphify update / save_manifest no longer rewrites manifest.json timestamps on a no-op run, so graphify-out/ stops showing as dirty (and stops producing a trailing graph commit) when nothing changed; a genuine change still updates and persists (#2838, thanks @hopstreax).class Svc(IRepo repo) emits the references edge to IRepo and calls through repo resolve — previously the class silently dropped its dependency; built-in and type-parameter types are not fabricated (#2829, thanks @brobl2008).confidence_score keyed to the relation (uses 0.95, indirect_call/unresolved cross-file calls 0.85) instead of landing at the rubric-forbidden 0.5 or a flat 0.8; the INFERRED default moves 0.5→0.55 so every score-less INFERRED edge is on the discrete rubric set (#2813, thanks @abhay-codes07).graph.json that stored a numeric edge confidence (from a pre-enum version) no longer warns once per edge on every incremental reload; the numeric value is normalized to the INFERRED tag with the original float preserved in confidence_score (thanks @Trantor-develops).Fix: node-id normalization is now caseless-stable for combining-mark sequences — casefold and NFKC don't commute, so a single pass left normalize_id(s
casefold and NFKC don't commute, so a single pass left normalize_id(s) != normalize_id(s.casefold()) for inputs like Greek ypogegrammeni followed by a combining accent; normalization now iterates casefold+NFKC to a fixpoint. Letter/digit-bearing ids are unchanged, so existing graphs are not re-keyed.references edges to their type — including class-literal arguments (@Repeatable(Foo.class), @Uses({A.class, B.class})) and annotation-member return types — so a container annotation is no longer a disconnected island; string/enum arguments are not mistaken for type references (#2426, thanks @oleksii-tumanov).graphify query treats _ as a token separator (like -), so an underscore-spelled query (user_service) matches a hyphenated label (user-service); coverage-scaling keeps the broader tokenization from surfacing unrelated single-token noise (#2473, thanks @nadiadatepe-eng).post-checkout hook skips its rebuild when HEAD is unchanged (e.g. git checkout -b with no start point), so creating a branch no longer triggers a full graph rebuild (#2421, thanks @nothariharan).node_kind (page vs heading) attribute so a docs corpus can be filtered by kind, and leading YAML frontmatter is parsed onto the page node as bounded, sanitized attributes; a # comment inside frontmatter is no longer mis-extracted as a heading (thanks @evanthomasgelders). Node ids are unchanged, so existing markdown graphs are not re-keyed..lisp/.cl/.lsp/.asd extraction via tree-sitter-commonlisp (optional [commonlisp] extra) — packages, classes, functions, methods, generics, macros, variable definers, and same-file calls; opened/:used packages resolve cross-file (thanks @fade).graphify query whose node set fits the budget but whose edges push the total over now prints an honest "complete answer over budget" notice with the real size, instead of silently returning a payload several times the requested budget (and no longer advises raising the budget, which was the exact trigger); edges are still never dropped from a complete answer (#2784, thanks @AromalBiju1)..gitignore/.graphifyignore saved in a non-UTF-8 encoding no longer silently drops its rules (which let an explicitly-excluded directory get scanned anyway); the file is decoded UTF-8-first, then by a UTF-16 BOM, then the host codepage/latin-1, so the rule survives intact with a warning instead of being truncated (#2798, thanks @abhay-codes07).GRAPH_REPORT.md, and exports; genuinely-isolated real nodes (which carry a source_file) are never touched (#2807, thanks @abhay-codes07).calls, imports, inherits, ...) instead of letting a generic references/uses/mentions overwrite it; previously a real calls could be downgraded to references and then dropped from the call graph (#2803, thanks @abhay-codes07).Fix: graphify install <platform> now advances the .graphify_version stamp only for the platform it actually (re)writes, instead of stamping every inst
graphify install <platform> now advances the .graphify_version stamp only for the platform it actually (re)writes, instead of stamping every installed platform as current; a platform whose skill content was left untouched keeps its old stamp so its staleness warning stays truthful (#2694, thanks @ousamabenyounes). This completes #2694 (the CLAUDE_CONFIG_DIR half shipped in 0.9.44)..graphify_root marker records a subfolder while stored source_file paths are relative to the repo root; the marker is validated against the stored paths before it is trusted as their anchor, so a mismatched marker can't make every unchanged source look deleted (#2603, thanks @catpotd). A genuinely deleted source is still evicted, and incremental ids stay identical to a cold build.Run and run, which are distinct in Go's case-sensitive visibility rules) no longer collapses them onto one node id and drops one; the exported symbol keeps its stable id and the unexported one is disambiguated, so an intra-file call to the unexported symbol resolves locally instead of phantoming to another package (#2779, thanks @catpotd). Only the Go extractor's id assignment is affected; the shared id normalization is unchanged, so no other language's ids move.graph.json that contains a hyperedge with no id field (the semantic extractor emits them and they persist verbatim) no longer crashes the incremental re-extract with KeyError: 'id'; id-less hyperedges are tolerated and retained (#2775, thanks @ousamabenyounes).Feature: graphify hook install reads a committed .graphifyrc ( viz_node_limit=<int> ) and bakes the visualization node limit into the generated git ho
graphify hook install reads a committed .graphifyrc (viz_node_limit=<int>) and bakes the visualization node limit into the generated git hooks, so a project-wide limit is shared via version control and survives hook regeneration; hook status reports it (#2760, thanks @hopstreax). The baked value uses a ${GRAPHIFY_VIZ_NODE_LIMIT:-<n>} default so an explicit per-run env var still wins, and hook status degrades gracefully on a malformed .graphifyrc.graphify install (Claude always-on) now writes the CLAUDE.md registration into $CLAUDE_CONFIG_DIR when that env var relocates the Claude profile, instead of always mutating the default ~/.claude/CLAUDE.md (part of #2694, thanks @AromalBiju1).function*(k){…}) — no longer fabricates an INFERRED indirect_call when one of its parameters/locals shares a name with an unrelated callable; the expression's own bindings now shadow the name (#2752, thanks @imagineers-tyler), completing the shadow family alongside catch/arrow/loop/external-import (#2757)..gitignore pattern (a committed file later added to .gitignore, or a force-added one) is no longer dropped from the corpus, matching git's own behavior of never un-tracking such a file; .graphifyignore/--exclude stay authoritative and a non-git corpus is unaffected (#2759, thanks @NithishKumar04). The git ls-files probe is skipped entirely when no .gitignore is in play, so ordinary corpora pay nothing for it.TEST_CASE("..."), SCENARIO, TEST_CASE_TEMPLATE), which tree-sitter-cpp drops as ERROR nodes, are recovered as callable nodes contained by the file (#2594, thanks @ousamabenyounes); a punctuation-only test name gets a distinct line-positional id instead of collapsing onto the file-stem id.graphify affected resolves an absolute-path seed against the repo root derived from the graph's own location instead of the current working directory, so a blast-radius query with an absolute seed run from anywhere (an editor, a script) no longer silently returns nothing; a seed outside the root still misses cleanly (#2706, thanks @ousamabenyounes).require(...) inside a function body (the idiom for breaking circular dependencies) now emits the same imports_from/imports dependency edges as a top-level require, attributed to the enclosing function, instead of being silently dropped; a dynamic require(variable) is still skipped (#2700, thanks @rajanpanth).lucide-react icon) is now shadowed, so using it as a value no longer fabricates an INFERRED indirect_call onto an unrelated same-named callable elsewhere in the corpus; a relative/in-corpus import still resolves to its real target (#2757, thanks @phudayyy).M.f to an external module (one not defined in the same file, e.g. Hardcaml's Reg_spec.create) no longer binds to a same-named local let f — which produced a false calls edge and, when the caller was that local f, a f -> f self-loop. External qualified calls are kept as a distinct target labelled by the full path; unqualified calls and calls into a locally-defined module still resolve locally, and cross-file Geo.area still collapses onto another file's area.OCaml support — .ml / .mli extraction via tree-sitter-ocaml (optional [ocaml] extra). Extracts modules, top-level and module-level values/functions, t
.ml/.mli extraction via tree-sitter-ocaml (optional [ocaml] extra). Extracts modules, top-level and module-level values/functions, types and their variant constructors, open imports, and function calls; qualified calls (Geo.area) resolve to the value, and cross-file open/call targets collapse onto the unique real definition via the corpus stub rewire.uses edge now binds to the symbol whose body actually references the imported name (a module-level function is a valid source; a co-located class that never touches the import gets no edge), instead of fanning out from the import line to every class in the file (#2652, thanks @ousamabenyounes).function declaration nested inside another function — including inside an arrow-defined component (const Panel = () => { function handleClick(){} }) or an arrow callback (useEffect(() => { function h(){} })) — is now noded, contained by its enclosing scope, and its calls resolve instead of dangling (#2653, thanks @himanshupatro-334).source path forms — source "$(dirname "$VAR")/lib/x.sh" and a .. suffix on a tracked-variable base — while a .. cannot walk past the base's parent to an arbitrary host path, so a hostile corpus can't make the extractor stat or record an out-of-tree file (#2596, thanks @hudsonwa).( ) & # or non-ASCII no longer produces a link that names no file on disk (#2597, thanks @abhay-codes07).NAME_MAX, so a long output directory on Windows no longer pushes an Obsidian/wiki note path past MAX_PATH and aborts the export mid-write (#2655, thanks @abhay-codes07)..ml/.mli extraction via tree-sitter-ocaml (optional [ocaml] extra). Extracts modules, top-level and module-level values/functions, types and their variant constructors, open imports, and function calls; qualified calls (Geo.area) resolve to the value, and cross-file open/call targets collapse onto the unique real definition via the corpus stub rewire.uses edge now binds to the symbol whose body actually references the imported name (a module-level function is a valid source; a co-located class that never touches the import gets no edge), instead of fanning out from the import line to every class in the importing file (#2652, thanks @ousamabenyounes). A reference at module top level, with no enclosing symbol, emits no edge.function declaration nested inside another function now gets its own node, a contains edge from the enclosing function, and its own call scope, so calls made from inside it are no longer dropped as dangling (#2653, thanks @himanshupatro-334). Coverage was extended to the arrow idioms too: a function declared inside an arrow-defined component (const Panel = () => { function handleClick(){} }) or inside an arrow callback (useEffect(() => { function h(){} })) is captured and attributed to the nearest enclosing named scope.source path forms — source "$(dirname "$VAR")/lib/x.sh" and a .. suffix on a tracked-variable base (source "$VAR/../lib/x.sh") — so those cross-file source edges are no longer silently dropped (#2596, thanks @hudsonwa). A .. on a guessed base is still rejected, and a tracked-base .. cannot walk past the base's parent to an arbitrary host path, so a hostile corpus can't make the extractor stat or record an out-of-tree file.( ) & # or non-ASCII characters no longer produces a link that names no file on disk; the link and the on-disk filename share one canonicalization (#2597, thanks @abhay-codes07).NAME_MAX, so a long output directory on Windows no longer pushes an Obsidian/wiki note path past MAX_PATH and aborts the export mid-write (#2655, thanks @abhay-codes07). The collision-suffix reserve was widened so a four-digit dedup suffix can't overrun the budget.A large correctness, determinism, and portability release with fixes from many community contributors.
A large correctness, determinism, and portability release with fixes from many community contributors.
Extraction / resolution
for...of / for...in loop bindings are shadowed, so they no longer fabricate indirect_call edges (#2685, @ousamabenyounes) — completes the loop/closure/catch shadow family (#2568/#2569/#2517).from ..pkg.sub import x) resolve to the package __init__ (#2688, @ousamabenyounes)..sql file that fails to parse with tree-sitter-sql installed-but-broken now reports the real error instead of "not installed" (#2602, @ousamabenyounes).Determinism / data integrity
graphify update incremental runs re-queue a file rewritten to the same length within one mtime tick (#2466, @itskaism), complementing the 0.9.40 file-hash guard (#2612).built_at_commit) is stamped from the analysed repo, not the shell cwd (#2699, @C0KERNEL).graph_has_legacy_ids no longer false-positives on a global MCP node id (#2408, @aryanbonigala).Paths / Windows portability
source_file and the model-facing paths are canonicalized to POSIX; atomic writes and installs are hardened for Windows, incl. a read-only packaged bundle (#2620/#2622, @rajarshidattapy; #2453, @bensleveritt).GRAPH_REPORT.md uses a portable basename, not an absolute host path (#2682, @ousamabenyounes).apm.yml fallback parser captures the package version (#2465, @itskaism).CLI / export
affected resolves a ./-relative seed instead of silently returning nothing (#2707, @phudayyy).graph.html are traced in convex-hull order, so the shaded polygon no longer self-intersects (#2449, @ysys143).Test / docs
for...of / for...in loop binding is now shadowed, so passing it as a call argument no longer fabricates an indirect_call edge to an unrelated same-named callable (#2685, thanks @ousamabenyounes); completes the loop/closure/catch shadow family (#2568/#2569/#2517).built_at_commit) is stamped from the analysed repository rather than the shell's working directory, so graphify extract run from elsewhere records the target's commit, not the caller's (#2534 family; #2699, thanks @C0KERNEL).affected resolves a seed passed as a ./-relative path (or an absolute path when run from the repo root) instead of silently returning nothing (#2707, thanks @phudayyy). Note: an absolute-path seed still requires the working directory to be the analysed repo root.from ..pkg.sub import x) now resolves to the package's __init__ instead of a nonexistent .py slug (#2688, thanks @ousamabenyounes)..sql file that fails to parse because tree-sitter-sql is installed but broken (e.g. an ABI mismatch) now reports the real load failure instead of the misleading "not installed" message (#2602, thanks @ousamabenyounes).GRAPH_REPORT.md header uses a portable basename instead of embedding the generator's absolute host path (#2682, thanks @ousamabenyounes).graphify update / _read_files hand the model a POSIX source_file, and several path/atomic-write behaviors are hardened for Windows (#2620/#2622, thanks @rajarshidattapy)..tmp file in the output directory on Windows (#2622, thanks @rajarshidattapy).ARCHITECTURE.md module table with a doc-parity test, and README notes on CI parity checks and Windows test prerequisites (#2620/#2622/#2126/#2642/#2646/#2647/#2648/#2651, thanks @rajarshidattapy, @redzwanmutalib, @nelsondeleonc-source).apm.yml fallback parser (used when PyYAML is absent) now captures the package version instead of dropping it (#2465, thanks @itskaism).graphify install no longer fails when the packaged bundle is read-only (e.g. a Nix store or root-owned site-packages); the staged skill references are made writable before the atomic rename (#2453, thanks @bensleveritt).graph.html are traced in convex-hull order instead of member-array order, so the shaded polygon no longer self-intersects (#2449, thanks @ysys143).graph_has_legacy_ids no longer false-positives on a global MCP node id (e.g. from a nested .mcp.json), which wrongly flagged a modern graph as legacy (#2408, thanks @aryanbonigala).Correctness, determinism, and data-integrity release — a large batch of community-contributed fixes.
Correctness, determinism, and data-integrity release — a large batch of community-contributed fixes.
graphify update refuses to overwrite the graph with a shrunken one when the shrink was caused by an extractor failure this run, instead of silently replacing good data (#2663, thanks @ousamabenyounes); a genuine deletion still shrinks the graph.catch binding passed as a call argument no longer fabricates an indirect_call edge to an unrelated same-named callable (thanks @imagineers-tyler); completes the 0.9.38/0.9.40 arrow-parameter fixes (#2568).Cargo.toml is recognized as a package manifest (#2434, thanks @ousamabenyounes), minting a canonical package node plus depends_on edges..gitignore (#2468, thanks @hopstreax).rationale attribute, matching the skill path (#2482, thanks @hopstreax). Invalidates cached semantic chunks, which re-extract on the next run.source_file is canonicalized to POSIX separators, so relative inputs on Windows no longer produce non-portable node ids (#2627, thanks @rajarshidattapy).source_file to a ghost path when the working directory differs from the graph root (#2632, thanks @rajarshidattapy).#if ... #endif preprocessor block are extracted and attached to their class (#2634, thanks @rohit-jsfreaky).query no longer prints the truncation banner when no nodes were actually cut (#2601, thanks @ousamabenyounes); a genuine node truncation still warns.use import written with a leading-backslash / fully-qualified prefix now resolves to its target definition (#2661, thanks @ousamabenyounes).ref target id instead of a per-checkout absolute-path slug (#2457, thanks @rohit-jsfreaky).graphify benchmark no longer crashes on a node whose label is None (#2674, thanks @Arthuro0103).catch binding passed as a call argument (catch (handler) { pool.submit(handler) }) no longer fabricates an indirect_call edge to an unrelated same-named callable (thanks @imagineers-tyler); the catch binding is now shadowed within its clause, completing the 0.9.38/0.9.40 arrow-parameter fixes (#2568).Cargo.toml is now recognized as a package manifest (#2434, thanks @ousamabenyounes), minting one canonical package node by name plus depends_on edges (dependencies, plus target-specific deps; virtual-workspace roots and workspace-inherited versions are handled)..gitignore that happens to match the root's own name (#2468, thanks @hopstreax); the match path is re-relativized to the scan root (and NFC-normalized) so a genuinely-ignored subdirectory is still skipped.rationale attribute (design intent / trade-offs), matching the skill path, so API-backed extraction no longer silently drops it (#2482, thanks @hopstreax). This invalidates cached semantic chunks, which re-extract on the next run.source_file is canonicalized to POSIX separators, so a run given relative inputs on Windows no longer produces non-portable node ids with backslashes (#2627, thanks @rajarshidattapy).source_file to a ghost path when the run's working directory differs from the graph root, keeping incremental and cold-build node ids identical (#2632, thanks @rajarshidattapy).#if ... #endif preprocessor block are now extracted and attached to their class instead of being dropped (#2634, thanks @rohit-jsfreaky).graphify update refuses to overwrite the graph with a shrunken one when the shrink was caused by an extractor failure this run, instead of silently replacing good data (#2663, thanks @ousamabenyounes); a genuine deletion still shrinks the graph.query no longer prints the truncation banner when no nodes were actually cut (only trailing edges overflowed the budget) (#2601, thanks @ousamabenyounes); a genuine node truncation still warns.use import written with a leading-backslash / fully-qualified prefix now resolves to its target definition instead of being dropped (#2661, thanks @ousamabenyounes).ref target id instead of leaking a per-checkout absolute-path slug (#2457, thanks @rohit-jsfreaky).graphify benchmark no longer crashes on a node whose label is None (#2674, thanks @Arthuro0103).Correctness and determinism release: fixes a TypeScript false-warning regression, several node-id / path / cache determinism bugs, a Python crash, a G
Correctness and determinism release: fixes a TypeScript false-warning regression, several node-id / path / cache determinism bugs, a Python crash, a Go phantom-reference, and more — with fixes from many community contributors.
& in a JSX string attribute or a semicolon-less in_* interface member is silent, while the genuine Kotlin one-line-body and Luau cases still warn.file_hash()'s stat fastpath no longer serves a stale digest when a file is rewritten to the same size within one mtime tick (#2612, thanks @rajarshidattapy).source_file from a Linux/CI-built graph no longer leaks into node ids on Windows (#2618, thanks @rajarshidattapy).normalize_id() is idempotent for Turkish İ and similar codepoints; no ASCII identifier ids change (#2614, thanks @rajarshidattapy).graph.json collection order is deterministic across runs (#2582, thanks @hjotha).explain / _find_node resolve node ids containing punctuation or non-ASCII characters (#2467, thanks @sean-soomgo)..graphifyignore patterns match regardless of Unicode NFC/NFD normalization, so an accented ignore rule works on macOS (#2544, thanks @bruno-growthsales)..obsidian, .smart-env) are skipped during detection (#2493, thanks @rohit-jsfreaky).x => f(x)) is shadowed, so it no longer fabricates an indirect_call edge to an unrelated same-named callable (thanks @imagineers-tyler); follows the 0.9.38 sibling-closure fix (#2568).pkg.Type) resolves by import path instead of binding by bare name to an unrelated same-named symbol (#2608, thanks @gnukeno).graph.html's document title no longer embeds the generator's absolute host path (#2598, thanks @michaelxer).& in a JSX string attribute, a semicolon-less in_* interface member) that still extract completely; the warning now fires only when recovery plausibly cost symbols (the file yielded at most the file node, or an error region spans multiple lines), so the genuine Kotlin one-line-body and Luau cases still warn.file_hash()'s stat fastpath no longer serves a stale digest when a file is rewritten to the same size within one mtime tick (#2612, thanks @rajarshidattapy); a racily-clean guard falls back to a content hash for recently-modified files.source_file from a Linux/CI-built graph no longer leaks into node ids on Windows (#2618, thanks @rajarshidattapy).normalize_id() is now idempotent for Turkish İ and similar codepoints by casefolding before the non-word filter; no ASCII identifier ids change (#2614, thanks @rajarshidattapy).graph.json collection order is now deterministic across runs (#2582, thanks @hjotha).explain/_find_node resolve node ids containing punctuation or non-ASCII characters (#2467, thanks @sean-soomgo)..graphifyignore patterns match paths regardless of Unicode NFC/NFD normalization, so an accented ignore rule works on macOS (#2544, thanks @bruno-growthsales)..obsidian, .smart-env) are skipped during detection (#2493, thanks @rohit-jsfreaky).x => f(x)) is now shadowed, so it no longer fabricates an indirect_call edge to an unrelated same-named callable (thanks @imagineers-tyler); follows the 0.9.38 sibling-closure fix (#2568).from ....x import y above the package root) (#2605, thanks @SinghAman21).pkg.Type) resolves by import path instead of losing the qualifier and binding by bare name to an unrelated same-named symbol (#2608, thanks @gnukeno).graph.html's document title no longer embeds the generator's absolute host path (#2598, thanks @michaelxer); it keeps the path from the output-dir marker onward.Correctness release: fixes an affected gap for in-function dynamic imports, stops a Python name-only member-call fabrication, tightens fuzzy dedup, cl
Correctness release: fixes an affected gap for in-function dynamic imports, stops a Python name-only member-call fabrication, tightens fuzzy dedup, closes a watcher deletion residual, and fixes three Objective-C resolution bugs.
affected now traverses a dynamic import('…') made inside a function or at module scope (#2584, thanks @phudayyy). The 0.9.38 dedupe keyed only on the target, so an in-function dynamic import (whose symbol-level edge is anchored on the enclosing function) suppressed the file-level edge affected follows; the dedupe now keys on the importing file, emitting one file-level dynamic_import edge per file/target while keeping the call-site edge.x.get(...)) no longer binds by name alone to a same-named module-level function, fabricating a false high-confidence calls edge and a god node (#2417, #2586, thanks @EZZEASY). Such a call is now resolved only with receiver-type, import, or self/cls/super evidence, matching the TypeScript fix from 0.9.37; super().method() still resolves.asset contribution flow and asset consumption flow stay separate while genuine typo and whitespace/case variants still collapse.graphify watch now rebuilds on a documentation-only deletion batch instead of only flagging it (#2580, thanks @angmeng), so a deleted doc's nodes are evicted immediately rather than waiting for the next code-file event. (The general deleted-file leak was already fixed in 0.9.10; this closes the live-watcher residual.)@protocol declaration is no longer treated as a receiver type (it collided with a same-named class); a category or class-extension interface (@interface Foo (Bar)) now folds into the base class instead of minting a duplicate node; and a message send to a @property or ivar receiver ([self.svc run], [_svc run]) now resolves through the property/ivar's declared type.Correctness release: fixes a callback-scoping regression from 0.9.37, collects Kotlin property-initializer calls, resolves Swift attribute/factory rec
Correctness release: fixes a callback-scoping regression from 0.9.37, collects Kotlin property-initializer calls, resolves Swift attribute/factory receivers, stops SQL CTE names becoming table refs, and captures nested/module-scope dynamic imports.
indirect_call in one sibling closure is no longer dropped because another sibling declared a same-named local. This can only restore dropped edges, never fabricate.val repo = createRepo()), a by lazy { ... } delegate, a companion-object property, and a top-level property initializer now produce calls edges attributed to the enclosing class (or file), including fully-qualified calls. A plain literal initializer produces no edge.@Environment(Store.self) properties and factory-initialised bindings (#2561, thanks @fakewaffle). A member call on a receiver typed only through an @Environment(Type.self) attribute, or bound to an in-corpus factory whose return type is known (let x = ServiceFactory.make()), now resolves. Ambiguous or non-concrete returns (opaque some P, arrays, out-of-corpus) stay unresolved rather than guessing.reads_from edge to a CTE name (#2577, thanks @wilyan09007). A WITH cte AS (...) name is scoped to its query and is no longer treated as a table, so it no longer mints a bare stub that could bind to an unrelated same-named symbol; an outer real table sharing a subquery-CTE's name still resolves.await import('…') inside a nested function or at module scope now produces an edge (#2575, thanks @phudayyy), and dynamic_import edges are now included in affected. Calls inside a nested named function are also collected now. A dynamic import already captured as a deferred imports_from is not double-counted.indirect_call in one sibling closure is no longer dropped because another sibling declared a same-named local. This can only restore dropped edges, never fabricate.val repo = createRepo()), a by lazy { ... } delegate, a companion-object property, and a top-level property initializer now produce calls edges attributed to the enclosing class (or file), including fully-qualified calls. A plain literal initializer produces no edge.@Environment(Store.self) properties and factory-initialised bindings (#2561, thanks @fakewaffle). A member call on a receiver typed only through an @Environment(Type.self) attribute, or bound to an in-corpus factory whose return type is known (let x = ServiceFactory.make()), now resolves. Ambiguous or non-concrete returns (opaque some P, arrays, out-of-corpus) stay unresolved rather than guessing.reads_from edge to a CTE name (#2577, thanks @wilyan09007). A WITH cte AS (...) name is scoped to its query and is no longer treated as a table, so it no longer mints a bare stub that could bind to an unrelated same-named symbol; an outer real table sharing a subquery-CTE's name still resolves.await import('…') inside a nested function or at module scope now produces an edge (#2575, thanks @phudayyy), and dynamic_import edges are now included in affected. Calls inside a nested named function are also collected now. A dynamic import already captured as a deferred imports_from is not double-counted.explain resolves node ids that contain punctuation or non-ASCII text (#2467). An id was only ever compared against the \w+-tokenized query, so concept:domain:x, every merge-graphs <repo>:: id, and every Hangul id failed to resolve; the id printed by explain could not be fed back into explain, and the ambiguity hint "Retry with […] the full node id" named a remedy that could not work. The exact tier now also compares the diacritic-folded id, and the trigram index carries the folded form so a non-ASCII id survives the prefilter. Only ids that previously failed to resolve can now resolve — label queries are unchanged, and an all-ASCII graph indexes byte-identically to before.Correctness release: stops TypeScript fabricating high-confidence call edges, fixes Kotlin import/call/parse gaps against the bundled grammar, retries
Correctness release: stops TypeScript fabricating high-confidence call edges, fixes Kotlin import/call/parse gaps against the bundled grammar, retries failed extractions on update, and surfaces claude-cli envelope errors.
calls edge by matching a receiver type by name alone (#2553, thanks @Earthfreedom). A member call resolves only when the receiver's type is defined in the same file or actually imported by the caller's file, so a third-party import type { Repo } can no longer bind to an unrelated local class Repo; table-inferred receivers are tiered to INFERRED rather than EXTRACTED.export const handler = wrapper(async (req) => { helper() })) are no longer dropped (#2552, thanks @Earthfreedom). The callback body is walked and its calls attributed to the declaration, through the same import-gated resolution so it cannot fabricate edges.import node (imports were silently dropped) and resolves each import to the real target node; a fully-qualified call like com.example.Foo.bar() now produces a calls edge; and a file with syntax the bundled grammar cannot parse (such as a one-line class C { val x }) now emits a warning instead of silently extracting nothing, with declarations recovered inside an error span keeping their enclosing class.graphify update now retries a file whose extractor failed on a previous run instead of stamping it up-to-date forever (#2543, thanks @michaelxer). A failed extraction is no longer recorded in the manifest as processed, a manifest already poisoned by the old behavior is healed on the next run, and genuinely-unchanged files are not re-processed.Correctness release: surfaces four silent CLI failures, fixes Swift cross-file extension call loss, makes node-id collision resolution deterministic,
Correctness release: surfaces four silent CLI failures, fixes Swift cross-file extension call loss, makes node-id collision resolution deterministic, and makes the Windows skill runnable on PowerShell.
cluster-only warns when --backend/--model/--batch-size are ignored because saved labels are being reused; the community-label prompt no longer collides with the discard sentinel (a model echoing the key back is no longer silently dropped); tree --root exits non-zero when the root matches no source file instead of silently flattening the tree; and cluster-only stamps built_at_commit from the analysed graph rather than the shell's working directory. Also folds in the cluster-only refused-write guard from #2522 (thanks @aniJani).extension Foo in a different file from Foo no longer drops static and singleton call edges into the type (#2538, thanks @pawelo446). The extension node id is now remapped consistently so the extension merges onto its base type before call resolution, and the merge is gated so it never absorbs a same-named type from another language.plans/_done/x.md vs plans/in-progress/x.md) are ranked by a lifecycle penalty computed on the root-relative path and a reversed-segment tie-break, so the winner no longer depends on ASCII filename order, absolute-vs-relative path form, or the checkout directory name.$(cat ...), rm -f, find -delete); they are now emitted as PowerShell (here-string interpreter invocations and Remove-Item cleanup), with POSIX skills unchanged and step parity enforced by a generator check.Correctness release: revives the merge shrink guard, fixes prune/eviction on absolute paths and newly-ignored files, stops a Java external-annotation
Correctness release: revives the merge shrink guard, fixes prune/eviction on absolute paths and newly-ignored files, stops a Java external-annotation conflation, and makes callflow and query direction/relation aware.
build_merge #479 shrink guard is no longer effectively dead (#2497, thanks @sortakool). It read the post-replace node count, so a broken partial re-extract could silently destroy nodes without tripping the guard, and the guard was skipped entirely under prune_sources. The guard now diffs the on-disk baseline by node identity and refuses any loss from a source that was neither re-extracted nor pruned this run (active even under prune_sources, skipped only under dedup), and reports how many nodes a re-extract replaced.build_merge/merge_raw_extraction prune_sources now prunes correctly when given absolute paths under a non-standard layout, deriving the scan root by suffix-matching stored source paths, and warns (instead of reporting "already clean") when a prune matches nothing (#2446, thanks @AI-invest).graphify update now removes newly-ignored files from an existing graph (#2495, thanks @alisson-acioli). A file added to .graphifyignore/--exclude (or a skip rule) is evicted even though it still exists on disk; .gitignore-driven eviction applies on an explicit full update. Files that merely changed are still preserved, and a file that leaves the corpus without matching any live ignore rule stays (fail-closed, #1795).class Component and Spring's @Component) no longer collapse into one node (#2504, thanks @te7ina-honey). The Java type resolver now runs before the unique-label stub rewire and parks an imported-but-external type on its fully-qualified name, and cross-file import resolution checks the package. In-corpus annotation resolution is unchanged.graphify callflow now respects edge direction, so the caller/callee columns are correct (#2508, thanks @Tomaskobel). The call-flow HTML loads the graph directed and recovers direction from the stored _src/_tgt markers (consistent with the path fix), and indirect calls are counted.query ("calls", "uses", "extends", ...) no longer seat spurious seeds (#2507, thanks @filipechagas). Such a verb is excluded from the per-term seed guarantee, so a decoy matching only the verb no longer becomes a traversal root, while a verb that is a genuine symbol name can still be seeded on merit.Correctness release: a C# receiver-typing regression fix, direction-respecting shortest paths, and a set of hyperedge merge/load integrity fixes.
Correctness release: a C# receiver-typing regression fix, direction-respecting shortest paths, and a set of hyperedge merge/load integrity fixes.
static local-function parameter keeps resolving even when an out var reuses the name in the enclosing body. This fixes a regression from 0.9.32 (#2346). Cross-method independence (#2299) and field-conflict poisoning are unchanged; an out var receiver itself remains untyped.graphify path (and the MCP shortest_path tool) now respect edge direction by default instead of running on an undirected view, so a returned path no longer traverses edges backwards (#2487, thanks @luliaz0601). Direction is recovered from the stored _src/_tgt markers. Pass --undirected (CLI) or undirected=true (MCP) to search ignoring direction; when no directed path exists the command says so instead of silently returning a reversed one.TypeError when a hyperedge carries dict-shaped members (#2486, thanks @adminwat). Members are normalized to ids (or dropped with a warning) so a malformed hyperedge can no longer destroy a completed extraction.graphify merge-graphs no longer drops hyperedges (#2484, thanks @sortakool, and @oleksii-tumanov for the approach in #1691). Hyperedge member ids and ids are now relabeled with the per-repo prefix, both inputs' hyperedges are unioned instead of one clobbering the other, and they are written to both the top-level and nested slots.build_from_json now reads hyperedges from both the top-level and nested graph slots, so label and re-cluster runs no longer silently empty a graph's hyperedge set (#2485, thanks @sortakool); a full validation wipeout is now reported loudly.to_json, so graph.json ships with community_name on nodes instead of dropping it (#2490, thanks @PapiScholz).Data-integrity release: fixes a C# partial-class regression from 0.9.32, stops incremental rebuilds from dropping cross-file call edges, and stops ext
Data-integrity release: fixes a C# partial-class regression from 0.9.32, stops incremental rebuilds from dropping cross-file call edges, and stops extract from silently losing a file when a worker crashes.
partial class merge (#2332) no longer conflates two same-named classes that live in different assemblies (#2411, thanks @JensD-git). The merge now keys on assembly (nearest ancestor directory containing a .csproj/.fsproj/.vbproj) in addition to namespace and name, so genuine partial halves within one project still merge while same-name types in separate projects stay distinct. A corpus with no project file keeps merging by namespace and name as before.graphify update no longer drops member-call and indirect_call edges from a changed file into an unchanged target (#2437, #2438, thanks @aryanbonigala). Incremental re-resolution now sees the unchanged corpus (its nodes, contains/method edges, and the _callable markers, which now persist to graph.json like _origin), so cross-file calls survive an incremental rebuild while edges to a genuinely removed target are still evicted.graphify extract no longer silently substitutes an empty result when a worker crashes (#2444, #2445, thanks @Baziar). A BrokenProcessPool now triggers the sequential fallback instead of being swallowed per future, a failed worker file is retried sequentially rather than merged as empty, and a whole-pass AST failure on a fresh build exits non-zero instead of writing a zero-node graph (use --allow-partial to opt into a best-effort partial graph).graphify install now prints a one-time pointer to the hosted platform (early access is open free before the public v1 launch) after the setup summary.Correctness release: a tier-aware merge that stops incremental/rebuild from dropping a file's other layer, plus a batch of language-resolution and CLI
Correctness release: a tier-aware merge that stops incremental/rebuild from dropping a file's other layer, plus a batch of language-resolution and CLI fixes.
_rebuild_code no longer drop a file's other tier (#2333, #2334, #2336). Merge is now tier-aware (an AST re-extract replaces only AST nodes and keeps the semantic layer, and vice versa), the _origin provenance marker is backfilled on load so old graphs self-heal, and the full-rebuild drop is scoped to sources actually regenerated.graphify update preserves the graph's directed flag instead of rebuilding it undirected (#2342, thanks @Rishet11).graphify query renders every edge between visited nodes, not just the traversal-tree edges (#2323, thanks @Rishet11).graphify update writes manifest.json to the target's graphify-out instead of the current working directory (#2316, thanks @Rishet11).coverage/ is no longer silently dropped; the prune is gated on coverage-report artefacts (#2339, thanks @Manoj21k).GRAPHIFY_OUT name no longer prunes every same-named directory in the tree (#2273, thanks @oleksii-tumanov).out var, is, case, and switch-arm patterns (#2346, thanks @JensD-git), and members of a partial class split across files now attach to one merged class node (#2332).object : Foo { ... }) are now extracted, with their implements and calls edges (#2347).module Foo::Bar and module Foo; module Bar canonicalize to the same label; extend ActiveSupport::Concern no longer binds to a local module named Concern; a genuine in-corpus include Foo::Concern still resolves._rebuild_code no longer drop a file's other tier (#2333, #2334, #2336). Node/edge ownership was keyed on source_file alone, so a semantic re-extract deleted a doc's AST headings and a full rebuild deleted document AST nodes. Merge is now tier-aware (an AST re-extract replaces only AST nodes and keeps the semantic layer, and vice versa), the _origin provenance marker is backfilled on load so old graphs self-heal, and the full-rebuild drop is scoped to sources actually regenerated.graphify update preserves the graph's directed flag instead of rebuilding it undirected (#2342, thanks @Rishet11), so God-node / path ranking keeps its direction on both the clustered and --no-cluster rebuild paths.graphify query renders every edge between visited nodes, not just the traversal-tree edges, so the returned subgraph matches the real induced subgraph (#2323, thanks @Rishet11).graphify update writes manifest.json to the target's graphify-out instead of the current working directory (#2316, thanks @Rishet11), so running it from elsewhere can no longer prune the target's own manifest rows.coverage/ is no longer silently dropped; the prune is gated on coverage-report artefacts (#2339, thanks @Manoj21k).GRAPHIFY_OUT name no longer prunes every same-named directory in the tree; only the configured output path is excluded (#2273, thanks @oleksii-tumanov).out var, is, case, and switch-arm patterns (#2346, thanks @JensD-git), and members of a partial class split across files now attach to one merged class node so cross-half calls resolve (#2332).object : Foo { ... }) are now extracted, with their implements and calls edges (#2347).module Foo::Bar and module Foo; module Bar are canonicalized to the same fully-qualified label, and include/extend/prepend keep the full constant path, so include Foo::Bar resolves. Mixin resolution is now scoped and lexical: a qualified external name like extend ActiveSupport::Concern no longer binds to any local module named Concern, while a genuine in-corpus include Foo::Concern still resolves. Nested-declared classes keep their last-segment index so typed-receiver calls (Processor.new) continue to resolve.Resolution-accuracy fixes, an MCP SDK compatibility widening, and community extractor fixes.
Resolution-accuracy fixes, an MCP SDK compatibility widening, and community extractor fixes.
MCP
mcp SDK 1.x and 2.x, lifting the mcp<2 cap from 0.9.30 to mcp>=1,<3 (#2308, thanks @NiSHoW). _build_server binds the same handlers via the 1.x decorator API or the 2.x on_* constructor callbacks at runtime, and adapts Tool.inputSchema, Resource.uri, and the dropped AnyUrl re-export. Verified with full stdio handshakes under mcp 1.29 and 2.0.Resolution / graph accuracy
calls edges when the same local name is reused across methods (#2299, thanks @JensD-git). Receiver typing is now per-method (mirroring the Java resolver) instead of per-file, so an untypable var x = ... in one method can't delete a typed-parameter call edge in another."public"."users" matches public.users.graphify path and explain no longer print reversed hops (#2309): they recover edge direction from the stored _src/_tgt markers instead of the persisted endpoint order.export const X = <scalar> now emits a graph node, so a named import of a scalar export is no longer left dangling (#2266, thanks @oleksii-tumanov).make, len, append, ...) no longer fabricate call edges to same-named user symbols (#2313, thanks @PathGao); the filter is scoped to Go bare-identifier callees.graphify explain refuses and lists candidates when a name matches symbols in more than one file, instead of silently resolving to an arbitrary one (#2233, thanks @0bLoM).Install
mcp<2 cap 0.9.30 introduced to mcp>=1,<3. The 2.0 SDK removed the low-level decorator API (Server.list_tools/call_tool/...); _build_server now binds the same handlers via the 1.x decorators or the 2.x on_* constructor callbacks, picked at runtime, and adapts Tool.inputSchema, Resource.uri (plain str in 2.x), and the dropped AnyUrl re-export. Verified with full stdio handshakes under both mcp 1.29 and 2.0.calls edges when the same local name is reused across methods (#2299, thanks @JensD-git). Receiver typing was per-file and poisoned a name on any conflicting/untypable rebind anywhere in the file; it is now per-method (mirroring the Java resolver), so an untypable var x = ... in one method can't delete a typed-parameter call edge in another."public"."users") matches an unquoted reference (public.users).graphify path and explain no longer print reversed hops (#2309). They now recover edge direction from the stored _src/_tgt markers instead of the persisted endpoint order, so a graph.json written with flipped storage order (older graphs, raw dumps, merge-driver output) renders the true direction.export const X = <scalar> now emits a graph node, so a named import of a scalar export is no longer left dangling (#2266, thanks @oleksii-tumanov).make, len, append, new, ...) no longer fabricate call edges to same-named user symbols (#2313, thanks @PathGao); the filter is scoped to Go bare-identifier callees so it can't affect other languages or same-file method calls.graphify explain refuses and lists candidates when a name matches symbols in more than one file, instead of silently resolving to an arbitrary one (#2233, thanks @0bLoM).Fixes a fresh-install failure of the MCP server, plus node-id portability, cache, bedrock, and merge-graphs fixes.
Fixes a fresh-install failure of the MCP server, plus node-id portability, cache, bedrock, and merge-graphs fixes.
Install
mcp below 2.0 so a fresh graphifyy[mcp] / graphifyy[all] install works again (#2277, #2279, #2291). mcp 2.0.0 dropped the mcp.types.AnyUrl re-export and the Server decorator-registration API that graphify/serve.py uses, so an unpinned resolve broke graphify-mcp on every new install with an ImportError. The mcp and all extras now require mcp>=1,<2 (resolving to 1.29.0) and starlette>=1.3.1,<2. Porting to the mcp 2.x API is tracked in #2308.Node-id portability
.tsx files no longer leak absolute-path / machine-slug ids into edge endpoints (#2262). The symbol-resolution pass parsed .tsx with the plain TypeScript grammar; JSX misparsed, nested handlers floated to top level, and calls edges were emitted from an absolute-stem source with no node. .tsx now uses the TSX grammar, a calls edge is never emitted from an unowned source, and a general backstop canonicalizes any node-less absolute-derived endpoint.Backends / graph ops
GRAPHIFY_API_TIMEOUT and GRAPHIFY_MAX_RETRIES instead of botocore's silent 60s default (#2284, thanks @zhiyanliu).merge-graphs preserves edge direction instead of rewiring import edges to the importing file (#2261, thanks @hopstreax).GRAPHIFY_MAX_CONTEXTS) instead of growing unbounded (#2268, thanks @Kkartik14).mcp below 2.0 so a fresh graphifyy[mcp] / graphifyy[all] install works again (#2277, #2279, #2291). The mcp 2.0.0 major dropped the mcp.types.AnyUrl re-export and the Server decorator-registration API that graphify/serve.py uses, so an unpinned resolve broke graphify-mcp on every new install with an ImportError. The mcp and all extras now require mcp>=1,<2 (resolving to 1.29.0) and starlette>=1.3.1,<2. Adapting to the mcp 2.x API is tracked as a follow-up..tsx files no longer leak absolute-path / machine-slug ids into edge endpoints (#2262). The symbol-resolution pass parsed .tsx with the plain TypeScript grammar, so JSX misparsed and nested handlers floated to top level, emitting calls edges whose source was an absolute-stem id for a caller with no node. .tsx now uses the TSX grammar, a calls edge is never emitted from an unowned source, and a general backstop canonicalizes any node-less absolute-derived endpoint.GRAPHIFY_API_TIMEOUT (and GRAPHIFY_MAX_RETRIES) instead of botocore's silent 60s default, so long generations no longer die with a read timeout (#2284, thanks @zhiyanliu).merge-graphs preserves edge direction instead of rewiring import edges to the importing file (#2261, thanks @hopstreax).GRAPHIFY_MAX_CONTEXTS) instead of growing unbounded per project (#2268, thanks @Kkartik14).Portability, hook-safety, and resolution-accuracy fixes.
Portability, hook-safety, and resolution-accuracy fixes.
Node-id portability
indirect_call sources, bash source/script-invocation targets, and other producers that minted an id from an absolute path are now canonicalized to the root-relative node id by a general backstop, so graph.json link endpoints are portable across machines and clones.Hook safety
graph.json it merely failed to read (#2251). If the existing graph is over the size cap or unparseable, the rebuild refuses to write (matching the CLI) instead of replacing it with a code-only extraction; the --no-cluster write is now atomic with a protected-graph backup.Resolution / extraction
indirect_call edges from JS/TS closure arguments are gone (#2241, thanks @Yyunozor): a closure parameter now shadows outer names, so rows.map(r => ...) no longer binds r to a corpus-wide callable of the same name.self-type annotations (self: A with B =>) emit requires edges to the required traits (#2052, thanks @Yyunozor).Filtering / export
.env.example / .env.sample / .env.template templates are indexed instead of dropped by the sensitive-file filter, while real .env files (and templates under a secrets directory) stay excluded (#2184, thanks @SyedFahad7)..env -> dot-env; an all-dot label falls back to unnamed) (#2205, thanks @SyedFahad7).indirect_call sources, bash source/script-invocation targets, and other producers that minted an id from an absolute path are now canonicalized to the root-relative node id by a general backstop, so graph.json link endpoints are portable across machines and clones.graph.json it merely failed to read (#2251). If the existing graph is over the size cap or unparseable, the rebuild now refuses to write (matching the CLI) instead of silently replacing it with a code-only extraction; the --no-cluster write is also atomic with a protected-graph backup.indirect_call edges from JS/TS closure arguments are gone (#2241, thanks @Yyunozor). A closure parameter (rows.map(r => ...)) now shadows outer names, so r no longer binds to a corpus-wide callable of the same name.CREATE_NO_WINDOW..env.example / .env.sample / .env.template templates are indexed instead of dropped by the sensitive-file filter, while real .env files (and templates under a secrets directory) stay excluded (#2184, thanks @SyedFahad7)..env -> dot-env); an all-dot label falls back to unnamed (#2205, thanks @SyedFahad7).self-type annotations (self: A with B =>) now emit requires edges to the required traits (#2052, thanks @Yyunozor).Fixes for incremental extraction correctness, graph loading, uninstall scoping, macOS paths, and Swift extraction.
Fixes for incremental extraction correctness, graph loading, uninstall scoping, macOS paths, and Swift extraction.
Incremental extraction
--update on macOS no longer re-extracts everything when the corpus path or a filename contains non-ASCII characters (#2221, thanks @SyedFahad7). Manifest keys are NFC-normalized.graph.html (#2218, thanks @bobspryn).Other fixes
graphify benchmark, the graph merge-driver, and the call-flow HTML export no longer crash or silently fail on a --no-cluster graph.json (#2212), which stores edges under edges rather than links.claude/gemini/codebuddy uninstall no longer delete the user-global skill when called with a project_dir (#2215); this also fixes graphify uninstall --project deleting the global codebuddy skill.var body: some View { ... }, get/set, willSet/didSet) now emit graph nodes, so SwiftUI views are no longer erased (#2181, thanks @ozdemirsarman).target_file stamp the incremental canonicalization needs, so a re-extracted file's imports/references dangled or vanished; both now stamp the resolved target and canonicalize to the root-relative node.graphify benchmark, the graph merge-driver, and the call-flow HTML export no longer crash or silently fail on a --no-cluster graph.json (#2212). Those graphs store edges under edges rather than links; a shared loader now normalizes both.claude/gemini/codebuddy uninstall no longer delete the user-global skill when called with a project_dir (#2215). A passed project_dir now scopes the removal; this also fixes a CLI bug where graphify uninstall --project deleted the global codebuddy skill.--update on macOS no longer re-extracts everything when the corpus path or a filename contains non-ASCII characters (#2221, thanks @SyedFahad7). Manifest keys are NFC-normalized so NFD and NFC path forms match.var body: some View { ... }, get/set, willSet/didSet) now emit graph nodes, so SwiftUI views are no longer erased (#2181, thanks @ozdemirsarman).graph.html (#2218, thanks @bobspryn).A large maintenance release: install-safety fixes, node-identity/canonicalization fixes, cross-file resolution improvements, and a batch of community
A large maintenance release: install-safety fixes, node-identity/canonicalization fixes, cross-file resolution improvements, and a batch of community contributions.
Install and data safety
claude/gemini/codex/codebuddy install no longer overwrite a settings/hooks file they cannot parse (#2167). On any JSON parse error they used to fall back to an empty config and rewrite the whole file, destroying the user's settings (most often triggered by a UTF-8 BOM). They now read utf-8-sig, refuse to modify a non-JSON-object file, and back up to <name>.graphify-bak before any write.extract --no-cluster no longer overwrites the full graph with just the changed files (#2169). It now merges the existing graph forward with the same replace/prune semantics as the clustered path and canonicalizes cross-file edge targets.~/.claude/~/.gemini/~/.codebuddy/~/.copilot (#2168).Node identity and caching
stat-index.json is stored with root-relative keys (re-anchored on load, mirroring manifest.json) and pruned of deleted-file entries, so a moved or cloned corpus keeps its cache hits instead of re-extracting everything (#2199).concept nodes with identical normalized labels now merge, matching the behavior already applied to near-identical labels (#2182).source_file paths (for example from a Windows scan) no longer break node identity (#2197), and build_from_json folds legacy field aliases (name/path/type/confidence_score) so alias-carrying nodes stop entering the graph invisible and unmergeable (#2194).Resolution
base./this.field receivers, inherited-member lookup through the inherits chain, and shadow-poisoning so a local shadowing a field of a different type no longer produces a wrong edge (#1609, adapted from #1620 by @TheFedaikin).affected <decorator> finds what a decorator touches, with builtin/stdlib decorators excluded (#2154, thanks @Rishet11).jsconfig.json/tsconfig.json baseUrl and paths (#2153, thanks @Rishet11).graphify query/explain no longer fabricate indirect_call edges to class definitions (#2137, thanks @Rishet11).sourced file resolve for extensionless scripts and bare source lib.sh (#2171), and source "${VAR}/lib.sh" resolves against the variable's real directory (#2172). Both thanks @Souptik96.Windows and hooks
PreToolUse hook is documented as an intentional no-op (#2165, thanks @Souptik96).claude/gemini/codex/codebuddy install no longer overwrite an existing settings/hooks file they cannot parse (#2167). The installers fell back to an empty config on any JSON parse error and then rewrote the whole file, destroying the user's settings (the likely trigger is a UTF-8 BOM, the same class as #2163). They now read utf-8-sig, refuse to modify a file that is not a JSON object (naming the path) instead of clobbering it, and back up to <name>.graphify-bak before any modifying write.extract --no-cluster no longer overwrites the full graph with just the changed files (#2169). The raw path wrote only the current run's extraction over graph.json, dropping every node and edge owned by an unchanged file, and the changed file's cross-file edges dangled on absolute-path ids. It now merges the existing graph forward with the same replace/prune semantics as the clustered path and canonicalizes cross-file edge targets; a corrupt existing graph is refused rather than overwritten.affected <decorator> finds everything a decorator touches (#2154, thanks @Rishet11). Builtin/stdlib decorators (@property, @staticmethod, @dataclass, @functools.wraps, ...) are excluded so they do not fabricate stub nodes or false edges.jsconfig.json/tsconfig.json baseUrl and paths (#2153, thanks @Rishet11), so imports like import x from 'src/utils' are no longer left dangling.Data, URL, Sendable, View, ...) are filtered from call resolution and god-node ranking (#2147, thanks @MasterFede5), so they no longer fabricate cross-file edges to user symbols or dominate the graph's high-degree nodes.~/.claude/~/.gemini/~/.codebuddy/~/.copilot (#2168). An autouse fixture sandboxes HOME for every test.sourced file now resolve for extensionless shebang scripts and bare source lib.sh (no ./ prefix) too (#2171, thanks @Souptik96). The bare-name binding is marked INFERRED since it resolves via $PATH at runtime.source "${VAR}/lib.sh" whose variable is a tracked top-level assignment (or the dirname "${BASH_SOURCE[0]}" idiom) now resolves against the variable's real directory instead of guessing the script's own, so it no longer binds to a same-named decoy under the script dir (#2172, thanks @Souptik96).CREATE FUNCTION/PROCEDURE routines with PL/pgSQL-only bodies that tree-sitter cannot parse are now recovered by a raw-text scan, gated on a failed parse so a cleanly-parsing file cannot fabricate routines from commented-out DDL or EXECUTE strings (#2180, thanks @Souptik96).PreToolUse hook (graphify hook-check) is documented as an intentional no-op (#2165, thanks @Souptik96); Codex Desktop rejects additionalContext there, so graph guidance comes from AGENTS.md.concept nodes whose labels normalize identically are now merged, matching the behavior already applied to near-identical labels (#2182). Gated to concept nodes with provenance, so code/rationale/document/image and cross-repo guards are preserved.graphify-out/cache/stat-index.json is now stored with root-relative keys (re-anchored on load, mirroring manifest.json) and pruned of deleted-file entries, so a moved or cloned corpus keeps its cache hits instead of re-extracting everything, and the index stops growing unbounded (#2199).source_file paths (for example from a Windows scan) no longer break node identity: build_from_json re-keys absolute-derived semantic ids to the canonical root-relative id and the semantic cache stores normalized paths (#2197).build_from_json now folds legacy field aliases (name->label, path->source_file, edge type->relation, confidence_score->confidence), so nodes carrying them stop entering the graph without a label or source_file where they are invisible and unmergeable (#2194). The extraction warning also breaks issues down by cause.using/scope instead of bailing), with base./this.field receivers, inherited-member lookup through the inherits chain, and shadow-poisoning so a local shadowing a field of a different type no longer produces a wrong edge (#1609, adapted from #1620 by @TheFedaikin).Maintenance release. Correctness fixes across Python call-graph inference, the git hook (Windows), and bash source resolution.
Maintenance release. Correctness fixes across Python call-graph inference, the git hook (Windows), and bash source resolution.
Fixes
graphify query/explain no longer fabricate indirect_call edges to class definitions (#2137, thanks @Rishet11). Passing a class as a value (select(Model), db.get(Model, id), except (ErrorA, ErrorB), getattr(obj, "Name", 0)) produced a false inferred call edge; classes are now excluded from indirect_call in both the intra-file and cross-file paths, while direct instantiation still emits its calls edge.case glob silently emptied any interpreter path containing a backslash, so the hook failed on Windows uv/venv installs. Both allowlist sites use a verified character class that admits backslashes while still rejecting shell metacharacters.signal.SIGALRM, which does not exist on Windows, so GRAPHIFY_REBUILD_TIMEOUT was a silent no-op and a hung rebuild ran unbounded. A threading.Timer fallback now terminates a runaway rebuild where SIGALRM is unavailable; the Unix path is unchanged.sourced file now get calls edges (#2141, thanks @HerenderKumar). Resolution was gated on same-file definitions, so a call to a sourced-library function looked like an external command and produced no edge. Both source file and . file are handled; resolution is in-corpus and single-match only, so a genuine external command still fabricates nothing.source edges built from a variable path now resolve (#2079, thanks @HerenderKumar). source "${BENCH_DIR}/lib/x.sh" baked the unexpanded ${VAR} into a dead node id; the leading expansion is stripped and the literal suffix resolved against the script's directory, emitted as INFERRED only when it resolves to a real file. Calls into a ${VAR}-sourced library resolve too..gitignore/.graphifyignore/info/exclude were read as utf-8, so a leading BOM stayed on the first line and silently dropped the first pattern. The ignore read sites now use utf-8-sig, matching git.graphify query/explain no longer fabricate indirect_call edges to class definitions (#2137, thanks @Rishet11). The callable guard admitted classes, so passing a class as a value (select(Model), db.get(Model, id), except (ErrorA, ErrorB), getattr(obj, "Name", 0)) produced a false inferred call edge in both the intra-file and cross-file paths. Classes are now tracked separately and excluded from indirect_call; direct instantiation still emits its calls edge. The suppression is context-blind, so a genuine higher-order class callback that is actually invoked (e.g. map(Point, coords)) also loses its edge, which is the intended tradeoff.case glob silently emptied any interpreter path containing a backslash (and the shebang-launcher allowlist admitted no : or \ at all), so the hook failed on Windows uv/venv installs. Both allowlist sites now use a verified character class that admits backslashes while still rejecting shell metacharacters.signal.SIGALRM, which does not exist on Windows, so GRAPHIFY_REBUILD_TIMEOUT was a silent no-op and a hung rebuild ran unbounded. A threading.Timer fallback now terminates a runaway rebuild where SIGALRM is unavailable; the Unix path is unchanged.sourced file now get calls edges (#2141, thanks @HerenderKumar). Call resolution was gated on same-file definitions, so a call to a sourced-library function looked like an external command and produced no edge. Both source file and . file are handled; resolution is in-corpus and single-match only, so a genuine external command still matches nothing and fabricates no edge.source edges built from a variable path now resolve (#2079, thanks @HerenderKumar). source "${BENCH_DIR}/lib/x.sh" baked the unexpanded ${VAR} into a dead node id; the leading expansion is now stripped and the literal suffix resolved against the script's directory, emitted as INFERRED only when it resolves to a real file. Calls into a ${VAR}-sourced library resolve too, not just the source edge..gitignore/.graphifyignore/info/exclude were read as utf-8, so a leading BOM stayed on the first line and silently dropped the first pattern (and turned a BOM'd first comment into a bogus pattern). The two ignore read sites now use utf-8-sig, matching git, which strips a single leading BOM.Maintenance release: a license change to Apache 2.0 and a dead-code removal.
Maintenance release: a license change to Apache 2.0 and a dead-code removal.
License
LICENSE-MIT and referenced from NOTICE.Removed
.graphifyinclude handling is gone (#2112). The file had been non-functional since dot directories became indexed by default (#873): its loader and matchers had no consumers, so detect parsed the file on every run and then discarded the result, making a .graphifyinclude a silent no-op. The dead loader and matchers are deleted, a leftover .graphifyinclude no longer appears in the unclassified list, and detect prints a one-time note when one is present at the scan root. To re-include ignored paths, use ! negation patterns in .graphifyignore.LICENSE-MIT and referenced from NOTICE..graphifyinclude handling is gone (#2112). The file has been non-functional since dot directories became indexed by default (#873): the loader and its matchers had no consumers, so detect parsed the file on every run and then ignored it, and a .graphifyinclude was silently a no-op. The dead loader and matchers are deleted, a leftover .graphifyinclude no longer shows up in the unclassified list, and detect prints a one-time stderr note when one is present at the scan root. To re-include ignored paths, use ! negation patterns in .graphifyignore.Maintenance release. Correctness fixes across extraction, dedup, query rendering, and the sensitive-file filter, plus a hang fix in the .NET/XAML path
Maintenance release. Correctness fixes across extraction, dedup, query rendering, and the sensitive-file filter, plus a hang fix in the .NET/XAML path.
Fixes
.cs scan is now bounded and prunes noise directories, so it can no longer hang. A standalone extraction on a .xaml under a large or shared parent (a temp dir, a big monorepo) could resolve the project root to a broad ancestor and recursively scan the whole tree. It now walks with node_modules/.venv/.git/dot-dir pruning and a directory cap: a real project scans fully, a runaway root degrades to a fast partial scan.privacy-tokens.md) and real source like service_account.py were dropped with no trace, while some genuine secrets (.npmrc, .pypirc, .git-credentials, case variants) were missed. The filter is now stricter on real secrets and no longer loses legitimate files, and both graphify extract and the skill flow now name the skipped files instead of only a count.calls edges now resolve through an aliased Python import (#2082, thanks @Yyunozor). from pkg import mod as alias recorded the import but dropped every downstream alias.func() call, so the callee looked like dead code.dedup preserves a node's attributes when two exact-ID records from the same source file collapse (#2091, thanks @Synvoya). Non-conflicting attributes are retained deterministically, records from different files stay isolated, and a dropped record can never stamp a false origin onto the survivor.claude-cli backend now reads the CLI's structured-output channel instead of free-form prose (#2076, thanks @Yyunozor), which had parsed to zero nodes and bisected forever on newer Claude Code.graphify explain on a high-degree node groups the cut connections by file instead of a bare ... and N more (#2009, thanks @Yyunozor).graphify query and MCP query_graph no longer print calls edges backwards (#2080, thanks @Yyunozor); the renderer recovers the stored direction from the edge.Features
get_neighbors and get_community (MCP) now honor a token_budget (default 2000) so one call on a god node or large community can't flood the client's context (#2069, thanks @ojmucianski). Truncation is announced at the top of the output.Docs
--code-only is surfaced in the extract usage text and README (#2071, thanks @HerenderKumar).graphifyy in system site-packages shadowing uv run --with graphifyy (#1540, thanks @HerenderKumar).Fix: the XAML code-behind .cs scan is now bounded and prunes noise dirs, so it can't hang. _xaml_csharp_class_nodes used rglob("*.cs") over a project root resolved by walking up for a .csproj/.sln; a standalone extract_xaml on a .xaml under a large or shared parent (a temp dir, a big monorepo) could resolve the root to a broad ancestor and then recursively scan the whole tree. It now walks with node_modules/.venv/.git/dot-dir pruning and a directory cap, so a real project scans fully while a runaway root degrades to a fast partial scan.
Fix: the sensitive-file filter no longer silently drops topic docs and real source (#2106). A prose file whose slug merely ends in a keyword (privacy-tokens.md, token-economics.md) and real source like service_account.py were dropped from the graph with no trace; the filter also missed genuine secrets (.npmrc, .pypirc, secring, .git-credentials, case variants). service_account/aws_credentials moved to the boundary-checked keyword path (real source spared, downloaded key files still excluded), a prose-note carve-out was added (multi-word slugs indexed, bare secrets.md/token.md still dropped), and the missed secret dotfiles are now caught — net stricter on real secrets while ending the false-positive loss. graphify extract and the skill flow now name the skipped-sensitive files instead of only a count, so a wrongly-flagged file is visible.
Fix: calls edges now resolve through an aliased Python import (#2082, thanks @Yyunozor). from pkg import mod as alias (and import pkg.mod as alias) recorded the import edge but dropped every downstream alias.func() call, so the callee looked like dead code even though the import graph looked complete. The local alias binding is now tracked and the call resolves to the real callee (in-corpus only, so external/stdlib aliases still fabricate nothing). This is the "invisible caller" family that distinguishes graphify from grep/AST name matching.
Fix: dedup (default on) preserves a node's attributes when two exact-ID records from the same source file collapse (#2091, thanks @Synvoya). The collapse discarded one record's fields (e.g. an AST node's source_location or a semantic node's summary), so the default path silently lost data the docs promised was merged. Non-conflicting attributes are now retained deterministically (independent of chunk order), records from different files or with no source path stay isolated, and a dropped record can never stamp a false _origin onto the survivor.
Fix: the claude-cli backend now reads the CLI's structured-output channel instead of trusting free-form prose (#2076, thanks @Yyunozor). Newer Claude Code treats the extraction prompt as an agentic task and reports a summary in the result field, which parsed to zero nodes and bisected forever. The backend now pins a JSON schema when the CLI supports it (feature-detected, with the old prompt as the fallback for older CLIs) and parses the structured_output object.
Fix: graphify explain on a high-degree node now groups the cut connections by file instead of a bare ... and N more, so the callers/callees beyond the top 20 are still visible (their file, direction, and count) without a repo-wide grep (#2009, thanks @Yyunozor).
Fix: graphify query no longer prints calls edges backwards (#2080, thanks @Yyunozor). The graph on disk is correct, but the CLI loads it undirected and BFS/DFS collect edges in traversal order, so seeding on the callee rendered callee --calls--> caller. The renderer now recovers the stored direction from the edge's _src/_tgt (ignoring stray/dangling values), and both CLI query and MCP query_graph show the real direction.
Feat: get_neighbors and get_community (MCP) now honor a token_budget (default 2000) instead of rendering unbounded, so one call on a god node or large community can't flood the client's context (#2069, thanks @ojmucianski). Truncation is announced at the top of the output (matching query), with a line count and a narrowing hint.
Docs: --code-only is now surfaced in the extract usage text and README (#2071, thanks @HerenderKumar); documented as an extract flag rather than a /graphify skill flag.
Docs: README troubleshooting note for an older graphifyy in system site-packages shadowing uv run --with graphifyy, which silently runs the old version (#1540, thanks @HerenderKumar).
Fix: caller / "call sites" listings now report the actual call-site line, not the caller function's definition line. explain, affected, and the MCP ge
explain, affected, and the MCP get_neighbors/query tools printed the caller node's source_location (its def line) for an incoming call, so a precise-looking citation sent users to the wrong line. The calls edge already carries the true call-site line; every caller/relation listing now reads the traversed edge's source_file:source_location, falling back to the node's own line only when the edge has none.query no longer silently drops the answer past its output budget. Rendered nodes were ordered by degree (so a low-degree definition node ranked last and was cut first), the queried symbol was not guaranteed to appear, and the truncation marker sat only at the end so silence read as absence. Nodes are now ranked by hop distance from the query seeds (deterministically), the seed the question named is always rendered first and never truncated, and a prominent notice at the TOP states how many of how many nodes were shown and how to widen the budget. (A branch merge had also silently dropped the seed-first ordering the renderer already supported; it is rewired.)graphify uninstall no longer deletes a user-authored ### graphify section (#2062). The uninstall strip used an unanchored ## graphify pattern that matched inside a user's H3 heading (and the "already installed" guard was a substring test), so hand-written content was destroyed. The heading is now matched only when a line is exactly the marker (mirroring the install-side #1688 hardening), across all six strip sites (CLAUDE.md, AGENTS.md, GEMINI.md, copilot-instructions.md, CODEBUDDY.md, and the H1 skill registration).graphify path (and the MCP shortest_path tool) now return a deterministic route and label each hop with the edge's actual stored relation (#2074). The route was computed over a hash-seeded undirected view, so it varied run-to-run among equal-length paths; and the printed relation was read from an arbitrarily-collapsed parallel edge, so it could show calls on a pair that only carries references. The traversal is now over a sorted graph, and each hop shows the real relation(s), falling back to an honest related when none is stored.cluster-only --no-label no longer permanently suppresses real community labels (#2073). It wrote Community N placeholders (plus a matching signature) into .graphify_labels.json, which the reuse path then treated as fresh forever. Placeholder-only runs no longer persist the sidecar, a stored placeholder is treated as absent so already-polluted graphs self-heal, and the watch/update rebuild got the same treatment.build_from_json's ghost-duplicate merge now keys on the full source path, not the bare basename (#2068). Unrelated nodes from different files sharing a common basename (index.md, README.md) and a generic label were silently merged onto one survivor with their edges rewired, corrupting multi-corpus doc graphs. The legitimate AST/LLM same-file merge is preserved; cross-directory false merges are eliminated.src/) lost most of its imports/imports_from edges when scanned from the repo root, because absolute imports resolved only against the scan root while file-node ids are scan-root-relative, so the dangling edges were silently dropped. Absolute imports now resolve against nested package roots (detected via the __init__.py chain), and import edges are repointed to the real file nodes, so the graph is identical whether scanned from the repo root or from src/.Fix: a node whose source_file is a URL/virtual scheme (gdoc://, s3://, http://, ...) is no longer evicted on the second graphify update (follow-up to
source_file is a URL/virtual scheme (gdoc://, s3://, http://, ...) is no longer evicted on the second graphify update (follow-up to #2051). The #2051 disk-absence sweep guarded such sources with a literal "://" check, but write-side path normalization collapses the double slash (gdoc://x becomes gdoc:/x), so the guard missed the node on the next run and dropped it into the disk-absence eviction branch. The scheme is now matched tolerantly (and a Windows drive letter like C:/ is not misread as remote).env/.env/*_env is no longer silently pruned as a false-positive Python virtualenv (#2058). detect's directory-noise heuristic matched those names before .graphifyignore negation and with no trace in any output bucket, so codebases using them as source dirs (common in UVM/ASIC verification) lost large subtrees undetectably. The venv heuristic for those names is now gated on an actual marker (pyvenv.cfg, an activate script, lib/python*, or conda-meta/); venv/.venv/*_venv stay name-only, and every pruned-as-noise directory is now recorded in a pruned_noise_dirs bucket for traceability..docx/.xlsx) and Google-Workspace sidecars are now named from the scan-root-relative path, not the absolute path (#2059). The absolute-path hash salted the sidecar name with the checkout location, so committing graphify-out/ (a supported workflow) produced a new duplicate .md per clone/worktree, each ingested as a distinct source document. The relative hash is stable across checkouts while still disambiguating same-stem files; the Google-Workspace sidecar path additionally gains the NFC normalization it was missing.serve.py's "graph.json is corrupted" recovery message is now reachable (#2005, thanks @kimdzhekhon). json.JSONDecodeError subclasses ValueError, and the broad except (ValueError, FileNotFoundError) clause was ordered first, so a truncated graph printed the bare Expecting value... instead of the documented rebuild hint. The JSONDecodeError clause now comes first.graphify god-nodes/god_nodes is now a real CLI subcommand, and graphify extract --output DIR is honored as an alias of --out (#2004). god_nodes was an analyzer, an MCP tool, and a documented capability but had no CLI command; --output was silently dropped on extract even though graphify tree documents it. (The affected/reverse-dep import-id mismatch from the same report is tracked separately.)contains edge from the enclosing type instead of the file node (#2040). Across ~19 languages the edge was hard-coded to source from the file, so the containment tree was flat (file -> Inner) rather than nested (file -> Outer -> Inner); it now sources from the enclosing type when present, with top-level types still contained by the file.explain/discovery can tell them apart (#2032). In directory-per-entrypoint repos (Supabase Edge Functions, Next.js page.tsx, Rust mod.rs, Python __init__.py) dozens of files named e.g. index.ts collided under one label, breaking free-text discovery for exactly those files. Colliding file nodes are relabelled to the shortest unique path suffix (process-order/index.ts); unique basenames stay bare, and node ids/edges are unchanged.Fix: graphify extract (headless, no --backend) now auto-detects Ollama from the standard OLLAMA_HOST env var, not only graphify's OLLAMA_BASE_URL (#19
graphify extract (headless, no --backend) now auto-detects Ollama from the standard OLLAMA_HOST env var, not only graphify's OLLAMA_BASE_URL (#1940, thanks @kimdzhekhon). An explicit OLLAMA_BASE_URL still wins; OLLAMA_HOST is normalized the way the Ollama client does (adds http://, defaults the port to 11434 when omitted, appends the /v1 OpenAI-compat suffix). Wired through both the client base URL and backend auto-detection, so ollama stays opt-in and never shadows a configured paid key. (Supersedes the vendored-bulk #1966.)graphify extract now honors the persisted --exclude patterns instead of silently re-including them (#2027, thanks @oleksii-tumanov). Mirrors the #1971 gitignore-persistence fix: the exclude set is read from .graphify_build.json when --exclude is absent and applied to the scan, and a flag-less run no longer clobbers it; an explicit --exclude still replaces the persisted list.--postgres extract (introspect a live DB with no filesystem corpus) no longer crashes before introspection (#2030, thanks @oleksii-tumanov). The no-path branch left detection unbound; it's now initialized, the semantic-cache prune and manifest writes are guarded so a DB-only run can't wipe the file cache or leave a poisoning manifest, and a stale manifest from a prior filesystem run is invalidated.source == target self-loop reported as a 1-file import cycle. build_from_json now drops any imports/imports_from/re_exports edge whose endpoints are identical; pre-existing self-loops self-heal on the next rebuild.import { X } from './barrel' where the barrel re-exports X from another module now points at X's real defining node, walking the barrel chain (bounded, cycle-safe). When a barrel re-exports the same local name from two different modules the name is ambiguous and left unresolved rather than guessed, so no wrong edge is fabricated. Builds on #1984.graphify update now evicts semantic nodes whose non-code source file (a .txt/.pdf/.png with no AST extractor) was deleted from disk (#2051). The corpus sweep only checked files it could re-extract, so a deleted doc's or image's LLM-derived nodes survived indefinitely and were served as authoritative. Disk absence is now used as the deletion signal for such sources; remote and virtual sources (anything with a :// scheme) are left untouched.--update runbook no longer marks a semantic file as done when its extraction produced no output (#2015). Step 9 stamped the entire detected corpus into the manifest, so a doc, paper, or image whose chunk failed or was omitted was recorded as complete and never re-queued on the next update, losing its content permanently. The runbook now builds the manifest with the same stamping the library uses (only files that actually produced nodes, edges, or hyperedges are stamped; dispatched-but-empty files have their stale hash cleared so they are retried), across the Claude, Aider, and Devin skill bodies and the shared update reference.build_merge (the --update runbook path) now prunes a deleted file's nodes, edges, and hyperedges regardless of whether their stored source_file is absolute or relative (#2012). When the caller passed no scan root, a node that had kept an absolute path slipped past the relative prune set and the deleted file's graph survived silently. Matching is now form-insensitive (raw, normalized-relative, then an absolute-identity fallback), a re-extracted file is still never pruned, and graphify extract records the scan root marker after every write so a later update relativizes paths correctly even under a custom --out.Fix: the graphify-first search nudge now fires on Claude Code's dedicated Grep tool, not just Bash (#1986, thanks @mdshzb04). The installed PreToolUse
Bash, so a Grep tool call (whose tool_input is {pattern, path, glob, ...}, not {command}) slipped through and never got nudged toward graphify query. The matcher is now Bash|Grep and the search guard recognizes the Grep shape; it stays nudge-only (never the strict deny), and the uninstall filters + #1840 gating are unchanged._resolve_graphify_exe now normalizes \ to / at the single choke point, covering every emitter (Claude/CodeBuddy PreToolUse, Gemini BeforeTool, Codex); quoting and the --strict suffix are preserved and POSIX is unaffected.--out, semantic-cache writes now anchor correctly so the cache round-trips (#1990, #1991, thanks @mdshzb04). The final semantic-cache save resolved a relative source_file against the output dir and wrote 0 entries, and per-chunk recovery checkpoints landed in the wrong directory (under the corpus instead of --out). Cache entries now key on the scan root (portable, matching #1989) while the cache directory sits at the output root, so check/save/checkpoint/prune all agree; composes with the #1989 salt-keying and #1939 prompt-fingerprint namespacing.export { X as Y } from './mod' produced a re_exports edge whose symbol target was an absolute-path-prefixed id with no matching node — the symbol-level residual left by #1967 (imports-only) and #1976 (file-level). The aliased re-export target is now rewritten to the canonical symbol node when unambiguous; external re-exports and owned ids are left untouched, so no real edge is dropped.Feat: opt-in strict PreToolUse hook that actually makes agents use the graph. The installed Claude Code hook has always *nudged* the agent to run grap
graphify query before reading raw files, but a nudge is advisory additionalContext the model routinely walks past mid-task. graphify install --project --strict (or graphify claude install --strict) now installs a hook that blocks the first raw source read of a session (permissionDecision: "deny") with a redirect to graphify query, then downgrades to the soft nudge — so it fires at most once per session and can never strand the agent (the next read proceeds even if no query ran, or if graphify query itself failed). Running any graphify query/explain/path refreshes a short-lived "recently oriented" stamp that suppresses the block. Strict mode is Claude Code only (Bash-grep and Glob stay nudge-only; Gemini/Codex/OpenCode can't hard-block and are unchanged); GRAPHIFY_HOOK_STRICT=1/0 toggles it at runtime without a reinstall. Default installs are unchanged (soft nudge).~/.claude/.../SKILL.md read), and when the graph is stale for the target file (the file changed after the last build, or graphify watch flagged the tree) it softens to a non-mandatory nudge that suggests graphify update instead of demanding the query. Gating is ~3 stat calls — no corpus walk — so it stays fast on large monorepos, and fails open on any error..ts wrapper that re-exports a hand-written .mjs runtime (export { N } from "./foo.mjs") had foo.ts and foo.mjs collapse onto one base file id (the id stem drops the extension), and while _disambiguate_colliding_node_ids correctly salts the two file nodes apart (foo_ts_foo / foo_mjs_foo), the re-export edge keyed its target salt by the importer's own source file — mis-pointing the ./foo.mjs target back at foo.ts, a source == target self-loop reported as a 1-file import cycle in GRAPH_REPORT.md. During disambiguation an import/re-export edge now carries the resolved target file as a transient salt key, so the salt lands on the real sibling node (generalizing the C/ObjC .h-sibling carve-out from #1475 to every language and to re_exports) and the phantom cycle disappears. That hint has no downstream reader and holds an absolute path, so it is popped once consumed and never persisted — and the graph serializer drops it as a backstop — keeping graph.json deterministic and byte-identical across checkout locations. Node ids are unchanged (the residual was purely at the edge layer). One caveat: a graph written by a pre-fix build still records the stale self-loop, and because graphify update only re-extracts changed files, an unchanged wrapper keeps that edge until it is next edited or a --force full rebuild runs — though any stale absolute hint a pre-fix graph happened to persist is dropped on the next build regardless. (The extension-aware-id alternative was rejected: it would rewrite every file and symbol id and force a full-rebuild migration in lockstep with the skill/validation id spec, #1033.)file_hash's stat-index memo is now keyed by the path salt, not the absolute path alone (#1989). The digest salts content with the file's path relative to the scan root (for cache portability), but the memo returned whichever digest was computed first for a given absolute path — so the same file hashed under two different roots (which happens within one --out run) got an order-dependent result, and the wrong digest was persisted into stat-index.json across runs. Each entry now stores one digest per salt; legacy un-salted entries are recomputed rather than trusted. Digest computation is unchanged, so existing cache entries still hit.--no-gitignore extraction opt-out for projects that keep useful code under .gitignore (#1971, thanks @JensD-git for the report and @Mzt00 for the fix). The flag disables only VCS ignore rules (.gitignore + $GIT_DIR/info/exclude); .graphifyignore, the sensitive-dir/secret screens (#1666/#1943), and noise-dir pruning all still apply, so .git/, node_modules/, and real secrets stay out of the graph. The setting persists across update/watch/hook rebuilds and is no longer clobbered back on by a later flag-less graphify extract..graphifyignore/.gitignore glob matching now keeps * within a single path segment (#1975, thanks @oleksii-tumanov). An anchored * used to cross / (hand-rolled fnmatch), so an exclude-all + re-include subtree pattern collapsed to zero files and /src/*.py wrongly ignored nested files. Matching now follows git segment semantics (* per segment, ** spans segments, dir/ matches directories not same-named files), verified against git check-ignore; composes with the #1873 anchor-scoping and #1922 diagnostic.#include paths are no longer mangled (#1978, thanks @Osamaali313). The extractor used str.lstrip("./") — a character-set strip that ate every leading .//, so ../shared/base.dm became shared/base.dm (and .hidden/x lost its dot), breaking include resolution. It now strips only a leading ./ prefix.graphify prs decodes gh/git/claude output as UTF-8 instead of the Windows locale codec (#1980, thanks @Luke J). On Windows text=True decoded child output as cp1252, mojibaking or crashing on emoji / non-ASCII names in PR titles and diffs; all subprocess reads now pass encoding="utf-8", errors="replace" (matching the #1505 precedent), which also fixes the encode side when feeding a non-ASCII prompt to the claude-cli backend.Fix: an incomplete extraction no longer force-writes a partial graph over a complete one (#1951, thanks @TPAteeq). A crashed AST/semantic pass, a some
to_json(force=True) path wrote anyway, bypassing the #479 shrink guard; the --no-cluster raw dump had no guard at all. Both paths now refuse to overwrite a larger existing graph when the run was incomplete (exit 1, nothing written) unless --allow-partial is passed, and a present-but-unparseable existing graph fails closed (a corrupt/mid-write file could be hiding a complete graph). detect()'s walk_errors now count as incomplete too.graph.json, manifest.json, and the other JSON artifacts are now written atomically (#1952, thanks @TPAteeq). A kill, OOM, or ENOSPC mid-write used to leave a truncated file — and a truncated graph.json then wedged every later run via the shrink guard's fail-safe. Writes now go to a temp file in the same directory and os.replace into place (writing through a symlink so shared-store setups keep working); the writers the original change missed (the --no-cluster dump, merge-graphs/merge-chunks/merge-semantic, the analysis/labels sidecars, and the global graph/manifest) are routed through it too.merge-chunks merges it (#1953, thanks @TPAteeq). A malformed chunk used to crash the whole merge (aborting good chunks) or silently pass an adversarial node id (path-escape) into the graph. Each chunk is now validated (reusing the #825 fragment validator, size-capped, id-charset checked) and a bad one is skipped with a warning rather than aborting; non-numeric token counts can no longer TypeError the merge either.file_type:"code" node from a document whose symbol name never actually appears in that source (an inferred or hallucinated symbol); it's now flagged verification: "unverified" (a dedicated node field, reported by graphify diagnose) rather than presented as a read fact. The check verifies against the node's label and id, only touches nodes the model itself presented as solid, and never drops a node.watch/update no longer re-scans and duplicates concept/rationale-only semantic docs on every rebuild (#1954, thanks @jw3b-dev). The #1915 semantic-doc gate in _rebuild_code recognized a doc as having a semantic (LLM) layer only via a file_type=="document" node, but the extraction spec's preferred shape for a doc full of named concepts is to represent it with ONLY concept/rationale nodes and no separate document node — such a doc never entered semantic_doc_identities, stayed in the AST quick-scan set, and got duplicate heading nodes minted on top of its real semantic nodes on every graphify update/watch/hook rebuild (the #1915 symptom returning for this doc shape). The gate now recognizes the full doc-shaped subset of the canonical six-value file_type enum (document, concept, rationale, paper — the same set build.py already treats as doc-representation types), while code/image nodes and AST-origin-marked nodes are still excluded, so the pre-#1865 legacy-graph safeguard the gate relies on is unchanged.save_manifest no longer seeds a stale semantic_hash for a file that was dispatched this run but produced no stamped output, masking an LLM-omitted doc on the next run (#1948, thanks @rsolanilla). A file omitted from the semantic result (a --force re-run where the model drops its chunk) is dropped from the files dict cli.py's _stamped_manifest_files() passes to save_manifest, per the #933/#1890 never-stamp-a-failed-chunk contract — but the seed loop that carries forward untouched rows for subset saves (#917) then copied that file's row from the on-disk manifest verbatim, including its semantic_hash from an earlier successful run. detect_incremental(kind="semantic") compared current content against that inherited hash, found a match, and silently reported the file unchanged — defeating the #1890 retry promise the exact way the issue's manual "blank the hash by hand" workaround worked around. save_manifest now accepts a clear_semantic set; the seed loop forces semantic_hash to "" for any file in it instead of inheriting the stale value. cli.py derives the set as semantic_files — what was actually sent to the backend this run (narrowed by the incremental gate and --code-only, widened by deep mode) — minus _stamped_manifest_files()'s result: dispatched-but-not-stamped, regardless of why. Untouched live files that were never dispatched are deliberately not in the set, so a partial incremental run cannot blank the rest of the corpus's stamps. The set is passed at all three _save_manifest call sites. clear_semantic defaults to None (no-op), so every existing caller and the #917/#1908 seed/pruning behavior for untouched and excluded-but-alive rows is unchanged.sha256(file content + path) alone, with no component for the extraction prompt that produced them, so a release that changed the prompt left every unchanged file a cache hit: the run exited 0, cost.json looked cheap, and the graph silently carried two prompt generations side by side. Semantic entries are now namespaced by a fingerprint of the extraction prompt (cache/semantic/p{fingerprint}/, mirroring the AST cache's v{version}/ layout), keeping both properties #1252 wanted — entries survive releases that don't touch the prompt, and invalidate only when it actually changed. The fingerprint normalizes line endings so a CRLF checkout doesn't look like a prompt change. Both extraction paths pass their prompt: the Python/CLI path (llm.py's _EXTRACTION_SYSTEM, all backends) automatically, and the skill path via a new prompt_file argument in Step B0/B3 pointing at the references/extraction-spec.md the subagents were handed. Pre-existing entries predate fingerprinting and have unknowable vintage: they are still served rather than re-billing a whole corpus, but check_semantic_cache now warns with the count, so the "no signal at all" the report describes becomes a visible one; --force (or GRAPHIFY_FORCE=1) re-extracts them. Old-fingerprint entries are pruned by liveness only, never swept wholesale the way stale AST versions are — two hosts with different prompts can share one graphify-out/, and a wholesale sweep would have each run delete the other's entries. (The two monolith skills, aider and devin, inline their prompt instead of shipping a spec sidecar and stay on the unfingerprinted path for now.)secrets/ or credentials/ directories (#1943, thanks @HerenderKumar). A directory named secrets/, .secrets/, or credentials/ is as often a real source package (Go internal/secrets, a credentials/ service module) as a credential store, but _is_sensitive pruned everything beneath one wholesale, with no trace and no override. The dir list is now split: dedicated credential stores (.ssh, .gnupg, .aws, .gcloud) still drop everything unconditionally, while the ambiguous bare-name dirs spare genuine programming-language source — the same carve-out Stage 3 applies to keyword-named files (#1666), extracted into a shared _is_graphable_source predicate so the two stages can't drift. Rescued source still falls through the Stage 2/3 filename screens (secrets/service_account.py and credentials/id_rsa stay dropped), and data/config formats under those dirs (secrets/db.json, .secrets/token.yaml) remain flagged — those are exactly the formats credentials ship in.references edges are no longer dropped when a routine in the same schema is unparseable (#1854, thanks @sekmur). pg_introspect builds one synthetic DDL document and parsed it with the function stubs emitted before the FK ALTER TABLEs, so a C-language (or otherwise unparseable) routine's stub parsed as a tree-sitter ERROR node that swallowed the trailing FK statements into the error region, losing every FK edge after it. The FK DDL is now emitted before the function stubs, so table-to-table references edges are produced first and can't be eaten by a later unparseable routine.graphify extract <root> --out <dir> no longer reduces every node's source_file to a bare filename, so a graph.json stays resolvable against its scan root (#1941, thanks @JensD-git). --out passes the output dir as cache_root to relocate the cache, but that value also anchored relativization — so every scanned file failed relative_to(root), fell through to the #1899 out-of-root fallback, tripped its updepth > 3 walk-up guard, and collapsed to a basename; on Windows an --out on another drive hit the cross-drive branch and basenamed unconditionally. extract() now takes an explicit root anchor the CLI pins to the scan root, independent of where the cache lives (completing the #1774 cache/anchor decoupling and matching build(root=target)). Cache location, out-of-root portability (#1899), and the no---out/watch paths are unchanged. A graph already written with basenamed paths does not self-heal on an unchanged corpus (the stale nodes are pruned as out-of-scope, leaving them empty); run graphify extract --force once, or re-extract after a file change, to rebuild it correctly.Your coding agent can read these notes before it upgrades. Set up the MCP server →