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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Record the run-scoped-clear contract on both loader docstrings so the next reader
does not reintroduce the process-durable-cache assumption the fix removed.
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).
Ships this cycle: the LLM-resilience batch — hollow-response same-chunk retry (#2880),
reasoning-first JSON recovery (#2882), deliberately-declined data JSON not counted as
failed (#2879); extractor fixes — C++ nested types + C++/CLI (#2876), markdown vault-wide
wikilinks (#2875); export fixes — control-char no longer aborts export (#2897), graph.html
restored for large graphs (#2853); and the --no-dedup opt-out (#2881).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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).
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.
The label and cluster-only commands did not pass the viz node limit to to_html, so a
graph over the node limit raised and the except branch silently unlinked the graph.html
that update had produced. Always pass the limit (the aggregated community meta-graph
renders instead of raising), preserve the existing file on a failed render via an atomic
publish plus a stale marker, and regenerate a missing graph.html on the no-topology-change
fast path without reclustering.