Releases: Vit129/graphify
Release list
v0.19.0
0.19.0 (2026-07-19)
- Fix (#16):
affected --relation Rdid exact set-membership on edgerelation— a bare filter likeshares_valuecould never match P15's parameterizedshares_value:<value>edge labels, since the caller can't know the exact value in advance. A bare filter (no:) now also prefix-matchesrelation:*edges.graphify query's own traversal never had this restriction and is unaffected. - Feature (#17):
const NAME = [ {title/name/label/description/id, ...}, {...}, ... ]JS/TS array literals — lesson/course/quiz content stored this way — were invisible to the graph beyond a single opaqueNAMEnode._is_content_data_arraygates narrowly, mirroring the JSON extractor's_is_config_jsonshape-probe (Graphify-Labs#1224's lesson: don't AST-walk arbitrary data): 3+ siblingobjectelements, 3+ properties each, a recognized title-like key. Plain data blobs and dispatch registries (arrays of bare identifiers) keep the pre-existing single-node behavior. Also extends the#1077module-level scope guard to recognize a top-level whole-file IIFE ((function(){...})()) as module scope — a common module-scoping idiom that previously meant the feature's own target array never got even its baseline placeholder node; narrowly scoped so an arbitrary nested callback inside such an IIFE is still rejected exactly as before.
v0.18.0
0.18.0 (2026-07-19)
- Feature: a fourth
graph.htmlview lens, "Calls" — alongside Community/File/Dependencies. Unlike Community (colors by inferred Leiden cluster, which mixes short code-symbol labels with long proseconcept/rationalenode labels from the LLM extraction pass) and Dependencies (collapses to one node per file), Calls stays at per-symbol granularity but hides every non-codefile_typenode (concept/rationale/document/paper) and restricts edges to the same real call/dependency relation whitelist Dependencies already used (calls/imports/imports_from/references/inherits/implements/indirect_call/re_exports/uses/embeds) — a plain function-to-function/class-to-class view without doc-node clutter or file collapsing.graphify/export.py; the relation whitelist is now hoisted to module scope and shared between the Dependencies and Calls lenses instead of being duplicated. - Fix/Feature:
graph.html's search box had four gaps that made it harder to find a node than the CLI'sgraphify query. (1) It ignored the active lens — a hiddenconcept/rationalenode in the new Calls lens could still surface as a search result, and clicking it silently panned the camera to nothing (isNodeHiddenis now checked). (2) Matching was plain substring-on-label only — it now also tokenizes on camelCase/snake_case boundaries (same split rule as the CLI'squery.py_tokenize, ported to JS) so e.g. searching "notification" findshandleNotification. (3)source_fileis now also matched, so a file path substring finds nodes in that file. (4) A file-type dropdown (code/concept/rationale/document/paper/all) narrows results directly. Deliberately does not port the CLI's BM25 scoring, typo-correction, or synonym expansion — this is a client-side filter over an at-most-20-result list per keystroke, not a scored corpus search; a lightweight tiered ranking (exact > token match > token prefix > file match > substring) is enough at that scale. - Feature: a new
PostToolUsehook auto-triggers a debounced backgroundgraphify updateafter every agentEdit/Write/MultiEdit, closing the one real gap in graphify's auto-sync story —graphify watch(a realwatchdogdaemon) must be started by hand, and the gitpost-commit/post-checkouthooks (graphify hook install) only fire on commit, so neither covered the uncommitted-edit window an agent session actually lives in.graphify/watch.py's newtrigger_background_update()spawns[sys.executable, "-c", ..., project_root, *changed_paths]fully detached (POSIXstart_new_session, WindowsDETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP, mirroringhooks.py's existing cross-platform launcher), coalescing via_rebuild_code's own existing non-blocking lock + queue/drain mechanism rather than a new debounce layer.python -m graphify.watch --trigger <path>is the thin CLI entry the hook's shell one-liner calls. Installed/removed alongside the existingPreToolUsehooks bygraphify install/graphify claude install—_install_claude_hook's own printed message already claimed this happened; it's now actually true. - Feature: opt-in
pagerank_rankingingraphify.toml(defaultfalse, same rollout shape asvalue_coupling/P15) precomputesnx.pagerank(G)once at build time (update,update --all, and the standaloneextractcommand all wired;cluster-onlyneeds no change, since it re-clusters an existing graph without altering structure) and stores it as apageranknode attribute ingraph.json.query/explain/path's_pick_seedscomposes a small bounded multiplicative boost (max +15%,_PAGERANK_BOOST_MAX) on top of the existing concept/prose seed penalty, meant to break near-ties in favor of a more central/well-connected node. Degrades gracefully everywherescipy(an optional dependency, not bundled in theallextras) is missing. Evaluated on 4 real graphified repos post-ship and found net negative-to-neutral as a default — one genuine regression (a real 5-way exact BM25 tie where the boost favored a well-connected UI dashboard file over the actual automation logic that answered the question), two no-effect cases (BM25 gaps too large for a 15%-capped boost to close), and one qualified-positive case (graphify's own codebase, under favorably-constructed queries, with the harm direction untested there) — confirming the opt-in default was the right call, not just a cautious placeholder. Left as opt-in; no plan to flip the default without materially different evidence.
v0.17.0
0.17.0 (2026-07-18)
- Feature:
.html/.htmextraction now also runs a JS pass over inline<script>block bodies (graphify/extract.py'sextract_html_with_scripts, reusing the Vue SFC extractor's_vue_mask_non_scriptblank-and-preserve-newlines technique — that regex was already generic HTML script-tag matching, not Vue-specific). Previously only elements with anidattribute became nodes; a function/class defined inside a<script>block (the common single-file-app pattern) was invisible to the graph entirely —graphify explain/querycould never find it.<script src="...">(external, no inline body) masks to an empty span and contributes nothing, unchanged. - Fix:
explain/get_neighbors/blast_radius/path(_find_node_coreingraphify/query.py) could return "No node matching" for a real, unambiguous node whose name spans two fields — e.g.ClassName.methodNamewhere the class lives insource_fileand the method is thelabel— because every existing tier (source-exact/exact/prefix/substring) only checks a single field for the whole query string. Added a last-resort BM25 cross-field fallback tier (same rankingquery/pathalready use), only engaged when all single-field tiers come up completely empty, returning every near-tied top scorer (10% gap) so a genuine tie still surfaces through the existing_find_node_tied_groupambiguity warning instead of a silent pick. - Fix:
graphify affected(graphify/affected.py'sresolve_seed) required an exact single-field match count of 1 at every tier, so a name matching several nodes by raw substring count (even when one was clearly the better match) returned a blunt "No unique node match" instead of resolving. Added the same BM25 cross-field fallback as above (_bm25_confident_pick), but keptresolve_seed's existing fail-closed contract intact: it only returns a pick when the top score clearly separates from the rest (10% gap) — a genuine near-tie (e.g. two identically-labeled nodes) still returnsNone, same as before. - Feature: editable git-clone checkouts now get a daily self-update notice, separate from the existing PyPI-upstream check.
graphify/git_update_check.pycompares this checkout'sCURRENT_VERSIONagainstversion.jsonon this repo'smainbranch and, only on a clean working tree in an interactive terminal, asks y/N before runninggit pull origin main. A dirty tree gets a notice instead of a prompt; declining (or a non-interactive run) remembers the dismissal in~/.config/graphify/update-dismissedso it doesn't ask again until a newer version ships. Never auto-pulls. - Fix (security):
collect_files()/detect()had no check that a symlink inside the scan root actually resolves to a target under that root — a symlink pointing outside (accidental or planted) was silently followed and its content ingested intograph.json. Ported upstream's_resolves_under_root()containment check into both the directory-walk pruning and the per-file filter, inextract.pyanddetect.py. This fork's own_auto_follow_symlinksauto-detect convenience feature (upstream removed it in favor of opt-in-only) was left intact — only the missing containment check was closed, not that separate design choice. The LLM extraction path (llm.py's_read_files/_build_image_refs) had the same gap — a symlinked text or image file was read straight off the escaped path before this fix — and now uses the same_resolves_under_root()guard, skipping with a stderr notice instead of reading through. - Fix (Graphify-Labs#1638): an unresolved JS/TS import (bare package name after local/tsconfig resolution fails) is now namespaced with a
ref:id prefix instead of a bare last-segment id, so it can no longer collide with an unrelated local file of the same stem and fabricate a phantom cross-languageimports_fromedge (e.g.tailwindcss/colorsno longer aliases onto an unrelatedbackend/utils/colors.py). - Fix (Graphify-Labs#1659): a JS/TS direct call with no import evidence no longer binds to a same-named export in an unrelated file by bare-name match alone — a real phantom cross-package edge in monorepos (a 14-package repo showed unrelated packages "depending on" each other purely because they exported generically-named symbols). Required porting a companion file-lookup hardening (
sf_to_file_nid) alongside it, since the id-based-only lookup can miss on a path-resolution/symlink mismatch and would otherwise make the new gate drop legitimately-imported calls too. Other languages (C/C++, Ruby, same-package implicit scope) are unaffected — this only changes JS/TS/JSX direct calls. - Fix (csharp, Graphify-Labs#1609): C# had no member-call resolver at all —
recv.Method()was matched by bare same-file label lookup with no receiver-type awareness, so_server.Save()could silently mis-bind to an unrelatedCache.Save()defined in the same file (a wrong edge, not just a missing one). Adds a per-file field/property/param/local type table and a receiver-typed resolver (registered into the existingresolver_registryframework alongside the already-shared Swift/ObjC/C++/Ruby resolvers):this.M()/Type.M()resolve at EXTRACTED confidence, typed-field/param/local receivers at INFERRED, and an ambiguous or untypable receiver emits no edge (single-definition god-node guard) rather than guessing.
These three (plus the 3 language-extractor gaps and CLI-surface claim) were found by a fresh audit against upstream's current source rather than trusting this repo's own README/CHANGELOG — see agent-memory/knowledge/architecture/upstream-comparison-2026-07-05.md for the full methodology and evidence.
v0.16.0
0.16.0 (2026-07-04)
-
Fix (P15 follow-up):
graphify updatewas silently emitting zeroshares_valueedges even withvalue_coupling = trueingraphify.toml, despiteextract(value_coupling=True)working correctly in isolation. Two bugs, both invisible to the full test suite because every existing test called_rebuild_codewithacquire_lock=False: (1) coupling-edge endpoints were resolved to the file-node id captured at YAML-extraction time, but a laterextract()pass remaps file-node ids, so the captured id no longer matched any node and the edge was silently dropped at graph assembly - fixed by resolving each endpoint's current id from the final node set bysource_fileinstead. (2)_rebuild_code(acquire_lock=True)(the real defaultupdateuses) takes a lock then recurses into itself, and that recursive call's hand-listed forwarded kwargs had never been updated to includevalue_coupling- it silently reverted toFalseregardless of what the caller passed. Added a regression test that specifically calls_rebuild_codewithacquire_lock=True(not theFalseevery other test used) to catch this class of bug going forward. -
Feature (P15, opt-in): cross-file value-coupling for config-as-code. Home Assistant automations couple through shared
entity_id/servicestring VALUES, not through anything the AST sees as a call/import - so two operationally-linked automations had no graph path at all (path "home_mode" "AC Arrival"→ "No path found" on the real repo). Withgraphify.tomlvalue_coupling = trueorextract --value-coupling(off by default, never implicit), two files sharing adomain.entityreference get a low-confidenceshares_value:<value>edge (INFERRED, weight 0.3). Gated behind a hard Phase 0 validation gate on the real Home-Assistant corpus: the plan's original 3 filters measured only ~27% meaningful (service-call verbs likefan.turn_onand Lovelace UI-display co-occurrence dominated the noise); adding two filters - excludedomain.<service_verb>values, exclude dashboard/UI files - and restricting to dotted entity-reference shape took it to ~96%, at which point it shipped. Verified end-to-end: flag off = 0 coupling edges; flag on connects the exact previously-unconnectedbedroom_ac_management ↔ home_modepair via oneshares_value:input_boolean.home_modehop; +1.6% edges on the full graph. ponytail-marked as a co-occurrence heuristic (shared value ≠ causal dependency), not reference resolution - that needs schema knowledge this tool refuses by design. -
Feature (P16 Phase 1):
pathandexplaingained source_file-prefix scoping flags to resolve a duplicate label deterministically instead of only being warned it's ambiguous.pathtakes--path P(narrows both endpoints),--source-path P/--target-path P(narrow each independently, for when both labels are duplicated in different dirs);explaintakes--path P. Mirrorsquery's existing--path/--exclude-pathconvention (source_file.startswith(P)). Verified live on the three real duplicate cases that motivated it: Language-LearningStats(--path japanese-practice/vsenglish-practice/), My-Investment-PortcalcPaperPortfolioValue(--path .../paperPricingService.jspicks the implementation a prior QA audit missed), and harness-terminalhandleNotification(--target-path .../NotificationCoordinator.swiftclears the target ambiguity while leaving an unscoped source correctly still flagged). Additive: no--pathgiven = identical behavior to before. The tie-warning already tells the user the flag exists as the escape hatch. Phase 2 (file:Labelsyntax in the shared resolver) intentionally deferred - the flag covers the real cases without touching_find_node_core. -
Fix: the two MCP-only gaps found stress-testing the
explain/get_neighbors/blast_radiusfix above are closed too. (1)_tool_get_nodeused its own standalone lowercase-substring-anywhere scan (matched 16 nodes for a query like "context" on a real graph, including unrelated ones likeContextualLogger, with zero ambiguity signal) - now shares the same tiered resolver and warning as the other three. (2)_tool_shortest_path(the MCPshortest_pathtool) only ever tried the single top-scored node on each side with a score-gap warning bolted on - so it could report a false "no path found" whenever that top candidate happened to be disconnected while a lower-scored, equally-plausible tied candidate had a real path (bug Graphify-Labs#828's exact failure mode, just never ported to the MCP tool). Fixed by extracting CLIpath's full resolution logic - near-tied-candidate retry across every (source, target) pair, plus the degree/label-based hub-avoidance - into a single sharedfind_path_with_disambiguation(graphify/query.py) that bothpathandshortest_pathnow call, instead ofpathhaving the complete fix andshortest_patha partial, independently-drifting copy. -
Fix:
explain, and the MCPget_neighbors/blast_radiustools, now warn when a label resolves to multiple equally-plausible nodes instead of silently picking the first match.pathgot this ambiguity check earlier, but it was written directly againstpath's own BM25-scored candidate list and never ported to the tier-based resolver (_find_node/_find_node_core) thatexplainand these two MCP tools share - so the same duplicate-label bug kept resurfacing in every callerpathdidn't touch. Found live re-running this exact class of bug against two independent real projects: My-Investment-Port'scalcPaperPortfolioValue()(two divergent implementations,paperPricingService.jsandinvestmentDecisionUtils.js- the one a prior QA audit had missed) and Home-Assistant'sDecisiondoc heading (repeated acrossliving-room-comfort-fallback.mdandpower-outage-scheduler.md) -explainreturned one of the two with zero indication a second existed. Fixed at the shared resolver: added_find_node_tiers/_find_node_tied_group(graphify/query.py) so any caller can ask "is this label ambiguous" without duplicating precedence-tier logic, then wired the warning into all three call sites._tool_get_node(a separate, older substring-only resolver) and the MCPshortest_pathtool (already has a partial score-based warning, but notpath's full retry-every-candidate-pair behavior) are known remaining gaps, left alone this round to avoid changing matching semantics beyond the reported bug. -
Fix:
path's hub-avoidance now also excludes intermediate nodes whose degree is at or above the graph's own 95th percentile (floor 10), in addition to the existing_BUILTIN_NOISE_LABELS/module-anchor label match. The label list only catches a primitive type someone already added for a specific language - never a project-specific-but-generic type (the real bug:Sendablematched the label list,CopyModeMatch- degree 14 - never could, since it's not a known-language primitive name anywhere). Measured stable across 3 real, differently-sized graphs (p95 ~11-14 despite 3k-12k nodes each) before landing. This is the language-agnostic complement: works for a language never added to the noise list, including ones that don't exist yet. -
Docs: every always-on skill instruction file (all 6 platforms) now tells the agent to actually use the existing
save-result/reflectfeedback loop - the mechanism (log useful/dead_end/corrected outcomes, aggregate intographify-out/reflections/LESSONS.md) already existed but wasn't referenced anywhere in the recommended workflow, so nothing used it. This is the closest thing graphify has to a self-correcting fix for vocabulary-mismatch-style dead ends: once an agent records that phrasing X means node Y, a future session doesn't have to re-derive it - but only if agents actually call it, which they now have a documented reason to. -
Feat:
querygained--path P(only consider nodes under P) and--exclude-path P(drop nodes under P), both repeatable, also exposed on thequery_graphMCP tool asinclude_paths/exclude_paths. Lets a caller who already knows a question is "find the code" vs. "what does our research say" scope a single query, instead of graphify guessing intent or the project needing a blanket.graphifyignorechange that would also break the queries the excluded directory is legitimately good for. Verified on a real repo where a largekb/research corpus was correctly kept in scope by design (per that repo's own.graphifyignore) but crowded out real business logic for a codebase question:--exclude-path kb/surfaces the real function directly. -
Fix:
pathno longer routes through generic/protocol-conformance or builtin-primitive hub nodes (e.g.Int -> Sendable) just because they happen to shorten the raw hop count - shortest_path is now degree-weighted and, on a first pass, excludes builtin-primitive-labeled and module/namespace-anchor nodes as intermediate hops entirely, falling back to the unweighted/unexcluded full graph (with a note) only if no path exists otherwise. A broad survey of real call-graph tooling (Go's callgraph, JetBrains' Call Graph plugin) found most real tools sidestep this by not offering arbitrary A-to-B shortest-path at all; graphify keeps the feature and fixes it with data already on hand (node degree, existing noise labels). Verified on the real Swift codebase that surfaced the bug: a path that used to renderInt -> CopyModeMatch -> Sendablenow renders the real 10-hop call chain with zero generic hubs. -
Fix:
pathnow tries every near-tied candidate on each side (within 10% of the top score) before reporting no path found, instead of only the single arbitrary top-of-tie pick. Found dogfooding a bilingual content repo: a duplicate section heading ("Stats") repeated across parallel English/Japanese files meant the wrong tied candidate could be picked, silently reporting "no path found" even when a different tied candidate WAS reachable. The ambiguity warning...
v0.15.0
- Feat:
query/explainoutput is now ranked by (hop distance from the seed, BM25 relevance, degree) instead of raw degree alone. Previously the non-seed portion of every query's output was sorted purely by node degree, so the token budget filled with whichever hubs happened to have the most edges rather than the nodes actually relevant to the question - measured live on graphify's own graph:"how does BM25 scoring pick seed nodes"used to lead withmain()/a Rust test fixture/generic hubs, now leads with_bm25_idf()/_pick_seeds()/_score_nodes()/_get_bm25_corpus(), the functions actually being asked about._bfs/_dfsreturn shapes are unchanged (a new_hop_distances()derives proximity from the edges they already return), so this is purely additive. - Feat:
explaingained--context C(repeatable), reusingquery's existing context-filter/normalization logic, so a node's connections can be narrowed to one edge kind (e.g.import) the same wayqueryalready supports. - Feat: new
cross_cutting_nodes()ranks by how many DISTINCT communities a node's neighbors span, not raw fan-in - a second axis alongside the existing degree-based god-node ranking. Answers a real, previously-unresolved question: a widely-used utility/config file (e.g. a color helper imported by 200 UI components, all in one community) can out-rank a genuine cross-area architectural coupler on the degree-only list despite touching only one part of the codebase. New "Cross-Cutting Nodes" report section. - Feat: god-node ranking now excludes a broader cross-language builtin/primitive denylist -
Int/UInt32/Double/String/Bool/etc. (Swift/Kotlin/Rust/Go/C#), extending the existing Python-only lowercase list. A non-Python project's #1 god node was a language primitive (Intat 927 edges on a real Swift codebase) outranking every real class, the same failure class 0.12's module/namespace-anchor fix (Foundation/HarnessCorecollapsing to a single over-connected node) addressed for import anchors rather than primitive types. - Feat: god-node ranking also excludes common
package.jsonboilerplate keys beyond the existingdependencies/devDependenciesblock (scripts,main,bin,engines,keywords,author,license,repository,private,workspaces) - same noise class, just missing entries. Verified against a real project: ascriptskey from two separatepackage.jsonfiles dropped out of the god-node list. - Feat: new
documents_bug_inextraction relation (source = a known-issue doc naming a concrete symptom/root cause, target = the file/symbol it documents) plus a[documents known bug]report tag and asurprising_connectionsscoring bonus, so a recorded bug can be looked up from the graph instead of grepped for by hand. Wired and unit-tested; not yet run against a real LLM extraction pass in this cycle. - Feat (opt-in): local, offline embedding-based seed fallback - tried only after BM25 + typo-correction + fuzzy-substring matching all find nothing, the deliberate recall gap for a query and its target sharing zero literal or synonym-map terms. New
embeddingsextra (sentence-transformers); returns nothing and has zero effect when the extra isn't installed. A hosted embedding API was rejected for this tool previously on infra/API-key-dependency grounds - a local model doesn't carry that dependency, so this revisits the same gap under a narrower, no-network constraint. Wired and unit-tested via mocked embeddings; not yet run against a real model. - Fix: aligned
query.py's token-budget estimate tollm.py's existing_CHARS_PER_TOKEN = 4(was a separate, inconsistent* 3in the query renderer) - a "2000-token budget" was only ever filling roughly 1,500 real tokens of query output. - Fix: a node with neither a
source_filenor asource_location(a pure concept/doc node) now renders ananchor=<neighbor file>hint from its highest-degree neighbor that has a real file, instead of being a dead end once a query resolves to it. Concept nodes are also now down-weighted (not excluded) at seed-selection time so a near-tied AST symbol - one with a line to open - wins the top seed slot; a concept still wins outright when it's the clearly better match. - Fix: extraction now skips files literally named
GRAPH_REPORT.md/GRAPH_SUMMARY.mdby default, extending the existing "never treat graphify's own output as source input" rule to loose copies sitting outside agraphify-out/directory (e.g. a demo/reference corpus). Found via this repo's ownworked/demo corpora: report headings like"Communities (141 total, 52 thin omitted)"were getting re-ingested as if they were real project documentation. - Feat:
graphify.toml/[tool.graphify]inpyproject.tomlnow sets project-local defaults (resolution, exclude_hubs, mode, wiki, no_viz, rank_by, etc.), precedencegraphify.toml>pyproject.toml> CLI. Newupdate --all/update-allbatch command refreshes every project under a path (or the global manifest), skipping clean repos via git HEAD vs.graph.json'sbuilt_at_commit.--wiki/--no-vizare now also available directly underextract. - Fix: import/module anchor nodes (e.g.
import Foundationcollapsing to one shared node referenced by every importing file) are excluded from god-node ranking - confirmed on a real Swift codebase whereFoundation/HarnessCoretopped the list ahead of the app's own core classes. - Refactor:
extract_apexmoved toextractors/apex.py(verbatim, byte-identical span), continuing theextractors/MIGRATION.mdplaybook. Backfilledtests/test_extractors_registry.pyfor the 4 previously-migrated languages (blade, zig, elixir, razor) that the playbook referenced but never actually created.
v0.14.0
v0.12.0
- Merged upstream through
safishamsi/graphify@v8's 0.9.5 (see that section below) into this fork'smain. One real overlap with this fork's own P1/P9 query-pipeline work: upstream independently fixed the same "question words dominate BFS seeding" bug this fork's P1 reopen already addressed, with a broader stopword list (why/when/where/has/have/work/works/working/etc., not in this fork's own list) and a fallback-to-unfiltered behavior for all-stopword queries ("how does it work"now seeds onhow/does/workinstead of nothing) that this fork's_query_termsdidn't have. Merged both stopword lists into one ingraphify/query.py's_STOPWORDSand ported the fallback logic in; this fork's version lives inquery.py(notserve.py, where upstream's fix landed) since P1's session already relocated the whole query pipeline there —serve.py's conflicting standalone copy of the pre-BM25 scoring logic was discarded in favor of this fork's BM25 rewrite, which already supersedes it.
v0.11.1
- Fix: critical — every language extractor added in 0.11.0 (YAML, Robot Framework, CSS, SCSS, HTML, Gherkin, TOML, fish, plus the
.gs/.resource/.hookdispatch wins) was unreachable from the realgraphify extract/graphify updateCLI commands.graphify/extract.py's dispatch table andgraphify/detect.py'sCODE_EXTENSIONSallowlist (which the file-discovery walker actually uses to decide what's worth extracting) are separate, and only the former was updated. Files with the 10 fully-new extensions were classified asNone(invisible to every pipeline, silently dropped);.yaml/.yml/.htmlwere classifiedFileType.DOCUMENTand routed to the LLM-backed semantic path instead of their new free local AST extractors. Every prior real-project validation this session calledextract()directly, bypassingdetect(), which is why this wasn't caught until re-validating through the actualdetect()->code_filespath. Fixed by adding the missing extensions toCODE_EXTENSIONS(watch.py/analyze.pyboth import it fromdetect.py, so the fix propagates automatically — no duplicate allowlists elsewhere). Re-validated end-to-end through the real pipeline this time: harness-terminal now correctly detects 643 code files including 12.robot/1.resource/2.fish/1.tomlthatdetect()previously excluded entirely.
v0.11.0
- Feat:
.kiro.hookfiles (Kiro Autopilot automation hooks — plain JSON with a distinctive filename suffix) now dispatch toextract_jsoninstead of silently producing 0 nodes as unrecognized data JSON. Found via a coverage sweep across all 9 personal projects (not just the one work project checked previously): 16 real files acrossHome-AssistantandMy-Investment-Port. Every one of the 9 personal projects now extracts with 0 errors using the current dispatch table (9arm-skillsthroughMy-Investment-Port's ~1839 files). Also triaged and explicitly rejected a batch of raw extension hits found along the way:.golden(terminal-reflow test fixtures — content has no structure worth extracting, the test name lives in the filename only),.strings(0 real git-tracked files — every raw hit was inside a compiled.appbundle, and the content itself was a binary compiled plist, not source),.plist(only 1 real file across all 9 projects, below the bar TOML/fish cleared), and Home-Assistant's.storage/*.iids/.aids/.state(git-tracked but pure generated HomeKit runtime state, not something a query would ever target). - Feat:
.tomland.fishfiles are now extracted into the graph. A wider filesystem search (beyond the narrower scope an earlier pass in this session used) found real, actively-maintained files for both — config TOML (starship.toml, multiplecliff.toml) and hand-written fish scripts with genuine function definitions — reversing an earlier low-real-file-count rejection.extract_toml(new hard deptree-sitter-toml) turns each[table]/[[array_of_tables]]header and root-level key into a node.extract_fishhas no published tree-sitter grammar (checked), so it's a hand-rolledfunction <name>line scanner, same approach as Gherkin. Also validated the full pipeline end-to-end against a real work project for the first time this session: 198 files, 0 extraction errors, and natural-language queries like"submit order flow"correctly seed the realsubmitOrderFlow()/submitEditFlow()/submitSmartFlow()functions. - Feat: lightweight query synonym expansion — a query and the code it's about can use different words for the same concept ("log the user in" vs.
authenticate) with zero literal terms in common, which BM25 can never bridge. Evaluated full embedding search for this gap and rejected it again (infra cost, network/API-key dependency for a local CLI/MCP tool, presented as an explicit choice to the user who picked this option) in favor of a small curated synonym map (_SYNONYM_GROUPS/_PHRASE_SYNONYMSingraphify/query.py) riding the existing BM25 pipeline as ordinary extra terms — no new dependency. Handles both single-token synonyms (delete/remove/erase) and separable phrasal verbs whose particle is a filtered stopword (log the user in-> addsauthenticate/login, matched with a bounded word-gap regex against the raw question). Ceiling: only helps pairs actually in the map, not open-ended concept similarity. - Feat:
.scssand.feature(Gherkin) files are now extracted into the graph..scssuses the separatetree-sitter-scssgrammar (optionalscssextra) since plain CSS's grammar errors on SCSS variables/nesting; shares its node-walking logic withextract_cssvia a common_extract_stylesheethelper..featurehas no published tree-sitter grammar, soextract_gherkinis a small hand-rolled line scanner (no new dependency) —Feature:/Scenario:/Scenario Outline:/Background:become nodes, step lines don't. Real local file count for both is 0 (checked, including re-verifying an earlier "1 real SCSS file" that turned out to be a vendoredcoveragepackage template) — built anyway per explicit request, validated against constructed fixtures instead of a real corpus. - Feat: Robot Framework
Resourceimports now resolve across files.extract_robotemits animports_fromedge for eachResource path.resourcesetting, and a keyword call not defined in the current file is deferred to the shared cross-filecallsresolver instead of being silently dropped..robot/.resourcealso added to the case-insensitive-language fold set (Robot Framework keyword names are case-insensitive by spec, same mechanism as Graphify-Labs#1581's PHP/SQL/Nim fold). Real-file validation on harness-terminal's actual test suite: 9imports_fromedges, 53 real cross-filecallsedges into shared keywords that previously had zero graph representation at all — closes the same-file-only limitation P6 originally scoped out. - Feat:
.cssand.htmlfiles are now extracted into the graph (newextract_css/extract_html,graphify/extractors/css.py/html.py). CSS: each rule set becomes a node labeled by its selector (.btn-primary,#header .nav-item); rules nested inside@media/@supports/@keyframesget a distinct node from a same-selector rule at the top level. HTML: deliberately scoped to elements with anidattribute only (the ones a developer actually references and searches for by name — extracting every element would be pure noise). Also:.resource(Robot Framework resource/library files) now dispatches to the same extractor as.robot, and.gs(Google Apps Script) dispatches to the JS extractor — both zero-new-code wins since they're the same underlying syntax as an already-supported format. - Feat:
.robot(Robot Framework) files are now extracted into the graph (newextract_robot,graphify/extractors/robot.py, optionalrobotextra) — previously entire QA test suites contributed zero nodes. Each test case and user-defined keyword becomes a node labeled by its real name (e.g."Bug 1 - Browser Pane Reuse On Rebuild"), and a test/keyword calling another locally-defined keyword gets acallsedge (calls to library/built-in keywords are dropped rather than fabricating phantom nodes). Real-file validation: 12 real.robotfiles in a QA-heavy project went from 0 to 131 nodes; a natural-language query correctly reaches a specific test case through the file->testcontainsedge. - Feat:
.yaml/.ymlfiles are now extracted into the graph (newextract_yaml,graphify/extractors/yaml_.py) — previously YAML had no extractor at all, so YAML-heavy projects (Home Assistant automations/scripts, CI configs, Kubernetes manifests) contributed zero nodes regardless of how much real logic lived there. Structural extraction: each top-level key becomes a node, each list item or nested mapping key under it becomes a child node labeled by analias/name/id/descriptionfield when present. Confirmed on a real repo: a file that previously produced 0 nodes now surfaces its content by natural-language query with no changes needed to the search/ranking code. - Fix: replaced
_score_nodes's hand-rolled three-tier scoring (exact=1000x/prefix=100x/substring=1x fixed multipliers) with real Okapi BM25 (term-frequency saturation + document-length normalization), and added a stopword filter to_query_terms. The old tier system was a weaker approximation of what BM25 already solves: no saturation meant one coincidental single-word exact match could outscore a multi-term broad match by 6-7x regardless of how many other words it actually covered, and stopwords (how/does/the/to) got scored as real query terms with no signal value. Root-caused against harness-terminal's real 14,838-node graph: querying"how does the daemon forward browser requests to the GUI"had.forwardBrowserRequest()stuck at rank 10 even after the original multi-term seed-ranking fix and its first proposed follow-up (both landed, neither touched the actual defect) — it now ranks #1._pick_seeds'sgap_ratiodefault raised from 0.2 to 0.8 alongside this, since it was calibrated against the old tier system's huge cliffs and let unrelated nodes through BM25's much smoother score curve; verified against 3 other real natural-language queries on the same graph, not just the one reproduction case. - Feat: Playwright/Jest
test(...)/it(...)/describe(...)calls in.js/.tsspec files now become their own graph nodes (label = the raw description string), instead of being invisible. Previously a spec.ts file only extracted the file node and any top-level helper functions — individual test cases had no node at all, so no amount of search improvement could find one. Nesteddescribe(() => { it(...) })blocks are captured too. Real-file validation: a spec file with 13 test cases went from 2 extracted nodes (file + one helper) to 15; a natural-language query for one of those test cases now ranks it #1. - Feat: typo/abbreviation recovery now also handles typos of compound spans (e.g.
wholesalsfor two adjacent camelCase wordsWholeSales) — previously the vocabulary only held individual sub-words and each label's full concatenated form, missing the middle ground. Adds bounded 2-/3-token n-gram spans to the typo-correction vocabulary, plus a Bitap/agrep-style approximate-substring search (no pre-enumerated vocabulary needed) as a last-resort tier for spans longer than that window, marked with an explicit low-confidence note. - Feat:
query_graph/get_node/get_neighbors/shortest_pathnow recover from typos and abbreviations when the primary lexical search finds nothing. A failing query term is spell-corrected against the graph's own identifier vocabulary (Damerau-Levenshtein for typos likesesion->sessionorrecieve->receive; ordered-subsequence matching for abbreviations likehus->handleUserSession) and the corrected term is re-run through the normal, unmodified scoring pipeline — not a second scoring algorithm, so it inherits IDF weighting and camelCase sub-word matching automatically. Only triggers when the uncorrected query returns zero results, so a query that already matches pays no extra cost. The response always states when a correction was applied (Note: no exact match; corrected possible typo(s) "sesion" -> "session") rather than silently substi...