Three findings from the automated review of #3562, all reproduced:
A dotted span with a non-code extension (README.md, pyproject.toml) was classified as a
symbol because only CODE_EXTENSIONS was checked, and every dotted form was reduced to its
last segment, so time.sleep matched a repo's own sleep(). The dotted form now keeps its
whole chain, the file early-exit also checks DOC_EXTENSIONS, and a bare-name match must
carry every qualifier as an owner label or a segment or stem of its source path.
_match_cited_file used lstrip("./"), which strips characters, so a .github/... cite could
never match by suffix. The leading ./ and ../ segments are now stripped as segments, and
the suffix form also matches a root-level file.
An explicit ./ or ../ cite that escaped the corpus fell through to the suffix rule and
could land on an unrelated copy of the file. It now resolves relative to the document
only.
(cherry picked from commit 3f84e7f3e2)
A module with no matching definition anywhere in the corpus, stdlib
or a hex dependency, must stay an external reference rather than
getting fabricated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit da5e23a921)
Two different files each declaring a module with the same bare name
must leave the reference exactly as extracted rather than guessing
which one the caller meant.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 690d387626)
A module importing something declared in the same file already
resolved before this fix, since both ids share the same stem, and
must keep working exactly as before.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 8320da005a)
A module aliased from a different file now resolves to that module's
real, non dangling node.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 53877c6874)
Pooling methods across every node sharing a type's bare label is
what makes a split impl block work, but it is only safe when every
one of those nodes really is an impl block for the SAME type. Two
unrelated structs that happen to share a bare name, each with its
own different methods, could still link a caller to a method that
only exists on the wrong one, most concretely when one type
overrides a trait default the other relies on and so never gets an
explicit method node for it.
A struct, enum, or trait declaration gets a contains edge from its
file; an impl block never does. So the type name itself is now
treated as ambiguous, and pooling skipped entirely, whenever two or
more nodes sharing the bare label are real declarations rather than
impl blocks. A single declaration with any number of impl blocks
spread across files, the shape this pass exists for, is unaffected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 141662b0eb)
A plain dict assignment silently overwrote an earlier target whenever
two method nodes shared both a source impl node and a stripped
label, which would have kept whichever one was seen last instead of
surfacing the case as the ambiguity it is. The index now keeps a set
per key and folds every match into the candidate set before the
exactly one candidate check runs, so a genuine duplicate still bails
instead of guessing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 2cbbf1a670)
The check against a pair already having an edge matched on ANY
relation, so a caller that already had, say, a references edge to
the exact same target (from also naming the type elsewhere) silently
suppressed a real call to it. The two relations are not mutually
exclusive and an existing non calls edge says nothing about whether
a call was resolved. Scoped the check to calls edges only, matching
an existing precedent elsewhere in this module for the identical
class of mistake.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 94735ce241)
Two different structs sharing a bare type name with a colliding
method name must yield no edge rather than a guess, and a self call
to a method that genuinely does not exist anywhere in the corpus
must not fabricate a target.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit afeeefa109)
Covers the issue's own shape (impl Foo split across two files, a
self call in one block reaching a method defined in the other) and a
same file negative control proving the pre existing bare name
resolution is unaffected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 2d161af639)
The markdown extractor's docstring promised a heading --references--> node edge
when a backtick Name matches a known pattern, but nothing implemented it, so an
AST build produced no document-to-code edge at all.
extract_markdown now collects inline code spans outside fences and frontmatter as
raw_calls tagged language "markdown", owned by the heading they sit under (the
page before the first heading). A new markdown_mentions language resolver runs
after the id-remap passes and turns them into references edges: the explicit
path::Symbol[::member] form resolves through the cited file's symbols and is
EXTRACTED 1.0; a bare or dotted name resolves only when exactly one callable code
node carries that label and is INFERRED 0.95. Ambiguous names, builtins, missing
files and non-callable matches emit nothing. The shared cross-file call pass
skips markdown raw_calls so prose never emits calls edges.
Tests cover span classification, raw_call ownership, end-to-end resolution,
ambiguity, and cache round-tripping.
(cherry picked from commit 2d833a0a3c)
#3558's fix landed but its diff omitted the 3 tests its description claimed;
add them (self-loop ignored, self-loops-only is 0, 0..1 bound holds). Also drop
a changelog line the #3559 cherry-pick injected into the released 0.9.61 section.
group_skipped feeds the dangling reference pruning pass and the write
loop must agree with it on whether a recovered group is written or
skipped. This proves an edge between two nodes inside the same
recovered group survives instead of being pruned as referencing a
supposedly skipped id.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 8a23b305fe)
Confirms recovery never runs when allowed_source_files is omitted: a
genuinely bogus path is skipped exactly as before this fix, and a
normal well formed path in the same batch still saves and loads back.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 12999a80f6)
Two allowed files sharing a basename must not let recovery guess
between them: an unresolvable source_file with an ambiguous basename
still falls through to the previous skip behavior unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit f59acf7c36)
A recovered group's write KEY pointed at the right file, but every
node, edge, and hyperedge inside it still carried the original
unresolvable source_file string. load_cached rejects an entry on read
if any item's source_file does not match the path it was loaded
under, so the previous commit alone wrote a group that reported as
saved but could never actually be read back, silently reproducing the
exact loss this fix exists for.
This corrects every item's source_file to the recovered path's
portable form before the group is written, only when recovery
actually redirected the key, so an unrecovered group stays byte
identical to before.
Includes the regression test proving the full round trip: a group
recovered by basename now survives a subsequent load_cached call.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 4b91dcd462)
Simulates the VMware HGFS PermissionError this issue reports by
monkeypatching os.replace to always raise it, then confirms an
incremental no_cluster rebuild still succeeds and lands the new
content instead of aborting the whole run.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 9e75593b02)
#1423 moved the Python rebuild bodies onto GRAPHIFY_OUT, but the shell that
runs BEFORE them still hardcoded graphify-out/. With the documented override
(#686, e.g. GRAPHIFY_OUT=custom-out for worktree/shared-output setups):
- post-checkout exited at `[ ! -d "graphify-out" ]`, so a branch switch never
launched the rebuild - a silent no-op with nothing in the log.
- post-commit's "only graph artifacts changed" filter only recognised
graphify-out/ paths, so a commit touching only custom-out/graph.json
launched a full rebuild - the loop the filter exists to prevent.
Both gates now read ${GRAPHIFY_OUT:-graphify-out}, the same source the rebuild
body reads. The commit filter is a POSIX case loop instead of grep so the dir
name is matched literally (no regex metacharacters); the leading ( on each
pattern keeps bash 3.2 (macOS /bin/sh) from mis-parsing the pattern's ) inside
the $(...) substitution.
Regression: tests/test_hooks.py runs the emitted scripts under sh with the
launcher stubbed and asserts whether the hook reaches the launch line.
Claude-Session: https://claude.ai/code/session_0134ujLF81GyXsCByibLcYsz
(cherry picked from commit 9efd052de4)
Three existing tests pinned the old, undercounting behavior directly,
including one whose own docstring asserted the exact shape this fix
corrects. Updates all three to the now correct figures and adds a
dedicated test asserting the two invariants the issue itself
suggested, shown equals what actually renders and shown plus thin
equals the total, at both thresholds already exercised in this file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit dd056ab9a8)
Pins the exact interaction the fix commit already describes: a
CommonJS const equals require of a module destructure must still
resolve to exactly one EXTRACTED calls edge, not zero, confirming
the export only gate keeps it out of the per name split entirely.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit e9c10258a7)
Confirms the reconstructed prefix set for an ASCII path is byte for
byte identical to what the old ASCII only regex produced, so the fix
changes behavior only for paths the old code was already getting
wrong.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 97bd1f37f0)
Runs deduplicate_entities on the defining and referencing Korean
node pair in both arrival orders and asserts the defining file own
node survives either way, matching the concrete case the issue
reports: a page whose filename equals the concept name losing its own
node to a page that merely mentions it in passing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 852c61098f)
Confirms the layer above _id_prefixes actually benefits from the
fix: a node whose id and source_file both encode a Korean path is
recognized as defining that id, and a node sharing that id from a
different file is correctly recognized as merely referencing it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 7d15c1bb26)
Confirms the exact repro shape from the issue: the reconstructed
prefix set for a Korean file path preserves every segment instead of
collapsing them to nothing, matching what the file's own id is
actually minted as.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit ab4bde5413)
A bare `require("mod")` statement with no assignment produces no imports
edge, while `local x = require("mod")` does. Cover the bare form in the
flattened and lua/config layouts, with and without parentheses, plus
guards for the callback recursion a fix must preserve and for method-style
calls that must not fabricate an edge.
(cherry picked from commit c87586c343)
Covers the same file case, the doubly nested repro shape from the
report itself, confirms the number 1740 named enclosing function
case is unaffected, and documents the direct import collision gap
called out in the fix commit as a pin rather than a silent
assumption.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 7983186f2f)
TOKENS.md / tokens.md read as design-token reference docs, not credential
dumps, unlike singular "token.md" or another keyword's bare plural
("secrets.md"), which still exclude. The .dart/.ts design-token source
files from the original report are already covered by the existing
graphable-source exemption; this closes the remaining prose-note gap.
Fixes#3527
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6anVY9hgp5ph5qLXRmc2p
(cherry picked from commit 64e85524ef)
- chinese extra: pin jieba-py from 3.12 (was 3.14); jieba's invalid escapes are
a hard error on 3.12+ and broke graphify.serve import.
- serve.py: suppress the tokenizer SyntaxWarning by category so -W error can't
escalate it; catch SyntaxError as a fallback.
- hooks.py: harden the #3492 rebuild-root guard for 3.13, whose resolve() no
longer raises on a symlink loop — require a real directory.
Verified full suite green on both 3.10 (5628) and 3.13 (5627).
The #3490 lock predated its own setuptools>=83 / pypdf / yt-dlp floor bumps, so
a frozen sync resolved setuptools 82.0.1 and the no-isolation wheel-build test
failed. Regenerate the lock so it honors the floors. Also update the windows
skill test: the troubleshooting item is now 'Skip Leiden' (graspologic ->
graspologic-native gating), which #3490 changed in the fragment but not the test.
The workspace `exports` resolver reduced a condition object to a single target,
so a package that splits its runtime source from its declarations never
produced a cross-package edge. Reported in #3487; the exports map itself is
present and works, the defect is in how one is chosen.
`_EXPORT_CONDITION_PRIORITY` listed `types` ahead of `require` and `default`.
`types` is declaration-only and normally points into `dist/`, which is build
output and not part of the corpus, so the resolver returned a path that is not
on disk and stopped there — `_package_entry_candidates` returned `[candidate]`
as soon as any condition matched, so the `default` source beside it was never
tried and the import became a dangling `ref_*` external.
That is not the order the map was added with. #1308 specifies
`source -> import -> module -> default -> require -> types`, and the commit
that implemented it says `default` is consulted LAST so a matching
`import`/`module`/`svelte` condition wins; the tuple shipped with `types` in
the middle instead.
Three changes, all on the workspace `exports` path:
- `types` moves to the end of `_EXPORT_CONDITION_PRIORITY`, restoring the
specified order. `require`/`default` keep their relative order, so the
behaviour pinned by `test_workspace_subpath_export_default_consulted_last`
is unchanged.
- `_resolve_export_targets` keeps every condition target in preference order
instead of returning the first, so a target that is not on disk falls
through to the next condition rather than taking the import down with it.
- `react-native` is honoured only for an importer that is itself native, and
each target is tried alongside its platform-suffixed siblings
(`Icon.tsx` -> `Icon.web.tsx` / `Icon.native.tsx`), which is how a bundler
reaches a platform split whose `exports` target names the unsuffixed path.
Importer platform comes from whole path segments, so `apps/mobile/src` is
native while `packages/web-utils` is not web.
`_resolve_export_target` keeps its signature and its semantics for the
`package.json` `imports` path, and platform expansion applies only to the
workspace `exports` path, so relative imports elsewhere are untouched.
Behaviour note for review: when every `exports` target escapes the package
directory, resolution still falls through to the bare-path fallback exactly as
before. The first revision of this change swallowed that fallback, which the
containment guard made reachable, and a test now pins it. The `"."` branch
additionally gains the containment guard its subpath sibling already had; that
is the only change here beyond the reported defect, and such a package
previously resolved outside its own directory.
Verified: same 29 pre-existing failures before and after, byte-identical
failure sets (`diff` clean; unrelated to this path — non-regular-file, watch
and uninstall-scope tests on Windows). Passing goes 5500 -> 5507, the seven
tests added here. The fixture from #3487 resolves its four previously-dropped
imports and leaves the three control cases unchanged; focused run 109 passed;
`ruff check` clean; all five `tools.skillgen` CI-parity checks OK.
(cherry picked from commit c704a5b2f2)
_js_top_level_function_bodies scanned only direct program children, so a
top-level `export function f(){}` / `export const g = () => {}` — wrapped in
an export_statement — was skipped and the calls inside it never became `uses`
facts. Only the use-fact path consults the per-file import-alias table, so an
aliased-import call (`import { bar as baz }; baz()`) produced no calls edge:
the plain-name global resolver has no `baz` to match, and the alias mapping
that would have pointed it at `bar` was never exercised. Unwrapping a
non-re-export export_statement to its inner function/arrow declaration treats
exported and non-exported functions identically, so the alias table resolves
the call to the real definition.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit 7d98266538)
detect() converts Office files and Google Workspace shortcuts into
markdown sidecars under a converted subdirectory of the output dir,
then ran the same scan ignore check on the sidecar's own path. The
documented gitignore advice puts the whole output dir inside a
gitignored tree, so that check rejected the tool's own output for
the same reason a user would gitignore it, and the source document
vanished from the corpus with nothing recorded about why. The check
exists to keep user files out of the scan, not to filter output this
same pass just produced from an already admitted source, so a
sidecar under the converted directory is now exempt from it. Fixes
#3504.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 900b376d39)