765 Commits
Author SHA1 Message Date
safishamsiandClaude Opus 4.8 3429264799 feat(js): extend nested-function extraction to arrow components/callbacks (#2653)
#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>
2026-08-14 15:02:15 +01:00
himanshupatro-334andClaude Opus 4.8 b401e8c29f fix(js): extract nested function declarations (#2653)
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>
2026-08-14 14:49:56 +01:00
safishamsiandClaude Opus 4.8 f52b8dbc69 harden(bash): stop .. source paths escaping the corpus tree (#2596)
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>
2026-08-14 14:48:56 +01:00
Josh HudsonandClaude Opus 4.8 b345dc4993 fix(extractors/bash): resolve dirname cmdsubst and dotdot suffix in source paths (#2596)
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>
2026-08-14 14:48:35 +01:00
safishamsiandClaude Opus 4.8 d79520198d harden(export): widen dedup reserve to 5 and add multibyte budget test (#2655)
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>
2026-08-14 14:26:12 +01:00
abhay-codes07andClaude Opus 4.8 b7c709e13d fix(export): budget export filenames against the destination path, not just NAME_MAX (#2655)
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>
2026-08-14 14:24:01 +01:00
safishamsiandClaude Opus 4.8 6c29d988bd test(wiki): make the bracketed-label parity case non-vacuous (#2597)
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>
2026-08-14 14:21:34 +01:00
abhay-codes07andClaude Opus 4.8 fc00f673c6 fix(wiki): make a link target the article filename verbatim, not its percent-encoded twin (#2597)
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>
2026-08-14 14:20:04 +01:00
safishamsiandClaude Opus 4.8 bd1ede9584 test(extract): cover per-symbol fan-out and module-top-level drop for #2652
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>
2026-08-14 14:18:54 +01:00
Ousama Ben YounesandClaude Opus 4.8 c08d9afa93 fix(extract): attribute cross-file INFERRED uses edges to the referencing symbol (#2652)
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>
2026-08-14 14:17:45 +01:00
safishamsiandClaude Opus 4.8 0302bfa7af feat(ocaml): add OCaml .ml/.mli extractor (optional tree-sitter-ocaml extra)
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>
2026-08-14 14:17:07 +01:00
Kai a2a9137d05 fix(detect): re-queue a same-tick same-length rewrite in incremental runs (#2466) 2026-08-13 14:21:22 +01:00
Kai c1f39954e3 fix(detect): skip non-regular files so a FIFO can't hang extraction (#2463) 2026-08-13 14:20:36 +01:00
Aryan 48fbe30ebc fix(build): exclude global MCP ids from legacy-id detection (#2408) 2026-08-13 14:17:42 +01:00
JAESOL SHIN a4e8446479 fix(html): trace hyperedge perimeter in convex-hull order (#2449) 2026-08-13 14:17:42 +01:00
Benjamin S. Leveritt 613c45cc7e fix(install): make the staged skill bundle writable so a read-only package installs (#2453) 2026-08-13 14:17:42 +01:00
Kai 5e4ab1dfa0 fix(manifest): capture package version in the apm.yml fallback parser (#2465) 2026-08-13 14:17:41 +01:00
Ousama Ben Younes b3ca490408 fix(report): use a portable basename in the GRAPH_REPORT header (#2682) 2026-08-13 13:30:24 +01:00
Ousama Ben Younes 5d8d6113db fix(cache): surface corrupt semantic cache entries instead of silently missing (#2683) 2026-08-13 13:30:24 +01:00
Ousama Ben Younes a30b56e2df fix(sql): distinguish a broken tree-sitter-sql grammar from a missing one (#2602) 2026-08-13 13:30:24 +01:00
Ousama Ben Younes 51cf1481b6 fix(python): resolve relative subpackage imports to their __init__ (#2688) 2026-08-13 13:30:24 +01:00
phudayyy a05b4084d9 fix(affected): resolve a seed given as ./relative or absolute path form (#2584 follow-up) 2026-08-13 13:30:24 +01:00
C0KERNEL cc0ee60c40 fix(export,cli): stamp graph provenance from the analysed repo, not the shell cwd (#2534 family) 2026-08-13 13:30:24 +01:00
Ousama Ben Younes a995921026 fix(js): shadow for...of / for...in loop bindings from indirect_call args (#2568 family) 2026-08-13 13:30:24 +01:00
rajashidattapy c1e41ea90e docs: refresh ARCHITECTURE.md module table + add a doc-parity test (keeps the #2558 'an' fix) 2026-08-12 20:57:39 +01:00
rajashidattapy 5cc20a4811 test: skip the unreadable-dir detect test on non-POSIX / as root 2026-08-12 20:56:28 +01:00
rajashidattapy cdf1f65656 test: probe-and-skip symlink tests where symlink creation is unavailable (#2642) 2026-08-12 20:56:28 +01:00
rajashidattapy 243f3e32c1 test(hooks): stop argv mangling the shell-arg verdict test on Windows (#2126) 2026-08-12 20:56:28 +01:00
rajashidattapy 0df2a701a5 fix(paths): clear read-only bit before unlinking the atomic-write temp on Windows (#2622) 2026-08-12 20:56:28 +01:00
rajashidattapy 1aab181d15 test: Windows path portability in tests; llm emits POSIX source_file to the model (#2620) 2026-08-12 20:56:28 +01:00
Arthuro0103 ee2fbf954b fix(benchmark): guard against a node with a None label (#2674) 2026-08-12 14:20:21 +01:00
rohit-jsfreaky 8be72ef7bf fix(js): stabilize unresolved local import target ids (#2457) 2026-08-12 14:20:21 +01:00
Ben Younes c285a94d45 fix(extract): resolve prefixed PHP use-imports to their target (#2661) 2026-08-12 14:20:21 +01:00
Ben Younes b645a3a7f1 fix(serve): suppress the query truncation banner when no nodes were cut (#2601) 2026-08-12 14:20:21 +01:00
Ben Younes 4730ed9abb fix(watch): refuse a shrink caused by an extractor failure during update (#2663) 2026-08-12 14:20:21 +01:00
rohit-jsfreaky cd04bf1bf3 fix(csharp): extract members inside #if preprocessor blocks (#2634) 2026-08-11 20:09:59 +01:00
rajashidattapy bedf32e07c fix(wiki): count each incident edge once in the audit trail (#2635) 2026-08-11 20:09:59 +01:00
rajashidattapy c838df83c5 fix(cache): re-anchor CWD-relative source_file on warm cache hits (#2632) 2026-08-11 20:09:59 +01:00
rajashidattapy ce942e144f fix(extract): canonicalize source_file to POSIX separators (#2627) 2026-08-11 20:03:55 +01:00
Jaeung Jang 33d217c02f fix(extract): shadow JS/TS catch bindings from indirect_call args (#2568 family) 2026-08-11 16:54:35 +01:00
himanshupatro-334 cb06a7decc fix(detect): preserve explicit scan roots vs parent unanchored .gitignore (#2468) 2026-08-11 16:54:35 +01:00
Ben Younes 638d9e2c37 fix(ingest): recognize Cargo.toml as a package manifest (#2434) 2026-08-11 16:54:35 +01:00
himanshupatro-334 91e43c7678 fix(llm): add rationale guidance to the API extraction prompt (#2482) 2026-08-11 16:54:35 +01:00
michaelxer fe67768b93 fix(export): use portable path in graph.html document title
The <title> previously embedded str(output_path), so Windows absolute
host paths leaked into a tracked artifact (regression of #433, #2598).

Prefer a cwd-relative label, else keep from the graphify-out segment
onward, else the filename only.
2026-08-11 15:18:33 +01:00
Jaeung Jang d06bab0629 fix(extract): shadow a single unparenthesised arrow parameter from indirect_call args
tree-sitter gives an arrow with one unparenthesised parameter a `parameter`
field (singular) and no `parameters` list node, so `_js_local_bound_names`
never saw it: `x => sink(x)` bound nothing, and `x` read as a by-name
reference to any same-named callable in the corpus, fabricating an
indirect_call edge (INFERRED, 0.8).

The parenthesised form was always handled, so `(x) => …` and `x => …`
behaved differently. Same singular/plural trap as `catch_clause.parameter`.

Strictly subtractive on real code: +0/-88 indirect_call edges over 5,402
files of node_modules, +0/-98 over a 6,000-file mixed corpus.
2026-08-11 15:18:33 +01:00
rohit-jsfreaky 3c843bd0cc fix: skip Obsidian vault metadata directories 2026-08-11 15:18:33 +01:00
Bruno Santanna 5ffaaa606a fix(detect): normalize Unicode so accented ignore patterns match on macOS
An ignore rule naming a directory with an accent silently does nothing on
macOS, and the files it was meant to exclude get scanned anyway.

macOS (APFS/HFS+) returns filenames in NFD — "ç" comes back as "c" + U+0327
COMBINING CEDILLA — while editors write ignore files in NFC, where the same
"ç" is the single codepoint U+00E7. The two render identically and compare
unequal, so `fnmatch` never matches and the pattern is a no-op.

Found in a real repo: a `.graphifyignore` containing `Orçamento/` failed to
exclude that directory, and 9 client contract PDFs were picked up for semantic
extraction — i.e. queued to be sent to an LLM — despite an explicit rule
against it. The failure is silent: there is no warning, and the only symptom
is a file count that does not match what you expect. A user who does not
count would never know. That is what makes this worth fixing rather than
documenting: the rule appears to work.

Both sides are now normalized to NFC before matching, at three boundaries:
the pattern (in `_parse_gitignore_line`, so it covers .graphifyignore,
.gitignore and $GIT_DIR/info/exclude alike) and the two path forms used in
`_is_ignored` (`target.name` and the anchor-relative path).

NFC is already the form Linux and Windows produce, so this is a no-op there
and only repairs the macOS mismatch.

Tests: two regression tests cover both directions (NFC pattern vs NFD path on
disk, and the reverse); both fail before this change and pass after. A third
asserts ASCII patterns are unaffected, so the normalization cannot regress
existing behavior.

Full suite: 3833 passed. The 13 failures in tests/test_terraform.py are
pre-existing on a clean upstream checkout (optional tree_sitter_hcl not
installed) and unrelated to this change.
2026-08-11 15:18:33 +01:00
sean-soomgo 1fdd11fa76 fix(serve): resolve punctuated and non-ASCII node ids in _find_node (#2467)
`_find_node_tiers` builds two normalizations of the query: `term`, which
tokenizes on \w+ so punctuation becomes a space, and `norm_query`, which
keeps it. The exact tier compared the node id against `term` only, so
`term == nid_lower` was false for every id carrying punctuation, and
`norm_query` — which already held the right form, and is even one of the
two trigram needles — was never compared against the id at all. Comparing
`norm_query` to the folded id closes that half.

It does not reach ids carrying non-ASCII text. `_node_search_text`
indexed the id raw while every query path folds through
`_strip_diacritics`, which NFKD-decomposes. Hangul syllables decompose
into conjoining jamo, and jamo have combining class 0, so they survive
the combining-character filter: the needle's trigrams and the posting's
trigrams were disjoint, `_trigram_candidates` returned a candidate list
without the node, and it was dropped before any predicate ran. The
folded id is now part of the indexed text.

Both halves are additive. An id that resolved before resolves to the
same node; only ids that previously resolved to nothing can now resolve.
The folded field is appended, and only when the fold actually differs,
so field positions do not move and an all-ASCII graph indexes byte for
byte what it indexed before. Index build, median of 7 runs:

    graph                     trigrams   postings   build
    5k all-ASCII     before        1723     238876    96ms
    5k all-ASCII      after        1723     238876    97ms
    17k real         before       33442    2311784   998ms
    17k real          after       33490    2312462   990ms
    5k half-Hangul   before        1735     256381   116ms
    5k half-Hangul    after        1739     266381   124ms

The real graph is the 17269-node one measured below; 354 of its ids are
non-ASCII, so the index grows 0.03% and the build stays inside run-to-run
noise. The half-Hangul row is a deliberate worst case — every other node
id Korean — and even there the cost is paid once per graph load, on a
graph where id lookup previously returned nothing at all.

On a real 17269-node graph with Korean source filenames, every node id
fed back to itself, full population:

    id class                  total   before   after
    contains punctuation       2326        0    2326
    contains Hangul             354        0     354
    ASCII, no punctuation     14589    14589   14589

And every query that graph can produce — all 17269 ids plus all 16537
distinct labels — through `_find_node_tiers` on both variants in one
process: 31126 identical, 2680 that returned nothing before and resolve
now, 0 with a changed first result, 0 lost, 0 with a widened exact tier.
Every difference is a query that previously returned nothing.

Left alone deliberately: `_score_query` compares the id raw in the same
way, so `path` and query seeding still cannot take a punctuated id, and
the prefix tier also matches ids against `term` only. Both are behaviour
changes beyond this defect rather than part of it.

One note for the regression tests: the non-ASCII case needs a graph of
at least ~10 nodes. `_trigram_candidates` bails out to a full scan when
`min(present) > int(n * 0.10)`, so on a small synthetic graph the index
path is never taken and the test passes with the defect still present.
2026-08-11 15:18:33 +01:00
hjotha 36b47ba25d fix(export): stabilize graph JSON collection order 2026-08-11 15:18:33 +01:00
rajashidattapy 21e75cfae4 fix(normalize_id): address idempotency issues with Turkish identifiers and update related test cases 2026-08-11 15:18:33 +01:00