Skip to content

Releases: Graphify-Labs/graphify

graphify 0.9.20

Choose a tag to compare

@safishamsi safishamsi released this 18 Jul 21:29
  • Fix: the graphify-first search nudge now fires on Claude Code's dedicated Grep tool, not just Bash (#1986, thanks @mdshzb04). The installed PreToolUse hook only matched 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.
  • Fix: installed hook commands now use forward slashes in the graphify exe path so Git Bash doesn't strip them (#1987, thanks @varuntej07). On Windows the resolved exe path had backslashes, which Git Bash (how Claude Code shells hooks) treats as escapes and drops, breaking the hook with "command not found". _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.
  • Fix: with --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.
  • Fix: alias-based named re-exports no longer emit dangling absolute-path symbol targets (#1983, thanks @oleksii-tumanov). 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.

graphify 0.9.19

Choose a tag to compare

@safishamsi safishamsi released this 18 Jul 11:56
  • 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 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).
  • Fix: the PreToolUse hook stops crying wolf (#1840), which applies to the default soft nudge too. It no longer fires for reads of files outside the indexed project (a common false trigger, e.g. a ~/.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.
  • Fix: a same-basename cross-extension re-export no longer manufactures a phantom self-cycle (#1814, thanks @Greg-Moskalenko for the report and @alphanury for the fix). A typed .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.)
  • Fix: 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.
  • Fix: --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.
  • Fix: .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.
  • Fix: DreamMaker parent-relative #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.
  • Fix: 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.

graphify 0.9.18

Choose a tag to compare

@safishamsi safishamsi released this 17 Jul 13:57
  • 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-chunks-failed run, or a walk that couldn't fully enumerate the corpus (permission-denied subtree) produced a smaller graph that the 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.
  • Fix: 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.
  • Fix: truncated LLM chunks are no longer promoted to the semantic cache as complete (#1950, thanks @TPAteeq). A chunk that hit the output-token limit and couldn't be split further was cached and manifest-stamped as authoritative, so its incomplete node set replayed forever. Such chunks are now marked partial (treated as a cache miss and re-dispatched), including the common case where the truncation parses to nothing: the give-up sites record the chunk's own files independently of parsed items, so a sliced document whose later slice truncated empty is no longer stamped complete, and a clean slice can't re-promote a partial entry.
  • Fix: untrusted subagent chunk JSON is validated before 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.
  • Fix: code-typed semantic nodes with no evidence in the source are flagged (#1949, thanks @TPAteeq). The LLM can surface a 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.
  • Fix: 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.
  • Fix: 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.
  • Fix: the semantic cache no longer replays extractions from an older prompt after an upgrade (#1939, thanks @HunterMcGrew and @SinghAman21). Entries were keyed on 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.)
  • Fix: the Stage 1 sensitive-directory check no longer silently drops legitimate source under 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.
  • Fix: PostgreSQL foreign-key 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.
  • Fix: 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 li...
Read more

graphify 0.9.17

Choose a tag to compare

@safishamsi safishamsi released this 16 Jul 10:58
  • Acknowledgement: Amp (ampcode.com) platform support, which shipped earlier in v8, was contributed by @zuwasi in #948 (the skill-amp.md skill and the graphify amp install wiring). Belated credit for the work.

  • Fix: a missing manifest.json no longer degrades graphify extract --code-only into a full scan that discards the committed semantic layer (#1925). On a fresh clone (or when the manifest is deliberately untracked because its mtimes churn), the incremental gate required both manifest.json and graph.json; with only the graph present it fell to a full scan, and under --code-only that dropped every doc/paper/image node — silently replacing a curated graph with an AST-only skeleton. An existing graph.json is now a sufficient incremental baseline: detect_incremental already treats an absent manifest as "everything new / nothing deleted", so build_merge + _stale_graph_sources preserve files that are merely out of this run's scope while still evicting genuinely deleted sources.

  • Fix: hyperedge-only documents are now stamped in the manifest instead of being re-extracted on every run (#1920). _stamped_manifest_files (#1897) decided a semantic doc "produced output" by inspecting only nodes and edges, never hyperedges, so a chunk whose only output for a doc was a hyperedge (3+ nodes sharing a concept) left that doc unstamped and perpetually re-queued. Stamping now counts hyperedge output too, mirroring the per-source_file keying the semantic cache already uses.

  • Fix: the PHP extractor now disambiguates same-named classes across namespaces (#1923). A bare class reference (extends Page) collapsed onto the only internal class named Page, so App\Models\Page and an imported Filament\Pages\Page fused into one node, manufacturing a false inherits/imports edge and a bogus cross-community bridge. A new namespace/use-aware resolution pass (mirroring the Java resolver, running before the unique-name rewire) re-points supertype/import references to the real definition, or parks provably-external ones on a fully-qualified stub the bare-name rewire cannot collapse. Plain, non-namespaced PHP is unchanged.

  • Fix: detect() now records files and directories dropped by a .gitignore/.graphifyignore rule in a new ignored diagnostic field (#1922). The nested-ignore scoping bug itself was fixed in 0.9.16 (#1873); this closes the remaining gap where an ignored path left no trace in any diagnostic, so an over-broad rule looked like a clean scan. Entries are per-directory where a subtree is pruned, keeping the list bounded.

  • Fix: _semantic_id_remap is now idempotent, so incremental rebuilds stop churning (#1917). When a file's canonical stem contained its own legacy stem as a prefix (parent dir name equals the file stem, e.g. .claude/CLAUDE.md, docs/docs.md), an already-migrated semantic node id re-matched the legacy branch and gained another stem segment on every build (claude_x -> claude_claude_x -> ...). Because _origin is persisted, every graphify update re-fed nodes through the remap, so the ids grew unboundedly and the same_topology/same_graph/no_change short-circuits never fired — rewriting graph.json and re-running clustering on every zero-delta update. The remap now skips an id that already carries its canonical stem (mirroring the graph_has_legacy_ids check), while a genuine one-time legacy migration still applies. (An already-corrupted graph needs one graphify extract --force to reset the grown ids.)

  • Perf: graphify query now scores the graph once per query instead of T+1 times for a T-term query (#1889 / #1918, thanks @Sirhan1). The per-term-guarantee (#1445) previously re-scored the whole graph once per token; _score_query now computes the combined ranking and each token's singleton winner in a single traversal, feeding _pick_seeds via best_seed_by_term. Behavior is preserved (verified byte-identical against the old per-term scoring across a differential fuzz); ~1.3-1.4x faster and independent of query length.

  • Fix: --mode deep is now effective over a warm cache instead of a silent no-op (#1894). The semantic cache is namespaced by mode (semantic vs semantic-deep) so a shallow-cached file no longer satisfies a deep run; graphify extract gains --force (and honors GRAPHIFY_FORCE) to bypass the incremental gate and the cache read; and a deep incremental run widens its dispatch to the full live doc set so the deep namespace actually gets populated. Cache prune/clear now sweep both namespaces so the deep cache can't accumulate orphans. (The skill-side flow that passes the mode through is a follow-up; the CLI is complete and backward compatible — the new mode argument defaults to the existing behavior.)

  • Fix: the semantic cache no longer persists dangling edges/hyperedges (#1916). When a node group was skipped on write (out-of-scope per the batch guard, or a ghost source_file), edges/hyperedges in the kept groups that referenced those never-written nodes were saved anyway and re-surfaced on every cache replay. Those references are now pruned at write time (gated on the scoping allowlist, so unscoped callers are unchanged), and build_from_json validates hyperedge members against the node set so a dangling hyperedge can't reach graph.json even from a live extraction.

  • Fix: graphify update/watch no longer produces a bloated graph by double-representing documents (#1915). _rebuild_code AST-quick-scanned Markdown/doc files and then preserved their existing semantic (LLM) nodes on top, so each doc appeared twice (a real corpus came out ~4x). A doc that already has semantic nodes in the graph is no longer AST-quick-scanned (its semantic nodes are the sole representation), while a doc with no semantic layer still gets the structural quick-scan; incremental rebuilds now preserve a doc's semantic nodes instead of evicting them, and previously-bloated graphs self-heal on the next full rebuild.

  • Fix: .cjs (explicit CommonJS) files are now recognized as code and parsed with the JavaScript grammar (#1912, thanks @Kookwater). The extension was half-registered (present in some internal maps but missing from the classification and dispatch sets), so .cjs files were silently dropped.

  • Fix: files that become excluded (.graphifyignore/.gitignore/--exclude) are now pruned from both the graph and the manifest instead of lingering (#1908 / #1909). Two coupled gaps: save_manifest retained any prior row that still existed on disk (disk-existence, not scan-membership), so an excluded-but-present file was reported as deleted on every run; and the incremental prune set was derived from the manifest alone, so a newly-excluded file's stale nodes carried forward from the existing graph.json were never removed (the state every 0.9.16 graph is in). Now the incremental prune set also derives from the existing graph's own source_files minus the post-exclude corpus (in-root only), save_manifest prunes rows outside the full scan corpus, and detect_incremental distinguishes truly-deleted files from excluded-but-present ones (which are no longer misreported as deleted).

  • Fix: PostgreSQL PL/pgSQL functions are no longer silently dropped (#1910). CREATE FUNCTION ... LANGUAGE plpgsql AS $$...$$ with OUT params, tagged dollar-quotes, or procedural body statements parses as a tree-sitter ERROR node, which the SQL extractor skipped entirely. It now recovers the function/procedure name from an ERROR node via the same regex-fallback pattern the extractor already uses elsewhere, so the function node and its contains edge are kept (the unparseable body is left opaque) and surrounding statements are unaffected.

  • Security/privacy follow-up: nodes whose source_file was never dispatched are now dropped from the graph, not just skipped from the cache (#1895). The #1757 guard stopped a mis-attributed node from clobbering another file's cache entry, but the node itself still flowed into graph.json; it is now filtered out of the merged result (real-file, non-dispatched attributions only), consistent with the cache rejection.

  • Fix: manifest.json now records every successfully-extracted file, not just the zero-node ones (#1897). The #933 stamping filter compared root-relative node source_files against absolute detect() paths, so it dropped every freshly-extracted semantic document from the manifest and broke the incremental-update baseline. Both sides are now resolved before comparison; genuinely omitted/zero-node docs stay unstamped so they retry.

  • Fix: graphify hook install now registers the graph.json union merge driver that the README and CHANGELOG have long documented (#1902). It writes the merge.graphify config via git config and an idempotent, append-only graphify-out/graph.json merge=graphify line in .gitattributes; uninstall removes them.

  • Fix: hook install/status no longer print a spurious "could not read core.hooksPath" warning on repos whose .git/config contains git-legal duplicate keys (VS Code writes these) (#1907). Config is now resolved via git rev-parse --git-path hooks instead of a strict configparser, which rejected duplicate keys.

  • Fix: graphify export obsidian prunes notes for nodes that left the graph instead of merging old and new on re-export (#1896). Only notes graphify itself wrote (tracked in its ownership manifest) are removed, with a vault-containment guard, so user-authored notes are never touched.

  • Fix: non-English query sentences no longer pick wrong BFS seeds because their filler words were unfiltered (#1900). The query stopword set now covers German and the major Romance languages (curated to avoid clobbering English content words), so Wie funktioniert die Authentifizierung? seeds the keyword, not the stopwords.

  • Fix: Python calls to an imported module now resolve (already shipped in 0.9.16); .skill files (Markdown-with-frontmatter agent files) are now ...

Read more

v0.9.16

Choose a tag to compare

@safishamsi safishamsi released this 14 Jul 23:04

graphify turns any folder of code, docs, papers, images, or videos into a queryable knowledge graph. 0.9.16 is a correctness and privacy release: it fixes two regressions from 0.9.15 that could zero out a graph, closes a residual path leak, and lands a batch of extraction and reliability fixes, most from community reports.

Highlights

  • Two 0.9.15 regressions that could empty your graph are fixed (#1873 / #1887 / #1880). A nested .gitignore with a bare * (a common scratch-dir idiom) was applied to the whole tree instead of its own directory, so detect() returned zero files and produced an empty graph; graphify update then saw 0 nodes and refused to overwrite. Nested ignore patterns are now scoped to their own subtree.
  • Residual path/username leak closed (#1899). Out-of-scan-root reference targets (an out-of-root .csproj/.sln project, a bash source) and degenerate symbols (a minified $, a JSONC "//" key) could still embed the absolute scan path in a committed graph.json. Both are now portable.
  • Silently-lost documents are now surfaced (#1890). When the semantic pass returns a clean response that omits some dispatched documents, the run now reconciles dispatched-vs-returned and warns instead of dropping them without a trace.

All fixes

  • Fix: nested .gitignore/.graphifyignore patterns are scoped to their own subtree; a nested bare * no longer zeroes the corpus (#1873 / #1887 / #1885, thanks @Alwyn93).
  • Fix: graphify update no longer emits 0 nodes and refuses to overwrite when the tree has a nested broad .gitignore (#1880).
  • Fix: close residual absolute-path/username leaks for out-of-root reference targets and degenerate symbols (#1899).
  • Fix: the incremental semantic-cache checkpoint no longer fails on sliced (oversized) documents (#1870).
  • Fix: --exclude patterns persist into update/watch/hook rebuilds instead of applying only to the initial scan (#1886).
  • Fix: Python calls to an imported module (module.function()) now produce a calls edge (#1883).
  • Fix: semantic cache writes are scoped so a mis-attributed source_file cannot clobber another file's entry — including the per-chunk checkpoint (#1757 / #1835, thanks @TPAteeq).
  • Fix: a full graphify update no longer evicts the LLM semantic edges of a re-extracted document (#1865 / #1868, thanks @xor-xe).
  • Fix: ID-collision resolves to a deterministic survivor and warns only about real loss (#1851 / #1852, thanks @bchan84x).
  • Fix: --cargo honors Cargo's package = "..." dependency rename (#1858 / #1861, thanks @thejesh23).
  • Fix: detect_incremental re-extracts a legacy-manifest file when its mtime moves backwards (#1859 / #1862, thanks @thejesh23).
  • Fix: the dedup summary reports the fuzzy-merge count even with zero exact merges (#1857 / #1860, thanks @thejesh23).
  • Fix: uppercase TypeScript extensions (.TS/.TSX/.MTS/.CTS) are parsed with the TypeScript grammar (#1881, thanks @xkam7ar).
  • Fix: Kotlin builtin/stdlib types are filtered from the references graph (#1876, thanks @kebwlmbhee).
  • Fix: the stale/missing-skill version warning prints to stderr, not stdout (#1805 / #1893, thanks @Mzt00).
  • Fix: semantic extraction reconciles dispatched vs returned files and warns on silently-omitted documents (#1890).

Install

pip install --upgrade graphifyy
# or
uv tool install graphifyy@0.9.16

Then graphify install to update the skill for your agent.

v0.9.15

Choose a tag to compare

@safishamsi safishamsi released this 13 Jul 22:22

A small, security-focused patch on top of 0.9.14.

Highlights

  • Security (stored XSS): the exported graph.html is fixed (#1838). The report's neighbor links dropped an unescaped stringified node id into an inline onclick — which broke every neighbor link and, when a node id/label contained a double-quote (e.g. from a document or a title scraped via graphify add <url>), let a hostile source inject a live event handler into a report opened locally. The id is now carried in an HTML-escaped data-nid attribute dispatched through a single delegated listener. If you generate reports from untrusted corpora, upgrade.

All fixes

  • Security: close a stored XSS and repair the (previously always-broken) neighbor "focus" links in the exported graph.html (#1838, thanks @edgestack-ai).
  • Fix: detection honors nested .gitignore/.graphifyignore files below the scan root, matching git — a vendor/sub/.gitignore deeper in the tree is now applied to its own subtree instead of being ignored (#1847, thanks @Mohak-Agrawal). Composes with .git/info/exclude (0.9.14): a nearer ! re-include still wins.
  • Fix: graphify update now keeps human-readable community_name labels instead of stripping them back to numeric ids on every incremental rebuild (#1808 / #1855, thanks @latreon).

Install

pip install --upgrade graphifyy
# or
uv tool install graphifyy@0.9.15

Then graphify install to update the skill for your agent.

v0.9.14

Choose a tag to compare

@safishamsi safishamsi released this 13 Jul 09:41

graphify turns any folder of code, docs, papers, images, or videos into a queryable knowledge graph. 0.9.14 is a correctness-focused release: eleven fixes across detection, the git hooks, extraction caching, query seeding, and export — most from community reports, several with their own PRs.

Highlights

  • Massive graph bloat from git worktrees is gone (#1810). Detection now honors .git/info/exclude — where git worktree add records nested worktree paths — so graphify no longer walks into worktree copies of the repo. One reporter's 5-worktree repo had ballooned from ~9,400 nodes / 10 MB to ~210,000 nodes / 311 MB, regenerated on every commit.
  • No more username leaks in committed graphs (#1789). Visual Studio solution-folder nodes were keyed off the absolute scan path, embedding the local username into graph.json. They're now relative.
  • graphify export graphml stopped crashing (#1831). Any dict/list-valued attribute (a node metadata dict, the graph-level hyperedges list) failed the entire export and left a 0-byte file; non-scalars are now JSON-serialized and the write is atomic.
  • The CLI no longer reports success as failure through a pipe (#1807). graphify query … | head used to exit 255 when the reader closed early, breaking CI wrappers and agent harnesses; an early-closing reader is now treated as success.

All fixes

  • Fix: Visual Studio solution-folder node ids no longer embed the absolute scan path / username (#1789, thanks @fremat79).
  • Fix: the CLI exits 0 (not 255) when a downstream reader closes the pipe early — head, Select-Object -First N, sed q (#1807 / #1811, thanks @varuntej07).
  • Fix: extract() writes its AST cache (and the stat-index) to CWD, never the analyzed source tree, with the cache location decoupled from the portable key/id anchor (#1774 / #1802, thanks @SimiSips).
  • Fix: graphify query deduplicates BFS seeds by label so homonymous generic symbols (many GET/POST handlers) no longer flood results (#1766 / #1832, thanks @devcool20).
  • Fix: semantic cache writes are scoped to each extraction batch's files, so a mis-attributed source_file can't clobber another file's cache — covering both the final write and the per-chunk checkpoint (#1757 / #1835, thanks @TPAteeq).
  • Fix: graphify export graphml coerces dict/list (and other non-scalar) attributes and writes atomically (#1831 / #1830, thanks @hofmockel).
  • Fix: .nox/ virtualenvs are skipped during detection alongside .tox/ (#1804, thanks @igorregoir-lgtm).
  • Fix: detection honors .git/info/exclude, ending git-worktree graph bloat (#1810, thanks @cdahl86-cyber).
  • Fix: the git hooks short-circuit inside linked worktrees, and GRAPHIFY_SKIP_HOOK now suppresses both post-commit and post-checkout (#1809, thanks @cdahl86-cyber; worktree guard co-developed with @Claude-Madera's #1806).

Install

pip install --upgrade graphifyy
# or
uv tool install graphifyy@0.9.14

Then graphify install to update the skill for your agent.

v0.9.13

Choose a tag to compare

@safishamsi safishamsi released this 12 Jul 10:17

Maintenance release: a batch of correctness and privacy fixes across extraction, incremental update, and query. No breaking changes.

Highlights:

  • Query log is now opt-in (off by default) — no more undocumented plaintext record of your queries in ~/.cache (#1797).
  • Incremental graphify update no longer silently evicts nodes for files that are merely newly-ignored but still on disk (#1795), and build_merge no longer drops a re-extracted file passed in prune_sources (#1796).
  • Markdown files no longer split into duplicate document nodes across the quick-scan and semantic passes (#1799).
  • New language coverage: Ruby .rake files (#1784) and cross-file Bash script execution edges (#1756).

Fixes

  • Fix: the query log is now opt-in (off by default) (#1797, thanks @adam-pond-agent). querylog wrote every query/path/explain question and corpus path (and full responses if GRAPHIFY_QUERY_LOG_RESPONSES) to a default-on, unbounded, fail-silent plaintext file at ~/.cache/graphify-queries.log — outside any repo's .gitignore/retention, and undocumented, which contradicts graphify's on-device / no-telemetry posture. Logging is now OFF unless you opt in with GRAPHIFY_QUERY_LOG_ENABLE=1 (default path) or GRAPHIFY_QUERY_LOG=<path>; GRAPHIFY_QUERY_LOG_DISABLE=1 still forces it off. All the query-log env vars are now documented in the README.

  • Fix: a markdown file that went through semantic extraction is no longer duplicated into two disconnected nodes on later graphify update (#1799, thanks @jerp86). The semantic pass mints <slug>_doc while the markdown quick-scan mints the bare <slug>, so the file's edges split across two twins (a docs->code path query would dead-end on the bare half; centrality and communities split too). build_from_json now merges the bare quick-scan node into the semantic _doc node when both share the same source_file and are file_type: document, consolidating their edges/hyperedges onto one node. Gated so an unrelated code symbol foo and foo_doc never merge.

  • Fix: incremental graphify update no longer silently evicts nodes for a file that left the scan corpus but still exists on disk (#1795, thanks @CJNA). _reconcile_existing_graph read "source absent from the collected corpus" as "deleted", but that's also what an ignore-rule/filter change looks like (e.g. an upgrade that starts honoring .gitignore) — in one 27k-node graph the first rebuild after such an upgrade mass-evicted 655 nodes whose files were present the whole time. Eviction now fails closed: a corpus-absent source is only evicted when Path(identity).exists() is False (true deletion), otherwise its nodes/edges/hyperedges are preserved and a loud line reports how many were kept and why. True deletions and renames evict as before; a full extract --force still purges deliberate exclusions.

  • Fix: build_merge no longer silently deletes a re-extracted file's fresh nodes when that file is also passed in prune_sources (#1796, thanks @erichkusuki). A file present in new_chunks is being replaced, not deleted, so it's now excluded from the prune set — "replace" wins over a contradictory "delete" of the same source. Previously, following the old edit-workflow (pass the changed file in prune_sources) deleted the just-built concept whenever an edit kept a node's label. Genuine deletions (a file in prune_sources but not new_chunks) still prune.

  • Fix: graphify path resolves each endpoint to the first candidate whose label contains every query token, instead of blindly taking the top-scored node (#1785, thanks @CJNA). _score_nodes' full-query bonus only fires when the query equals/prefixes a label, so a query that is a token subset of the intended label ("Reject-everything judge" vs "Degenerate Reject-Everything Judge") got no bonus and a node prefix-matching one rare token could outscore it — anchoring the path on an unrelated, often disconnected node and yielding a false "No path found". When the top candidate already full-matches (the common case) the pick is unchanged. Applied to both the path CLI and the MCP shortest-path tool; the close-runner-up ambiguity warning now fires only when the score head is what was actually picked.

  • Fix: the report's "Suggested Questions" weakly-connected-node count now matches its "Knowledge Gaps" count (#1768, thanks @balloon72). suggest_questions() omitted the file_type != "rationale" filter that report.py's Knowledge Gaps section applies, so the same GRAPH_REPORT.md showed two different numbers for the same concept (e.g. 757 vs 245), making a healthy graph look like it had a major documentation gap. Both computations now use the same filter.

  • Fix: Bash scripts that run each other by execution now get a cross-file edge (#1756, thanks @balloon72). extract_bash only linked source x.sh / . x.sh; the two most common forms — bash x.sh and ./x.sh — produced no edge, so execution topology was missing. They now emit a calls edge (context script_invocation) to the invoked script's entry node when the target resolves to a real file on disk (script runners bash/sh/zsh/ksh/dash and bare ./x.sh), skipping missing or shadowed targets.

  • Fix: Ruby .rake files are now extracted and participate in Ruby cross-file resolution like .rb (#1784, thanks @krishnateja7). .rake is plain Ruby but the extension was gated out of seven places (classification, extractor dispatch, the language-name/family maps, the ruby_member_calls resolver's suffix set, both .rb-suffix filters in ruby_resolution.py, and the build repo-tag map), so every rake task was skipped and its calls were invisible. All seven now include .rake; Widget.tally from a .rake task resolves to its .rb definition.

  • Fix: cross-module references to a function now resolve to its definition instead of dangling on a name-only stub (#1781, thanks @EmilNyg). _rewire_unique_stub_nodes gated merge targets through _is_type_like_definition, which rejects any label ending in ) — so function/method defs could never absorb their reference stubs, and "who references this function" returned nothing on the definition node while a sourceless stub held all the edges. Top-level function defs are now eligible rewire targets when the label match is globally unique, gated by a language-family match with the referrers (a Python get_db reference can't bind to a unique Go get_db()) and excluding stubs used as a supertype (inherits/implements/extends — you don't inherit from a function). Types are unchanged.

Install

uv tool install --upgrade graphifyy    # or: pipx upgrade graphifyy

v0.9.12

Choose a tag to compare

@safishamsi safishamsi released this 10 Jul 10:42
  • Fix: live PostgreSQL introspection (--postgres) now emits foreign-key references edges under a read-only role (#1746, thanks @rithyKabir). The FK query read information_schema.referential_constraints, which is privilege-filtered — a role with only SELECT sees zero FK rows while tables/views/routines still appear, so every references edge silently vanished. It now reads the world-readable pg_catalog.pg_constraint (keyed by oid, which also fixes same-named constraints on sibling tables cross-matching in the old name-based joins), preserving composite-FK column order via UNNEST ... WITH ORDINALITY.

  • Fix: json_config no longer emits imports/extends edges to node IDs it never creates (#1764, thanks @oleksii-tumanov). package.json dependencies and tsconfig.json extends/$ref targets produced edges whose endpoint node was absent, so build_from_json silently dropped them (the "no matching node id" case is filtered out of real errors) — losing dependency/extends structure on two of the most common files in any JS/TS repo. The extractor now creates the referenced target as a concept node before adding the edge.

  • Fix: graphify update no longer deletes semantic hyperedges on every run (#1755, thanks @oleksii-tumanov). The AST-only rebuild treated every rebuilt corpus file as grounds to evict hyperedges anchored to it, but the AST pass never re-emits hyperedges, so doc-sourced hyperedges (exactly what semantic extraction produces) were permanently lost on the first update after a full build — even a no-op run. Hyperedge eviction is now scoped to genuinely deleted (or symlink-outside) sources, mirroring node/edge handling; replacement-by-id and dangling-member cleanup are unchanged.

  • Fix: Java member calls resolve against the receiver's declared type instead of a bare method-name match (#1696/#1697, thanks @oleksii-tumanov). gw.charge() where gw: PaymentGateway now binds to PaymentGateway.charge, not a same-named AuditLog.charge in another file. Explicit-type receivers and this are exact; current-class fields, method parameters, and explicitly-typed locals resolve via a method-scoped type table; a missing, ambiguous, inherited, or chained receiver is skipped rather than guessed (same god-node guard as the C#/Swift/Ruby resolvers). Fully-qualified and nested-type receivers are deferred (they need package/nesting-aware type identity).

  • Fix: output/cache artifacts no longer land in the scanned corpus or CWD when --out/--graph point elsewhere (#1747, thanks @bbqboogiedwonsen). extract <corpus> --out <dir> correctly wrote the graph to <dir> but detect()'s word-count/stat-index cache still created a stray graphify-out/cache/ inside the corpus (it uses the scan root); it now honors the --out dir via a threaded cache_root. And cluster-only --graph <elsewhere>/graphify-out/graph.json wrote GRAPH_REPORT.md/labels/analysis/re-clustered graph to the CWD instead of beside the input; it now writes beside --graph when that graph lives in a graphify-out/ dir, while still restoring into the CWD for an archived backup/graph.json (#934).

  • Fix: imports/references edges no longer bind across a language boundary (#1749, thanks @philberndt). The spec already forbids cross-language calls, but an unresolved Python import time could still resolve by bare stem onto a src/time.ts file node — welding a polyglot repo's halves together at a phantom edge (in the reporter's repo, 3 such edges were the only thing bridging 2409 Python nodes to 1403 TS nodes, inflating time.ts betweenness ~90x and making it the #1 "god node"). The build-time cross-language guard now covers imports/imports_from/references in addition to calls, dropping an edge only when both endpoints are known code languages of different interop families (so a config/manifest → code reference is untouched).

  • Fix: files whose extractor bailed out for a missing optional dependency no longer vanish without a trace (#1745, thanks @rithyKabir). .sql files (and other extra-gated languages) have a dispatch entry, so the #1689 no-extractor warning can't fire, and extract_sql returns an error result when tree-sitter-sql is absent, so the #1666 zero-node warning skips it too — the graph built "successfully" while an entire SQL corpus contributed nothing. extract() now surfaces these grouped by extension, naming the extra that restores the language (e.g. pip install "graphifyy[sql]").

  • Fix: build_from_json is deterministic across process runs again (#1753, thanks @erasmust-dotcom). The ghost-node merge iterated set(G.nodes()), so which node survived a (basename, label) collision depended on CPython's per-process string-hash seed — rebuilding the same extraction JSON in a fresh process could silently pick a different canonical id (breaking the cluster→relabel workflow with a KeyError on an id that vanished). The Pass 1/Pass 2 loops now iterate in sorted order. Additionally, two non-AST (semantic) nodes sharing a key but from different files are now treated as distinct concepts and both survive (mirroring the AST/AST ambiguity guard #1257) instead of one arbitrarily merging away; a genuine same-file duplicate still collapses.

  • Fix: a Java field/parameter/return-type reference to a class whose simple name is shared by two modules no longer dangles on a sourceless phantom node (#1744, thanks @aviciot). Both same-named classes already survive as distinct path-scoped nodes, but the cross-module references edge was left pointing at a bare no-source stub because _resolve_java_type_references re-pointed implements/inherits/imports but not references — so a query about the referenced class could miss it. The Java resolver now disambiguates references by the importing file's import statement (falling back to same-package), mirroring the C# resolver, and drops the orphaned phantom.

v0.9.11

Choose a tag to compare

@safishamsi safishamsi released this 09 Jul 00:31
  • Fix: file enumeration no longer silently drops a directory subtree. detect()'s os.walk had no onerror handler, so an os.scandir failure (a permission error, or a directory created/deleted mid-walk by concurrent writes) was swallowed and that whole subtree vanished from the scan with no log, yielding a silently partial graph.json. The walk now records every skipped directory (surfaced in the result's walk_errors) and warns to stderr, while still enumerating the rest. Relatedly, to_json's anti-shrink guard (#479) now fails safe: a non-empty but unreadable existing graph.json refuses the overwrite (pass force=True to override) instead of silently clobbering a good graph; an empty file still proceeds.

  • Fix: Pascal/Delphi extractors no longer emit duplicate method/contains/inherits edges. A class method declared in the interface section and defined in the implementation section each emitted an edge to the same node, so ~half of a Pascal graph's method edges were doubled (skewing degree/centrality and tripping the new cross-file resolver's god-node guard). Both extractors now dedup edges on (source, target, relation), mirroring the existing node dedup.

  • Fix: Pascal/Delphi call resolution is scoped to the caller's class + inherits chain, and calls to methods inherited across file boundaries now resolve (#1739, thanks @richtext). Both extractors previously resolved every call via a single file-wide {name: node_id} dict, so two unrelated classes with a same-named method (property accessors, generated COM/TLB wrappers) collapsed onto whichever was inserted last, producing wrong cross-class calls edges. Resolution now walks own-class then ancestor chain then file-level free functions, emitting no edge when ambiguous (same god-node guard as the Ruby resolver). A new corpus-wide resolver (graphify/pascal_resolution.py) resolves calls from a descendant to a base-class method declared in a different file (the common generated-base/manual-descendant split). Also stops emitting a duplicate cross-file base-class stub carrying the wrong source_file.

  • Fix: query ranking no longer lets a lone generic term that exact-matches a short leaf label hijack seed selection in multi-term queries (#1602/#1724, thanks @fkhawajagh). _score_nodes scales the per-term exact/prefix tiers by squared term coverage; single-term and full-coverage queries are unchanged.

  • Fix: Kotlin enum entries are extracted as nodes with case_of edges to their enum (#1700, thanks @ivanzhilovich). Closes the Kotlin half of #1700 (the Java half shipped in 0.9.10 via #1719); enum class ChatType { NORMAL, GROUP, SYSTEM } now yields NORMAL/GROUP/SYSTEM nodes and "where is ChatType.X used" works for Kotlin.

  • Fix: SKILL.md's POSIX interpreter probe no longer silently falls back to a graphify-less system python (#1735, thanks @mohammedMsgm). Step 1 ran uv tool run graphifyy python -c ..., but the graphifyy package's executable is graphify, so uv treated python as a missing graphifyy command; 2>/dev/null hid uv's own --from hint, leaving PYTHON on an interpreter without graphify. The probe now runs uv tool run --from graphifyy python -c .... The PowerShell path was already correct.

  • Refactor: decomposed the two largest modules into focused, single-responsibility modules — verbatim moves only, every original import path preserved via re-exports, no behavior change (#1737, thanks @TPAteeq). extract.py 17,054 → 4,740 LOC (the tree-sitter engine, cross-file resolution, shared models, and 23 language extractors moved under graphify/extractors/), __main__.py 5,368 → 673 (install/uninstall + CLI dispatch split into graphify/install.py and graphify/cli.py), export.py 1,671 → 962 (HTML + graph-DB exporters under graphify/exporters/). Full suite unchanged.

  • Fix: merge-graphs gives each input a distinct repo tag so same-stem nodes from different source graphs don't collapse (#1729). Two graphs under a same-named repo dir (src/graphify-out and frontend/src/graphify-out, both → src) shared the src:: prefix, so a backend src/app.js and a frontend App.jsx (both bare app) merged into one node with edges from both — false cross-runtime path results. Colliding tags are now widened (frontend_src) with an index-suffix backstop, and the command prints a note when it disambiguates.

  • Fix: uninstall removes the graphify hook/section from Claude's local-only files too (#1731, thanks @TPAteeq). It now cleans .claude/settings.local.json and both CLAUDE.local.md locations in addition to the standard files, via both graphify uninstall and graphify claude uninstall.

  • Feat: graphify extract --code-only indexes code (local AST, no API key) and skips the doc/paper/image semantic pass, so a mixed repo no longer hard-fails when no LLM backend is configured (#1734). Reports what it skipped; the no-key error now points users at the flag.