* fix(fastapi): carry package router mount prefixes to child routes
* fix(fastapi): address review feedback on nested router prefixes (#3408)
- Skip unprefixed includes in the parse-impl legacy loop so a bare
include_router in another file no longer shadows the real prefix.
- Union exact-file prefixes with legacy long/short prefixes via a shared
mergeMountPrefixes helper in both ingestion and the group extractor.
- Join the parent APIRouter(prefix=...) between the mount prefix and the
child include prefix.
- Resolve the group layer over every repo path (empty files included) so
absolute-import ambiguity matches ingestion.
- Memoize (file, prefix) frames so diamond-shaped include graphs stay
linear; drop the stack.pop() non-null assertion.
- Accept extra keyword arguments and a trailing comma in unprefixed
include_router calls without double-firing on prefix= calls.
- Document that pass-through is limited to a host named `router`.
- Bump parse-cache SCHEMA_BUMP to 123 for the new capture fields.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(fastapi): seed prefix propagation from bare router mounts (#3408)
- A router mounted without a prefix now seeds traversal with an empty
prefix (only when no prefixed mount targets the same file), so its own
APIRouter(prefix=...) reaches unprefixed children on both surfaces.
- An all-empty chain records nothing and leaves the child on its legacy
fallback.
- The bare-mount integration test no longer asserts that the test app's
unprefixed mount is absent; it pins only that the real prefix survives.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(fastapi): capture include prefixes after nested-call arguments (#3408)
- Let the Shape A/B and unprefixed include_router patterns step over one
level of nested calls such as dependencies=[Depends(auth)], so a
prefix= written after them is captured by the worker (the group
layer's tree-sitter patterns already handled this shape).
- Replace the unit test that pinned the dropped prefix with one that
pins the captured prefixes and the unprefixed Depends-only edge; add a
group-layer parity test.
- Correct the diamond test comment to 2^39 root-to-leaf paths.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): model restoring helper calls in decorator identity (#3414)
A bare module-level call to a same-file helper whose every global
binding of a descriptor name is an unconditional del or builtins import
now restores the builtin at the call site, matching CPython. A nonlocal
rebind nested in the enclosing function now shadows an owned builtins
import, closing a false builtin. Unprovable call orders stay fail-closed
and are pinned against CPython 3.11.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(review): apply review findings
Close three false-builtin paths the review found: a match-pattern
capture now counts as a binding (at any scope, including inside a
restoring helper), a call before the helper's def no longer counts as a
restore, and the helper-name uniqueness check sees match captures.
Pin the helper rejections (conditional restore, async, early and nested
return, wildcard import) against CPython 3.11.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cache): bump parse-cache schema to v122 for #3414
Python decorator identity verdicts changed, so warm v121 ParsedFiles
would replay stale receiver bindings.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): require an argument-free call to a helper with no required parameters
A call that fails to bind the helper's parameters raises TypeError
before the body runs, so it proves no restore. Keep such calls
fail-closed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): count only real captures and module bindings for decorator identity
Match value patterns, class names and keyword keys read a name rather
than capture it, so they no longer shadow a builtin descriptor. A local
of the same name as a restoring helper no longer disqualifies the
module-level helper; only a module binding or a global rebind does.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(python): state the fail-closed contract of the descriptor identity table
`false` means the resolver does not prove the builtin, not that CPython
shadows it. Cases where CPython keeps the builtin but the resolver fails
closed carry a comment saying so.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): resolve decorator identity like CPython
Decorator identity was decided by two different predicates. One read
the raw decorator text, so a trailing comment such as
`@staticmethod # type: ignore` hid the builtin and turned an explicit
first parameter into a fabricated receiver. The other trusted any
`staticmethod` spelling, including a module-level rebinding and a
`staticmethod(classmethod(f))` stack, which CPython cannot call.
Read the decorator expression node only, and recognize a bare builtin
descriptor only when the file does not rebind that name. Publish subtype
capacity only for a plain function or a single builtin staticmethod or
classmethod wrapper. Drop a no-op coverage guard, move the implicit
classmethod comment next to the code it describes, and bump the parse
cache to v121.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): shadow builtin descriptors only by visible bindings
The whole-file identifier scan counted plain reads (`staticmethod(f)`),
`from builtins import staticmethod`, and bindings that run after the
decorator as rebindings. CPython evaluates a class-body decorator with
LOAD_NAME when the `def` runs, so none of those change which object the
decorator names. Methods decorated with the real builtin lost their
subtype call shape, and static methods lost their first parameter in
arity metadata.
Move decorator identity into builtin-descriptors.ts and count only
binding occurrences (assignment and loop targets, walrus, def/class,
parameters, import aliases, except/with/match captures, del, type
parameters, and wildcard imports) that are visible where the decorator
runs: the class body or module before the definition, a repeating
enclosing loop, any binding in an enclosing function, and any
global/nonlocal rebind.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): model global and del like CPython's symbol table
`global x` and `nonlocal x` bind nothing; they redirect the declaring
function's own bindings of `x` to an outer scope. A bare declaration
was treated as an unconditional rebinding, so `@staticmethod` anywhere
in the file lost builtin recognition.
A module- or class-level `del` restores the outer lookup rather than
binding the name. Treat an unconditional `del` that runs after a
binding and before the decorator as undoing that binding. A `del`
inside control flow may not run, and a `del` inside a function still
makes the name local there.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): order global rebinds and honor builtins re-exports
A function that declares `global staticmethod` and assigns it rebinds
the module name only when called, and it cannot be called before the
top-level statement that defines it runs. Treat such a rebind as
visible only when that statement precedes the decorator, or when the
decorator sits in a deferred class body. `nonlocal` rebinds stay
visible anywhere in the enclosing function.
`from builtins import staticmethod as staticmethod` binds the builtin
to its own name, so it no longer counts as shadowing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(python): scope the descriptor-identity CPython claim
The wildcard-import case expects the fail-closed resolver verdict, not a
CPython outcome, because the imported module's exports are unknown.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): resolve descriptor names as LOAD_NAME does
Model each binding by its effect on the namespace (Language Reference
4.2.1): an import that binds the `builtins` object itself (plain, aliased
to the same name, or `from builtins import *`) restores the builtin, a
module- or class-level `del` unbinds so lookup falls through, and any
other binding shadows. Resolve the decorator like LOAD_NAME (4.2.2):
the class namespace, then module globals, then builtins, each as it
stands when the `def` runs.
A restoring effect counts only as an unconditional simple statement that
runs before the decorator, so an import or `del` under `if`/`try` or a
loop stays fail-closed. A helper's `global` delete depends on whether
the helper is called, which the resolver does not model, so it keeps the
override.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* refactor(python): simplify decorator descriptor lookup
Derive the descriptor type and name set from one `as const` list,
replace indexed non-null assertions with destructuring, build the scope
chain without an assertion, and skip the enclosing-function owner lookup
when the decorator has no enclosing function. Key the stacked-decorator
verdicts by case name so a failure names its case.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): resolve enclosing-function and deferred descriptor lookups
A class body reads a free name from the innermost enclosing function that
binds it (Language Reference 4.2.2). Evaluate that function's namespace
the same way as the class and module ones, so an unconditional
`from builtins import staticmethod` there resolves to the builtin. Any
other binding still shadows, including a local assigned only after the
class, which raises NameError rather than falling back to the builtin.
A class body inside a function runs whenever that function is called,
which can be any time after its top-level statement starts. Read module
state at that statement instead of after the whole module, so an earlier
`del` restores the builtin, and treat any later module override as
possibly visible.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): suppress uncertain decorated receivers
* fix(ci): keep uncertain Python receivers out of method arity
Unrecognized decorators now leave the receiver kind unproven, but the
first parameter is still the implicit receiver slot for ordinary bound
calls. Method extraction stopped stripping it, so decorated methods
reported one extra parameter and shifted capture arity metadata.
Share the uncertain-receiver classification between type-binding
synthesis and parameter extraction, then refresh the Python capture
golden and benchmark fingerprint for the intended capture change.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): resolve mixin calls with CPython C3 order
Breadth-first MRO bound a diamond mixin call to the wrong base, and dropping the site left the real method out of the graph. Use C3 and take the first compatible method in that order.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
* fix(python): correct mixin receiver baseline counts
* fix(python): guard incomplete mixin inheritance
* fix(python): record unresolved MRO tail coverage (#3393)
Track a missing subtype target when the last indexed MRO owner has an unindexed parent, and cover the case with an integration test. Correct the C3 fixture description.
Note: local full npm test timed out amid parse-worker startup failures; focused tests and benchmark baseline passed.
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat(mcp): add read_file + grep tools (REST parity for /api/file slice + /api/grep)
* chore(autofix): apply prettier + eslint fixes via /autofix command
* Address PR review feedback (#3377)
- Fail read_file and grep when full source is unavailable, matching the HTTP 410 contract instead of an empty grep or a not-found on a missing checkout.
- Reject branch on those tools so a pinned index is not labeled onto checkout bytes, and stop advertising branch in their schemas.
- Point the grep hint at a 0-based read_file window, pass caseSensitive and literal through, and test the handlers.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3377)
- Keep read_file and grep in the multi-repo schema requirement without advertising branch.
- Reject negative maxLines and return integer slice bounds for fractional line positions.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3377)
- Reject a negative read_file endLine before slicing so JavaScript does not treat it as an offset from the end of the file.
- Drop the fractional startLine/endLine claim so the integer schema is the advertised contract.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3377)
- Skip indexed grep paths whose realpath leaves the checkout so a symlink cannot return lines from outside the repo.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(bench): record the 19-tool MCP roster
read_file and grep are real tools, so tools/list and GITNEXUS_TOOLS both
moved from 17 to 19. The timing ratios were already inside budget.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(mcp): share read_file and grep contracts with existing helpers
Boolean grep flags go through isFlagTrue, the whole-file cap is one constant, and checkout tools stay on the per-repo schema without advertising branch.
---------
Co-authored-by: svjack <svjack@example.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(#2965): system headers must not resolve to in-repo files
C and C++ use two syntactically distinct include forms:
#include <x.h> -- angle-bracket: search system include paths only
#include "x.h" -- quoted: search relative to the including file first
The old suffix-match fallback in resolveCImportTarget had no awareness
of this distinction, so a repo containing its own stdio.h would capture
every #include <stdio.h> and resolve it to the local file.
Fix:
- Add isSystem?: boolean to the wildcard variant of ParsedImportSyntax
- interpretCImport / interpretCppImport now set isSystem from the
@import.system tree-sitter capture (present for angle-bracket form)
- resolveImportTarget in both cScopeResolver and cppScopeResolver
short-circuits to null when context.parsedImport.isSystem is true,
refusing to suffix-match system headers against workspace files
- Remove C and C++ from KNOWN_GAPS in the conformance test; add proper
test cases with a parsedImport factory that distinguishes angle-bracket
(isSystem:true) from quoted (isSystem:false) includes
Test: all 36 external-import-conformance cases pass, including the two
new c/cpp arms that were previously in KNOWN_GAPS.
* fix(#2965): update cpp-imports unit tests for isSystem field
Two test assertions were broken by the interpreter change:
1. Local include: the wildcard ParsedImport now always includes
isSystem (false for quoted includes). Updated expected object
to include isSystem:false.
2. System header: the old test asserted interpretCppImport returned
null for system headers. The refactored design moves the null
decision to the resolver layer (cppScopeResolver.resolveImportTarget)
so the call graph and resolution stay separate concerns. The
interpreter now returns { kind:'wildcard', isSystem:true } and
the test name/assertion are updated to reflect this.
* fix(#2965): resolve C and C++ includes on search paths
Angle includes follow each translation unit's include roots, so a local
stdio.h no longer captures system headers or another file's -I list.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3341)
- Accept in-repo include roots whose names start with `..` while still rejecting parent escapes.
- Correct the import-target bench comments so CONTEXT_LANGS and newPass match C/C++ header passes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(c,cpp): read include config per directory for monorepos (#2965)
Angle includes now resolve only against declared search roots, so config
read only at the repo root left monorepo sub-projects with nothing declared.
- compile_commands.json, compile_flags.txt, .ccls, .clangd and
c_cpp_properties.json are read in every directory; a file takes the
nearest one, and a nearer database's entry beats a shallower one (clangd).
- CMake include_directories / target_include_directories are read:
directory-scoped and PRIVATE roots reach their subtree, PUBLIC and
INTERFACE roots reach every file. ${CMAKE_CURRENT_SOURCE_DIR} and friends
expand; unresolvable variables and generator expressions are dropped.
- Declared roots win: implicit include/Headers/inc roots apply only when no
config speaks for the file, and never to a database-listed file.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(c,cpp): give CMake PUBLIC includes only to linked targets (#3341)
target_link_libraries now decides who sees PUBLIC and INTERFACE roots, so an unrelated target no longer resolves another package's headers.
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(auto-sync): allow self-hosted remotes via allowed_hosts
Auto-sync skipped any remote whose host was not github.com, gitlab.com, or gitee.com. Operators can now name exact extra DNS hosts in watch_config.yml without opening the default set.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3391)
- Dial auto-sync DNS names as absolute hosts and store that URL so a later fetch cannot follow a search domain.
- Reject ambiguous numeric host spellings; an exact dotted IPv4 the operator listed stays opt-in.
- Document allowed_hosts on the root auto-sync contract.
Note: pre-existing failure in unit tests that require dist/cli/index.js and parse-worker.js (this worktree has no build); not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(swift): discover nested Package.swift manifests for module grouping
A Swift monorepo laid out as Core/<pkg>/Package.swift has no root manifest
and no root Sources/, so every Swift file fell into one __default__ module.
The Swift resolver now walks the repo (bounded, skipping dot, ignored, and
Xcode bundle directories) and adds each nested package's targets, keyed by
repo-relative directory and ordered deepest-first so first-match grouping
picks the most specific target. Import resolution keeps the root view.
Refs #3355
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(swift): bound implicit IMPORTS edges by a total budget
Implicit same-module IMPORTS are n*(n-1) edges per module, and the graph
keeps every relationship in one Map capped by V8 at 2^24 entries. Emission
now fills a 4M-edge budget smallest module first and skips, with a warning,
any module that does not fit, so no module layout can crash analyze.
Refs #3355
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(swift): skip pairwise sibling passes for oversized modules
The target-siblings and sibling-type-bindings passes copy every file's
declarations into every other file of a module, so heap grows as n^2:
about 3.8 GB at 1,000 files with 15 defs each, about 15 GB at 2,000.
Modules over 1,000 files now skip both passes with a warning and resolve
through the global name fallback. GITNEXUS_SWIFT_MAX_MODULE_FILES changes
the ceiling.
Refs #3355
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(swift): cover nested SwiftPM packages end to end
A fixture with three nested Package.swift manifests (two declaring a target
named Net) and no root manifest. Each target gets its own implicit IMPORTS,
none cross packages, and Config() resolves to the caller's own package.
The Swift capture golden and scope-capture fingerprint grow with the new
fixture corpus only.
Refs #3355
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(review): apply review findings
- Rebase nested target paths with the existing Zig path helpers, which also
reject Windows drive paths and paths that resolve to the repo root (whose
empty prefix would group every file into one target).
- Document GITNEXUS_SWIFT_MAX_MODULE_FILES in the README env table.
- State that skipped modules resolve through lower-confidence fallback edges,
why nested-type fragments still run for oversized modules, and fix a stale
loader name in the target-grouping header.
Refs #3355
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(swift): cover every skipped Xcode bundle suffix in the package walk
Refs #3355
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(swift): model modules the way the compiler does
Replace the caps from the first round with representations that stay
linear, and derive module identity from the same sources the compiler uses.
- Same-module visibility is one File -> Module IMPORTS edge per file
(reason `module-membership`) instead of an edge per ordered file pair.
Incremental importer expansion treats files sharing a Module hub as
importers of each other. The 4M edge budget is gone.
- Sibling declarations and type bindings live in one shared table per
module in the namespace channel C# uses since #1871, instead of being
copied into every file. The 1,000-file ceiling and
GITNEXUS_SWIFT_MAX_MODULE_FILES are gone.
- Modules come from root and nested SwiftPM manifests (Sources, Source,
src, srcs; plugins under Plugins; the newest Package@swift-X.Y.swift)
and from Xcode native targets in project.pbxproj, including Xcode 16
synchronized folders. Target paths are matched from the repo root, so
a vendored copy of the same layout is no longer grouped into a root
target (this reverses #2931's floating match).
- `import X` resolves to the modules named X instead of any folder named
X. A name no module carries is external when every manifest and
project was read completely.
- The global-name-fallback veto uses the same membership, so test
targets, custom-path targets and Xcode targets have module identity.
Refs #3355
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(bench): move the Swift package bench to module grouping
The bench still imported the removed groupSwiftFilesBySpmTarget. Its
first-wins probe keeps its meaning under root-anchored grouping: the
clash file sits under Sources/Mod0, and the Sources/Mod1 further down its
path is a vendored copy.
Plugins are now non-importable modules under Plugins/, so parse_targets
counts importable source targets (still 3) and parse_binary_skipped also
checks that the plugin is recorded that way. Baseline values unchanged;
the notes say why the definitions moved.
Refs #3355
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(swift): match compiler module names, manifests, and target filters
- Module names follow the compiler's c99 mangling (`my-lib` imports as
`my_lib`); Xcode targets use a literal PRODUCT_MODULE_NAME or
PRODUCT_NAME when the project sets one.
- Package manifests are one-file modules, as SwiftPM compiles them. Files
outside every target are one-file modules once every manifest and
project was read; otherwise they keep the shared __default__ module.
- Xcode 16 synchronized-folder exceptions add a file to a target that
does not list the folder, and remove it from one that does.
- SwiftPM `sources:` / `exclude:` narrow a target; a computed list marks
the manifest unreadable.
- The default target folder is chosen once per package, as SwiftPM does,
instead of per target.
Refs #3355
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Address PR review feedback (#3387)
- Inferred Swift folders keep SwiftPM's first predefined parent when a
target name repeats (Sources before srcs).
- The pbxproj parser rejects a \U escape without four hex digits instead
of decoding garbage, so the project reads as incomplete.
- Extension owners are stamped in every Xcode membership of a shared file.
- A plugin name never reaches a plugin: neither the fallback veto nor the
folder-index fallback treats a known non-importable module as imported.
- A file an Xcode target compiles keeps that membership even when it also
lies under a SwiftPM target directory.
- The workspace scan bounds the queue, not only the directories read.
- Integration tests require each module hub to exist before comparing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(analyze): --memory-budget flag with heap-limit override and worker-pool degradation (#3137)
Adds an explicit `--memory-budget <mb>` CLI flag that overrides the
RAM/cgroup auto-sized main-thread heap ceiling for the parse phase:
- CLI validation (integer >= 200 MB) before bar.start(), matching the
--workers pattern
- Threaded CLI → runFullAnalysis → PipelineOptions → parse-impl as
memoryBudgetBytes
- parse-impl resolves the heap limit as budget ?? v8.heap_size_limit, so
both the preflight projection warning and the #2649 mid-loop abort
probe honor the budget
- Graceful degradation: when the projected heap need exceeds the budget
at the computed pool size, the pool shrinks (never below 1, never
above the operator's --workers) before sub-batch math and pool
construction, so all downstream consumers see the degraded size
Omitting the flag keeps the auto-sizer path byte-identical.
Refs #3137
* feat(analyze): collapse rebuild-gate log into one summary + persist needsFullRebuild verdict (#3137)
The nine meta-mismatch rebuild gates (pdg mode, content retention,
schema fingerprint, graph-write collapse, analysis features, Spring
vendor prefixes, runner identity, FTS CJK mode, embedding dims) each
logged individually and set force:true independently. An upgrade that
trips several at once printed a scattered wall of near-identical
warnings.
- Gates now collect into rebuildReasons[]; a single summary block
prints them (inline for one, numbered for many) and sets force once.
Per-gate Tip text is preserved verbatim inside the entries.
- The verdict persists to meta (needsFullRebuild: {reasons, recordedAt})
BEFORE the rebuild starts. If the rebuild is interrupted, the next
run announces the recorded reasons up front instead of quietly
attempting an incremental write on a half-rebuilt index — the gates
may not all re-fire against a wiped DB.
- The verdict is cleared on the next successful completion (the final
meta does not carry the field forward).
Semantics unchanged: every gate was already evaluated (none
early-returns), force is idempotent, and a rebuild happens iff at
least one reason fired.
Refs #3137
* refactor(cli): share one integer flag parser across analyze, watch, and wiki
Replace the duplicated Number.isInteger checks for --workers, --embeddings,
the positive env-backed analyze flags, the watch interval flags, and wiki's
--timeout/--retries with parseIntegerOption (per-flag minimum, optional
scale for the safe-integer bound). User-facing messages are unchanged.
* fix(analyze): make --memory-budget set the real V8 heap through the respawn
The budget now drives ensureHeap's existing respawn instead of a parse-phase
override, so the #2649 preflight, mid-loop abort, remedy text, and GC pacing
all see one heap limit. The respawn sizes old space plus three semi-spaces to
equal the budget, the child resolves as already at the budget (no second
respawn), and GITNEXUS_HEAP_LIMIT_SOURCE drives budget-aware OOM advice.
Budget validation moves to the preAction hook so analyze and watch reject a
bad value before any respawn. Removes the pool-shrink block and the
memoryBudgetBytes plumbing through PipelineOptions and run-analyze.
* docs(analyze): describe --memory-budget accurately and translate its help
The help text claimed graceful worker-pool degradation, which no longer
exists; it now says the flag sets the main-thread V8 heap and that parse
workers keep their own caps. Wires the option through the help i18n map
with en and zh-CN strings, and documents it in both READMEs and the
out-of-memory troubleshooting section.
* feat(analyze): add a pure rebuild-reason collector
One collector per run holds keyed rebuild reasons, merges by key, flattens
reasons stored by an interrupted rebuild into one recovery entry, validates
stored reasons on read, and formats the single up-front summary plus one
follow-up line for reasons added after the pipeline.
* fix(analyze): route every forced rebuild through one reason collector
Every path that forces a full rebuild (the nine meta gates, --force,
--skills, --no-parse-cache, --drop-embeddings, --repair-fts retention,
Spring Actuator, AsyncAPI, shared-store graph gaps, dirty-flag recovery,
the post-pipeline capability gate, and the #2409 escalation) now adds a
keyed reason to one collector. The rebuild decision is applied from the
collector at fixed checkpoints, one summary prints right before the
pipeline, and late reasons print one follow-up line. The escalation stays
non-forcing. runFullAnalysis returns the collected keys, which replaces the
runner-identity source-regex test with a behavior test. Removes the separate
needsFullRebuild field and its announcement, and stops folding --skills and
--no-parse-cache into --force.
* fix(analyze): persist rebuild reasons on the existing crash marker
Every incrementalInProgress writer (the full-rebuild stamp before the wipe,
the incremental pre-write, saveIncrementalDirtyState including the #2409
escalation, and buildFtsDirtyStamp) now carries the collected reasons into
the active slot's metaDir, so an interrupted rebuild explains itself on the
next run through one merged recovery entry. A successful run still clears
the marker and its reasons; the FTS-park recovery clears them without
forcing.
* test(analyze): cover every rebuild-reason key through runFullAnalysis
Add a coverage table that the typechecker keeps complete: every
RebuildReasonKey maps to a test file that drives it through
runFullAnalysis and asserts the returned key. Adds the missing
graph-write-collapse and drop-embeddings drivers, asserts the key in the
existing pdg-mode, spring-vendor-prefixes, cjk-segmentation, and
embedding-dims tests, and removes plan-local IDs from test names and
comments.
* fix(review): apply review findings
- A --max-old-space-size pin equal to --memory-budget no longer counts as
the exact budget heap (V8 adds the young generation on top); only the
budget-respawned child skips the respawn, so the limit really equals the
budget.
- Snapshot the analyze env before ensureHeap and restore
GITNEXUS_HEAP_LIMIT_SOURCE, so a kept process does not leak its heap
source into a later programmatic analyzeCommand call.
- --skills and --no-parse-cache keep the forced storage requirements they
had before force stopped being folded from them.
- Merge the duplicated follow-up announcement into one helper and fix a
stale --drop-embeddings comment.
- The rebuild-reason coverage table no longer greps driver files for the
key string; add tests for a programmatic invalid budget and the
multi-cause interrupted-rebuild text.
* fix(review): don't announce the escalated write as a full rebuild
The #2409 escalation is a non-forcing reason, but its follow-up line used
the 'Full rebuild also required' lead. A follow-up that carries only
non-forcing reasons now leads with 'Write plan changed'.
* docs(analyze): document GITNEXUS_HEAP_LIMIT_SOURCE in the env table
CONTRIBUTING requires every new GITNEXUS_* variable to have a row; this one
is internal (set by analyze itself) and exists so OOM advice points at
--memory-budget.
* fix(review): address GitNexus review threads on #3386
- heapPressureRemedy measures pressure against the real auto-sized cap
(heapCapMbFor) instead of a flat 0.75 x RAM, and no longer tells a
GITNEXUS_MEMORY=off run with no pin to drop a pin that does not exist.
- toStored() persists the interrupted rebuild's reasons first, as documented.
- ensureHeap's doc names which paths leave GITNEXUS_HEAP_LIMIT_SOURCE unset.
- The heap-respawn suite restores the caller's GITNEXUS_MEMORY.
- The non-forcing follow-up test rejects any 'full rebuild' wording.
* fix(review): require both budget flags and check key coverage at runtime
- A budget-respawned child is recognized only when the inherited heap-source
marker comes with both the budget's old-space and semi-space flags; the
marker alone is an inherited env var, not proof. The old-space parser is
generalized to any V8 size flag instead of copying its regex.
- REBUILD_REASON_KEYS is exported and RebuildReasonKey derives from it, so the
coverage table is checked at runtime (CI does not type-check test files).
* fix(review): don't claim a full rebuild in a non-forcing summary
formatSummary and formatFollowUp now share one leadFor helper, so a block
of only non-forcing reasons reads 'Write plan changed' in both.
---------
Co-authored-by: ChunxueLi <mecoloud@users.noreply.gitee.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
* fix(dart): resolve package imports by pubspec identity
* fix(dart): keep package-identity edges out of the cycle check
Pubspec identity edges invalidate importers when a manifest changes. They cannot form an init cycle, so the cycle query excludes them before the row cap. Discovery reads each manifest once, with a size bound, and resolution shares one package-URI parser with those edges.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3369)
- Reject package URIs with an empty library path so they do not emit identity edges
- Skip the pubspec permission test where chmod cannot deny reads
- Document that the Dart heap probe is not a uniqueTarget spelling
Note: pre-existing failure in test/unit/incremental-index-extension-dml-gate.test.ts not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(dart): list package directories through a no-follow descriptor
A directory replaced by a symlink between the parent listing and the next visit must not be traversed. The walk opens it with O_DIRECTORY|O_NOFOLLOW and lists that inode.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
* fix(mcp): keep the Dart identity reason out of MCP startup
The cycle query still excludes the same reason string. The constant now lives with the other non-initializing import reasons, so MCP startup does not load a language provider.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3369)
Open discovered pubspecs and child directories through the parent directory inode on Linux, so replacing that directory with a symlink cannot redirect the walk.
Note: pre-existing failure in test/unit/incremental-index-extension-dml-gate.test.ts (worker pool startup timeout) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(dart): reject Windows junctions during pubspec walk
* Address PR review feedback (#3369)
Refuse pubspec discovery that cannot set O_NOFOLLOW, and verify macOS child opens against the pinned directory chain instead of reopening a mutable path.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(dart): bound live pubspec descriptors and close the macOS check-then-open
A deep directory chain held one descriptor per level until open failed with EMFILE, and macOS child opens statted the path before using it. Refuse the next directory at 64 live handles, and stat only the descriptor opened with O_NOFOLLOW.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(dart): open pubspecs non-blocking so a FIFO cannot hang discovery
A listed pubspec can be replaced by a FIFO before open. O_RDONLY alone waits inside open for a writer, so the file-type check never runs. O_NONBLOCK returns immediately and the walk rejects the non-regular file.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(dart): cap names read from each pubspec directory
readdir kept every entry before the visit budget could run, so one huge directory could allocate without bound. Read the listing one name at a time and fail closed past 100,000 entries.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(dart): reject a pubspec that grows while its descriptor is read
The size cap was taken from the stat before the read, so a file that grew in that window could be parsed from a short prefix. Re-stat the same descriptor afterward and fail closed when the size no longer matches the bytes captured.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ci): rebaseline the Dart scope-capture fingerprint for package-import fixtures
The benchmark hashes every dart-* fixture. The new package-import corpus adds six Dart files and 33 capture groups. Parking that directory restores the previous fingerprint, so this is corpus growth, not a capture change.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(impact): fail closed on id-less targets and follow ??/||/?: callable values (#3354)
#3354 reports `impact` returning a byte-identical 1037/CRITICAL/`exact`
result for three unrelated targets, with only `target.name` differing.
The reporter's `target` had no `id` and no `filePath`, which the traversal
path always emits, and a synthetic reproduction of their monorepo (pnpm,
Cloudflare worker-configuration.d.ts in five packages, Hono, a Durable
Object) resolves every target correctly on main. So the identical result
was not reproduced. The repro did surface two real gaps and one hardening
point:
- `_runImpactBFS` now throws when the target has no node id. Every
caller already catches, so impact reports `impactedCount: null,
risk: UNKNOWN` instead of a normal-looking blast radius that cannot be
about this symbol.
- Callable-value flow followed only a single designator on the RHS, so
`const sweep = env.__sweep ?? runSweep; await sweep(env)` produced no
flow and `scheduled` was missing as a caller of `runSweep`, while the
result still claimed `epistemic: exact`. Each branch of `??`, `||`,
`or`, and `?:` now flows into the binding (language-neutral: operator
and field names, no language checks). Parse cache bumped 104 -> 112
(105-111 are claimed by open PR #3326).
- A whitespace-only `target_uid` (strict adapters materialize omitted
optional strings) is treated as omitted and falls back to the name.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(mcp): treat a non-string uid as omitted instead of throwing (#3354)
Review follow-up on #3373. `query.uid?.trim()` called `.trim()` on a
client-supplied value, and the MCP envelope is not type-validated, so
`context({uid: 42})` threw a TypeError that `context()` does not catch.
Before #3373 the same input ended as a structured not_found.
A non-string uid now counts as omitted, which matches how
normalizeToolParams already treats a non-string `target_uid`. The impact
and trace not-found messages print a trimmed uid only when it is a
non-blank string, so a strict adapter sending " " sees the name it
searched for instead of `' '`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(ingestion): expand ??/?:/ternary branches through a provider hook (#3354)
Review follow-up on #3373. The shared value-alternatives rule keys on
tree-sitter field names, and several grammars spell the same construct
differently, so the expansion never fired for them:
- Kotlin `elvis_expression` has no fields.
- Swift uses `value`/`if_nil` and `if_true`/`if_false`.
- Dart uses `first`/`second`, and its conditional has no `condition` field.
- Python `a if c else b` has no fields.
The `'?:'` operator entry was dead, since no bundled grammar emits it.
Add an optional `valueAlternatives` hook to CallableFlowCaptureOptions,
consulted before the shared rule, and implement it in the Kotlin, Swift,
Dart, Python and Ruby providers. Shared code still names no language.
Ruby's statement-bodied `if`/`unless`/`elsif` also carries
`condition`/`consequence`/`alternative`, so the shared ternary rule dug
an identifier out of an arbitrary statement (`g = h; 0` flowed `h`) and
produced a wrong CALLS edge. The Ruby hook now keeps a multi-statement
branch as one opaque source, as before #3373.
Tests: new provider fixtures for Python, Kotlin, Swift, Dart and Ruby
(including a Ruby negative), and TS chain, parenthesized, callable-left
and `&&` negative cases. Captures goldens gain one entry each for the
new fixtures. SCHEMA_BUMP stays 112 (same unreleased PR).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(ingestion): keep long ||/??/?: chains linear in callable-flow capture (#3354)
Expanding value-selecting sources into per-branch flows made long chains
super-linear and, past a few thousand operands, a stack overflow. A
generated `w === "k0" || w === "k1" || ...` keyword table took 6.7 s at
1000 operands and 48 s at 2000, and threw RangeError at 8000.
Two causes, both fixed without changing any emitted capture:
- valueAlternatives recursed once per operator level and spread the
partial results at every level. It is now an explicit stack that
writes to one output array. The left-to-right order, the paren
unwrapping, and the provider-hook contract (`[node]` means opaque) are
unchanged.
- Every alternative ran its visibility walks from its own leaf, which
can be as deep as the chain is long, all the way to the root. Also,
tree-sitter's `parent` re-descends from the root, so each step costs
the node's depth. The walks now jump between "anchor" nodes, the only
nodes any check can match: region ids and formal owners. The nearest
anchor is memoized per node across the file, and parents come from a
map recorded by the one DFS the synthesizer already does.
After the fix: 0.34 s / 0.23 s / 1.6 s at 1000 / 2000 / 8000 operands.
That is within about 1.2x of main without the expansion; what remains
is tree-sitter query time. Capture fingerprints are byte-identical
before and after the fix for all 16 scope-capture bench languages and
for the Python harness.
A `typescript-deep-chain` case added to the scope-capture bench guards
the scaling: 1.11-1.22 now, 7.5-8.0 before. A unit test pins a
10000-operand chain: it must still yield the seed for a callable
operand, the copy for a formal at the deepest leaf, and the invoke.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(kotlin): see through braced if-branches in callable alternatives (#3354)
tree-sitter-kotlin wraps a braced branch as
`control_structure_body > statements > <expr>`, so for any non-empty
block kotlinValueAlternatives saw exactly one named child (`statements`)
and pushed the wrapper itself as the branch value. operandSyntax emits
nothing for a `statements` node, so
`val run = if (c) { ::f } else { ::g }; run()` produced no flow edges,
and the "multi-statement block stays opaque" guard could never fire.
Descend one level through `statements` and require exactly one
non-comment expression there. Empty blocks (`{}` has no named children),
multi-statement blocks, and `if` without `else` still return the whole
`if` as one opaque source. The doc comment now describes that.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(ruby): skip only the multi-statement branch in callable alternatives (#3354)
rubyValueAlternatives returned `[node]` (opaque) as soon as one branch
held more than one statement. Two things went wrong because of that:
- At the top level, `run = if c then g = h; 0 else method(:f) end`
dropped the single-statement `else`, so `f` got no flow edge.
- In an elsif chain, the outer `if` pushed the `elsif` node as a branch.
The shared loop called the hook on it again, got `[elsif]` back, and
used the whole elsif subtree as one source. So in
`if a then method(:run_a) elsif b then g = h; 0 else method(:run_b) end`
the `run_b` edge was lost.
The hook now walks the elsif chain itself and skips each multi-statement
branch, while every single-statement branch still becomes an
alternative. This cannot add a wrong edge: each emitted alternative is a
value the conditional really evaluates to, and nothing is taken from the
skipped branch. Before, that branch did not contribute a resolvable
value either. A whole conditional used as a source becomes a
qualified-name seed, which resolves to nothing when it has more than one
identifier leaf. With a single identifier leaf, it could even seed the
condition variable. When no branch is a single statement, the
conditional still stays one opaque source, as before.
Ruby captures golden: ruby-callable-alternatives/app.rb goes from 33 to
58 capture groups. 23 of them come from the new fixture functions
(checked against the old source). The other 2 are the new branch
alternatives, `statement_if -> run_sweep` and `elsif_chain -> run_b`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(ingestion): flow the right operand of && / and into callable bindings (#3354)
`a && b` / `a and b` yields `a` when it is falsy and `b` otherwise. A
falsy value is never a callable, so the right operand is the only one
that can be invoked later. The capture left `&&` unexpanded, and the
compound source became a qualified seed that resolves to nothing:
`const run = x && f; run()` gained no edge to `f` while impact claimed
`exact`. Python `x and f or g` reached only `g`.
The shared expansion now maps `&&` / `and` to the right branch only.
It recurses, so `x and f or g` reaches both `f` and `g`. Where `&&`
yields a boolean (Java, C#, Go, Rust, C, C++, PHP, Zig), the
destination cannot be invoked, so the flow never meets a call. A
before/after CALLS diff over all 83 lang-resolution fixtures that
contain `&&` or `and` shows exactly one new edge,
logicalAnd -> runAndRight. PHP and Ruby bind low-precedence `and`
looser than `=`, so `$g = $x and $y` never reaches this rule.
The Python golden digest changes only for the extended
python-callable-alternatives fixture.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(ingestion): keep operator branches of a value-selecting source opaque (#3354)
The fan-out sent every branch of `??` / `||` / `or` / `?:` to
emitAssignmentFact on its own, including branches that compute a value.
`Handlers.fallback === run || fb` then emitted the comparison as a seed
whose qualified text sliced to receiver `Handlers`, member `run`, and
resolveBoundMemberCandidates minted a CALLS edge to `Handlers.run`. The
same happened for `this.state !== run ?? this.fallback`,
`this.state + run || this.fallback`, and Python
`self.state != run or self.fallback`. Before the fan-out, the whole
compound source was one opaque seed.
A branch that is a binary operator expression now contributes nothing.
The check uses the field vocabulary valueBranches already reads (a
`left`/`right` pair, or an `operator`/`operators`/`op` token after the
expression start), not grammar type names. Member accesses that field
their `.`/`->` as `operator` (Ruby `call`, C/C++ `field_expression`) also
field a member name through the list memberParts uses, now shared as
memberNameNode, so they stay designators. Unary `&f`/`*fp` lead with
their operator and stay designators. Call results were already dropped
by emitAssignmentFact, and lambdas and callable references are unchanged.
CALLS-edge diff over 87 lang-resolution fixtures (505 -> 501 edges):
only the four false edges above were removed, and none were added.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(ingestion): pin the LEFT operand of ?? / or / elvis per provider (#3354)
The Python, Kotlin, Swift and Dart "override ?? fn" cases put an
unresolvable parameter on the left, so a fan-out that kept only the
last operand still passed them. Each fixture now adds a callableLeft
case with a real function on the left and a parameter on the right
(`run_left or fallback`, `::runLeft ?: fallback`, `runLeft ?? fallback`),
and each language asserts `callableLeft → runLeft`.
Mutation check: dropping the left branch from the shared `??`/`||`/`or`
rule and making the four provider hooks return only their last operand
fails all four new tests, while the existing right-operand tests still
pass.
Capture goldens were regenerated with UPDATE_GOLDEN=1:
- python app.py: 35 -> 85 groups. 35 -> 75 is stale drift from
8ad0d8db7, which added the comparison cases without regenerating.
75 -> 85 is the new case: 2 declarations, 2 scopes, the variable,
the `run()` reference, and 4 callable-flow captures (seed -> run_left,
copy <- fallback, formal, invoke).
- swift App.swift: 25 -> 36 groups for the same new case, plus
Swift's type-binding for the fallback parameter.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(bench): re-baseline scope-capture fingerprints for #3354 callable alternatives
This PR makes a callable chosen by a value-selecting source flow every
branch it can yield (??, ||, or, && right operand, ternary, statement
if, elvis). It also keeps an operator branch opaque. Both change
@callable-flow captures, and the PR adds *-callable-alternatives
regression fixtures that these benches glob.
To verify, the BASE (merge-base 233ca2849) and HEAD emitters were run
over the same HEAD fixture corpus plus the synthetic source. Every
added or removed match is an @callable-flow.* match on one of those
sources. The pre-existing corpus is byte-identical for all 16
scope-capture languages and for Python. Emitter delta, then corpus
growth:
- ruby: +5/-0 app.rb (+1 file, +58 groups)
- swift: +6/-3 App.swift (+1 file, +36 groups)
- dart: +6/-3 app.dart (+1 file, +33 groups)
- kotlin: +8/-2 App.kt (+1 file, +71 groups)
- typescript: +18/-14 4 files (+288 groups)
- python: +11/-7 app.py (+1 file, +85 groups)
The removed matches are whole-expression seeds and copies that carried
a qualified name of the compound source. They are replaced by one
seed or copy per operand. Synthetic scaling counts are unchanged and
every scaling ratio stays under budget. Per-language notes are in
baselines.json under _rebaselined_3354_callable_alternatives.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* refactor(mcp): share one blank-uid check across impact, trace and symbol lookup (#3354)
The "trimmed uid, or omitted when blank/non-string" rule was inlined four
times, three of them trimming twice. nonBlankUid() owns it now; behaviour is
unchanged. The whitespace and empty target_uid tests collapse into one it.each.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* docs(cfg): correct the Kotlin and Python grammar-field notes (#3354)
The Kotlin CFG visitor claimed no control-flow node has fields; a parse of
the vendored grammar shows if_expression fields condition/consequence/
alternative (when/for/while/do/try/elvis are fieldless). The Python harvest
note listed conditional_expression's children like field names; the node is
fieldless and they are positional. Both notes were misleading reviewers of
the #3354 value-alternatives hooks. Comment-only.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(ingestion): skip optional-grammar suites in callable-alternatives providers test (#3354)
Kotlin, Swift and Dart grammars are optional installs. Guard their describe
blocks with isLanguageAvailable, as swift.test.ts and dart.test.ts do, so an
install without one of them skips instead of failing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(ingestion): probe the Dart parser before enabling its providers suite (#3354)
isLanguageAvailable only proves the module loaded; tree-sitter-dart can still
fail on setLanguage. Probe loadParser/loadLanguage and skip on failure, the
same guard dart.test.ts uses.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>