Ships: id-less hyperedge load tolerance (#2775), per-platform version stamp
completing #2694, Go case-only symbol collision (#2779), and .graphify_root
anchor validation (#2603).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two small hardening tweaks on top of #2778: comment that in the hook path
Path.cwd() is the load-bearing anchor rescue (project_root == watch_root ==
the bad marker), and add hyperedges to the _anchors_stored_sources bucket
tuple to match the sibling loop. Adds a deletion-still-evicts test (the anchor
validation must not over-preserve) and an incremental==cold id-parity test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A graph built on a subfolder stores source_file relative to the repo root but
the skill writes .graphify_root as the absolute subfolder, so an incremental
rebuild anchored stored paths under the wrong root, judged every unchanged
source deleted, and collapsed the graph. The anchor is now adopted only if the
stored sources actually resolve under it, falling back to the build root / cwd
otherwise, so a stale subfolder marker can no longer evict the whole corpus.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the two cases the deep-dive flagged as untested: a receiver-based method
collision (Get/get on the same type) and the all-salted branch where no member
is the unique exported one (Run/RUN). Also adds the CHANGELOG entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Node ids casefold (deliberately, for AST/LLM/builder parity), but Go is
case-sensitive and uses case for visibility: exported `Run` and unexported
`run` in one file collapsed to one id, so the second was dropped by add_node
and a local `run()` call phantomed to a same-named symbol in another package.
The Go extractor now salts the non-canonical half of a same-file case-only
collision (exported keeps the stable plain id), so both survive and the
in-file resolver binds the unexported call locally. ids.py / normalize_id are
untouched, so the id contract holds and non-Go corpora are unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the end-to-end assertion the version-stamp fix was really about: after
installing one platform, a different stale platform actually emits the
staleness warning on stderr (the behavior the over-stamping suppressed). Adds
the CHANGELOG entry closing #2694.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
install() called _refresh_all_version_stamps(), which stamped every installed
platform's .graphify_version with the current version WITHOUT rewriting that
platform's skill content — so a genuinely stale platform was stamped current
and its staleness warning was suppressed. Removes that bogus refresh; each
platform's stamp now advances only when its own content is (re)written via
_copy_skill_file, so a not-reinstalled stale platform correctly still warns.
Completes #2694 (part 1 shipped in 0.9.44 via #2753).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the multi-id-less case (the real corpus had 183/234 id-less) and the
empty-string-id case (falsy, treated as id-less). Dates the released 0.9.44 and
opens the 0.9.45 section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The semantic extractor emits hyperedges with no id and build.py persists them
verbatim, so a prior graph.json can contain id-less hyperedges. Seeding the
dedup set with a hard h["id"] raised KeyError: 'id' on every incremental
re-extract, silently failing the whole graph load. Guard the comprehension with
h.get("id"), symmetric with the incoming-set guard already below it; id-less
entries are retained in the graph, id-bearing dedup is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
The always-on registration wrote `~/.claude/CLAUDE.md` unconditionally, so
installing from a Claude profile relocated via CLAUDE_CONFIG_DIR mutated the
DEFAULT profile instead — the same fix _platform_skill_destination already
applies to the skill-copy path, missed for this path. The global branch now
writes to $CLAUDE_CONFIG_DIR/CLAUDE.md with a matching skill reference; env
unset/blank still falls back to ~/.claude. Refs #2694 (part 1; the version-stamp
half is unaddressed).
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>
Cuts a 0.9.43 section (OCaml extractor + #2652/#2653/#2596/#2597/#2655) and
dates the released 0.9.42.
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>