Covers the review finding directly: a Windows drive letter key, a
UNC key, and a POSIX absolute key must all be recognized as already
absolute regardless of which platform the test runs on, and must not
be joined onto the scan root. Confirmed against pre fix code via git
stash, the test fails there since the recognizing helper does not
exist yet.
(cherry picked from commit 33b3a7a00a)
Covers the review finding directly: a relative manifest key with a
redundant dot dot segment must canonicalize onto the same absolute
path as a plain key for the same file and collapse with it, keeping
the more recently seen entry, instead of surviving as a second,
uncollapsed row.
Confirmed against pre fix code via git stash: the test fails there
and passes only with the fix applied.
(cherry picked from commit 7505a29bb3)
One test covers load_manifest directly, checking both on disk
orderings of a stale absolute entry and a fresh relative entry for
the same file, confirming the fresher one wins regardless of which
key iterates last. A companion test covers the write side, seeding a
save through an untouched file's existing duplicate rows and
confirming the relativize step keeps the fresher entry there too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 4839ae9aba)
_detect_url_type tested substrings of the whole URL, so any host ending in
x.com (dropbox.com, netflix.com, linux.com, box.com) was treated as a tweet
and saved as an empty oEmbed stub, and a name in the path or query was
enough for the arxiv, github and youtube types. Match the parsed host or
its subdomains instead.
(cherry picked from commit d48ba15fe4)
Under the spawn start method (the Windows default) each worker re-imports the
parent's __main__ by its __file__ path. A stdin (`… | python -`), `python -c`,
or REPL caller has no such file — the path is missing or `<stdin>` — so every
worker dies during bootstrap and the pool raises BrokenProcessPool before any
work is done. The shipped SKILL.md pipes a heredoc into the interpreter, so on
Windows this is the path most users take, not an edge case: every skill-driven
run prints one full worker traceback per core, then a fallback warning that
blames a missing `if __name__ == "__main__":` guard — a fix the caller cannot
apply, because there is no file to put a guard in.
Detect the unusable-__main__ case before creating the pool: when the start
method is spawn (or the platform is Windows) and __main__ has no importable
file, run sequentially and print one note that names the real cause (stdin/-c/
REPL) and the remedy (write the step to a .py file, or pass parallel=False). A
script that DOES have a __main__ file but omits the guard is a different failure
this cannot catch here; it still surfaces via the existing BrokenProcessPool
fallback, whose __main__-guard message is now accurate for that remaining case.
Tests cover the detector (stdin, REPL-without-__file__, a real script, and the
fork start method) and that extract() with >= _PARALLEL_THRESHOLD uncached files
and an unusable __main__ never attempts the pool yet still produces correct
output. All five fail without the guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit b475196ed1)
`_read_tsconfig_aliases` walks `extends` chains but never `references`. In the
standard Vite / `tsc -b` layout the root `tsconfig.json` is a solution file —
`"files": []` with `references: [{path: "./tsconfig.app.json"}, …]` — and every
`compilerOptions.paths` block lives in a referenced project config. The loader
finds the solution file, sees no `paths` and no `extends`, and stops, so every
path-alias import in the project silently produces no `imports_from` edge and
`graphify affected` returns a partial consumer set with no warning.
Follow `references` the same way `extends` is followed: merge each referenced
config's aliases like an additional parent (the referencing config's own `paths`
still override), guarded by the existing `seen` set that protects against
cycles. A reference `path` may name a directory (resolved to its
`tsconfig.json`, per `tsc -b`) or a config file directly.
Regression tests cover both the solution-file layout and a directory-valued
reference; both fail on the unpatched loader and pass with the fix. The existing
JS/TS resolution, jsconfig-baseUrl, extension-resolution and Astro suites stay
green (135 tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit 4e84a23822)
Two core Julia constructs were dropped by the extractor:
- macro name(...) ... end produced no node, even though macros are first-class,
heavily used definitions. The grammar gives a macro the same signature shape
as a function, so it now mints a definition (labelled @name) and its body is
walked for calls -- with the signature skipped so the macro does not call
itself.
- @enum Name a b c (and the @enum Name begin ... end block form) is a
macrocall_expression that was ignored entirely. The first identifier in the
argument list is the enum type; the rest, including members inside a begin/end
block, are contained by it. Other macro calls still fall through so
definitions nested in e.g. an @testset block are walked.
Added tests covering macro extraction with body-call attribution (no self-loop)
and both @enum forms with members contained by their type.
(cherry picked from commit 87edb5a278)
The OCaml walker had no branch for class_definition, so a class object --
its methods, its instance variables, and the calls made from method bodies --
was dropped entirely. A file made only of classes produced no nodes at all.
Add a class_definition branch that mints the class node (defines/contains),
walks the object body (object_expression in .ml, class_body_type in .mli) for
method and instance-variable members, and attributes calls in each method body
to that method. Method names are deliberately not registered as local defs:
they are reachable only through obj#m, so binding a bare call to a same-named
method would be a false edge.
(cherry picked from commit 63572f0cbe)
Polish on #3848: remove the unused mid-file tree_sitter imports (F401)
and add a negative test asserting a bare constructor param without
val/var is not emitted as a field property.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A derived type's contains section binds procedures as the type's methods
(procedure :: area => circle_area, or plain procedure :: scale), but the
derived_type_definition branch only minted the type node and ignored the
binding list entirely. The type ended up in the graph with no connection to
its own methods, and calls dispatched through the type were unrepresented.
Collect the bindings while walking the type and, after the whole tree is
walked (so the module's internal procedures already have nodes), add a method
edge from the type to each implementing procedure. A binding to a procedure
from another module resolves to a sourceless stub via ensure_named_node, the
same treatment parameter and return types already get, so the corpus rewire
can collapse it and no dangling edge is produced.
(cherry picked from commit 75f8fff533)
Covers issue 3847 directly: an incremental detect call given a
cache_root outside the scan root must not create anything under the
scan root, and the word count stat index cache file must land under
the requested cache_root instead. Confirmed against pre fix code by
restoring the prior versions of detect.py and cli.py directly, the
call raises a type error there since the parameter did not exist yet.
(cherry picked from commit d745f7b00e)
defprotocol and defimpl were not recognized as definition keywords, so the
walker fell through to generic recursion with no parent module. The protocol
and implementation nodes were never created, and their functions were attached
to the file node instead of their container (two orphaned size() nodes hanging
off the file rather than off the protocol and its List implementation).
Handle both like module-like containers: mint the protocol/impl node, contain
it in its enclosing scope, and walk the do-block so each def becomes a method
of the protocol or implementation. The implementation also gets an implements
edge to its protocol; a cross-file protocol target is filtered by the existing
dangling-edge guard rather than left hanging.
(cherry picked from commit 5f4e979693)
Extracts the real Step 1 bash block from the committed skill.md
artifact, substitutes INPUT_PATH the way an agent following the
instructions would, and actually runs it, so the fix is checked
against what users receive rather than the fragment source alone. One
test confirms a command substitution embedded in the substituted path
never runs, another confirms a semicolon separated command never
runs, a third confirms a legitimate path with a space in it still
resolves correctly, and a fourth confirms a nonexistent path still
fails the whole step loudly. All four were checked against the
pre fix block first, three failed as expected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit ad98afa9f5)
Covers the review finding directly: a candidate with a
source_location but no source_file must not out rank a fully bare,
richer candidate. Confirmed against pre fix code via git stash, the
test fails there and passes only with the fix applied.
(cherry picked from commit f6afc87140)
Covers the review finding directly: a record with a source_file but
no source_location must not lose to a richer, unlocated stub that
also carries a source_file, and the tiebreak must stay inert when
both sides already have a real location, preserving the existing
richness ordering there.
Confirmed against pre fix code via git stash: the direct repro test
fails there and passes only with the fix applied.
(cherry picked from commit 4915270490)
One test is the issue's own exact reproduction, a source less stub
that genuinely out scores a real located declaration on field count
alone, checked in both argument orders. A companion test confirms the
provenance tiebreak stays inert when both candidates carry a source
file, so the existing richness then length ordering this file already
established is untouched. The first test was checked against the pre
fix code and failed there as expected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 0e155019e3)
Covers issue 3801 directly: a code plus concept only graph (no
document, paper or image nodes anywhere) must not offer undocumented
components as a reason for an isolated node, only possible missing
edges. Adding a single document node elsewhere in the same graph
shape brings the fuller reason back, confirming the gate tracks the
graph's actual content rather than being tied to a particular test
fixture shape.
Confirmed against pre fix code via git stash: the first test fails
there and passes only with the fix applied.
(cherry picked from commit 02e1b4d6c2)
A variable or output names its secret in the block label, so the literal
sits under the generic key default/value and the key-name check never
flags it: variable "db_password" { default = "..." } reached graph.json
verbatim. Redact default/value when the label matches the sensitive-key
pattern or the block sets Terraform's own sensitive = true.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 01e417166b)
On Windows os.exec* does not replace the process: CPython spawns a new one
and the caller exits at once, so update/extract/cluster-only/label returned
before graph.json was written, and the exiting parent intermittently died
with an access violation (exit 139). Run the child with subprocess, wait for
it and propagate its exit status there; POSIX keeps the real os.execvpe.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 94a9657e93)
`PERFORM A THRU Z` (and `THROUGH`) runs the whole paragraph range A..Z, but the
PERFORM pattern captured only the first name, so a `calls` edge was emitted to A
and the range-end Z got none. "What performs Z?" then answered nothing even
though this paragraph does, and any reachability walk stopped short of the
range's tail.
Capture the optional `THRU`/`THROUGH` target and enqueue both endpoints for
resolution against the paragraph table. Intermediate paragraphs in the range are
not individually linked (that would need paragraph source ordering); the two
endpoints are the reliable, common signal and both now resolve. A plain
`PERFORM A` is unchanged.
Regression test: `PERFORM A-PARA THRU Z-PARA` now links MAIN to both A-PARA and
Z-PARA (fails on the old single-capture pattern).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit abcdde1a65)
The fixed/free format detector counted a line as fixed-format only when its
first six columns were blank (`^ {6}[ *\-/ ]`). Legacy mainframe COBOL — the
main reason to have a COBOL extractor — puts a sequence NUMBER in columns 1-6,
so every such file scored zero fixed markers and was classified free-format.
In free mode the code is taken verbatim (no column-8 slice), so the sequence
number stays glued to the statement: `000400 MAIN-PARA.` no longer matches the
paragraph anchor `^NAME .? .$`, and the whole procedure layer disappears. A
seven-line program with two paragraphs and a PERFORM collapsed to just the file
and program nodes — zero paragraphs, zero `calls` edges — silently.
Match a sequence area that is six spaces OR six digits, followed by a valid
indicator column (space = code, `*`/`/` = comment, `-` = continuation, `D` =
debug). Once the file is correctly read as fixed-format the existing
column-8..72 slice strips the sequence number and paragraphs/PERFORMs resolve
as they should. Blank-sequence fixed-format and genuinely free-format source are
unaffected (a 6-digit prefix is not something free-format code begins a line
with).
Regression test: a sequence-numbered program now yields its program, both
paragraphs, and the PERFORM `calls` edge — and fails on the old detector.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit 96b9522bd5)
#3461fixes#3409 (PHP closures produce no nodes / inner calls lost). Adds the
coverage its tests omitted:
- the reporter's exact repro — a FILE-SCOPE argument-position closure — asserting
the closure node exists AND its inner call resolves to the closure (not the file);
- a no-duplicate-node assertion guarding the walk partition against double-walk;
- a method-scope closure asserting its inner call attributes to the closure, not
the enclosing method (pins the function_boundary_types behavior).
Verified end-to-end: closure->handler() edges attribute to {closure#N} nodes with
no duplicates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address edge cases identified in review:
- Nested route closures (e.g., inside group() or prefix()) now correctly compose their full path by walking up the AST across closure boundaries.
- Non-routing closures passed to methods that share verbs (like ->get('user:42')) are no longer misidentified as routes. A route path must begin with '/' to qualify, otherwise it correctly falls back to an ordinal name.
(cherry picked from commit 38861aa1c4)
Address reviewer feedback:
- Replace line-based {closure@N} names with stable ordinal {closure#N}
names scoped per enclosing class or file. Ordinals are unaffected by
line insertions/deletions above a closure; only reordering closures
or inserting one earlier in the same scope shifts later ordinals,
which is a much rarer edit.
- Route closures passed to routing verbs (get/post/put/patch/delete/
options/any/match/map) receive a semantic 'VERB /path' label that
is both stable and directly meaningful to graph queries (the primary
use-case from the issue).
Note: adding anonymous_function_creation_expression and arrow_function
to function_boundary_types (introduced in the first commit) means calls
inside a closure now attribute to the closure rather than the enclosing
named function. This is the correct behaviour; existing codebases with
closures inside named functions will see those edges move on next
incremental update.
(cherry picked from commit 5400b9cbd7)
The bug:
PHP closures (`anonymous_function_creation_expression` and `arrow_function`)
were invisible to the graph, disconnecting API routes and callbacks from
the backend code.
The fix:
- Added closure syntax node types to _PHP_CONFIG's `function_types`.
- Updated engine.py to synthesize line-bound names (e.g., `{closure@42}`)
for unnamed closures matching these AST node types, enabling them to be
extracted as linkable nodes in the graph without ID collisions.
(cherry picked from commit c8c3af1960)
`to_wiki()` and `to_obsidian()` regenerate the full page set on every call and
write every page unconditionally — `to_wiki` even unlinks every `*.md` up front,
guaranteeing a full rewrite. An export re-runs on any graph.json change, so on
an always-on `watch`/export setup this rewrites tens of thousands of identical
pages per run (measured: 31,417 pages / 137 MB per export, ~7 GB/day), and each
rewrite is also a modification event that re-fires Obsidian re-indexing, sync
clients, and downstream inotify pipelines for content that did not change.
Compare before writing:
- New `paths.write_text_atomic_if_changed(path, text) -> bool` writes (and
reports True) only when the content differs from what is on disk, comparing
DECODED text — text-mode writes translate `\n`↔`\r\n` on Windows, so a byte
compare would report every file changed.
- `export._owned_write` uses it and still records the note in `_written`
(the ownership + prune-exclusion set, #1896), so an unchanged note stays owned
and is neither pruned nor dropped from the manifest.
- `to_wiki` drops the up-front unlink-all — which forced the rewrite and opened
a window where a reader saw an empty `wiki/` mid-export — for an end-of-run
sweep that removes only the `*.md` this run did not produce, keeping the same
orphan guarantee for relabelled/removed communities.
Tests: the helper skips an identical write (mtime untouched) and writes on a
diff or a missing file; an Obsidian re-run leaves an unchanged note's mtime
untouched yet still owned (not pruned) and still rewrites a note whose body
changed; a wiki re-run leaves unchanged articles untouched and still sweeps a
relabelled community's stale page. The wiki/export/obsidian suites stay green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit 91546de3b4)
`_redact_value` (the #3644 guard that keeps hardcoded credentials in .tf files
out of graph.json and the MCP query/get_node surface) recursed into map values
but not list values. HCL routinely nests objects inside tuples —
`list(object(...))` variables, `dynamic` blocks, tuple defaults — and
`_parse_attr_value` turns those into Python lists of dicts, so:
config = { password = "x" } -> redacted (map recursion)
configs = [{ password = "x" }] -> LEAKED (list not recursed)
The list form left the secret in the serialized node verbatim, defeating the
guard for a very common Terraform shape (a `connections`/`containers`/`secrets`
tuple of objects).
Recurse into lists and tuples as well, under the same key — the list branch is
only reached when the key is not itself sensitive (a sensitive key redacts the
whole value above), so a scalar element carries no key signal and is returned
as-is while a dict element is checked against its own inner keys. A non-secret
list of scalars is untouched.
Tests: a unit check that a secret inside a list-of-objects and a nested list is
redacted (while a sensitive key still redacts the whole list and a plain scalar
list is left intact), and an end-to-end check that a `password`/`token` inside a
tuple-of-objects attribute never reaches the serialized node. Both fail without
the fix. The terraform suites otherwise stay green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit 2998cb2121)
The existing probe assertions now expect the module flag form. A new
test drives the probe with a launcher stub path that is not even a
real file in place of argv zero, confirming the fix no longer depends
on it being anything runnable, the exact shape of the Windows uv
launcher regression this closes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit e2fd1c4f30)
One test sets a shared, absolute GRAPHIFY_OUT and confirms the marker
written by an update from a relative path is resolved to an absolute
form. A companion test confirms the ordinary, relative GRAPHIFY_OUT
case still preserves the caller supplied relative path exactly as the
existing 777 test already expects, so this fix only changes the
shared output case.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 181a46dca9)
Cover an unrelated mention of the word graphify no longer suppressing
the real registration block, a stale or hand edited block correctly
getting refreshed on re install instead of being left alone, and both
the CLAUDE.md and GEMINI.md writers now passing newline equals empty
string so an existing file's own line endings are never translated on
write.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit ddf2497f68)
The two node-lookup MCP tools read arguments["label"] directly, so a client
that passes the node identifier under "node_id" (or "id") gets a
KeyError('label') instead of a result. Several of the agent frameworks graphify
advertises support for send the argument that way, so the tools are unusable
from those clients while query_graph/god_nodes work fine against the same graph.
Extract a small module-level _node_arg() that accepts label/node_id/id, return
a short guidance string when none is given (instead of raising), and declare
node_id in both inputSchemas so the parameter is discoverable. Add a unit test.
(cherry picked from commit cf84c59735)
Cover a Tauri style layout where the manifest lives one level down and
must still be found, a discovered workspace manifest whose own
members resolve relative to its own directory rather than the outer
scan root, a decoy manifest inside a dependency install folder that
must never be searched into, and a manifest nested past the discovery
depth cap that is correctly left unfound.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 6d2ef5455f)
One test runs extract with the cargo flag against a repo that has no
Cargo.toml at all and confirms it still succeeds and still writes the
graph built from the code already indexed, with a note explaining the
crate edges were skipped. A companion test confirms a repo that does
have a valid manifest still gets its crate node, so the fix only
changes the missing manifest path.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit a9b14fef06)
Since a real re exec replaces the process it runs in, every test that
needs to observe whether it fired runs a small probe in its own
subprocess with a deliberately constructed environment, rather than
mocking inside the pytest process itself, which pytest's own handling
of its current test marker would defeat anyway. Cover each of the
four affected commands re executing with the seed pinned when unset,
an explicit seed never being overridden, unrelated commands never re
executing, the guard against firing while pytest itself is mid test,
a failed re exec being survived rather than raised, and a full real
invocation of update still completing successfully end to end through
the whole re exec.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 2f5f2d705e)
extractors/go.py mints every import target as `go_pkg_<raw import string>` with
no module lookup, so an intra-module import never reaches a file: the sink has
no source_file and nothing links it onward. `affected` and any reachability
walk miss every consumer that arrives through an import, while qualified calls
(`b.F()`) resolve, because the call pass already filters callees by
_go_import_path_for_file. This inverts that mapping once per run and repoints
the intra-module `imports_from` edges onto the imported package's file nodes,
the way the JS/TS resolver repoints module specifiers. Stdlib and external
imports have no file in the corpus and stay sinks by design.
Measured on a 744-file single-go.mod module, same commit, with and without
this change: a 111-file handler package goes from 0/21 direct intra-module
imports resolved to 21/21 (24/24 transitive); the 57 intra-module go_pkg_
nodes disappear; +16,616 imports_from edges; no other relation changes.
Closes#3746
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit fb40961f4a)