1740 Commits

Author SHA1 Message Date
safishamsi fe66389083 release: 0.9.61 — fix Python 3.12/3.13 breakage from 0.9.60
- chinese extra: pin jieba-py from 3.12 (was 3.14); jieba's invalid escapes are
  a hard error on 3.12+ and broke graphify.serve import.
- serve.py: suppress the tokenizer SyntaxWarning by category so -W error can't
  escalate it; catch SyntaxError as a fallback.
- hooks.py: harden the #3492 rebuild-root guard for 3.13, whose resolve() no
  longer raises on a symlink loop — require a real directory.
Verified full suite green on both 3.10 (5628) and 3.13 (5627).
v0.9.61
2026-09-12 22:23:00 +01:00
safishamsi 7464f5b125 release: 0.9.60 — Windows os.replace fallback, unclassified-file surfacing, C/C++ newline parse, Office sidecar fix, exported-function call resolution, workspace export conditions, Python 3.14 support v0.9.60 2026-09-12 21:54:14 +01:00
safishamsi 2b58bddf8b chore(lock): sync uv.lock for the #3490 dependency-floor bumps; fix stale skillgen windows test
The #3490 lock predated its own setuptools>=83 / pypdf / yt-dlp floor bumps, so
a frozen sync resolved setuptools 82.0.1 and the no-isolation wheel-build test
failed. Regenerate the lock so it honors the floors. Also update the windows
skill test: the troubleshooting item is now 'Skip Leiden' (graspologic ->
graspologic-native gating), which #3490 changed in the fragment but not the test.
2026-09-12 21:49:56 +01:00
taazbro 200f3a15b5 fix: address Python 3.14 review findings
(cherry picked from commit c49db38935)
2026-09-12 21:35:25 +01:00
taazbro 0fc31470b0 fix: raise vulnerable dependency floors
(cherry picked from commit 7590ec5856)
2026-09-12 21:33:43 +01:00
taazbro b42e5bc5c2 fix: support Python 3.14 optional dependencies
(cherry picked from commit 76bb841de0)
2026-09-12 21:33:43 +01:00
dajiaohuang 47ddc5c47b fix(imports): pick workspace exports by importer platform, runtime before types
The workspace `exports` resolver reduced a condition object to a single target,
so a package that splits its runtime source from its declarations never
produced a cross-package edge. Reported in #3487; the exports map itself is
present and works, the defect is in how one is chosen.

`_EXPORT_CONDITION_PRIORITY` listed `types` ahead of `require` and `default`.
`types` is declaration-only and normally points into `dist/`, which is build
output and not part of the corpus, so the resolver returned a path that is not
on disk and stopped there — `_package_entry_candidates` returned `[candidate]`
as soon as any condition matched, so the `default` source beside it was never
tried and the import became a dangling `ref_*` external.

That is not the order the map was added with. #1308 specifies
`source -> import -> module -> default -> require -> types`, and the commit
that implemented it says `default` is consulted LAST so a matching
`import`/`module`/`svelte` condition wins; the tuple shipped with `types` in
the middle instead.

Three changes, all on the workspace `exports` path:

- `types` moves to the end of `_EXPORT_CONDITION_PRIORITY`, restoring the
  specified order. `require`/`default` keep their relative order, so the
  behaviour pinned by `test_workspace_subpath_export_default_consulted_last`
  is unchanged.
- `_resolve_export_targets` keeps every condition target in preference order
  instead of returning the first, so a target that is not on disk falls
  through to the next condition rather than taking the import down with it.
- `react-native` is honoured only for an importer that is itself native, and
  each target is tried alongside its platform-suffixed siblings
  (`Icon.tsx` -> `Icon.web.tsx` / `Icon.native.tsx`), which is how a bundler
  reaches a platform split whose `exports` target names the unsuffixed path.
  Importer platform comes from whole path segments, so `apps/mobile/src` is
  native while `packages/web-utils` is not web.

`_resolve_export_target` keeps its signature and its semantics for the
`package.json` `imports` path, and platform expansion applies only to the
workspace `exports` path, so relative imports elsewhere are untouched.

Behaviour note for review: when every `exports` target escapes the package
directory, resolution still falls through to the bare-path fallback exactly as
before. The first revision of this change swallowed that fallback, which the
containment guard made reachable, and a test now pins it. The `"."` branch
additionally gains the containment guard its subpath sibling already had; that
is the only change here beyond the reported defect, and such a package
previously resolved outside its own directory.

Verified: same 29 pre-existing failures before and after, byte-identical
failure sets (`diff` clean; unrelated to this path — non-regular-file, watch
and uninstall-scope tests on Windows). Passing goes 5500 -> 5507, the seven
tests added here. The fixture from #3487 resolves its four previously-dropped
imports and leaves the three control cases unchanged; focused run 109 passed;
`ruff check` clean; all five `tools.skillgen` CI-parity checks OK.

(cherry picked from commit c704a5b2f2)
2026-09-12 21:31:35 +01:00
abhay-codes07 cd6e05df8a fix(js): resolve calls inside exported functions, incl. aliased imports (#3346)
_js_top_level_function_bodies scanned only direct program children, so a
top-level `export function f(){}` / `export const g = () => {}` — wrapped in
an export_statement — was skipped and the calls inside it never became `uses`
facts. Only the use-fact path consults the per-file import-alias table, so an
aliased-import call (`import { bar as baz }; baz()`) produced no calls edge:
the plain-name global resolver has no `baz` to match, and the alias mapping
that would have pointed it at `bar` was never exercised. Unwrapping a
non-re-export export_statement to its inner function/arrow declaration treats
exported and non-exported functions identically, so the alias table resolves
the call to the real definition.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit 7d98266538)
2026-09-12 21:31:34 +01:00
ayushcodes10 600e4e7491 Exempt Office/Workspace sidecars from the ignore check
detect() converts Office files and Google Workspace shortcuts into
markdown sidecars under a converted subdirectory of the output dir,
then ran the same scan ignore check on the sidecar's own path. The
documented gitignore advice puts the whole output dir inside a
gitignored tree, so that check rejected the tool's own output for
the same reason a user would gitignore it, and the source document
vanished from the corpus with nothing recorded about why. The check
exists to keep user files out of the scan, not to filter output this
same pass just produced from an already admitted source, so a
sidecar under the converted directory is now exempt from it. Fixes
#3504.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 900b376d39)
2026-09-12 21:31:33 +01:00
Imanol Gutiérrez 81f7d68838 Add tests for newline char at EOF fix
(cherry picked from commit 543c171ec1)
2026-09-12 17:39:27 +01:00
Imanol Gutiérrez c32d30060d Add a newline char at EOF on C and C++ headers
(cherry picked from commit ff900a00d8)
2026-09-12 17:39:27 +01:00
ayushcodes10 cd8552242c Report unclassified files from graphify update too
graphify extract has printed this since #1692, but the update and
watch rebuild path never did, so a corpus with no extractor for its
language, Lean 4 in the reported case, rebuilt successfully with
those files completely absent and nothing said about it. Prints the
same wording from the extract command, and threads the count into
the local detection summary the rebuild path already builds for
GRAPH_REPORT.md, which previously dropped it. Fixes #3511.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 120405673f)
2026-09-12 17:36:10 +01:00
ayushcodes10 c9da36d87c Surface unclassified files in GRAPH_REPORT.md
detect() already returns every file it saw but could not classify, no
supported extension or shebang, but nothing read it: a corpus mostly
in an unsupported language got the same well covered verdict as one
that was actually extracted, silently. Adds an Unclassified line to
the Corpus Check section naming the count and the biggest offending
extensions. Toward #3511.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit f08e50534d)
2026-09-12 17:36:10 +01:00
ayushcodes10 0fd1f141d4 Guard os_replace_with_fallback against src equal to dst
Replacing a path with itself, if the initial os.replace call ever
fails for that call, crashed: the fallback backs dst up by renaming
it aside before landing the new content, and when src and dst are
the same path that rename moves src out from under itself, so the
final unlink of src then raised FileNotFoundError. Not reachable
through any current caller, every one passes a distinct temp file,
but cheap to guard against directly. Found by the graphify review
bot on PR 3509.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 5b108e1a1f)
2026-09-12 17:36:09 +01:00
ayushcodes10 4c27b61058 Restore the destination if the fallback swap fails
The fallback removed whatever sat at the destination and then
renamed the new content into place as two separate steps, so a
failure in the second step left nothing at that path at all, worse
than the original write it was trying to protect. Now renames the
existing destination aside as a backup instead of unlinking it, and
renames the backup straight back if the final rename fails, so a
mid swap failure restores the original rather than losing it. Found
by the graphify review bot on PR 3509.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit b56c77a107)
2026-09-12 17:36:09 +01:00
ayushcodes10 4489552b55 Preserve replace semantics for a symlinked destination
os.replace atomically swaps whatever sits at the destination, a
symlink included, by replacing the link itself. install.py relies on
exactly that for its managed skill symlinks, per #3286. The
fallback's plain shutil.copy2 does the opposite when the destination
is a symlink: opening it for writing follows the link and overwrites
whatever it points at instead of the link. Copies to a fresh temp
file beside the destination first, then swaps the destination out
from under it with an unlink plus rename, matching replace's own
behavior and, as a side effect, shrinking the window where a reader
could see a half written file down to two fast metadata operations
rather than however long the copy itself takes. Found by the graphify
review bot on PR 3509.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 39effa0c28)
2026-09-12 17:36:09 +01:00
ayushcodes10 784b1fb1bd Use the replace fallback for skill install writes
Fixes #3508's other reported failure: the Kiro skill install died on
the same WinError 17 while replacing SKILL.md. Covers the version
stamp write too, which shares the same pattern beside it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit e6ac02a49d)
2026-09-12 17:36:09 +01:00
ayushcodes10 c9aff7fb06 Use the replace fallback for GraphML export
The GraphML writer replaced its temp file directly with no Windows
fallback of any kind, so a mid size export could hit the same class
of failure as the cache and install paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit ab2bde0956)
2026-09-12 17:36:09 +01:00
ayushcodes10 f71e75eb31 Use the replace fallback for AST cache writes
The AST extraction cache writes its own temp file and replace, a
separate code path from the shared atomic writer, and only caught
PermissionError. This was the exact write named in the report: worker
failed for a file with WinError 17 while replacing a cache entry
under the output directory's ast cache. Also covers the stat index
write, which had no Windows fallback at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 7638d47565)
2026-09-12 17:36:09 +01:00
ayushcodes10 df5a9042e3 Add a shared Windows replace fallback helper
os.replace can fail on Windows even when the temp file and the
destination are in the same directory on the same drive: WinError
17, "cannot move to a different disk drive", maps to a plain OSError
in Python rather than PermissionError, so the existing copy then
delete fallback for WinError 5 and 32 never caught it. Adds
os_replace_with_fallback, checking both PermissionError and winerror
17, and points the shared atomic writer at it instead of its own
inline PermissionError only version. Toward #3508.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 32cbf1f83e)
2026-09-12 17:36:09 +01:00
safishamsi 522ea9662e release: 0.9.59 — incremental dedup node-loss fix, pool-start fallback, 47% faster Python resolution, explain path::Symbol, hook rebuild-root clamp v0.9.59 2026-09-12 15:17:30 +01:00
ayushcodes10 7a09d9ae57 Catch symlink loop RuntimeError in the root guard
Path.resolve() raises RuntimeError, not OSError, when a symlink chain
loops back on itself, so the #3265 in repo check let that propagate
up instead of falling back to the repo top like every other bad
marker value. Since a fork or PR can commit an actual symlink loop
alongside a crafted .graphify_root value, an untrusted checkout could
still turn the guard itself into a failure path. Both rebuild bodies
now catch RuntimeError alongside OSError. Found by the graphify
review bot on PR 3492.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 96d48c76b5)
2026-09-11 23:58:32 +01:00
ayushcodes10 e581565e95 Add regression tests for the git hook root guard
Executes the shipped snippet directly (extracted from the rebuild
body text) rather than a hand copy that could quietly drift from it,
covering both the rejected case (a marker pointing outside the repo)
and the case the guard must not break (a subdirectory scoped root).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 5976f7122a)
2026-09-11 23:58:32 +01:00
ayushcodes10 9e734a1ad2 Keep the git hook rebuild root inside the repo
Team setup documents committing the graphify output directory, and
.graphify_root lives inside it, so its contents are checkout
controlled. Both generated git hooks read that file and pass its
value straight to _rebuild_code with no bound, so a value planted
there by a forked PR could point the rebuild, and therefore what it
reads and what it writes back into that same committed directory, at
a location outside the repository the hook runs in. Both rebuild
bodies now only adopt the saved root when it resolves inside the
working tree the hook is running from, falling back to the repo top
otherwise. Fixes #3265.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 31aa4d4e9d)
2026-09-11 23:58:32 +01:00
ayushcodes10 3cd785c65d Stop path::Symbol from shadowing a literal :: label
A native Rust or C++ label containing "::" could be hijacked by the
new path::Symbol resolution when the substring before "::" happens to
match some unrelated file elsewhere in the graph, for example an
extensionless file literally named the same as a Rust module. The
query would then resolve to a node in that unrelated file instead of
the node the literal label actually names. Path scoped resolution now
only runs when the raw, unsplit label does not already have a literal
exact match among real node labels, since a genuine label essentially
never equals a whole path plus symbol string verbatim. Found by the
graphify review bot on PR 3491.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 642f62a0e1)
2026-09-11 23:58:31 +01:00
ayushcodes10 6e19e0eb31 Point the ambiguity message at path::symbol
The prior retry suggestion, a bare repo relative path, resolves to the
file node rather than the symbol, so following it never actually
disambiguates anything. Now that path::symbol exists, point both the
explain CLI and the shared MCP resolver at it, using a generic
placeholder rather than one of the rival paths so the message stays
the same regardless of node iteration order.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit c18f3d0a2c)
2026-09-11 23:58:31 +01:00
ayushcodes10 f9b6f21194 Add path::Symbol resolution for explain
A bare symbol name that exists in multiple files is ambiguous, and the
only suggested retry (a bare repo relative path) resolves to the file
node itself, never the symbol, so it could not actually disambiguate
anything. This adds a path::Symbol form that restricts the label match
to nodes defined in that file, wired into _find_node_tiers ahead of
the ordinary tiers so it is available to explain, get_node and
get_neighbors alike. Fixes #3485.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 84a2da2769)
2026-09-11 23:58:31 +01:00
himanshupatro-334 2f2bfd1bc2 test: route carried-hyperedge dedup through a re-extracted chunk (#3503)
Under #3503 (#3477) existing nodes from untouched files are protected from
dedup collapse, so the carried-hyperedge remap test must deliver the duplicate
via an incoming re-extracted chunk to fold into the protected survivor.
2026-09-11 22:51:35 +01:00
safishamsi 047dd93187 fix(resolution): clear the #3500 path-resolution caches per extract() run
The memoized path/source-key lru_caches are keyed by (path, cwd) with no mtime
component; without a per-run clear they replay stale results across repeated
extract() calls in a long-lived watch/MCP process. Wire them into the existing
cache-clear block. (follow-up to #3500)
2026-09-11 22:47:27 +01:00
abhay-codes07 cd8d99b2f4 perf(resolution): memoize Path.resolve() across the symbol-resolution passes
The symbol-resolution and cross-file import passes call Path.resolve() once
per import/export/use fact and per node — ~44k calls over a few hundred
distinct corpus paths, each walking nt._getfinalpathname on Windows. A
per-(path, cwd) memo collapses that to one syscall per distinct path. On a
364-file self-corpus a sequential extract drops from ~27s to ~14s, graph
byte-identical. The cwd is part of the key so a relative path resolved after
a chdir misses rather than replaying a stale answer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit 5ca30dfd8a)
2026-09-11 22:46:36 +01:00
abhay-codes07 d7de19d47d perf(resolution): memoize source-key resolution in the id-disambiguation pass
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit 2adfa810c9)
2026-09-11 22:46:36 +01:00
abhay-codes07 f077217191 perf(resolution): parse each Python file once across both resolution passes
The symbol-resolution facts pass and the cross-file import pass each parsed
the whole .py corpus, back to back, in the main process — every file read
and tree-sitter-parsed twice for nothing. _parse_python_tree now memoizes on
(path, mtime, size), so the second pass reuses the first pass's tree while a
file changed between watch runs still re-parses. On a 364-file self-corpus a
sequential extract drops from ~27s to ~24s, graph byte-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit dcbab53879)
2026-09-11 22:46:27 +01:00
abhay-codes07 a572b88777 perf(resolution): walk the Python tree iteratively, not via recursive yield-from
_walk_python_tree drove the symbol-resolution passes through a recursive
generator: one suspended frame per tree level, every node re-propagated up
the whole chain — ~25M frame resumptions on a 364-file corpus for ~2.8M
nodes. An explicit stack yields the identical preorder (node identity and
order verified) in ~30% less time, and no longer risks RecursionError on a
deeply nested tree. Same rewrite _walk_js_tree already carries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit e1544e0ad4)
2026-09-11 22:46:26 +01:00
Curtis Muir 3f6ca655dd fix(extract): fall back to sequential when the process pool cannot start
_extract_parallel falls back to sequential on BrokenProcessPool, but an
OSError raised while constructing the ProcessPoolExecutor escaped and
killed the whole extraction (and `graphify update`).

Seen on macOS: ProcessPoolExecutor -> multiprocessing SemLock -> sem_open
fails with OSError(ENOSPC), "No space left on device", once leaked POSIX
named semaphores exhaust kern.posix.sem.max — with the disk nowhere near
full. Sequential extraction needs no semaphore, so hand back False the
same way the BrokenProcessPool branch does, with a warning that names the
OS error rather than the Windows __main__ hint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit c154c01d50)
2026-09-11 22:46:26 +01:00
himanshupatro-334 8a89253fec Scope deduplication during incremental merges
(cherry picked from commit 9e65a71993)
2026-09-11 22:46:15 +01:00
safishamsi 23f2ffaa43 release: 0.9.58 — python/php/bash import resolution, dedup source-path normalization, SQL indexes, rust statics, install/labelling robustness, all-extra psycopg v0.9.58 2026-09-10 23:19:02 +01:00
safishamsi 519f322125 chore(lock): sync uv.lock for the psycopg[binary] all-extra (#3483) 2026-09-10 23:05:15 +01:00
L4XB fc179f3394 fix(packaging): include psycopg[binary] in the all extra
The postgres extra's psycopg[binary] was the sole dep missing from all; a new
union test guards the all-extra against future drift. (#3483)
2026-09-10 23:05:04 +01:00
L4XB ec12e5e341 test: isolate backend-detection tests from the developer's environment
Autouse conftest fixture clears every backend env var detect_backend() reads
(API keys, Azure/AWS/Ollama), with a self-guard test that the cleared list
covers every variable the detector reads. (#3484)
2026-09-10 23:04:40 +01:00
safishamsi 2f9279f108 test(labeling): keep the no-backend test hermetic against the claude-cli fallback
#3475 added a PATH probe for a claude-cli labelling fallback; the no-backend
test must neutralize it so it stays deterministic on machines with claude
installed (follow-up to #3475).
2026-09-10 22:45:31 +01:00
ktsang622 d9e094c2e2 fix: label via an installed claude CLI, and stop pinning a path that expires
Two independent failures, both silent, found while repairing 16 repositories whose
graphs had quietly stopped tracking their code.

1. `graphify label` downgraded graphs while reporting success

`detect_backend()` is key-based, and `claude-cli` is the one backend with no API key
to find — `_get_backend_api_key` can never return one for it, and the fallback loop
excludes it by name. Nothing else looked for it. So on a machine with the Claude Code
CLI installed and no API key, labelling announced "no LLM backend configured",
replaced every real community name with a `Community N` placeholder, and exited 0.
Run over an existing graph, that overwrites good names with worse ones and looks like
success; taking the exit code at face value commits the damage.

The fallback is placed in the labelling path, NOT in `detect_backend()`. Widening
detection itself was the first attempt and it broke
`test_mixed_repo_without_key_errors_and_points_at_code_only`: extraction deliberately
refuses to run without a configured backend and points at `--code-only`, and on any
machine with the CLI present that contract would have silently changed to "shell out
to the CLI instead". That contract is unchanged here, and a test now pins it.

2. The post-commit hook pinned a path with an expiry date

`_pinned_python()` wrote `sys.executable` verbatim. graphify installed from inside a
snap-confined editor lives under `~/snap/<app>/<revision>/`, and snap swaps that
revision on update and prunes the old tree. Every hook pinned to it then dies —
observed across 15 repositories at once when an editor snap moved past revision 259.
Each commit printed "could not locate a Python with graphify installed" and the graphs
stopped following the code, with nothing failing loudly enough to notice.

Two changes: `_pinned_python()` declines to pin a path under a rotating revision
directory, which its docstring already describes as safe degradation; and the
uv-tools probe — which already scans tool environments — additionally scans
snap-confined HOMEs by glob, since an install made inside a snap lands in that snap's
private HOME and the plain `$HOME` roots never see it once the hook runs from an
ordinary shell.

Verified: same 25 pre-existing failures before and after (unrelated: terraform
fixtures, a TS scaling test); passing goes 5110 -> 5119, the nine tests added here.
Each change was mutation-tested — removing it fails a test that the others still pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit c6065e24ea)
2026-09-10 22:44:40 +01:00
dajiaohuang 496112d6fc fix(install): do not abort the install when always-on registration is unwritable
`install()` copies the skill files first and writes the always-on
registration afterwards, so an unguarded write to `~/.claude/CLAUDE.md`
left a half-completed install plus a traceback whenever that file could
not be written. That is the normal shape for a declaratively managed
dotfile: nix/home-manager symlinks it into a read-only /nix/store, and
chezmoi or stow with read-only sources do the same.

Route the two registrations inside `install()` (CLAUDE.md and
CODEBUDDY.md, which had the identical unguarded pattern) through one
helper that reports the skip on stderr and lets the install finish, so
the skill files that were already laid down stay usable.

Refs #3474

Co-Authored-By: Claude Code <noreply@anthropic.com>
(cherry picked from commit 47041eef5d)
2026-09-10 22:44:40 +01:00
himanshupatro-334 437c9629bd Fix same-file source path collision during merge
(cherry picked from commit fa1284a356)
2026-09-10 22:44:39 +01:00
L4XB ab80fa7413 fix(rust): extract module-level static and const declarations
The Rust extractor gated every non-function item on struct_item,
enum_item and trait_item, so tree-sitter-rust's static_item and
const_item never produced a node: a constant existed in the graph only
through the files that referenced it, never from the Rust that defines
it (0 of 783 names in a jdx/mise snapshot).

Add a branch for both node types: a module-level declaration gets a
node with a file-level contains edge, an associated const inside an
impl is attributed to the impl like a method, and the declared type is
referenced the way a struct field type is.

Fixes #3471

(cherry picked from commit 70ff464064)
2026-09-10 22:44:38 +01:00
safishamsi adf814fd5e docs(readme): replace the header card with the graphify logo (png) 2026-09-10 15:39:24 +01:00
safishamsi b15cc167ee docs(readme): replace the header logo with the animated graphify card 2026-09-10 15:30:39 +01:00
L4XB d2abe4ed0c fix(sql): emit index nodes for CREATE [UNIQUE] INDEX statements
Adds an AST branch for create_index in the SQL extractor, emitting an index
node with an 'indexes' edge to its table (cross-file stub when the table is
defined elsewhere), mirroring the existing trigger handling. (#3470)
2026-09-10 14:51:54 +01:00
L4XB 355eefd391 fix(ts): keep star-export resolution off same-named interface methods
A workspace-barrel import (`import { evaluate } from '@scope/domain'`)
resolves through `export *` by probing each star target for a symbol
node with the imported name. The symbol map normalizes labels by
stripping the leading dot, so an interface/class METHOD `.evaluate()`
in the first star target satisfied the probe before the exported
function in the second one was reached. The import edge and the
cross-file call then bound to the method node, and the real function
lost its inbound `calls` edge (read as dead code).

Track member nodes separately and skip them in the star-export walk: a
module can only re-export top-level bindings. A member never shadows a
same-named top-level symbol in the map anymore either.

Fixes #3436

(cherry picked from commit 52e4a2b131)
2026-09-10 14:03:23 +01:00
xuxiongjian 76de060036 Never infer an empty repo tag for global add
`tag or source.parent.parent.name` is empty whenever the graph is not two levels
below a named directory: `graphify global add /tmp/graph.json`, or any relative
path. Every node is then prefixed `::`, the prune key is `""`, and the manifest
entry cannot be removed because the `remove` subcommand reads an empty tag as a
missing argument.

Infer through `distinct_repo_tags`, the helper `merge-graphs` already uses, so the
two entry points cannot diverge again and the degrade is `"repo"`; absolutize
first so a relative path names the repo dir rather than the cwd placeholder. An
omitted `remove` tag stays a usage error, while an explicitly empty one is now
addressable so stores written by earlier revisions can be cleaned up.

(cherry picked from commit 6a317e6ef2)
2026-09-10 14:03:22 +01:00
xuxiongjian dba63f7c9e Import Iterable in build.py, the annotations already use it
build.py annotates chunks and ast_sources with Iterable but never imports it, so
ruff reports four F821 on v8 and typing.get_type_hints() raises on
_tier_replacement_sources, merge_raw_extraction and build_merge. Nothing breaks at
import because `from __future__ import annotations` keeps the hints as strings.

The added test walks build.py's own functions rather than the package: nx under
if TYPE_CHECKING in cross_repo_calls.py, cross_repo_types.py and prs.py is
unresolvable on purpose, while build.py imports every annotated name at module scope.

(cherry picked from commit c315a15c24)
2026-09-10 14:03:22 +01:00