Two fixes on top of #2768: (1) the baked line was an unconditional
export GRAPHIFY_VIZ_NODE_LIMIT="<n>", which clobbered an explicit
GRAPHIFY_VIZ_NODE_LIMIT=... git commit; use the ${VAR:-<n>} default form
(mirroring GRAPHIFY_MAX_WORKERS) so the per-run value wins, and widen the
status drift regex to read the new form (still accepting the old bare one).
(2) A malformed .graphifyrc turned the read-only hook status into a traceback;
status now catches the parse error, warns, and continues. Adds precedence and
malformed-status tests. Also adds the CHANGELOG entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a committed .graphifyrc (viz_node_limit=<int>) that `graphify hook install`
reads and bakes into the generated post-commit/post-checkout hooks, so a
project-wide viz node limit is shared via version control and survives hook
regeneration instead of needing a hand-edit that the next --force clobbers.
`hook status` reports the value and flags drift.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the coverage the fix lacked (the shared home sandbox deletes
CLAUDE_CONFIG_DIR, so it must be set explicitly): env set -> registration lands
in $CLAUDE_CONFIG_DIR/CLAUDE.md and the default ~/.claude profile is untouched;
env unset -> unchanged ~/.claude/CLAUDE.md with the tilde skill ref. Also adds
the CHANGELOG entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the gap #2752's title implies: a generator function EXPRESSION
(function*(k){}) parses as generator_function, which was in neither
_JS_DESCEND_TYPES nor the JS/TS function_boundary_types, so it still fabricated
an indirect_call to a same-named callable. Adds generator_function to both.
Adds a generator-FE regression test and a .tsx test that locks the
_TSX_CONFIG-by-reference coupling (untested before). Full JS/TS indirect_call
/calls regression re-run green (812 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An inline/nested function expression's own params and locals were not folded
into the indirect_call shadow set, because function_expression was missing from
the JS/TS function_boundary_types even though walk_calls' closure-descend branch
already handles it. A bare reference to such a param, passed as a call argument,
fabricated an INFERRED indirect_call to an unrelated same-named callable. Adding
function_expression to the boundary sets routes it through the same shadow path
arrows use. Same family as the catch/arrow/loop/external-import (#2757) shadows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#2766 ran `git ls-files` on every scan of a git repo. Gate it on .gitignore
actually contributing patterns beyond the explicit (.graphifyignore/--exclude)
set — with no .gitignore in play nothing can be gitignore-dropped, so the
tracked-file exemption is moot and an ordinary corpus pays no subprocess cost.
Adds a spy test asserting the probe is skipped without a .gitignore and runs
once with one. Also adds the CHANGELOG entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A file that is git-tracked but also matches a .gitignore pattern (a committed
file later added to .gitignore, or a force-added one) was silently dropped from
the corpus, even though git never un-tracks such a file — so the graph lost
real, shipping source. detect now runs `git ls-files` once per scan, keeps a
set of tracked-file identity keys plus their ancestor directories (so a
git-only-ignored parent dir is not pruned before the walk reaches the tracked
file below it), and exempts tracked paths from .gitignore. .graphifyignore and
--exclude stay authoritative, and a non-git corpus is unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The #2751 recovery minted node ids via _make_id(stem, name); a punctuation-only
name (TEST_CASE("***")) normalizes to empty, collapsing onto the bare file-stem
id — colliding with the file namespace and swallowing every later such test
under one id via seen_ids (#1899). Detect the collapse and fall back to a stable
line-positional id so each stays distinct. Adds a regression test (also covers
SCENARIO) and the CHANGELOG entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
doctest/Catch2 register tests with a string-literal name (TEST_CASE("...")),
which tree-sitter-cpp parses as an ERROR node, so every test function was
silently dropped. Adds a line-anchored regex fallback that appends one code
node per top-level TEST_CASE/TEST_CASE_TEMPLATE/SCENARIO plus a contains edge
from the file node, mirroring the Groovy Spock fallback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the two gaps the deep-dive flagged: an absolute seed outside the derived
root must miss cleanly (not resolve to a same-basename in-root node), and a
--graph pointing at a graph.json NOT under graphify-out uses the else gp.parent
fallback root. Also adds the CHANGELOG entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
affected anchored an absolute-path seed to Path.cwd(), so running it from
anywhere but the repo root made relative_to(cwd) raise and the query fell
through unmatched, silently returning nothing. It now derives the repo root
from the graph's own location (<root>/graphify-out/graph.json) and anchors the
seed there, so an absolute seed resolves regardless of cwd. Composes with the
#2707 relative-seed fix (root defaults to cwd for other callers).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the two invariants the deep-dive flagged: require(variable) must not
fabricate an edge, and a module-scope require still yields exactly one
imports_from edge (guards the module-vs-body pass partition against future
double-counting). Also adds the CHANGELOG entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A require(...) written inside a function body (the canonical lazy-require idiom
for breaking circular deps) produced no edge at all, while the identical require
at module scope resolved as EXTRACTED imports_from/imports edges. walk_calls now
feeds nested require declarations to the same _require_imports_js routine,
attributing the edge to the enclosing callable; dynamic require(var) is still
skipped and no bare local node is minted (#1077 scope guard).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the collision case the fix must survive: a name both imported externally
and defined as a callable in another corpus file. The external use must not
fabricate a cross-file indirect_call while a genuine local by-name reference
still binds. Also adds the CHANGELOG entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An identifier bound by an import whose target resolves OUTSIDE the scanned
corpus (e.g. a lucide-react icon) is now shadowed within the file, so using it
as a value no longer fabricates an INFERRED indirect_call onto an unrelated
same-named callable elsewhere in the corpus. The internal-vs-external decision
is delegated to the existing import resolver, so a relative/in-corpus import
still resolves to its real target. Same shadow family as #2241/#2568/#2685.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A qualified call `M.f` where `M` is not a module defined in the same file is
an external-library call (e.g. Hardcaml's `Reg_spec.create`, `Scope.create`).
Resolving it by bare last name bound it to a same-named local `let f`,
producing a false `calls` edge and a `create -> create` self-loop when the
caller was that local `f`. Now: track locally-defined module names; a
qualified call whose root module is not local and whose bare name collides
with a local def is kept as a distinct external target (stub labelled by the
full path), so it neither self-loops nor collapses onto the local def.
Unqualified calls and calls into a locally-defined module still resolve
locally, and cross-file `Geo.area` still collapses onto another file's `area`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#2705 fixed `function` nested in a `function`, but the React idiom that
motivated the issue -- a function declared inside an arrow-defined component
(`const Panel = () => { function handleClick(){} }`) or an arrow callback
(`useEffect(() => { function h(){} })`) -- was still missed: the main walk
never recurses into arrow bodies and the scan bailed at the arrow boundary.
Refactors the inline scan into a module-level _scan_js_nested_function_
declarations that also descends through arrow / function-expression bodies
(attributing nested declarations to the nearest enclosing named scope), and
invokes it from the const-arrow branch of _js_extra_walk. Nested bodies join
function_bodies, so the central _tracked_body_ids guard prevents double-walk.
Adds tests for both arrow idioms.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A named `function`/`generator_function` declaration nested inside another
function body now gets its own node, a `contains` edge from the enclosing
function, and its own call-attribution scope, so calls made from inside such a
function are no longer dropped as dangling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The #2690 form-3 (`$(dirname …)`) branch had no `..` guard and the tracked
form-4 branch let `..` walk past the base to an arbitrary host path, so a
hostile corpus could make the extractor stat and record an out-of-tree file
(source "$(dirname "$VAR")/../../../../etc/passwd") — a corpus-side info leak,
since resolve_bash_source_edges only filters *resolved* cross-file edges, not
the extractor's own edge/probe. Adds a lexical _within_tree gate: form 3
rejects `..` outright (its base is a guess), and a tracked form-4 base may
reach a sibling via $VAR/../lib but cannot escape past base.parent. Adds two
traversal-rejection tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends variable-built `source` resolution to two path-construction forms
that were silently dropped: `source "$(dirname "$VAR")/lib/x.sh"` (command
substitution in the source argument) and `source "$VAR/../lib/x.sh"` (a
`..` suffix when the leading var is a tracked var_bases entry).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Widens _SLUG_SUFFIX_RESERVE/_DEDUP_SUFFIX_RESERVE from 4 to 5 so a four-digit
collision suffix (_1000..) can't push a truncated stem past MAX_PATH; the
suffix is technically unbounded but 5 chars covers ~10k identical stems. Adds
an end-to-end test that CJK labels at a tight budget stay within the window,
keep their non-ASCII characters, and produce links that resolve on disk.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds paths.stem_filename_budget(output_dir, *, reserve, limit=200) and threads
it through the Obsidian and wiki exporters so a filename stem is budgeted
against the whole Windows MAX_PATH window (drive + dirs + name + NUL), not
just the per-component 200-char NAME_MAX cap. On POSIX the helper returns the
limit unchanged, so existing vaults stay byte-identical; on Windows a long
output directory no longer pushes the total path over MAX_PATH and aborts the
export mid-write.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The parity check keyed on the whole [display](target) pattern, so a link
whose display text contains brackets (Array[T] Models) never matched and the
bracket case asserted nothing. Key on the ](target) boundary instead — wiki
targets contain no ) or whitespace — so bracketed labels are actually checked.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The wiki exporter wrote each article as {slug}.md but emitted the link as
quote({slug}.md), so any label with ( ) & # or non-ASCII produced a
percent-encoded target that names no file on disk. Removes the quote() fork
and hardens _safe_filename so the slug is URL-safe by construction; the link
and the on-disk filename are now the same string.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds two regression tests on top of #2684: each referencing symbol gets its
own INFERRED uses edge (guarding against the old every-class fan-out and
against source collapse), and a true module-top-level reference emits no edge
(the deliberate drop). Also adds the CHANGELOG entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrites Pass 2 of the Python cross-file import resolver so an INFERRED
`uses` edge anchors on the symbol whose body actually references the
imported name (a class as a unit, or a module-level function) at the real
reference line, instead of fanning out from the import statement line to
every class in the importing file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New graphify/extractors/ocaml.py handles both the implementation grammar
(language_ocaml, .ml) and the interface grammar (language_ocaml_interface,
.mli). Emits nodes for modules, top-level/module-level values and functions,
types and their variant constructors; edges for defines/contains, open ->
imports_from, and application -> calls. Qualified paths (Geo.area) resolve to
the final value name, not the module qualifier; local let ... in bindings do
not mint nodes or steal call attribution. Cross-file open/call targets are
sourceless stubs so the corpus rewire collapses them onto the unique real
definition (no #1402 sourced-stub leak).
Wired into detect.py (CODE_EXTENSIONS), extract.py (dispatch +
_EXTRA_FOR_EXTENSION), pyproject.toml ([ocaml] extra + all + dev dep), and
README. Adds tests/test_ocaml.py (behind importorskip).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>