Skip to content

Releases: Vit129/graphify

v0.19.0

Choose a tag to compare

@Vit129 Vit129 released this 19 Jul 12:37

0.19.0 (2026-07-19)

  • Fix (#16): affected --relation R did exact set-membership on edge relation — a bare filter like shares_value could never match P15's parameterized shares_value:<value> edge labels, since the caller can't know the exact value in advance. A bare filter (no :) now also prefix-matches relation:* 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 opaque NAME node. _is_content_data_array gates narrowly, mirroring the JSON extractor's _is_config_json shape-probe (Graphify-Labs#1224's lesson: don't AST-walk arbitrary data): 3+ sibling object elements, 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 #1077 module-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

Choose a tag to compare

@Vit129 Vit129 released this 19 Jul 04:06

0.18.0 (2026-07-19)

  • Feature: a fourth graph.html view lens, "Calls" — alongside Community/File/Dependencies. Unlike Community (colors by inferred Leiden cluster, which mixes short code-symbol labels with long prose concept/rationale node labels from the LLM extraction pass) and Dependencies (collapses to one node per file), Calls stays at per-symbol granularity but hides every non-code file_type node (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's graphify query. (1) It ignored the active lens — a hidden concept/rationale node in the new Calls lens could still surface as a search result, and clicking it silently panned the camera to nothing (isNodeHidden is 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's query.py _tokenize, ported to JS) so e.g. searching "notification" finds handleNotification. (3) source_file is 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 PostToolUse hook auto-triggers a debounced background graphify update after every agent Edit/Write/MultiEdit, closing the one real gap in graphify's auto-sync story — graphify watch (a real watchdog daemon) must be started by hand, and the git post-commit/post-checkout hooks (graphify hook install) only fire on commit, so neither covered the uncommitted-edit window an agent session actually lives in. graphify/watch.py's new trigger_background_update() spawns [sys.executable, "-c", ..., project_root, *changed_paths] fully detached (POSIX start_new_session, Windows DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP, mirroring hooks.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 existing PreToolUse hooks by graphify install/graphify claude install_install_claude_hook's own printed message already claimed this happened; it's now actually true.
  • Feature: opt-in pagerank_ranking in graphify.toml (default false, same rollout shape as value_coupling/P15) precomputes nx.pagerank(G) once at build time (update, update --all, and the standalone extract command all wired; cluster-only needs no change, since it re-clusters an existing graph without altering structure) and stores it as a pagerank node attribute in graph.json. query/explain/path's _pick_seeds composes 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 everywhere scipy (an optional dependency, not bundled in the all extras) 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

Choose a tag to compare

@Vit129 Vit129 released this 18 Jul 14:28

0.17.0 (2026-07-18)

  • Feature: .html/.htm extraction now also runs a JS pass over inline <script> block bodies (graphify/extract.py's extract_html_with_scripts, reusing the Vue SFC extractor's _vue_mask_non_script blank-and-preserve-newlines technique — that regex was already generic HTML script-tag matching, not Vue-specific). Previously only elements with an id attribute became nodes; a function/class defined inside a <script> block (the common single-file-app pattern) was invisible to the graph entirely — graphify explain/query could 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_core in graphify/query.py) could return "No node matching" for a real, unambiguous node whose name spans two fields — e.g. ClassName.methodName where the class lives in source_file and the method is the label — 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 ranking query/path already 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_group ambiguity warning instead of a silent pick.
  • Fix: graphify affected (graphify/affected.py's resolve_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 kept resolve_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 returns None, 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.py compares this checkout's CURRENT_VERSION against version.json on this repo's main branch and, only on a clean working tree in an interactive terminal, asks y/N before running git 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-dismissed so 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 into graph.json. Ported upstream's _resolves_under_root() containment check into both the directory-walk pruning and the per-file filter, in extract.py and detect.py. This fork's own _auto_follow_symlinks auto-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-language imports_from edge (e.g. tailwindcss/colors no longer aliases onto an unrelated backend/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 unrelated Cache.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 existing resolver_registry framework 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

Choose a tag to compare

@Vit129 Vit129 released this 04 Jul 07:46

0.16.0 (2026-07-04)

  • Fix (P15 follow-up): graphify update was silently emitting zero shares_value edges even with value_coupling = true in graphify.toml, despite extract(value_coupling=True) working correctly in isolation. Two bugs, both invisible to the full test suite because every existing test called _rebuild_code with acquire_lock=False: (1) coupling-edge endpoints were resolved to the file-node id captured at YAML-extraction time, but a later extract() 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 by source_file instead. (2) _rebuild_code(acquire_lock=True) (the real default update uses) takes a lock then recurses into itself, and that recursive call's hand-listed forwarded kwargs had never been updated to include value_coupling - it silently reverted to False regardless of what the caller passed. Added a regression test that specifically calls _rebuild_code with acquire_lock=True (not the False every 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/service string 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). With graphify.toml value_coupling = true or extract --value-coupling (off by default, never implicit), two files sharing a domain.entity reference get a low-confidence shares_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 like fan.turn_on and Lovelace UI-display co-occurrence dominated the noise); adding two filters - exclude domain.<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-unconnected bedroom_ac_management ↔ home_mode pair via one shares_value:input_boolean.home_mode hop; +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): path and explain gained source_file-prefix scoping flags to resolve a duplicate label deterministically instead of only being warned it's ambiguous. path takes --path P (narrows both endpoints), --source-path P/--target-path P (narrow each independently, for when both labels are duplicated in different dirs); explain takes --path P. Mirrors query's existing --path/--exclude-path convention (source_file.startswith(P)). Verified live on the three real duplicate cases that motivated it: Language-Learning Stats (--path japanese-practice/ vs english-practice/), My-Investment-Port calcPaperPortfolioValue (--path .../paperPricingService.js picks the implementation a prior QA audit missed), and harness-terminal handleNotification (--target-path .../NotificationCoordinator.swift clears the target ambiguity while leaving an unscoped source correctly still flagged). Additive: no --path given = identical behavior to before. The tie-warning already tells the user the flag exists as the escape hatch. Phase 2 (file:Label syntax 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_radius fix above are closed too. (1) _tool_get_node used its own standalone lowercase-substring-anywhere scan (matched 16 nodes for a query like "context" on a real graph, including unrelated ones like ContextualLogger, with zero ambiguity signal) - now shares the same tiered resolver and warning as the other three. (2) _tool_shortest_path (the MCP shortest_path tool) 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 CLI path's full resolution logic - near-tied-candidate retry across every (source, target) pair, plus the degree/label-based hub-avoidance - into a single shared find_path_with_disambiguation (graphify/query.py) that both path and shortest_path now call, instead of path having the complete fix and shortest_path a partial, independently-drifting copy.

  • Fix: explain, and the MCP get_neighbors/blast_radius tools, now warn when a label resolves to multiple equally-plausible nodes instead of silently picking the first match. path got this ambiguity check earlier, but it was written directly against path's own BM25-scored candidate list and never ported to the tier-based resolver (_find_node/_find_node_core) that explain and these two MCP tools share - so the same duplicate-label bug kept resurfacing in every caller path didn't touch. Found live re-running this exact class of bug against two independent real projects: My-Investment-Port's calcPaperPortfolioValue() (two divergent implementations, paperPricingService.js and investmentDecisionUtils.js - the one a prior QA audit had missed) and Home-Assistant's Decision doc heading (repeated across living-room-comfort-fallback.md and power-outage-scheduler.md) - explain returned 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 MCP shortest_path tool (already has a partial score-based warning, but not path'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: Sendable matched 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/reflect feedback loop - the mechanism (log useful/dead_end/corrected outcomes, aggregate into graphify-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: query gained --path P (only consider nodes under P) and --exclude-path P (drop nodes under P), both repeatable, also exposed on the query_graph MCP tool as include_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 .graphifyignore change that would also break the queries the excluded directory is legitimately good for. Verified on a real repo where a large kb/ 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: path no 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 render Int -> CopyModeMatch -> Sendable now renders the real 10-hop call chain with zero generic hubs.

  • Fix: path now 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...

Read more

v0.15.0

Choose a tag to compare

@Vit129 Vit129 released this 03 Jul 10:36
  • Feat: query/explain output 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 with main()/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/_dfs return shapes are unchanged (a new _hop_distances() derives proximity from the edges they already return), so this is purely additive.
  • Feat: explain gained --context C (repeatable), reusing query's existing context-filter/normalization logic, so a node's connections can be narrowed to one edge kind (e.g. import) the same way query already 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 (Int at 927 edges on a real Swift codebase) outranking every real class, the same failure class 0.12's module/namespace-anchor fix (Foundation/HarnessCore collapsing to a single over-connected node) addressed for import anchors rather than primitive types.
  • Feat: god-node ranking also excludes common package.json boilerplate keys beyond the existing dependencies/devDependencies block (scripts, main, bin, engines, keywords, author, license, repository, private, workspaces) - same noise class, just missing entries. Verified against a real project: a scripts key from two separate package.json files dropped out of the god-node list.
  • Feat: new documents_bug_in extraction 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 a surprising_connections scoring 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 embeddings extra (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 to llm.py's existing _CHARS_PER_TOKEN = 4 (was a separate, inconsistent * 3 in 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_file nor a source_location (a pure concept/doc node) now renders an anchor=<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.md by default, extending the existing "never treat graphify's own output as source input" rule to loose copies sitting outside a graphify-out/ directory (e.g. a demo/reference corpus). Found via this repo's own worked/ 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] in pyproject.toml now sets project-local defaults (resolution, exclude_hubs, mode, wiki, no_viz, rank_by, etc.), precedence graphify.toml > pyproject.toml > CLI. New update --all/update-all batch command refreshes every project under a path (or the global manifest), skipping clean repos via git HEAD vs. graph.json's built_at_commit. --wiki/--no-viz are now also available directly under extract.
  • Fix: import/module anchor nodes (e.g. import Foundation collapsing to one shared node referenced by every importing file) are excluded from god-node ranking - confirmed on a real Swift codebase where Foundation/HarnessCore topped the list ahead of the app's own core classes.
  • Refactor: extract_apex moved to extractors/apex.py (verbatim, byte-identical span), continuing the extractors/MIGRATION.md playbook. Backfilled tests/test_extractors_registry.py for the 4 previously-migrated languages (blade, zig, elixir, razor) that the playbook referenced but never actually created.

v0.14.0

Choose a tag to compare

@Vit129 Vit129 released this 02 Jul 15:43

feat: Obsidian-like settings control panel, auto-open, and live reload

v0.12.0

Choose a tag to compare

@Vit129 Vit129 released this 02 Jul 13:18
  • Merged upstream through safishamsi/graphify@v8's 0.9.5 (see that section below) into this fork's main. 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 on how/does/work instead of nothing) that this fork's _query_terms didn't have. Merged both stopword lists into one in graphify/query.py's _STOPWORDS and ported the fallback logic in; this fork's version lives in query.py (not serve.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

Choose a tag to compare

@Vit129 Vit129 released this 02 Jul 12:41
  • Fix: critical — every language extractor added in 0.11.0 (YAML, Robot Framework, CSS, SCSS, HTML, Gherkin, TOML, fish, plus the .gs/.resource/.hook dispatch wins) was unreachable from the real graphify extract/graphify update CLI commands. graphify/extract.py's dispatch table and graphify/detect.py's CODE_EXTENSIONS allowlist (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 as None (invisible to every pipeline, silently dropped); .yaml/.yml/.html were classified FileType.DOCUMENT and routed to the LLM-backed semantic path instead of their new free local AST extractors. Every prior real-project validation this session called extract() directly, bypassing detect(), which is why this wasn't caught until re-validating through the actual detect() -> code_files path. Fixed by adding the missing extensions to CODE_EXTENSIONS (watch.py/analyze.py both import it from detect.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 .toml that detect() previously excluded entirely.

v0.11.0

Choose a tag to compare

@Vit129 Vit129 released this 02 Jul 12:09
  • Feat: .kiro.hook files (Kiro Autopilot automation hooks — plain JSON with a distinctive filename suffix) now dispatch to extract_json instead 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 across Home-Assistant and My-Investment-Port. Every one of the 9 personal projects now extracts with 0 errors using the current dispatch table (9arm-skills through My-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 .app bundle, 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: .toml and .fish files 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, multiple cliff.toml) and hand-written fish scripts with genuine function definitions — reversing an earlier low-real-file-count rejection. extract_toml (new hard dep tree-sitter-toml) turns each [table]/[[array_of_tables]] header and root-level key into a node. extract_fish has no published tree-sitter grammar (checked), so it's a hand-rolled function <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 real submitOrderFlow()/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_SYNONYMS in graphify/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 -> adds authenticate/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: .scss and .feature (Gherkin) files are now extracted into the graph. .scss uses the separate tree-sitter-scss grammar (optional scss extra) since plain CSS's grammar errors on SCSS variables/nesting; shares its node-walking logic with extract_css via a common _extract_stylesheet helper. .feature has no published tree-sitter grammar, so extract_gherkin is 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 vendored coverage package template) — built anyway per explicit request, validated against constructed fixtures instead of a real corpus.
  • Feat: Robot Framework Resource imports now resolve across files. extract_robot emits an imports_from edge for each Resource path.resource setting, and a keyword call not defined in the current file is deferred to the shared cross-file calls resolver instead of being silently dropped. .robot/.resource also 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: 9 imports_from edges, 53 real cross-file calls edges into shared keywords that previously had zero graph representation at all — closes the same-file-only limitation P6 originally scoped out.
  • Feat: .css and .html files are now extracted into the graph (new extract_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/@keyframes get a distinct node from a same-selector rule at the top level. HTML: deliberately scoped to elements with an id attribute 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 (new extract_robot, graphify/extractors/robot.py, optional robot extra) — 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 a calls edge (calls to library/built-in keywords are dropped rather than fabricating phantom nodes). Real-file validation: 12 real .robot files in a QA-heavy project went from 0 to 131 nodes; a natural-language query correctly reaches a specific test case through the file->test contains edge.
  • Feat: .yaml/.yml files are now extracted into the graph (new extract_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 an alias/name/id/description field 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's gap_ratio default 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/.ts spec 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. Nested describe(() => { 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. wholesals for two adjacent camelCase words WholeSales) — 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_path now 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 like sesion->session or recieve->receive; ordered-subsequence matching for abbreviations like hus->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...
Read more