888 Commits
Author SHA1 Message Date
hopstreax ae074b2122 fix(extract): rescan code with --force --code-only (#3125)
--force --code-only over an existing graph set incremental_mode, so unchanged code files
were skipped and stale alias/import resolution persisted. Separate scan-mode from
graph-merge: --force --code-only now full-scans all code (via a new merge_existing_graph
gate) while still carrying the existing semantic tier forward (preserving the #2923 fix) and
still pruning deleted/excluded files. Other flag combinations are unchanged.
2026-08-28 01:20:57 +01:00
abhay-codes07 a8852e8740 fix(extract): hollow, unparseable and omitting chunks count as incomplete (#3105)
The #479 shrink guard is disarmed when a build is known-incomplete, but a run that silently
lost content to a hollow, unparseable, or file-omitting chunk was treated as complete, so it
could overwrite the existing graph with a smaller one. Mark those cases incomplete (uncovered
files or partial-semantic files) so the guard stays armed and refuses the shrink; a complete
run is untouched, a #2880-retry-recovered chunk does not count, and --allow-partial still
overrides.
2026-08-28 01:20:27 +01:00
abhay-codes07 3378ae8fef fix(build): route carried hyperedges through the dedup survivor remap (#3102)
A hyperedge carried from a prior graph.json kept its original member ids, so when dedup
merged one of those members away the hyperedge dangled. Feed carried hyperedges into build()
on the base chunk so deduplicate_entities remaps their members onto the survivor at the same
time as edge endpoints; a member that resolves to nothing is dropped (the whole hyperedge if
none survive), and a hyperedge re-emitted by a new chunk is not duplicated. Materialise
new_chunks so the added second iteration is safe.
2026-08-28 01:20:10 +01:00
safishamsi bad832f225 test(cluster): pin partition invariance to edge-endpoint orientation (#3146)
Build the same logical undirected graph with forward vs flipped edge endpoints and shuffled
node insertion, and assert _partition yields identical communities — guarding the canonical
edge-ordering the determinism fix introduced.
2026-08-28 01:19:45 +01:00
abhay-codes07 c60ddf7b3e fix(cache): the atexit stat-index flush must not resurrect a deleted corpus (#2974)
The atexit stat-index flush did an unconditional mkdir(parents=True) on the cache path, so
if the corpus/output tree was deleted during the run the flush recreated the whole
graphify-out/cache/ chain as a husk. Skip the write when the index root no longer exists
(clearing the dirty flag so nothing retries); a live run still writes, and a cold start is
unaffected because the root exists before graphify-out/ does.
2026-08-28 01:18:04 +01:00
hopstreax 590258fe1b fix(ts): resolve new_expression constructor calls (#3116)
A TypeScript/JavaScript `new Foo()` emitted no edge. Fall back to the new_expression's
constructor child in the generic callee branch, so it emits a calls edge to the constructed
class (member, chained, and generic forms), reusing the existing built-in-global guard so
new Map()/new Promise() are not fabricated. Consistent with the C# object-creation handling
(#2997).
2026-08-27 20:57:16 +01:00
santhiprakash c66465c77e fix(elixir): extract functions whose only clause has a when guard (#3111)
A guarded function head (def foo(x) when is_integer(x), do: ...) nests the head call under a
binary_operator, so a def whose only clause carries a guard was not extracted. Unwrap the
guard to the head call (the head is always the first child, so the guard predicate is never
selected) and emit the real function name; multi-clause, multi-condition guards, and defp
are handled.
2026-08-27 20:56:15 +01:00
guitelesc e906227cf6 fix(commonlisp): derive node ids from the full path stem
The Common Lisp extractor minted its id prefix from the bare path.stem instead of the
canonical _file_stem used by every other extractor, so two same-basename .lisp files in
different directories collided (one file's nodes vanished on merge) and nested .lisp files
tripped a spurious legacy-id nudge. Use _make_id(_file_stem(path)) to match the shared
convention; cross-file rewire is unaffected (it matches on label, not id).
2026-08-27 20:20:36 +01:00
guitelesc 8d611dc4d0 test(falkordb): skip when the port answers but is not FalkorDB
The integration test guarded only on ping(), so a plain Redis (or an SSH-forwarded one) on
6379 passed the liveness check and then FAILED both tests on 'unknown command GRAPH.QUERY'.
Add a read-only GRAPH.LIST probe that identifies the service and skips with a distinct
message when the graph module is absent, so the test still runs (and can fail) against a
real FalkorDB but no longer breaks on a wrong service.
2026-08-27 20:20:16 +01:00
safishamsi 996c6add76 test(cluster): pin native-leiden vs wrapper equivalence and absent-binding fallback (#3104)
Guard the output-equivalence claim the perf change rests on: _partition via the direct
graspologic_native path must produce the same node groupings as the graspologic wrapper
(skipped unless both are installed), and _native_leiden must return None when the native
binding cannot be imported so the wrapper/Louvain fallback still runs.
2026-08-27 18:03:10 +01:00
rohit-jsfreaky 984ff116d3 fix(ruby): match a qualified receiver by its constant path (#3078)
A call on a qualified constant receiver (ActiveRecord::Base.transaction) truncated the
receiver to its last segment (Base) and bound to any lone corpus class named Base, a false
EXTRACTED edge. Keep the full constant path and suffix-match it against declared classes,
emitting an edge only on a single unambiguous hit; a leading :: pins a whole-path match.
Bare receivers are unchanged.
2026-08-25 18:12:02 +01:00
hopstreax b48005276e fix(ruby): preserve suffixed method node IDs (#3077)
normalize_id strips !/?/= from Ruby method names, so save and save! (or foo and foo=) minted
the same node id and the second was dropped as a seen_ids collision. Add a Ruby symbol
sanitizer (!->_bang, ?->_pred, =->_eq) applied to the id only (the label keeps the raw
spelling), and rekey method_index on the raw name so member-call resolution still matches.
Ids stay stable across incremental re-extraction.
2026-08-25 18:10:43 +01:00
safishamsi ef00185bb2 test(merge): pin same-order community-offset byte-reproducibility (#3014)
Document that the per-input community-id offset is deterministic for a fixed input order
(consumers rely on stable merged output), while noting the offsets are position-dependent
by design.
2026-08-25 18:09:58 +01:00
santhiprakash 4e2e23278a fix(build): offset community ids per input in merge-graphs (#3014)
merge-graphs kept each input's community ids as-is, so community 0 from repo A and community
0 from repo B fused into one meta-community spanning unrelated repos. Offset each input's
community ids past the running maximum so every input's communities stay globally distinct
after merge, preserving within-input structure and recording the original id in
local_community.
2026-08-25 18:09:25 +01:00
hopstreax ad7c7d58d1 fix: capture HOF-wrapped CommonJS exports (#3035)
A CommonJS export wrapped in a higher-order function (exports.x = wrap(fn),
module.exports.y = onCall({...}, handler)) was not captured, so the exported symbol was
missing. Reach through the wrapping call to the topmost function closure argument (mirroring
the const-export HOF path #2552/#2568); the wrapper stays a call edge, not the export's
identity, and a non-function arg fabricates nothing.
2026-08-25 18:07:55 +01:00
safishamsi 5cb6532c24 fix(windows): read .graphify_root BOM-tolerantly in the update-recovery path too (#3028)
Complete the read-side BOM fix: the graphify update (no-args) recovery path at cli.py read
.graphify_root with plain utf-8, so a PowerShell-written BOM would fail the exists() check
and abort with path not found. Switch it to utf-8-sig like the other five reader sites.
Also clean up two invalid-escape f-strings in the windows skillgen test.
2026-08-25 18:07:40 +01:00
rohit-jsfreaky 3c448660cd fix(windows): stop a PowerShell BOM breaking hook rebuilds (#3028)
Windows PowerShell 5.1 writes a UTF-8 BOM, so the .graphify_root marker it produced carried
a leading BOM that Python's utf-8 decode kept as U+FEFF; .strip() left it in, breaking the
post-commit rebuild (WinError 123) and silently mis-rooting _infer_merge_root. Write the
marker BOM-less from PowerShell (WriteAllText + UTF8Encoding $false) and read the marker
with utf-8-sig at the reader sites so already-broken installs recover too.
2026-08-25 18:06:46 +01:00
safishamsi d8abe52415 perf(detect): kill the globstar matcher's per-call cycle, bound the pattern cache (#2226)
Senior tweaks on the landed #3074:
- Lift the **-aware matcher out of the per-call @lru_cache closure (which referenced itself
  and leaked a reference cycle every call) into a module-level function with an explicit
  dict memo, plus a no-** fast path; port of the fix from the alternative #2845.
- Cap the process-global parsed-pattern cache so a long-lived watch across many repos cannot
  grow it without bound.
- Add a differential test pinning _lexical_relative against pathlib relative_to, and a
  gc test asserting the matcher leaves no cyclic garbage.
2026-08-25 15:52:10 +01:00
Azeem1985 258ab11db2 perf(detect): stop building pathlib objects per pattern per file in ignore evaluation (#2226)
Ignore evaluation built a Path and called relative_to (with its CPython quadratic parents
cost) per pattern per file, and re-parsed each pattern's flags every call. Replace it with a
lexical string-space relative computation (no Path construction, no exception control flow),
parse each pattern once into a process-wide cache, and memoize per-anchor relatives, segment
splits and is_dir per evaluation. Ignore decisions are unchanged (differential-fuzz
verified); ~50x faster on a pattern-heavy monorepo.
2026-08-25 15:46:44 +01:00
durmazoguzhan 3b12d5ce68 feat(typescript): emit a node per enum member (#3064)
A TypeScript enum's members got no graph node. Emit a member node per member with a case_of
edge to the enum (matching the C# handler), handling both member spellings
(property_identifier and enum_assignment), const enums, and unquoting a whole string member
name instead of one fragment; the member initializer is still walked so a nested expression
is not dropped.
2026-08-25 15:45:33 +01:00
durmazoguzhan 0e566345fc feat(csharp): emit a node per enum member (#3063)
A C# enum's members got no graph node, so an enum case was invisible. Emit a member node
per enum_member_declaration with a case_of edge to the enum (mirroring the Java/Kotlin/Swift
enum handlers), deduped against a case-only sibling collision; explicit and implicit values
both work and no builtin types are fabricated.
2026-08-25 15:45:06 +01:00
Azeem1985 62371ad28d fix(postgres): carry tree-sitter-sql in the postgres extra and surface grammar errors
The postgres introspection path synthesizes DDL and parses it with the SQL grammar, but
tree-sitter-sql was not in the postgres extra, so `pip install graphifyy[postgres]` left it
unable to parse and returned a silent empty result. Add tree-sitter-sql to the extra and
raise an actionable ImportError (with the install hint) when the grammar is missing or has
an incompatible ABI, instead of reporting zero nodes.
2026-08-25 15:44:52 +01:00
Azeem1985 b022c3469c fix(watch): ignore read-only inotify events so the watcher stops re-triggering on its own reads
On Linux, inotify emits opened / closed_no_write events for every file open and close,
including the watcher's own AST rebuild reading the tree, so the watcher fed itself a
rebuild loop. Drop those two read-only event types; close-after-write and
create/modify/move/delete still trigger, and the filter is a no-op on the macOS/Windows
backends that never emit them.
2026-08-25 15:44:45 +01:00
safishamsi 960b6f5a0b test(merge): pin that a shared type links even when the declarations drift (#3007)
Document the cross-repo join's precision boundary: the key is namespace+name by design,
not structural, so a shared contract type whose declarations have drifted across repos
still links (and repo-unique types do not). Guards against a future change that tried to
gate the edge on member equality.
2026-08-24 14:12:53 +01:00
durmazoguzhan c46b756cde feat(merge): link a type declaration two repos share (#3007)
When merging graphs from multiple repos, a type declared in more than one repo under the
same fully-qualified namespace and name (a shared contract type) now gets a same_type_as
edge linking the declarations, so a cross-repo contract is navigable. Matching requires a
non-empty namespace plus label, a real sourced type declaration (not a method/field or
sourceless stub), and at least two distinct repos, so two unrelated types that merely share
a short name are not linked; the edge is INFERRED/0.9.
2026-08-24 14:09:46 +01:00
durmazoguzhan 3ef625b3b6 feat(csharp): emit a member node per property (#3006)
C# properties got no graph node, so a property was invisible as a class member (only
fields and methods appeared). Emit a member node per property_declaration with the same id
scheme, ownership, and defines/field-context edge as the C++ data-member branch; all
property shapes (auto, expression-bodied, read-only) are covered, the #2913 property-type
references walk is preserved, and no builtin types are fabricated.
2026-08-24 14:08:25 +01:00
winesoft-namjin-yun da8ca1684c fix(extract): keep the definition site when a decl/def pair is merged (#2990)
When a C/C++/ObjC declaration (header prototype) and its definition (implementation body)
merge into one node, the survivor kept the declaration's site, so the node pointed at the
prototype instead of the body. Record the definition's file/location on the survivor and
surface it as a Defined in: line; the pick is deterministic regardless of file order, and a
declaration with no definition keeps its own site.
2026-08-24 14:07:48 +01:00
akshitj11 5a56bd6943 fix(bash): emit calls edges for x=$(fn) command substitution (#2978)
A command substitution on the right of an assignment (x=$(foo)) was not recorded as a
call, unlike a bare $(foo). Gate call emission on the ancestor chain: allow a call inside a
command_substitution when it sits under a variable_assignment, while still suppressing bare
and arg-position substitutions and process substitutions.
2026-08-24 14:07:07 +01:00
akshitj11 6a06b652cc fix(sql): extract CREATE TABLE inside BEGIN/COMMIT blocks (#2953)
DDL wrapped in a transaction parses under a `transaction` node the statement walker never
descended into, so a CREATE TABLE inside BEGIN/COMMIT was never extracted while a top-level
one was. Recurse into transaction blocks; nested statements get the same handling, no
double-emit.
2026-08-24 14:06:55 +01:00
durmazoguzhan 7f928bc1d0 feat(csharp): link interface methods to their single implementation (#3003)
A call through an injected interface dependency lands on the interface method, leaving the
implementation unreachable from the call site. When an interface has exactly one
implementing class, add a dispatches_to edge from each interface method to the matching
implementation method. Guarded to avoid false links: requires a real implements edge, a
single implementer, and a single case-sensitively same-named method; both ends must be C#;
0 or 2+ implementers emit nothing. Lives in graphify.csharp_dispatch as a registered
resolver.
2026-08-24 13:22:09 +01:00
durmazoguzhan 7f476e1447 fix(csharp): emit calls edges for object creation expressions (#2997)
A `new Foo()` object-creation expression emitted no edge, so constructor usage was invisible
in the graph. Add object_creation_expression to the C# call types and emit a calls edge to
the constructed type (member/qualified/generic forms), resolving a qualified construction
against declared namespaces without binding to an in-file placeholder. Built-in and
out-of-corpus types are not fabricated, and repeated constructions dedup to one edge.
2026-08-24 13:17:23 +01:00
rajatnagda45 be5110a789 fix(powershell): extract enum definitions and their members
A PowerShell enum definition and its members were not extracted, so a [Color] type
reference resolved to a sourceless phantom stub instead of the real enum. Emit the enum as
a real sourced node with its members as contained children (mirroring class_statement), so
references resolve to it; no builtins are fabricated.
2026-08-24 13:16:45 +01:00
rajatnagda45 46d3e5d5b8 fix(commonlisp): keep inherits edge when the superclass is in another file
A defclass whose superclass is defined in another file lost its inherits edge. Mint a
sourceless stub for the cross-file base (source_file empty, origin_file set) like the
import-stub path, so the corpus rewire collapses it onto the real definition and the
inherits edge survives; a same-file superclass still binds locally.
2026-08-24 13:16:39 +01:00
rajatnagda45 3c0eda3bdd fix(julia): extract abstract types declared with a supertype
An abstract type declared with a supertype (abstract type Dog <: Animal end) matched only
the bare-identifier form, so the subtyping declaration dropped the node entirely. Parse the
type_head shape shared with structs (factored into a helper) and emit the inherits edge; a
bare abstract type still works.
2026-08-24 13:15:31 +01:00
rajatnagda45 6930e4fa90 fix(zig): extract methods declared on enums and unions
Zig lets enums and unions carry methods like structs, but only the struct branch recursed
into the container body, so enum/union methods and the calls in their bodies were dropped.
Walk enum/union children the same way, parenting methods to the container with the same id
scheme; variants and fields are not turned into nodes.
2026-08-24 13:15:23 +01:00
abhay-codes07 4cbc8603dd fix(export): adopt a pre-manifest vault's own notes instead of orphaning them (#2863)
An Obsidian vault written before the ownership manifest existed had no record of which
notes graphify wrote, so on the next export those notes were orphaned. Seed ownership by
recognizing graphify's own notes (a graphify/ frontmatter tag for node notes; the
_COMMUNITY_ prefix plus the FROM #community/ Dataview marker for community notes) when the
manifest is absent, so the manifest starts out honest. The .obsidian config is not
adopted (Obsidian writes it too).
2026-08-21 19:22:21 +01:00
SinghAman21 245eb9dc9c fix(extract): scope fresh semantic results to dispatched files (#2926)
The #1757 guard scoped the semantic-cache WRITE to dispatched files, but the unfiltered
fresh result still fed build_merge, whose replace-set logic swapped a non-dispatched
file's entire prior contribution for a stray misattributed fragment (and logged the
'skipped out-of-scope source_file' warning). Apply the same allowlist to the result dict
before it reaches the merge, via a shared _semantic_source_matcher so the write guard and
the graph filter cannot drift.
2026-08-21 19:21:20 +01:00
hopstreax 24cf52d51e fix(cache): retry zero-node semantic results (#2927)
A semantic result with no nodes and no hyperedges (only edges, or nothing) was cached and
stamped into the manifest, so an empty/degenerate LLM reply for a file froze that file:
detect_incremental saw it unchanged and never re-dispatched it. Reject zero-node results
from the cache read and write, drop edges from the manifest stamp tuple, and heal an
existing manifest by re-queueing files that were already stamped with a zero-node result.
2026-08-21 18:26:49 +01:00
santhiprakash 5be36dac0d fix(extract): preserve existing semantic layer on --code-only --force (#2923)
A `--code-only --force` rebuild over an existing graph dropped the doc/paper/image
semantic tier, because force took the full-rebuild path and code-only never re-dispatched
those files. When an existing graph is present, keep incremental mode so build_merge
carries the semantic layer forward; files deleted from disk are still pruned.
2026-08-21 18:26:24 +01:00
santhiprakash 9452ef216b fix(hooks): resolve the uv tool venv when pin and launcher probes miss (#2852)
Installed via `uv tool install`, the post-commit hook could not find graphify: the pin and
launcher/shebang probes miss the uv tool venv, so the hook silently exit-0'd. Add a fourth
fallback probe that scans $UV_TOOL_DIR and the conventional uv tool locations for a python
whose env has graphify importable, firing only after the earlier probes miss and verifying
importability before adopting. Also gate the launcher read on a real #! header so a binary
trampoline is not misparsed as a shebang.
2026-08-21 18:26:11 +01:00
ousamabenyounes 037970159d fix(extract): stop a built-in base class from inheriting across languages (#2812)
A class extending a built-in base (PHP `extends \Exception`) emits a sourceless supertype
stub, and the corpus rewire bound it to the unique same-labelled real definition anywhere
in the corpus — so in a PHP+TS monorepo a PHP class inherited from the TypeScript
Exception, a phantom cross-language edge / god node. Gate the supertype-target rewire with
a per-language curated builtin-base set: refuse the rewire only when the referring file's
language names the base as a builtin and the resolved target is in a different language
family. Same-language inheritance and user classes named like a builtin still link.
2026-08-21 18:26:00 +01:00
safishamsi ada1c095ab fix(llm): bisect PDF slices on extracted text; reconcile splittable-PDF tests (#2906)
Three fixes landing the PDF-slicing set coherently:
- bisect_slice read raw container bytes, so the adaptive-retry path (a lone oversized
  slice that still overflows, #2880) searched for the newline cut in binary coordinates
  and could cut mid-line for a compressed PDF. Index unit_source_text instead, matching
  read_slice_text and expand_oversized_files; any converter failure falls back to None.
- #2900's test asserted a PDF is not splittable, which #2906 overturns; rewrite it to
  guard the real invariant (an image has no addressable text and is never sliced). The
  unreadable-PDF passthrough case is covered by test_pdf_slicing.
- add a bisect_slice-on-PDF regression test: the cut lands on an extracted-text line
  boundary and the halves tile the slice exactly.
2026-08-21 16:57:29 +01:00
abhay-codes07 4faa86c8d5 fix(llm): slice oversized PDFs through the converter (#2906)
An oversized PDF was sliced as raw container bytes, so slice offsets indexed binary data
instead of the extracted text the prompt carries. Introduce unit_source_text so both the
oversized-file expansion and per-slice reads index the same extracted text, and make PDFs
splittable through the converter.
2026-08-21 16:51:17 +01:00
abhay-codes07 b6b2fb17c8 fix(llm): slice every text document type, not just five of them (#2900)
The oversized-file slicer only split five suffixes while the doc allowlist reaching the
LLM was larger, so a large .qmd/.skill/.html/.yaml/.yml was truncated at the char cap
and silently dropped past it. Extend the splittable-text set to cover every text
document type, with a contract test so future allowlist additions fail loudly.
2026-08-21 16:51:00 +01:00
abhay-codes07 7c5203dde4 fix(llm): estimate PDF tokens from the extracted text, not the container (#2903)
Token estimation for a PDF read the raw container bytes, which are mostly binary and
bear no relation to the extractable text, so a small-text PDF could be judged oversized
(or vice versa). Estimate from the extracted text instead, memoized on path+size+mtime.
2026-08-21 16:50:44 +01:00
santhiprakash a4cb89083c fix(csharp): walk generic type arguments at call sites (#2911)
A type used as a generic argument at a call site (repo.Get<User>(),
services.AddSingleton<IFoo, Foo>()) did not emit a references edge, so DI registrations
and generic calls dropped their type dependencies. Walk the invocation's type-argument
list (member and static forms, nested args included); builtins and in-scope type
parameters are not fabricated.
2026-08-21 16:50:31 +01:00
brobl2008 09c985c553 fix(csharp): walk generic type arguments in field position
A type used as a generic argument in a field declaration (private List<Order> _orders)
did not emit a references edge to the argument type, so field-injected dependencies were
invisible. Walk the field's type through the shared type-ref collector, bringing fields
to parity with the property handler; builtins and in-scope type parameters are not
fabricated.
2026-08-21 16:50:25 +01:00
sashankh cce41efb7c fix(extract): invalidate tsconfig alias/baseUrl caches per run (#2917)
The tsconfig/jsconfig alias and baseUrl caches were keyed by config path with no
mtime/content component and never cleared, so an edit to compilerOptions.paths or
baseUrl was never observed again for the life of the process — graphify watch and the
MCP server, which call extract() repeatedly in one process, kept resolving imports
through the stale alias map. Clear both caches per run alongside the other run-scoped
caches (within-run caching is preserved).
2026-08-21 16:49:55 +01:00
safishamsi e16b01ce48 test(extract): assert --no-dedup graph invariant, not just kwarg propagation (#2881)
The PR's tests spy on the dedup kwarg reaching build/build_merge. Add graph-level
assertions for what the kwarg does: dedup=False preserves a fuzzy near-duplicate pair
that dedup=True merges, and exact-id collisions still collapse to one node either way
(the structural invariant that makes the flag safe).
2026-08-20 16:13:45 +01:00
rajarshidattapy eccc6cf0bf feat(extract): add --no-dedup so incremental merges can skip fuzzy dedup (#2881)
Adds --no-dedup (default off, so dedup stays on) to skip the fuzzy near-duplicate merge
pass on build and incremental merge, for operators who would rather keep distinct symbols
that fuzzy-matched than pay the merge. Exact-id uniqueness is unaffected (it is a graph
structural invariant, not a dedup responsibility), and the flag arms the #479 shrink
guard so a surprising node drop is refused loudly. Mutually exclusive with --dedup-llm.
2026-08-20 16:10:49 +01:00