Commit Graph
1281 Commits
Author SHA1 Message Date
ozdemirsarman d1f303e237 fix(swift): extract computed & observed properties (#2181)
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.
2026-07-27 10:22:48 +01:00
safishamsiandClaude Opus 4.8 62b1d4370b docs(readme): link the YC badge to the company page
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 00:19:11 +01:00
safishamsiandClaude Opus 4.8 f5a3592882 chore(release): 0.9.27
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.9.27
2026-07-26 13:01:17 +01:00
safishamsiandClaude Opus 4.8 b4dd6d7b25 docs: changelog for the Track A / node-identity / C# batch (0.9.27)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 13:00:06 +01:00
safishamsiandClaude Opus 4.8 0858954db9 feat(csharp): namespace-aware member-call resolution + shadow poisoning (#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>
2026-07-26 12:43:17 +01:00
safishamsiandClaude Opus 4.8 8adb261d16 fix(build): fold legacy node/edge aliases and re-key absolute-derived 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>
2026-07-26 11:52:20 +01:00
safishamsiandClaude Opus 4.8 334cff6172 fix(cache): portable stat-index keys + normalized semantic source_file (#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>
2026-07-26 11:52:20 +01:00
safishamsiandClaude Opus 4.8 e395ff9b43 fix(dedup): merge cross-file concept nodes with identical normalized 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>
2026-07-26 11:52:20 +01:00
safishamsiandClaude Opus 4.8 d16510ed4a fix(extract): canonicalize regex-rescue import target ids (#2195)
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>
2026-07-26 11:52:20 +01:00
safishamsiandClaude Opus 4.8 9eb76ace30 fix: review follow-ups for the bash/SQL contributor PRs (#2171/#2172/#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>
2026-07-26 11:18:13 +01:00
Souptik Chakraborty 911d58178f fix: document the Codex PreToolUse hook as an intentional no-op (#2165)
`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.
2026-07-26 11:10:35 +01:00
Souptik Chakraborty ffa2a2471a fix: recover every declared SQL routine from unparseable PL/pgSQL (#2180)
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).
2026-07-26 11:08:10 +01:00
Souptik Chakraborty cfe15f9161 fix: pin interpreters whose path contains a space (#2166)
`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=''`.
2026-07-26 11:08:10 +01:00
Souptik Chakraborty 44241dd10c fix: resolve ${VAR} bash sources against the variable's real base (#2172)
#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.
2026-07-26 11:08:10 +01:00
Souptik Chakraborty 4710b864ea fix: resolve bash sources for extensionless libs and bare names (#2171)
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.
2026-07-26 11:08:09 +01:00
Souptik Chakraborty 556e615cc6 fix: skip the process pool when only one worker is available (#2173)
`_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.
2026-07-26 11:08:09 +01:00
safishamsiandClaude Opus 4.8 4191b7dee3 chore: sync uv.lock to 0.9.27
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 11:06:51 +01:00
safishamsiandClaude Opus 4.8 52e1ed4050 test(resolution): assert canonical import-target ids in jsconfig tests (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>
2026-07-25 23:16:52 +01:00
safishamsiandClaude Opus 4.8 a8da0d3821 chore: bump to 0.9.27; changelog for #2167/#2169/#2154/#2153/#2147/#2168
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 22:54:48 +01:00
safishamsiandClaude Opus 4.8 fbc24c7d3f fix(extract): suppress builtin/stdlib Python decorators from reference 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>
2026-07-25 22:53:43 +01:00
safishamsiandClaude Opus 4.8 137dcf23fe fix(extract): incremental --no-cluster merges instead of overwriting the graph (#2169)
An incremental `extract --no-cluster` wrote only the changed files over
graph.json with no merge, dropping every node/edge owned by an unchanged
file; and the id-canonicalization pass only learned batch files, so the
changed file's cross-file edges kept absolute-path target ids and
dangled. The raw path now merges the existing graph forward with the same
replace/prune semantics as the clustered path (new merge_raw_extraction
helper in build.py, shared loader), refuses to overwrite a corrupt
existing graph, and the remap pass now also learns in-root edge
target_file paths (existence-gated) so cross-file targets canonicalize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 22:53:43 +01:00
safishamsiandClaude Opus 4.8 05ee568969 fix(install): never clobber an unparseable settings file; back up before write (#2167)
The hook installers fell back to settings={} on any JSON parse error and
then overwrote the whole file, destroying the user's config (the likely
trigger is a UTF-8 BOM, same class as #2163). All four installers now
read utf-8-sig, refuse to modify a file that isn't a JSON object (naming
the path) instead of clobbering it, back up to <name>.graphify-bak before
any modifying write, skip the write when content is unchanged, and guard
the PreToolUse filter against non-dict entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 22:53:43 +01:00
safishamsiandClaude Opus 4.8 c18ec81741 test: sandbox HOME for the whole suite so installers can't touch real config (#2168)
Several test files call install/uninstall functions that operate on the
real user home (~/.claude, ~/.gemini, ~/.codebuddy, ~/.copilot), so
running the suite deleted/overwrote the developer's actual config. An
autouse conftest fixture now points HOME/USERPROFILE/LOCALAPPDATA at a
throwaway dir and clears CLAUDE_CONFIG_DIR/XDG_CONFIG_HOME for every
test. Supersedes the per-file sandbox proposed in #2057 (thanks
@erlandl4g for surfacing it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 18:02:33 +01:00
MasterFede5andClaude Fable 5 33aa89c722 fix(extract,analyze): filter Swift/Foundation/SwiftUI builtins from resolution and god-node ranking (#2147)
_LANGUAGE_BUILTIN_GLOBALS and _BUILTIN_NOISE_LABELS covered only JS/TS and
Python, so on Swift codebases framework symbols (Foundation, NSLock, View,
Data, Sendable, ...) ranked as god nodes, and the Swift member-call resolver
could bind a builtin-typed receiver (let d: Data) to a same-named user symbol
in another file — the same phantom-edge shape #1726 fixed for TypeScript.

- extractors/base.py: add Swift stdlib value types, conformance protocols,
  Foundation types, and SwiftUI View/Color/Font to _LANGUAGE_BUILTIN_GLOBALS
- analyze.py: add the same set plus framework module names (Foundation,
  SwiftUI, UIKit, AppKit, Combine) to _BUILTIN_NOISE_LABELS
- extract.py: _resolve_swift_member_calls now skips builtin receiver types,
  matching the guard the TS/Python member-call resolvers already have (#1726)
- tests: god_nodes exclusion (parametrized) + Swift builtin-receiver
  no-bind regression + user-type-still-resolves guard

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 15:44:50 +01:00
Rishet Mehra 6107f14545 test(resolution): cover jsconfig/baseUrl module resolution (#2153)
Eleven cases: the webpacker repro (jsconfig + baseUrl, no paths) for
static, dynamic and extensionless specifiers; the same for tsconfig;
tsconfig winning over jsconfig in one directory; and four preservation
guards that pass before and after — declared paths and directory-prefix
aliases are not shadowed, relative imports are untouched, an absent
baseUrl changes nothing, and an external package is not fabricated.
2026-07-25 15:44:50 +01:00
Rishet Mehra 9be345951c fix(resolution): honor jsconfig.json and compilerOptions.baseUrl (#2153)
Non-relative imports produced no edge in a Rails/webpacker project, so
every module under `baseUrl` was orphaned and `affected` answered nothing.

Two defects. First, only `tsconfig.json` was probed, never
`jsconfig.json` — the plain-JS spelling of the same file, which
json_config.py already indexes, so the config's nodes appeared in the
graph while resolution ignored it entirely. Second, `baseUrl` was
consumed only as the base that `paths` targets resolve against, so a
config declaring `baseUrl` and NO `paths` produced an empty alias map and
every bare specifier died.

`_find_js_config` now probes both names, tsconfig winning within a
directory as tsc and editors do. `baseUrl` is exposed separately and used
as a resolution root of LAST RESORT, tried only when no declared alias
matches, so `paths` precedence (#1269, #927, #1531) is untouched. It is
deliberately not modelled as a synthesized `*` alias: that would score
(1, 0) in _match_tsconfig_alias and beat a declared non-wildcard
directory-prefix alias at (2, -len), silently shadowing it. The fallback
also returns a candidate only when it is a real file, so an external
package import is not fabricated into a <baseUrl>/<pkg> edge.

Threaded through the three regex-rescue dynamic-import paths (Svelte,
Astro, TS/TSX) as well as static resolution, since the issue reports both.
2026-07-25 15:44:50 +01:00
Rishet Mehra 8d8005b835 test(extract): cover Python decorator reference edges (#2154)
Nine cases: the issue's imported-decorator repro, same-file resolution to
the local definition, called and attribute decorators, stacked
decorators, class-qualified method owners, a decorated class, the #1050
@property class-qualification regression guard, and an absence control.
2026-07-25 15:44:50 +01:00
Rishet Mehra 794cf9ebb6 fix(extract): emit references edges for Python decorators (#2154)
Applying a Python decorator emitted no edge to the decorator symbol, so
`affected <decorator>` answered "No affected nodes found" for every
function it wraps — a silent false negative on reverse-impact queries.

TS/JS already emit these edges via `_ts_emit_decorator_edges`. The Python
`decorated_definition` branch walked its children only to propagate the
parent class id (#1050) and never looked at the `decorator` children.

Python now emits the same shape: a `references` edge with
context="decorator" from the decorated function/class to each decorator
symbol. Owner ids reuse the definition branches' own formulas, so the
edge lands on the node the walk creates. Targets go through
`ensure_named_node`, so an imported decorator becomes a sourceless stub
the corpus rewire collapses onto its real definition. Stacked, called
(`@retry(3)`) and attribute (`@app.route`) decorators are covered; the
attribute form targets the symbol, not the module alias, matching
`_ts_decorator_name`.
2026-07-25 15:44:50 +01:00
safishamsiandClaude Opus 4.8 66d8110a53 chore(release): 0.9.26
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.9.26
2026-07-25 00:09:40 +01:00
safishamsiandClaude Opus 4.8 a4fb87d1bd chore: bump to 0.9.26; changelog for #2137/#2126/#2148/#2141/#2079/#2163
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 23:48:21 +01:00
safishamsiandClaude Opus 4.8 eebf030f5b test(extract): cover calls into a ${VAR}-sourced bash lib (follow-up to #2139)
The #2139 ${VAR} source handler now also records bash_sources so
resolve_bash_source_edges binds calls into the sourced lib's functions,
not just the source edge. Add the end-to-end oracle plus the previously
untested _bash_source_suffix guards (mid-path $, whole-var, .. traversal
fabricate nothing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 23:47:25 +01:00
safishamsiandClaude Opus 4.8 7e955d223b fix(detect): honor ignore files saved with a UTF-8 BOM (#2163)
.gitignore/.graphifyignore/info-exclude read with encoding=utf-8 kept a
leading BOM (U+FEFF) on the first line, so the first pattern (e.g. *.log
or .fable-wt/) silently matched nothing and a BOM'd full-line comment
became a bogus pattern. git strips a single leading BOM; switching the
two ignore read sites to utf-8-sig matches git exactly (strips at most
one, file-start only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 23:47:25 +01:00
HerenderKumar 0019fc4d90 fix(extract): resolve bash source edges built from ${VAR} paths (#2079)
`source "${BENCH_DIR}/lib/x.sh"` (the `dirname "${BASH_SOURCE[0]}"` idiom)
took the bare-name branch, which baked the unexpanded `${BENCH_DIR}` text
into the target id via `_make_id`. That id matches no node, so the edge was
flagged dangling and dropped at export — shared shell libraries looked
orphaned and were split into separate communities.

Detect a `$`-expansion in the source argument, strip the leading
expansion segment(s), and resolve the literal suffix against the script's
own directory (which is what the canonical idiom makes the variable). Emit
`imports_from` as INFERRED only when it resolves to a real file on disk;
skip otherwise instead of emitting a dead id. Bare-name sources keep their
existing behavior.
2026-07-24 23:41:28 +01:00
HerenderKumar bdcae25a26 fix(extract): resolve bash calls into sourced-file functions (#2141)
extract_bash only linked calls whose callee was defined in the same file, so a
call to a function from a `source`d library was silently dropped and shell
scripts looked disconnected from the libraries they use. The resolver for exactly
this, resolve_bash_source_edges, already existed with tests but had no call site
and no raw_calls/bash_sources to work from.

extract_bash now emits `bash_sources` (the files it `source`s) and `raw_calls`
(calls whose callee isn't defined locally), and the pipeline runs them through
resolve_bash_source_edges after the id-remap passes, so caller and function node
ids are final and the already-emitted source edge is de-duped. Resolution is
scoped to the source relationship: a call to an external command never binds to a
same-named function in an unsourced file, and bash raw_calls are excluded from the
generic global-name resolver for the same reason.
2026-07-24 23:37:14 +01:00
Rishet Mehra 0da6929e57 fix(hooks): match the post-checkout log prefix in the timeout fallback
The post-checkout body logs with [graphify] throughout, but the new
non-SIGALRM fallback used [graphify hook], so the same timeout read
differently depending on the platform. The post-commit body does use
[graphify hook] everywhere, so only the checkout copy was wrong.

Assert each body uses a single log prefix so this cannot drift again.
2026-07-24 23:37:14 +01:00
Rishet Mehra ca3113ac7b fix(hooks): arm the rebuild timeout without SIGALRM on Windows
The GRAPHIFY_REBUILD_TIMEOUT watchdog in both embedded rebuild bodies was
guarded by hasattr(signal, 'SIGALRM') with no else branch, so on Windows it
never armed and a hung rebuild survived indefinitely -- the exact tail case
the #791 timeout was added to catch.

Fall back to a daemon threading.Timer that prints the same message and calls
os._exit(1). The hard exit is deliberate: the process is already stuck, so a
clean shutdown may itself be blocked, and _rebuild_lock degrades to a no-op
yield on platforms without fcntl, so there is no lock to leave stale.
2026-07-24 23:37:14 +01:00
Rishet Mehra ce9ea7d7b4 test(hooks): surface bash failures in _shell_verdict helper
Assert returncode == 0 so a malformed case snippet fails fast with
stderr instead of silently returning an empty string. Addresses
Copilot review feedback on #2133.
2026-07-24 23:37:14 +01:00
Rishet Mehra e1e041b09b fix(hooks): accept Windows backslash paths in interpreter allowlist (#2126)
The post-commit/post-checkout hook's interpreter-detection allowlist
silently rejected valid Windows paths (C:\...\python.exe) on Git-Bash.
bash treats a lone backslash inside [...] as an escape that consumes
itself, so the emitted glob never matched a real backslash at runtime,
even though the pattern looked correct in Python's install-time re dialect.

Fix both allowlists (.graphify_python file path and shebang-parsed
launcher path) to emit [!a-zA-Z0-9/_.@:\\-], a doubled-backslash form
verified against bash and dash: it accepts Windows paths and still
rejects ; ` $ injection. The shebang allowlist additionally lacked :
and backslash entirely. Install-time _pinned_python() re is left as-is.

Add shell-runtime tests that execute the emitted case/esac glob directly
against Windows paths and shell metacharacters.
2026-07-24 23:37:14 +01:00
Rishet Mehra 26cac4192d test(extract): cover cross-file class-ref indirect_call suppression (#2137)
Copilot flagged that the #2137 regression test only exercised the
intra-file suppression path; a regression in the cross-file resolver
guard in extract.py would still pass. Add a cross-file test: class and
function imported from another module, asserting the imported class is
never an indirect_call target while a genuine imported callback still
emits its edge.
2026-07-24 23:37:14 +01:00
Rishet Mehra f6742bb6f0 fix(extract): exclude class refs from indirect_call edges (#2137)
Classes are callable via their constructor but are frequently referenced as
descriptive values, not invoked: ORM args (select(Model), db.get(Model, id)),
exception tuples (except (ErrorA, ErrorB)), and string-literal getattr resolving
to a same-named class. The indirect_call guard treated any callable-def target
identically, so these produced false edges (~41% of indirect_call edges in the
reported sample targeted classes), inflating centrality and traversals.

Track class defs in a callable_class_nids set parallel to callable_def_nids,
mark class nodes with a _callable_class attribute, and exclude class targets
from indirect_call emission in both the intra-file (_emit_indirect_by_name) and
cross-file resolver paths. Marker is stripped before output like _callable.

Covers all languages: both class-node creation sites (the generic
config.class_types branch and the Ruby Struct.new/Class.new/Data.define
synthesis) register into the new set.

Tradeoff: suppression is context-blind, so a genuine higher-order class
callback (e.g. map(Point, coords)) also loses its indirect_call edge. This is
far rarer than the false-positive noise removed and matches the issue framing.

Verified before/after on the same input: 4 class-targeted indirect_call edges
-> 0, function callbacks preserved.
2026-07-24 23:37:14 +01:00
safishamsiandClaude Opus 4.8 2fa6cd3d55 chore(release): 0.9.25
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.9.25
2026-07-22 23:56:15 +01:00
safishamsiandClaude Opus 4.8 ba7f9ea9d8 chore: relicense from MIT to Apache-2.0
Apache 2.0 adds an explicit patent grant, a patent-retaliation clause,
and explicit inbound-contribution terms. MIT's sublicense right permits
relicensing the combined work, so this needs no per-contributor consent;
prior contributions were made under MIT and remain available under those
terms. The original MIT text is retained in LICENSE-MIT and referenced
from NOTICE.

- LICENSE: verbatim Apache License 2.0
- LICENSE-MIT: preserved MIT text for prior contributions
- NOTICE: attribution + pointer to LICENSE-MIT
- pyproject: license = "Apache-2.0" (PEP 639 SPDX), license-files, setuptools>=77

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:50:47 +01:00
safishamsiandClaude Opus 4.8 363f036e5e docs(readme): drop the broken star-history chart
The inline api.star-history.com SVG rate-limits on a repo this large and
serves a 503 instead of a chart, so the image was permanently broken.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 22:30:22 +01:00
safishamsiandClaude Opus 4.8 0d1f25c221 refactor(detect): remove dead .graphifyinclude handling (#2112)
The .graphifyinclude loader and its two matcher helpers had no consumers:
commit df40e4d (#873, index dot dirs) removed the blanket dot-prefix
exclusion and with it the only call sites, leaving detect() parsing the
file on every run and then discarding the result. A .graphifyinclude was
silently a no-op.

Delete _load_graphifyinclude, _is_included, _could_contain_included_path
and the orphaned assignment; add .graphifyinclude to _SKIP_FILES so a
leftover file no longer lands in unclassified; and print a one-time
stderr note when one is present at the scan root, pointing to ! negation
patterns in .graphifyignore. Bump to 0.9.25.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:35:54 +01:00
safishamsiandClaude Opus 4.8 ad53f751f9 chore(release): 0.9.24
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.9.24
2026-07-22 19:46:09 +01:00
safishamsi 9b1ac65de5 docs: changelog for the XAML .cs-scan hang fix (0.9.24) 2026-07-22 19:31:42 +01:00
safishamsi 2da192db56 fix(extract): bound the XAML code-behind .cs scan so it can't hang
_xaml_csharp_class_nodes did `sorted(root.rglob("*.cs"))` where `root` comes from
_xaml_project_root walking up for a .csproj/.sln marker. On a standalone
extract_xaml call (no corpus boundary), a .xaml under a large/shared parent (a
temp dir, or a big monorepo) could resolve `root` to a broad ancestor, and the
rglob then recursively scanned the entire tree — effectively hanging (it stalled
the test suite intermittently once the shared temp dir grew). Replace the rglob
with an os.walk that prunes noise/hidden dirs (node_modules/.venv/.git/...) during
traversal and caps directories visited, so a real project scans fully while a
runaway root degrades to a fast, partial scan instead of a hang. Regression test
asserts a decoy .cs in node_modules is pruned and the real ViewModel still links.
2026-07-22 19:31:19 +01:00
safishamsi 8658cc3105 fix(skillgen): commit rendered skill artifacts for the #2106 traceability line (missed in 65b81bf) 2026-07-22 19:01:53 +01:00
safishamsi a3e283e109 docs: changelog for #2106 (0.9.24) 2026-07-22 18:41:21 +01:00
safishamsi 65b81bfa80 fix(skillgen): skill flow lists skipped-sensitive filenames, not just a count (#2106) 2026-07-22 18:40:19 +01:00