Skip to content

v0.9.13

Latest

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