[pull] v8 from Graphify-Labs:v8#112
Open
pull[bot] wants to merge 292 commits into
Open
Conversation
safishamsi
force-pushed
the
v8
branch
2 times, most recently
from
July 6, 2026 16:38
580b8a3 to
aba1232
Compare
Moves the cross-file symbol-resolution / import-resolution / decl-def-merge
passes — the largest remaining self-contained subsystem in extract.py — into
two new modules, verbatim:
- extractors/models.py: the shared data types (LanguageConfig, the
_Symbol*Fact / _SymbolResolutionFacts dataclasses) and two shared caches
(_WORKSPACE_PACKAGE_CACHE, _JS_CACHE_BYPASS_SUFFIXES). Homed here so both
extract.py and resolution.py import them without a cycle; the mutable
workspace cache keeps a single shared object identity (only ever .clear()'d
in place), verified in the smoke test.
- extractors/resolution.py: 60 resolution functions (_resolve_*, _collect_*,
_apply_symbol_resolution_facts, _disambiguate_colliding_node_ids,
_merge_decl_def_classes, the JS/TS/Python import walkers, tsconfig/workspace
resolution) and their 12 private constants.
extract.py re-exports every moved name, so importers (__main__, watch, tests
that reach into _JS_RESOLVE_EXTS / _resolve_cross_file_imports / the fact
dataclasses) are unchanged. Import direction is strictly extract.py ->
resolution -> {models, base}; AST analysis confirmed no moved non-entry symbol
is referenced from outside its new module.
extract.py 12,632 -> 10,270 LOC (17,054 at the start of this branch, -40%).
Full suite unchanged: 3036 passed, 29 skipped; skillgen --check OK.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tch_install_cli main() carried a 256-line if/elif chain routing every install-family command (install, uninstall, and the 22 per-platform targets: claude, codebuddy, gemini, cursor, vscode, copilot, kilo, kiro, devin, pi, amp, agents/skills, aider/codex/opencode/claw/droid/trae/trae-cn/hermes, antigravity). That block only ever reads sys.argv and calls functions that already live in install.py, so move it there verbatim as a guarded dispatch_install_cli(cmd) -> bool; main() now does `if dispatch_install_cli(cmd): return`. The command set is derived from the block's own conditions (so none is missed), and the branch's early `return` becomes `return True` so the help path still short-circuits. Verified end-to-end: `graphify claude install/uninstall`, unknown command, and the install test suite all behave identically. __main__.py 3,641 -> 3,388 LOC (5,368 at branch start, -37%). Full suite unchanged: 3036 passed, 29 skipped; skillgen --check OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
setuptools uses an explicit package list, so the new graphify/exporters/ package (base, html, graphdb — split out of export.py) was absent from the built wheel, which would break `import graphify.exporters.*` for installed users. Add it alongside graphify and graphify.extractors. No version change. Verified: fresh-venv install of the rebuilt wheel imports every new module, resolves all backward-compat re-exports, builds a multi-language graph, and runs query/path/explain and install/uninstall. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…to extractors/engine.py
Moves the config-driven extraction engine — _extract_generic (~2.2k LOC) and its
67-function closure of per-language tree-sitter walkers/type-collectors
(_js_*, _ts_*, _python_*, _java_*, _csharp_*, _cpp_*, _swift_*, _kotlin_*,
_php_*, _ruby_*, _scala_*) plus 10 engine-private constants (language builtin/type
tables) — into graphify/extractors/engine.py, verbatim.
AST analysis proved the engine closure references neither `extract` nor
`_DISPATCH`, so the dispatcher facade (extract, _DISPATCH, _get_extractor,
collect_files, the thin config-driven extractor wrappers) stays in extract.py and
imports the engine. Import direction: extract.py -> engine -> {resolution, models,
base}; no shared constant crosses the boundary (verified), and engine pulls a
single name (_resolve_js_import_target) from resolution, so there is no cycle.
extract.py re-exports every moved symbol, so importers and tests reaching into
engine internals are unchanged.
extract.py 10,270 -> 5,947 LOC (17,054 at branch start, -65%). Full suite
unchanged: 3036 passed, 29 skipped; skillgen --check OK.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…actors/ Now that the resolution passes and the tree-sitter engine live in their own modules, these three bespoke extractors are self-contained and move cleanly (verbatim), pulling only the specific engine/resolution/base helpers they need: - extractors/pascal.py: extract_pascal + regex helpers + _PAS_* constants - extractors/objc.py: extract_objc + Objective-C member-call resolution - extractors/julia.py: extract_julia Re-exported from extract.py and registered in extractors/__init__.py (27 langs). The remaining in-file extractors (js/ts config family + vue/svelte/astro/xaml) stay because they share _JS_CONFIG/_TS_CONFIG and the xaml dispatch caches with extract_js/extract_csharp and the dispatcher. extract.py 5,947 -> 4,740 LOC (17,054 at branch start, -72%). Full suite unchanged: 3036 passed, 29 skipped; skillgen --check OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
main() was a 2,760-line function that was almost entirely a 2,538-line if/elif chain dispatching every non-install subcommand (query, path, explain, diagnose, affected, reflect, save-result, extract, update, cluster-only, label, build, global, merge-graphs, merge-driver, watch, tree, export, benchmark, clone, prs, provider, hook*, check-update, and the `graphify <path>` redirect). Move that chain, plus its 5 chain-only helpers (_StageTimer, _clone_repo, _default_graph_path, _enforce_graph_size_cap_or_exit, _run_hook_guard) and 4 chain-only constants (_SEARCH_NUDGE, _READ_NUDGE, _HOOK_SOURCE_EXTS, _GEMINI_NUDGE_TEXT), into graphify/cli.py as dispatch_command(cmd). main() now does its setup (encoding, stale-skill check, version/help) then `if dispatch_install_cli(cmd): return` else `dispatch_command(cmd)`. The chain ends in its own unknown-command exit, so it's called as a statement — no return-value plumbing. The one cli->__main__ coupling, the path-redirect's recursive main() call, is handled by a lazy import (_reenter_main), so import direction stays __main__ -> cli with no cycle. __main__ re-exports every moved symbol, so importers/tests are unchanged. __main__.py 3,388 -> 662 LOC (5,368 at branch start, -88%). Verified end-to-end: version/help/unknown-command, the `graphify <path>` redirect (rebuilds a graph), and query/path/explain. Full suite unchanged: 3036 passed, 29 skipped; skillgen OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Decompose extract.py / __main__.py / export.py into focused modules (#1737, verbatim, no behavior change). Plus fixes since 0.9.10: merge-graphs distinct repo tags (#1729), uninstall cleans Claude .local files (#1731), and extract --code-only for keyless code-only indexing of a mixed repo (#1734). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1700 Kotlin half, #1738) Kotlin enum entries weren't extracted: the walker never descended into the enum body (`enum_class_body` wasn't in _KOTLIN_CONFIG.body_fallback_child_types, so _find_body returned None). Add `enum_class_body` to the fallback body types and a `_kotlin_extra_walk` (dispatched for tree_sitter_kotlin, mirroring the Java/Swift handling) that emits each enum_entry as a node with a `case_of` edge to the enum. Re-applied from PR #1738 (@ivanzhl) onto the post-#1737 module layout: the walk engine now lives in graphify/extractors/engine.py while _KOTLIN_CONFIG stays in extract.py. Closes the Kotlin half of #1700 (Java was #1719). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) Step 1's POSIX interpreter probe ran `uv tool run graphifyy python -c ...`, but the graphifyy package exposes its executable as `graphify`, so uv treated `python` as a missing `graphifyy` command. The probe failed, and `2>/dev/null` swallowed uv's "use --from" hint, leaving PYTHON on a system interpreter that does not have graphify installed. The probe now runs `uv tool run --from graphifyy python -c ...`. Applied to the skillgen source fragments (shell/posix.md and the aider/devin monoliths) and regenerated all skill*.md; added a sanctioned monolith-diff predicate and re-blessed the expected/ snapshots. The PowerShell path was already correct (it probes the venv python.exe directly). Reported and originally patched by @mohammedMsgm in #1736. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both fixes landed after the initial 0.9.11 section was written but before 0.9.11 was published, so they belong in that release's notes rather than a new version. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…overage (#1602) In a multi-term query, a lone generic term that exactly equals a short leaf label (`home` vs a `home()` action, `list` vs a `list()` function) received the full exact-tier bonus and outscored nodes matching several of the query's terms by ~10x, so `_pick_seeds`' gap cutoff discarded the genuinely relevant candidates and `path`-style consumers of `scored[0]` had no backstop. `_score_nodes` now scales the per-term exact/prefix tier contributions by the squared fraction of query terms the node's label matches (`tiered * (matched / n_terms) ** 2`). Squared because the exact tier is 10x the prefix tier; label hits only (source-path hits score but don't count as coverage, so a colliding leaf in the target's own directory can't win its exact tier back via path fragments); query tokens deduped order-preserving so a repeated word can't quadratically restore the collision. Single-term and full-coverage queries are unchanged (coverage == 1), keeping identifier exact-match dominance. Guarded against zero-term division. Dropped the unrelated README badge changes from the PR; kept the scoring fix and its two regression tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… calls across files (#1739) Both Pascal extractors resolved every call via a single file-wide {method_name: node_id} dict, so two unrelated classes declaring a same-named method (property accessors, generated COM/TLB wrapper classes) collapsed onto whichever declaration was inserted last, producing wrong cross-class `calls` edges. Resolution is now scoped: own class -> ancestor chain (inherits) -> file-level free function -> unambiguous file-wide match; ambiguous at every level emits no edge rather than guessing (same god-node guard as the Ruby resolver). Adds graphify/pascal_resolution.py, a corpus-wide post-extraction resolver (registered via resolver_registry) that walks the inherits chain across file boundaries, so a call from a manual descendant to a method it inherits from a base class in a separate unit (the generated-base/manual-descendant split) resolves. Also stops both extractors from emitting a duplicate base-class stub carrying the referencing file's source_file, which collided with the real node under cross-file id disambiguation. cache.py gives the new raw_calls bucket the same portable-path treatment as nodes/edges so it round-trips. Re-applied to the post-#1737 module layout (extractor hunks land in graphify/extractors/pascal.py; registration stays in extract.py). Added one adaptation the original PR predated: the cross-file resolver's god-node guard now counts DISTINCT method nids, because the tree-sitter extractor emits a method edge for both the interface declaration and the implementation, so the same method_nid arrives twice -- without deduping, every inherited call looked ambiguous and resolved to nothing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two correctness fixes found while analysing the reported 'graphify update occasionally writes a partial graph.json' bug. Enumeration (P0): detect()'s os.walk had no onerror handler, so any os.scandir failure -- a transient PermissionError, or a directory created/deleted mid-walk by concurrent writes (e.g. benchmarking racing the scan) -- was silently swallowed and that entire subtree dropped out of the file list with no log, no error. Downstream that becomes a silently partial graph.json. The walk now records each skipped directory (surfaced as walk_errors in detect()'s result) and warns to stderr, while still enumerating the rest of the tree. This stays visible even when a --force/GRAPHIFY_FORCE rebuild bypasses the shrink guards. Relatedly, to_json's #479 anti-shrink guard was fail-OPEN: a non-empty but unreadable existing graph.json (corrupt or mid-write) proceeded with the overwrite. It now fails SAFE -- refuse and point at force=True -- while an empty/whitespace existing file (no nodes to lose) still proceeds. The size-cap check keeps running before any read, so an oversized existing file is not loaded into memory. Pascal edges (P1): a class method declared in the interface section and defined in the implementation section each emitted a "method" edge to the same node id, and the edge helpers (unlike the node helpers) did not dedup, so ~half of a Pascal/Delphi graph's method edges were doubled -- inflating degree/centrality and tripping the #1739 cross-file resolver's single-owner god-node guard. Both extractors now dedup edges on (source, target, relation). Adds regression tests for all three behaviours. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) Two classes with the same simple name in different Maven modules (FinancialEntryValidator in payment/ and core/) already survive as distinct path-scoped nodes on v8 -- the "node silently disappears" report from 0.9.9 is fixed. But a cross-module field/type `references` edge was still left dangling on a sourceless phantom stub: _resolve_java_type_references (#1318) re-pointed implements/inherits/extends/imports edges to the real definition using the importing file's `import` statement, but its REPOINT_RELATIONS omitted `references`, so bare-name resolution's shadow stub survived for field types. A query about the referenced class could then miss it. Add `references` to the Java resolver's REPOINT_RELATIONS. The C# sibling already covers references; this brings Java to parity. The reference now resolves to the imported package's class (falling back to same-package), and the orphaned phantom is dropped. Regression test covers the ambiguous two-module case: both reals present, no phantom, reference lands on the imported class. Reported with a precise root-cause and repro by @aviciot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ge split (#1721) The extract_terraform move #1721 proposed already landed on v8 via the #1737 decomposition (extractors/terraform.py exists, extract.py re-exports it, and extractors/LANGUAGE_EXTRACTORS registers it), so the code move is a no-op now. But the regression test @Cekaru added with it had no equivalent on v8. Salvage and generalize it: sweep every LANGUAGE_EXTRACTORS entry and assert graphify. extract re-exports the SAME object (facade identity) and the registry maps to it (registry identity), plus the concrete terraform anchor from the PR. This institutionalizes the re-export-identity guarantee the split relies on, so a future move that forgets a facade re-export fails loudly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1753) build_from_json's ghost-node merge iterated set(G.nodes()), so when two nodes shared a (basename, label) key the "canonical" survivor was chosen by CPython's per-process string-hash order — rebuilding the same extraction JSON in a fresh process could pick a different survivor, silently changing which node id represents a concept. That breaks any workflow persisting ids across a rebuild; concretely it broke the cluster->relabel step (community membership referenced an id that the second build merged away -> KeyError in report generation). Two changes: - Pass 1 and Pass 2 now iterate sorted(node_set), not set(node_set), the same deterministic-order fix the edge loop below already uses on purpose. - The #1257 ambiguity guard is extended to the case it did not cover: two NON-AST nodes sharing a key but from DIFFERENT source files are distinct concepts, not an AST ghost/canonical twin, so the key is marked ambiguous and both survive rather than one arbitrarily merging away (data loss). A genuine same-file duplicate (identical source_file) is not flagged and still collapses to one node. Reported with a precise root-cause, minimal repro, and real-world impact by @erasmust-dotcom. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dep (#1745) When the [sql] extra is absent, .sql files are counted as code and scanned but extract_sql returns an error result and zero nodes — and the graph builds "successfully" with the entire SQL corpus missing. Neither existing warning catches it: #1666's zero-node warning skips results carrying an "error", and #1689 only covers files with NO extractor at all (.sql HAS a dispatch entry). extract() now scans per-file results for a "not installed" error, groups the affected files by extension, and prints a warning naming the extra that restores the language (pip install "graphifyy[sql]"), via a small _EXTRA_FOR_EXTENSION map (sql, terraform, dm). The map is only consulted after an extractor actually reports the dependency missing, so it can't mislabel a language that has a working fallback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1749) The extraction spec forbids cross-language `calls` edges, and build already dropped cross-language INFERRED `calls`. But `imports`/`references` had no such guard: an unresolved Python `import time` resolved by bare stem (the #1504 old-stem alias) onto a `src/time.ts` file node, welding a polyglot repo's two language halves together. In the reporter's repo three such edges were the only bridge between 2409 Python and 1403 TS nodes, so every backend<->frontend shortest path routed through time.ts, inflating its betweenness ~90x and making it the #1 reported god node. Hoist the interop-family map to a module constant and extend the edge-loop guard to `imports`/`imports_from`/`references`. For these relations the edge is dropped only when BOTH endpoints are known code languages of different families, so a config/manifest -> code reference (unknown ext) is never mistaken for a phantom. `calls` behavior is unchanged (still INFERRED-only, still drops when either family differs). Regression tests: py->ts import dropped, ts->ts import kept, config->code reference kept. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…CWD (#1747) Case 1 — `extract <corpus> --out <dir>`: the graph went to <dir> (cache_root is already passed to the AST extractor), but detect()'s word-count/stat-index cache uses the scan root, so a stray graphify-out/cache/ was created inside the corpus (and left behind even when the run aborted at the no-LLM-key gate). Thread an optional cache_root through detect() -> cached_word_count() -> _ensure_stat_index() and pass out_root from the extract CLI, so the stat index lives under --out. Entry keys are absolute paths, so relocating the index file is safe. Case 2 — `cluster-only --graph <elsewhere>/graphify-out/graph.json`: outputs (GRAPH_REPORT.md, re-clustered graph.json, labels, analysis, html) were written to the CWD's graphify-out/, ignoring where --graph lives. They now write beside the input graph when it sits in a graphify-out/ dir (another project/tenant's output), while still falling back to the CWD for an arbitrary archived backup/graph.json — the restore-into-place workflow #934 pins. Regression tests for both cases; #934 still passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Java member calls (`gw.charge()`) resolved by bare method name, so a call bound to any same-named method in the corpus — e.g. `PaymentGateway.charge` and `AuditLog.charge` were indistinguishable, producing phantom cross-class edges and a false god node. The extractor now preserves the receiver and its static type, and _resolve_java_member_calls binds the call against the receiver's declared type: explicit-type receivers and `this` are exact; current-class fields, method parameters, and explicitly-typed locals resolve via a method-scoped type table; a missing/ambiguous/inherited/chained receiver is skipped rather than falling back to a bare name match (same single-owner god-node guard as the C#/Swift/Ruby resolvers). Fully-qualified and nested-type receivers are deferred since they need package- and nesting-aware type identity. Verified: `gw.ping()/gw.charge()` (gw: PaymentGateway) bind to PaymentGateway, the three charge() calls dedup to one edge, and no edge targets the same-named AuditLog methods. Applies cleanly to the post-#1737 layout (extract.py + extractors/engine.py). 13 new tests; full suite 3135 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`graphify update` (the watch._rebuild_code / _reconcile_existing_graph path) evicted every hyperedge whose source_file is in the corpus, because on a full update every corpus file counts as "rebuilt" and hyperedge eviction reused the node/edge eviction set. But the AST pass never emits hyperedges, so nothing replaced them — doc-sourced hyperedges (what semantic extraction produces) were permanently lost on the first update after a full build, even on a no-op run. Split out a hyperedge_evicted_source_identities set scoped to genuinely deleted (and symlink-target-outside) sources only, not merely-rebuilt ones. Replacement- by-id (new_hyperedge_ids) and dangling-member cleanup are unchanged, so a real semantic re-extraction still replaces its own hyperedges and orphaned ones are still dropped. Parametrized regression test (full + incremental doc update). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
extract_json emitted `imports` edges for package.json dependencies and
`extends`/`$ref` edges for tsconfig.json to target ids (`_make_id("ref", ...)`
/ `_make_id(key)`) that it never created as nodes. build_from_json drops edges
to unknown node ids silently (that case is filtered out of real_errors), so
dependency and extends structure vanished from the graph on two of the most
common files in any JS/TS repo, surfaced only by diagnose_extraction after the
fact.
The extractor now adds the referenced target as a `concept` node (external ref,
not a corpus file) before emitting each edge, so the edges survive build.
Regression test asserts no dangling endpoints, the concept nodes exist, and the
import/extends edges land on real targets with no self-loops.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t references edges (#1746) The live-introspection FK query joined information_schema.referential_ constraints, which Postgres only exposes for constraints where the current user has WRITE access to the referencing table. A read-only introspection role therefore got zero FK rows — while tables/views/routines still appeared (SELECT is enough for those views) — so the graph silently lost every `references` edge, contradicting the documented FK-mapping behavior. Switch to pg_catalog.pg_constraint (world-readable, not privilege-filtered), keyed by constraint oid rather than name — which also fixes a latent bug where same-named constraints on sibling tables could cross-match in the old name-based key_column_usage joins. Composite-FK column order is preserved with UNNEST(conkey/confkey) WITH ORDINALITY. Mock test asserts the query targets pg_constraint and not the privilege-filtered view, plus composite-FK ordering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The badge's href pointed at www.ycombinator.com/companies/graphify, which 404s — the public S26 company page isn't published yet. Show the badge without a link rather than ship a dead click; re-add the href once YC publishes the page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e edges (follow-up to #2154) The decorator reference edges added in #2154 fabricated sourceless stub nodes for @property/@staticmethod/@dataclass/@functools.wraps and, via the unique-function rewire, could stamp a false edge onto a corpus's own def wraps(). Add _PYTHON_DECORATOR_NOISE (mirroring _PYTHON_ANNOTATION_NOISE) and skip those names, same accepted tradeoff as patch/Mock in annotations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s (follow-up to #2169) #2169 canonicalizes cross-file edge targets to the root-relative file-node id (the same id the target file gets as a node) instead of the old absolute-path form. The #2153 baseUrl tests asserted the absolute form; update them to the canonical id via a _cid() helper. Resolution behavior is unchanged — only the expected id form. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_extract_parallel` spawned a ProcessPoolExecutor whenever there were at least _PARALLEL_THRESHOLD (20) uncached files, even when the resolved worker count was 1. A one-worker pool buys no parallelism: it still pays a process spawn plus an IPC round trip per file, and it is the one residual case where the parent's rebuild watchdog (os._exit) can orphan a worker that is mid-task. The Windows post-commit hook exports GRAPHIFY_MAX_WORKERS=1, so this was the default there for any rebuild touching 20+ uncached files. Gate the pool on the resolved worker count -- after the GRAPHIFY_MAX_WORKERS override and the win32/floor clamps -- and return False when it is 1. That reuses the existing contract: the caller already falls back to `_extract_sequential` in-process when `_extract_parallel` returns False. Tests: no pool is constructed with GRAPHIFY_MAX_WORKERS=1 and 25 uncached files, and a multi-worker run still takes the pool path. Only item 2 of #2173 is addressed here. Item 1 (the `graphify watch` rebuild timeout) needs a maintainer decision first: `watch()` currently arms no timeout at all on any platform -- there is no signal.SIGALRM branch in graphify/watch.py to add an `else` to -- so applying the hook's shape means adding a watchdog that os._exit(1)s a long-running foreground watcher on a slow-but-healthy rebuild. That is a behaviour change rather than a Windows-compat fix, so it is left out of this PR.
Two coverage gaps left by #2141 / #2157, both misses rather than fabrications. 1. Extensionless shebang scripts. _SHEBANG_DISPATCH already routes a `#!/usr/bin/env bash` file with no extension to extract_bash, so its functions are indexed, but the cross-file source-resolution pass picked participants by filename suffix (`p.suffix in (".sh", ".bash")`). A sourced extensionless lib was therefore excluded and calls into it never bound. Select by shape as well: the bash extractor tags every node it emits with metadata.language == "bash". The suffix check stays so an empty .sh file, which has no nodes to inspect, still participates. 2. Bare `source lib.sh` (no ./ prefix). Only the `raw.startswith((".", "/"))` branch recorded a bash_sources entry; a bare name fell through to the opaque `imports` fallback, so neither the source edge nor calls into the lib resolved even though the file usually sits beside the script. The else branch now binds a sibling of that name when one exists. The existence gate from the ./-prefixed branch carries over: a bare name that resolves to no sibling keeps the old `imports` edge and records no bash_sources entry, so nothing is invented. is_file() is wrapped against OSError so a name that is invalid for the platform degrades instead of raising. Transitive sources (a->b->c) remain unresolved, as the issue notes.
) #2079 resolved `source "${VAR}/lib/x.sh"` by stripping the leading expansion and resolving the literal suffix against the sourcing script's own directory. That is correct for the canonical `DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"` idiom, but wrong whenever the variable points somewhere else. With ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" source "${ROOT}/lib/utils.sh" the real target is <root>/lib/utils.sh, yet a same-named decoy under the script dir (<root>/scripts/lib/utils.sh) won: a wrong imports_from edge to a real node. Track top-level variable assignments and use the assigned base for the leading variable: - the script-dir idiom, including any number of trailing `/..` hops (and the `$0` spelling), resolves to the script dir walked up that many times - a literal value resolves as-is when absolute, or against the script dir when relative - anything else -- a value built from other variables, or command substitution we do not model -- stays untracked, so the previous script-dir guess is kept Only the base changes. The suffix guards are untouched: an expansion left in the suffix, an empty suffix, and a `..` component in the suffix are all still rejected, the edge is still gated on is_file() and still emitted as INFERRED.
`graphify hook install` emitted `_PINNED=''` for some Windows uv-tool installs, so every interpreter probe failed, each commit printed "could not locate a Python with graphify installed" and the graph never rebuilt. Root cause is the install-time allowlist in `_pinned_python()`, not the uv layout: it accepted `[a-zA-Z0-9/_.@:\-]` but not a plain space, so any `sys.executable` under a profile whose name contains one -- `C:\Users\First Last\AppData\Roaming\uv\tools\graphifyy\Scripts\python.exe`, or the equally common `C:\Program Files\Python312\python.exe` -- was rejected wholesale and nothing was recorded. A space-free Windows uv path pins correctly, which is why this looks layout-specific. A space is safe to allow because every consumer already quotes the value: the hook scripts embed it as `_PINNED='<path>'` and dereference `"$_PINNED"`, so a space can neither split a word nor start a command. Adding it to the allowlist therefore fixes the pin without weakening the injection guard -- `$`, backtick, `;`, `'` and `"` are all still rejected. `_register_merge_driver` did interpolate the path unquoted into the `merge.graphify.driver` command, which git runs through a shell; that would split a spaced path into two words, so it is now double-quoted. Double quotes are safe here precisely because the allowlist keeps `$` and backticks out. Tests: spaced Windows/POSIX paths are pinned; metacharacter paths are still rejected (including `'` and `"`); the merge driver quotes a spaced interpreter; and the installed post-commit/post-checkout hooks carry the real path rather than `_PINNED=''`.
) tree-sitter-sql cannot parse PL/pgSQL-only statements, and #1910's ERROR-node name recovery only covered one of the shapes that produces. Two others dropped the routine silently -- no node, no warning, exit code 0: 1. The statement is shredded into loose top-level tokens (keyword_create, keyword_function, object_reference, ..., keyword_begin) and the ERROR node holds only the offending body line, e.g. `PERFORM other_fn();` or `x := 1;`. No ERROR node contains any CREATE text, so scanning ERROR nodes finds nothing. This is what still dropped PERFORM and := after #1910. 2. The routine name is a quoted identifier -- CREATE OR REPLACE FUNCTION "public"."fn"(...) -- which the recovery's bare [\w$.]+ pattern cannot match, because it stops dead at the leading quote. Generated schema dumps quote every identifier, so whole files recovered nothing. Verified on the reported repro: the same body that drops under a quoted name is recovered fine under an unquoted one, which is why the drop looked like it depended only on the body statement. Fix mirrors the global REFERENCES fallback already in this extractor: after the tree walk, scan the raw source for every CREATE [OR REPLACE] FUNCTION/PROCEDURE and emit any routine the walk missed. Name parts accept bare or double-quoted identifiers. _add_node dedupes by node id, so routines already recovered from the tree are not emitted twice. Adds tests/fixtures/sample_plpgsql_quoted.sql -- generated-style quoted DDL whose bodies use RAISE, RAISE NOTICE, PERFORM, :=, IF..THEN and bare NULL; -- plus tests that every routine is recovered and that the file stays clean (tables before and after still extract, no duplicate ids or labels, no empty/ERROR labels, and every routine keeps its contains edge from the file node).
`graphify codex install` registers `graphify hook-check` in .codex/hooks.json, and #2165 reported that as a stale/unrecognized subcommand producing a silent no-op. `hook-check` is in fact a real, deliberate no-op command: Codex Desktop rejects hookSpecificOutput.additionalContext on PreToolUse, so the Codex hook intentionally does nothing and AGENTS.md carries the always-on guidance (cli.py dispatches `hook-check`; __main__.py lists it in _silent_cmds). Repointing the installer at `hook-guard` would reintroduce the #522-class breakage on Codex Desktop, so the behavior is left as is. What actually misled the report was the documentation and the installer's own output, which both describe the Codex hook as if it enforced graph usage: - README: the Codex row claimed a PreToolUse hook that "fires before every Bash tool call, same always-on mechanism as Claude Code". It now states that the hook is a deliberate no-op, why (Codex Desktop rejects additionalContext), and that AGENTS.md is the always-on mechanism on this platform. - `_install_codex_hook` printed "PreToolUse hook registered (... hook-check)" with no hint that the entry is inert. It now says so inline. Also adds the regression guard the issue implicitly asks for: a test that reads the command out of the generated .codex/hooks.json and asserts its subcommand is one the CLI actually dispatches. A genuinely renamed/stale hook command now fails the suite instead of shipping a permanently dead hook. Note: contrary to the report, an unrecognized subcommand already exits non-zero (`graphify totally-bogus-subcommand` -> "error: unknown command", exit 1), so no change was needed there.
…2180) - bash: mark the bare-name `source lib.sh` sibling binding INFERRED (it resolves via $PATH at runtime, so it's a heuristic, not EXTRACTED) (#2171) - bash: un-join a comment accidentally merged onto the _BASH_SCRIPT_RUNNERS line during #2172 - sql: gate the global routine-recovery raw-text scan on root.has_error so a cleanly-parsing file can't fabricate routines from commented-out DDL, EXECUTE-string bodies, or MySQL 'CREATE FUNCTION IF NOT EXISTS' (#2180) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Svelte/Astro/Vue regex-rescue import passes minted stub target nodes with absolute-path ids (ghost nodes alongside the real file node, plus dangling imports_from edges). They now resolve via _resolve_js_module_path and stamp edge target_file so the #2169 canonicalization repoints them; when the target is an in-root real code file, only the edge is emitted (no duplicate stub). The final relativization pass also remaps ids in its in-root branch, closing the residual class. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…labels (#2182) Pass 1 deferred cross-file exact matches to Pass 2, but Pass 2's candidate filter keeps only the first node per normalized label, so identical-label cross-file concept pairs could never merge (while fuzzy pairs did). Pass 1 now unions the cross-file residue of each label group, gated to concept nodes with provenance and above the entropy floor, so code/rationale/ document/image/empty-source and cross-repo guards are all preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#2199, #2197) stat-index.json keyed entries by absolute path and never pruned, so a moved/cloned corpus got 0% cache hits and the index grew unbounded. Keys are now stored root-relative and re-anchored on load (mirroring the manifest.json portability fix), and dead-file entries are pruned on flush. save_semantic_cache also normalizes source_file to root-relative before persisting so an absolute/backslash fragment can't poison later updates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… semantic ids (#2194, #2197) build_from_json now folds name->label, path->source_file, edge type->relation, and confidence_score->confidence=INFERRED before validation, so alias-carrying nodes stop entering the graph without label/source_file (invisible, unmergeable ghosts); the same folds run before dedup. _semantic_id_remap now also learns the absolute-path stem form, so a Windows absolute-derived semantic id re-keys to the canonical root-relative id. The extraction warning now breaks errors down by cause. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ng (#1609) Adapted from #1620 by @TheFedaikin, reworked onto v8 as a focused change (without the module split or the references-fallback behavior change). Builds on the shipped #1609 resolver: instead of bailing when a receiver's class name is ambiguous corpus-wide, the declared type is resolved with a shared CsharpNameResolver (same-namespace, using-directive, and alias aware) against the caller's namespace/scope, falling back to the unique bare match only when scoping is non-decisive. Adds base./this.field receivers and inherited-member lookup through the inherits chain (an out-of-corpus base poisons the lookup, so no wrong edge). The per-file type table now poisons a name on any conflicting rebinding, killing the wrong-edge class where a local shadows a field of a different type. C#-gated; never emits a wrong edge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
function_types only recognised func/init/deinit/subscript, so computed properties (var body: some View { ... }) and willSet/didSet observers produced no node and their bodies were never walked — erasing the whole SwiftUI view layer. Emit a function-like member node for them and defer the body to the call-walk via function_bodies; stored properties are unchanged. Adds tests.
Stamped keys can already be NFC while Path(root).resolve() is NFD, so os.path.relpath treated in-root files as ../ and kept them absolute. Normalize both operands inside _to_relative_for_storage (and the join in _to_absolute_from_storage).
…bels Community labels are saved keyed by community id, but re-clustering reassigns those ids: after a rebuild that adds nodes, cid 30 can cover a completely different community and its saved name is then simply wrong. cluster-only already guards this — it validates each reused label against the `.graphify_labels.json.sig` membership fingerprints and re-hubs any community that changed (the case community_member_sigs() was written for). _rebuild_code() skipped that check entirely: it reused every label whose cid was present and hub-filled only the *missing* ones, so stale names survived — and then wrote them back to labels.json, laundering them as current. It also never refreshed the .sig sidecar, so the signatures kept describing an older clustering and drifted out of step with the labels they sit beside, leaving the cluster-only guard nothing accurate to check. Adding ~3.5k nodes to a real graph re-clustered 463 -> 515 communities and mislabeled 162 of them this way: a `domain.audit` namespace reading "ACH / bank payments", a `domain.auth` namespace reading "document-sensitivity.up.sql". Node and edge data stayed correct, so nothing failed loudly — only the names lied. Apply the same signature check in the incremental path, write the sidecar in step with the labels, and print the same "run `graphify label`" notice cluster-only emits so a drifted community set is visible rather than silent.
Crossing MAX_NODES_FOR_VIZ left the project with no graph.html at all: _rebuild_code unlinked the existing file and wrote nothing in its place. The delete on its own is defensible — a kept graph.html would describe an older, smaller graph — but the file is gone before the user reads the message, and the next incremental rebuild silently removes it again, so a repo that grows past the threshold just loses its visualization with no way to keep one. The export path already solved this (#1019): over the cap it re-renders the community-aggregation view rather than going without. Do the same here, so the artifact is current AND present instead of current OR present. GRAPHIFY_VIZ_NODE_LIMIT=0 still means "no HTML viz" (CI runners) rather than "aggregate", and if the aggregated render also fails the old skip-and-remove behaviour stands.
…o incremental targets canonicalize (#2211, #2213) The #2169 incremental canonicalization only rewrites edge targets that carry a target_file stamp. Python relative imports and markdown reference links emitted absolute-path-derived target ids without one, so on an incremental/subset extraction they dangled on an absolute id instead of resolving to the canonical root-relative node (dropping md->md references and leaving a dangling imports_from on --no-cluster). Both now stamp the resolved target (existence-gated); the stamp is popped before graph.json ships. Also register the unresolved target form in the remap loop so a symlinked root (macOS /tmp) can't cause an id-form mismatch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#2212, #2210) #2212: benchmark, the merge-driver graph load, and callflow_html crashed or silently failed on a --no-cluster graph.json (edges stored under 'edges', not 'links'). A shared load_node_link_graph helper normalizes links/edges before node_link_graph and is used at all three sites. #2210: _stale_graph_sources compared graph source_file spellings to the scan with a raw string test (no NFC), and pruned any non-match with no liveness check, so alive files (macOS NFD paths, legacy basenames) were pruned as 'deleted'. It now compares NFC-on-both-sides and is fail-closed: a corpus-missing source whose file still exists is pruned only when the exclusion is provable, else kept with a warning. Prune message corrected to 'deleted or excluded'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ng the global skill (#2215) claude_uninstall/gemini_uninstall/codebuddy_uninstall accepted project_dir but, with the default project=False, still deleted the user-global skill tree, so a library/test caller passing a project_dir nuked ~/.claude et al (the API trap behind #2168). They now take remove_user_skill and treat a passed project_dir as authoritative; uninstall_all opts in explicitly to preserve 'graphify uninstall' behavior. Also fixes a live CLI bug where 'uninstall --project' deleted the global codebuddy skill. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the regression test PR #2224 shipped without: an NFD-keyed manifest (portable and legacy-absolute) must match an NFC scan so --update is a no-op instead of re-extracting everything. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )