mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 16:52:09 +08:00
446d08c272f18e8ce9e61ea537228240893fc131
1793
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
446d08c272 |
feat(nextjs): a route links to the proxy that runs before it (#3119)
Next wires proxy.ts (middleware.ts before Next 16) by file name and
config.matcher, never by an import, so to the graph it was a function
nobody calls. `codegraph impact middleware` came back with just the
function, for the one file that gates every authenticated route.
A new synthesis pass links each route the matcher accepts to what runs
in the proxy: a `calls` edge, synthesizedBy next-proxy, carrying the
matcher entry and where it is written. It looks for the proxy only where
next build loads it, beside pages or app, app by app, and takes a lone
proxy.ts only when the app doesn't pin Next below 16.
Three decisions differ from PR #2265, each from a real-repo result:
- The edge is `calls`, not `decorates`. `callers` and explore's Flow ride
calls, so with decorates the proxy has no callers and every flow stops
at the route. routeRoots and the Screens walk leave the edge out, as
they do for other edges that don't say where a route's code starts.
- Regular-expression matchers are read. In a sample of 157 proxies from
GitHub code search, about two thirds use a negative lookahead such as
/((?!api|_next/static).*), which a reader of literal paths links
nothing from. compileMatcher builds its expression from the parts it
recognizes and never from the entry's text, since new RegExp() on a
pattern in an indexed repository could hang the index.
- A wrapped default export links to the file. About a third of that
sample, which two search queries looked for, is `export default
clerkMiddleware(...)` or similar, which has
no node, and the file node is where the wrapper's calls are recorded.
Validation:
- nextjs-proxy.test.ts, 56 tests; 8 end-to-end ones fail without the
pass, and the Screens one fails without the walk change.
- Against path-to-regexp 6.3.0, Next's own matcher library: 344 entries
from the 157 proxies, 339 read, all agreeing on 27 addresses; 5 not
read, which link nothing. Route by route on the four apps below, 387
of 387 agree.
- A/B on main
|
||
|
|
a2870563d7 |
feat(drupal): services, plugin definitions and #[Hook] methods are part of the graph (#3118)
Drupal wires most of a module by id, attribute and folder, so services,
plugins and OOP hooks were missing from the graph, and a call through the
service container stopped at `\Drupal::service()`.
Built on PR #1077, the stronger base, with what's sound in PR #301:
- *.services.yml: a component node per service, linked to its class and
to the services it is given, in every place Symfony takes one
(arguments, calls, factory, parent, decorates, aliases), class-named
services included. Event subscribers are services and come with it.
- A call on what the container hands out, `\Drupal::service('x')->m()` or
`$container->get('x')->m()`, links to the method on that service's
class, its own or inherited. Neither PR had this; it's what connects
the services to a flow.
- Plugins: an attribute or docblock annotation with an id, on a class
under src/Plugin, src/Entity or src/Element, read by folder as Drupal's
discovery does, rather than by the allowlists in both PRs. Those miss
every contrib type: in farmOS and Commerce, LogType, AssetType,
CommerceCondition and the like are a large share of the definitions.
- `#[Hook('x')]` on a method, stacked, or on a class naming a method or
meaning __invoke, from PR #301, reading the method's id from the
attribute's line. Tree-sitter counts attributes as part of the method,
so PR #301's id, taken from the `function` line, attached to no node.
Left behind from PR #301: guessing a plugin's base class by name, which
fell back to any class containing the type's name; matching a hook to
any method of that name; and a stray CODEGRAPH_MCP_TRACE change to the
proxy.
Two things the A/B caught:
- Named by bare id, as in PR #1077, a plugin or service answered for the
class of that name: `new Currency()` landed on the plugin `Currency`,
and `use Drupal\x\Y;` on the service `Drupal\x\Y`. Nodes are now
`service:<id>` and `<Type>:<id>`, which no symbol can be, and search
still finds them by id.
- A FQCN resolved by its last segment, so `use Drupal\commerce_price\Price`
also linked to the unrelated Element\Price. The namespace now decides
first, and services use it alone.
Validation:
- drupal-services.test.ts, 25 tests, on the kernel and the wasm path; 21
fail with main's resolver. drupal.test.ts is unchanged and passes.
- A/B on main
|
||
|
|
d494e432ed |
fix(graph): a path search keeps one predecessor per node, so it stays linear
findPath's breadth-first search copied the whole path so far into every queue entry and dequeued with Array.shift(). A long path cost time and heap quadratic in its length, and a queued frontier held one copy per entry. The search now keeps one predecessor per discovered node in a map, walks a queue of ids with a head index, and builds the path once at the end. It keeps the enqueue-once set from PR #2016. Targets are fetched once each even when parallel edges point at the same node. Red, green and A/B: - Red on main: a 400-hop chain walks 81,001 array elements, against under 4,000 now, and a fan-in batch asks for the same node twice. No timing in either test. - Same paths: 400 seeded pairs each on excalidraw, gin and flask gave the same node and edge sequence on both arms (1,200 of 1,200). - A 20,000-hop chain: 1,007 ms and 47 MB of heap on main, 30 ms and 13 MB here. The test added in PR #2016 now counts node lookups per id instead of patching Array.prototype.push, because the queue no longer holds objects with a path. Its expectation is unchanged. Original PR #1583 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c0b490626b |
fix(python): a class-name call doesn't follow a base imported from outside the project (#3115)
PR #3109 stopped a base imported from a package linking to a project class of its name, but only for the extends edge. The reader behind class-name calls follows the base list as written: declaredSupertypes gives the names and findInheritedClassMethod looks each up by name. So with class Color(Enum) after from enum import Enum, Color.name_of() still reached a project Enum.name_of, as did color.name_of() on a receiver named after the class. On sqlalchemy, Query(..., Generic[_T]) with Generic from typing walked to a test's local class Generic, then to that test's Base and on to FunctionElement, so query.select() linked to FunctionElement.select. The walk now skips a project class whose name the head binds outside the project, with the check PR #3109 added, moved to name-matcher.ts so both can call it (the import resolver already imports from there, not the reverse). A dotted base is bound through its first word: enum.Enum through import enum, e.Enum through import enum as e, collections.abc.Mapping through import collections.abc, which the check now counts as binding collections. Validation: - python-outside-bases.test.ts gains the calls: named import, receiver, dotted, aliased and deep bases reach nothing, and a project base and a class in a file the import path names still reach their methods. Without the gate 2 of the 4 new tests fail, and taking out the dotted word or the import a.b binding fails the dotted one. 11 pass here. - A/B on 11 Python repos plus express and gin, 496,746 edges: 10 calls removed, 0 added, 0 moved, 0 extends touched. All 10 are in sqlalchemy and all were wrong: 6 query.select(), query.alias() and query.compile() and 4 value.self_group(), each through typing's Generic into a test's own class Generic and its Base. - Controls express and gin unchanged. - Index cost on sqlalchemy, median of 3 interleaved: 27.8 s on main, 26.4 s here, which is noise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
61e1621813 |
feat(installer): nine more agents: Bob, Droid, Windsurf, Qwen, dsh, Grok, Kimi, Qoder, Pi (#3113)
codegraph install now sets up IBM Bob, Factory Droid, Windsurf, Qwen Code, DeepSeek Harness, Grok Build, Kimi Code, the Qoder CLI and Pi. Each target is one new file plus a registry entry, and each path and format follows the agent's own docs. These are our own implementations of contributor PRs 642, 674, 2525, 952, 968, 2511, 1591, 1645, 1859, 2066 and 2112, credited in the CHANGELOG. Where a PR and the docs disagreed, the docs won: - Bob's global file is ~/.bob/settings/mcp.json; a lone mcp_settings.json, the name IBM's watsonx.data guide uses, is still honored. Its entry has no type. - Qwen honors QWEN_HOME, Kimi KIMI_CODE_HOME, and Kimi gets the AGENTS.md block its docs describe. dsh gets the block too, in $DSH_HOME/AGENTS.md. - Qoder's docs name no IDE file, so the target is the Qoder CLI: ~/.qoder/settings.json and .qoder/settings.local.json, not .qoder/mcp.json. - docs.windsurf.com now redirects to Devin's docs. The stable channel's file stays documented, and Devin imports it by default. What the targets share: - json-config.ts edits mcpServers.codegraph in place with jsonc-parser, so comments and formatting survive, for the seven JSON agents. A file left empty by uninstall is deleted through removeFileUnlessLinked. - An entry already there is the user's, in all nine. A re-install or refresh only fills in missing keys, so a pinned cwd or command, a disabled switch and Pi's exposure all survive. This is what replaces the cwd reset in PR 1591. - writeTomlTable and deleteTomlTable move out of codex.ts, so Grok gets the same conflict handling as Codex and never defines the table twice. - configDirFromEnv reads the agents' home overrides, and the line helpers move from hermes.ts to shared.ts for the dsh patch editor. Validation: installer-targets.test.ts has 624 tests, 614 passing and 10 POSIX-gated. New tests pin each target's path and entry at both locations, comments, CRLF, a BOM, unparseable files, user-owned entries, TOML conflicts and 18 dsh cases. The real CLI ran install, re-install, refresh and uninstall for all nine against a private home, and 20 dsh and Grok file shapes through PyYAML and tomllib, with no failures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
002dd4d517 |
feat(frameworks): CodeIgniter 3 routes, and calls through what $this->load loads (#3112)
A CodeIgniter 3 project had no routes in the graph and its flows stopped
at the controller: `$this->users_model->find($id)` goes through a property
nothing declares, so most such calls failed and the rest were guesses by
method name.
The resolver is detected by system/core/CodeIgniter.php, or by a routes
file naming a default controller, at any depth. It makes:
- a route per public method of a controller, implicit-public ones too,
at /<folder>/<class>/<method>, and HMVC modules with the module first;
- a route per $route entry in routes.php, verb-scoped or not, and the
default controller at /. `controller/method/$1` walks to the controller
as CodeIgniter does, and a variable in the method's place binds the
class;
- a `calls` edge for `$this->x->m()`, `$this->CI->x->m()` and
`$CI->x->m()` to the method on the class x holds, its own or inherited.
A class is read by name whatever the case, as PHP and the loader read
it, and an alias comes from the load that gives it;
- a `calls` edge from `$this->load->view('a/b')` to the view's file, where
the calls a template makes are recorded, so a request can be followed
to the end.
Differences from PR #167, each from the real apps: its controller test
is a list of base classes, which misses EA_Controller, Secure_Controller
and every other project's own; its property match is PascalCase only,
which misses `$this->providers_model` and `$this->mdl_invoices`, the form
two of the four apps use throughout; and it stopped at a file-level
`imports` edge to the class, so the flow still ended at the controller.
A reference it made to a method by name could land on a same-named method
in another controller, so a route's reference here names its own file.
Validation:
- codeigniter.test.ts, 21 tests, on the kernel and the wasm path; the 10
on a real index fail with the resolver unregistered.
- A/B on main
|
||
|
|
bea5e205f4 |
fix(python): a class with PEP 695 type parameters reads its base list
pythonBaseList, the resolver's reader of a Python base list behind module-global, self. and class-name dispatch, expected the ( of the list right after the class name. In class Repo[T](Base[T]): it found the [ of the type parameters, so the class seemed to have no bases there, though extraction gave it its extends edge: current = Repo() then current.run(), and Repo.run(x), reached nothing. The reader now skips a type parameter list between the name and the bases, with brackets balanced, so bounds like T: dict[str, int], *Ts and **P go whole, and a constraint tuple like [T: (int, str)] isn't taken for the base list. Either list can wrap over several lines. Anything else after the name and its parameters, such as the : of class NoBases[T]:, still means no base list. Validation: - The test file: 4 of 7 fail on main (the controls pass), 7 pass here, and the multiline, generic-base and type-argument suites still pass. - A fixture of ours on the built CLI, kernel on: Repo.run(None), a wrapped list, a constraint tuple and a module global reach no method on main and Base::run here. class NoBases[T] reaches neither run, and Typed[T](Base[User]) does not reach User.save. - A/B against main on 11 Python repos, 486,377 edges (requests, flask, litestar, mypy, sqlalchemy, polar's and django's packages, and four small apps that use PEP 695 bases): nothing changes. Neither do express and gin, whose full dumps are byte-identical. polar has 39 such classes, and none is reached by a call this reader decides. - Index cost on sqlalchemy, median of 3 interleaved: 23.4 s on main and here. Original PR #3105 by @JosefAschauer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b1e6c9767d |
test: move the test workspace to Vitest 4 and align the Vite toolchain
The repo ran two Vite stacks. ui/ builds the viewer with Vite 7 and
vite-plugin-svelte 6, while Vitest 2 at the root kept Vite 5.4 and plugin 4 with
their own esbuild, whose dev tree carried the critical audit findings. The move
leaves one stack.
Tests now run as test.projects in vitest.config.mts, so vitest.workspace.mts goes
away: engine extends the base (env, global setup, home sandbox), ui stands alone
with the Svelte plugin, jsdom and the browser conditions. The WASM flags move to
test.execArgv, because Vitest 4 ignores poolOptions.forks.execArgv with a
deprecation line and the #1779 worker death comes back. git-index-currency injects
its failures through the mocks' own vi.fn: under Vitest 4 a spy on a mocked module
is that vi.fn, so the captured real function recursed into itself, and
restoreAllMocks no longer resets one, so the injection leaked into later tests.
devEngines sets Node 22.12 as the floor for working in the repo, which is what
Vite 7 asks for; engines, what users run, is unchanged.
Changes on top of the contributor's commits:
- The lockfile is regenerated from main's, not taken from the PR: the PR's would
have put devalue, postcss and ignore back below the fixes in issues 3044 and
3050, and restored the @types/better-sqlite3 that 3044 dropped. ui's packages
hoist to the root at the versions it already built with, and rollup stays.
- testTimeout and hookTimeout rise to 30 s and 60 s. Since Vitest 3 a synchronous
test or hook that outlasts its limit fails; under Vitest 2 the timer could not
fire until the body returned, so a body blocked in spawnSync never timed out. A
6 s synchronous test passes on 2.1.9 and fails on 4.1.11, and an 11 s hook does
the same and skips its tests. The first full run lost frontload-hook to this,
the contributor's lost two bundle-launcher listings, and 31 tests passed 4 s on
a quiet machine, so on a loaded one they would all flake.
- A new test reads the WASM flags from the worker's own command line, since the
config can stop reaching workers with no failure, and a Vitest 5 move could
repeat that.
- CONTRIBUTING and AGENTS state the new floor and the projects setup; two test
files name TestProject, which replaced the removed GlobalSetupContext type.
- Left out as out of scope: a trailing blank line in ui/src/lib/trail-codec.ts.
Validated on Windows, Node 22.22, npm 10.9:
- what ships is unchanged: npm ci --omit=dev, the bundle's step, gives 193 files
byte-identical to main's; npm run build:lib gives 171 byte-identical files;
dist/viewer matches main's once Svelte's scope hashes, which come from the
checkout path, are masked (main built in two folders differs the same way).
- every test is kept: at
|
||
|
|
274b43c44b |
fix(python): a base imported from outside the project is not a project class of that name (#3109)
A Python class whose base the file imports from a package, like class Color(Enum) after from enum import Enum, linked to any project class named Enum: the import found no project file, and the name match or a framework resolver then took the first class of that name. sqlalchemy's enums all extended its own sql/sqltypes.py Enum, and litestar's pydantic models extended a BaseModel in a docs example. PR #2998 extended this to parameterized bases, like Sequence[_T] from typing. isBoundToOutOfRepoImport is the gate that drops such an edge for JS, TS and Rust. It now answers for Python too: the name is outside when every import binding it is absolute and resolveImportPath, which searches for a module the way Python does, finds none. A class in a file the import path names stays, because that search can miss a module on a path the program sets up itself, and so does a class of the referring file, which may rebind the name. A name no named import binds, like one from a star import, is left as it was. Validation: - New python-outside-bases.test.ts: the 7 outside bases fail on main, plain and parameterized, and every case that must keep its edge passes both ways. Green here (7 passed). Taking out each of the 4 guards fails exactly its own test. - A/B on 7 Python repos, 469,927 edges with the controls: 67 extends removed, 0 added, 0 moved. All 67 read against their imports and all were wrong: sqlalchemy 31 (28 from enum, 3 from typing), litestar 21 (pydantic BaseModel), polar 14 (11 fastapi APIRoute, 2 enum IntEnum, 1 opentelemetry Meter), django 1 (ctypes Union). - Controls express and gin unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
173953824d |
feat(sync): CODEGRAPH_SYNTH_MIN_INTERVAL_MS bounds how often live syncs rebuild the synthesized edges
A sync decides it needs the synthesized edges rebuilt when a changed file was a synthesis input, had synthesized edges, or looks like one, and that is nearly every TypeScript or Vue file. Those passes read the whole graph, so on a large repo the daemon's watcher paid for a repo-wide pass on almost every edit: issue 2911 measured a 30.8 s median sync on a 36k-file monorepo. Re-synthesizing only what changed would need every pass to work from a file subset, and they are whole-graph scans merged in registry order. With the variable set (whole milliseconds, 1 s to 24 h; unset, empty or out of range means no hold), a sync within that long of the instance's last synthesis puts the rebuild off: synthesis_pending stays set, the next sync after the interval runs it, and a timer runs it when the interval is up even if no edit follows. The first synthesis of an instance and a full index are never held. Until the rebuild runs, synthesized edges can be up to the interval behind the code, with no notice in tool output, which the maintainer accepted for 2911. Changes on top of the contributor's commit: - The timer's run had no error path, and an unhandled rejection ends the process through the fatal handlers. A database is locked at the wrong moment killed a real serve --mcp with exit 1, which in daemon mode is the process every session shares; a run outliving close() did the same with database is not open. The run now logs a failure and tries again after the interval, and arms nothing once the database is closed. - The deferred run gets the WAL handling a sync gives the rebuild: autocheckpoint off with the valve running and backpressure passed down, so the commit after a whole graph's edges are rewritten does not checkpoint inline on the daemon's main thread (issues 1231 and 1248). - The test failed on Windows with EBUSY: it ended while the run's off-thread maintenance held the database. It now waits for the run to finish. Holds that must bite use 10 s rather than 3 s so a sync on a saturated machine stays inside the interval, and three tests cover the failure, close and WAL paths. - The README says what the setting costs; the changelog bullet credits the author. Validated on four real repos, one long-lived instance per arm through six edit+sync rounds: main, branch with the variable unset, and branch with a 60 s hold, where synthesis_pending stayed set through all six syncs and the timer then ran. Final graph dumps were byte-identical across all three arms on gin (19,919 lines, 311 synthesized edges), flask (11,455 lines, 3) and excalidraw (119,419 lines, 761), and nest (170,610 lines, 1,026 synthesized edges). Median sync: nest 13115 ms to 4317 ms, excalidraw 2607 ms to 957 ms, gin 663 ms to 488 ms, flask 664 ms to 638 ms. Real serve --mcp with an outside writer holding the lock across the timer: exit 1 before the fix; after it the server stays up, logs the failure and the retry converges. Original PR #2922 by @danusha2345. Fixes #2911 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fb71a9f4b3 |
feat(init): --local-only keeps .codegraph/ out of git entirely (#3104)
The generated `.codegraph/.gitignore` ignores everything but itself, so the rule can be committed and shared. For someone using CodeGraph alone, that leaves `.codegraph/` showing as untracked in `git status`, with a file to commit that nobody else needs. `codegraph init --local-only` makes the file the wildcard alone, so it ignores itself too and nothing of CodeGraph's shows in git. It works on a new project and on one that's already initialized, where it converts the file without indexing again. Plain `codegraph init` is unchanged. Only a file CodeGraph generated is replaced. A file with the user's own rules is left alone, and so is a default the repository has committed, because rewriting a tracked file would show up as a change, the opposite of what the flag is for. Both cases say why they were left. The local-only file carries no generated header, so every later open leaves it alone. A flag rather than the proposed CODEGRAPH_LOCAL_GITIGNORE env var: the file is only written when it is missing, so an env var does nothing for a project that already has one, which is the reporter's case; an env var in a shell profile would also silently stop every new project sharing the rule, team repositories included. The flag is also what the issue proposed. Validation: 22 new tests, the CLI ones against real git repositories. They cover `git status --porcelain` staying empty, the default unchanged without the flag, conversion leaving the database untouched, and the file surviving sync, status, index and a plain init. Reimplements PR 1013 by @jiangyq9. Fixes #910 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
27c904b011 |
fix(python): a base list wrapped over several lines is read whole
Both of the resolver's readers of a Python base list stopped early. pythonBases, behind module-global and self. dispatch, read only the class line. declaredSupertypes, behind class-name calls, read three lines and needed the closing parenthesis on them. black and ruff wrap a long list one base per line, so class Wrapped(Mixin, Base) written that way seemed to have no bases: current = Wrapped() then current.run(), and Wrapped.run(x), reached nothing a base declares. Extraction already read the whole list. A new pythonBaseList reads from the class line to the list's closing parenthesis, counting nested parentheses, within the class's extent, and leaves out comments, so a ) in one doesn't end the list. Both readers use it. declaredSupertypes still drops type arguments first, the composition the author posted on PR #3001, and keeps its metaclass= and factory rewrites. Validation: - The test file: 7 of 9 fail without this change (the two one-line controls pass), 9 pass here. Putting the type-argument drop out of the merged case 'python' fails the new composition case and the two of PR #3083. - Wrapped.run(None) on a wrapped base list reaches Base::run with the kernel and with wasm, as do a list over five lines, a decorated class, and a wrapped Base[Optional[User]]. - A/B on top of PR #2998, 7 Python repos, 459,440 edges: 118 added (102 calls, 16 references), 0 removed, 959 call sites moved, 113 kept their target with higher confidence. requests, flask, litestar, mypy and django do not change. polar: 942 XRepository.from_session(session) calls move from a sibling repository's from_session at 0.65 to RepositoryBase.from_session at 0.85. sqlalchemy: wrapped heads like class Table( and class ColumnElement( now reach what they inherit: t.select() reaches FromClause.select. Its 17 moved sites read right; 15 of them land on the typing-only SQLCoreOperations overload of the same ColumnOperators method. - Controls express and gin: full dumps byte-identical. - No kernel change. Index cost with PR #2998, sqlalchemy, median of 3 interleaved: 23.7 s on main, 25.2 s here. Original PR #3001 by @JosefAschauer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
08b187aaf6 |
feat(cli): codegraph implementations <name> lists the types that implement or extend a type
Which types implement this trait is the first question when a flow crosses a
trait or interface boundary, and no command answered it: node shows members and
callers, and impact mixes implementors with dependents and tests under one list.
The command renders the shared hierarchy derivation (type-hierarchy.ts, the one
codegraph_explore and the viewer read), not a third walk. Each row gives the
kind, the edge that ties the type (implements or extends, and from depth 2 the
type it hangs off), file:line, and [test] or [synthesized]. --depth follows
subclasses of subclasses, --file narrows an ambiguous name, --json gives the
same rows. Test code is read in a new test-code.ts: the file path, a test
segment in the qualified name, and for Rust a test cfg module in the source,
since the extractor neither names such a module nor prefixes what it declares.
Changes on top of the contributor's commit:
- The command opens the index with openIndexToRead, as every other read command
does. A plain open repairs and migrates: run during another process's bulk
load it made the writer's next write fail with database is locked (issue
2913), and on an older release's index a read took the schema from 4 to 16. It
joins the read-commands test, which was red on all three scenarios.
- The Rust module reader counts braces over the existing blankRustCode instead
of its own masker, which missed character literals, raw strings and block
comments, so one '{' in a test module left a mock unmarked. It also reads test
inside cfg(all(...)), the form tokio and others use.
- The README lists the command; the changelog bullet credits the author.
Validated on real repos. Across 2,761 Rust files (8,819 declarations, ripgrep,
gitbutler, lightningcss, Cap, yaak and three more) the new reader keeps all 196
test-module declarations the old one found and adds 20, spot-checked as real
fixtures in test modules. implementations ran on ripgrep (Matcher, Sink), gin
(ResponseWriter, IRouter, Binding, with Go's implicit edges marked synthesized),
flask (View at depth 2), excalidraw and Minestom. CLI start stays within 3 ms
(116 ms to 119 ms, median of 25, interleaved). The site's CLI page is left for
the release, so a push does not deploy a command nobody has yet.
Original PR #2839 by @danusha2345.
Fixes #2833
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
3fca009e7b |
fix(python): a parameterized base links its class, like class Leaf(Base[str])
A Python class with a parameterized base had no extends edge. The grammar parses Base[str] in a base list as a subscript, and the base-list reader took only bare identifiers and attributes, so the base was skipped, on the wasm path and in the kernel alike. Everything that follows the class hierarchy missed such a class. The resolver's own text reader of a base list, pythonBases, also looked up Base[str] verbatim and split Pair[str, int] at its inner comma, so current = Leaf() then current.run() did not reach Base.run. A subscript base is now unwrapped to its value before the identifier or attribute check, in src/extraction/tree-sitter.ts and in the kernel's python.rs. pythonBases drops bracketed type arguments before splitting the list. Generic[T] and Protocol[T] stay outside the project like their plain forms; a computed base, a subscript of a subscript and keyword arguments link nothing. Validation: - The test file runs on both arms: red on main's source and kernel (12 of 16 fail, 6 per arm), green here (16 passed). - Kernel parity with this kernel on 3,155 Python files of flask, requests, mypy, litestar, sqlalchemy, polar and django: 0 diffs, and the same 5 files defer to wasm as on main. The kernel and wasm edge dumps of the repos run both ways are identical. - A/B against main on 7 Python repos, 457,948 edges: 1,492 added (901 extends, 555 calls, 36 references), 0 removed, 607 call sites moved, 223 kept their target with higher confidence. polar: 491 self.get_one_or_none() style calls move from the protocol stub RepositoryProtocol at 0.4 to RepositoryBase and its mixin at 0.9. litestar: 12 calls in GenericSyncMockRepository move from AbstractAsyncRepository to AbstractSyncRepository. sqlalchemy: 435 extends and 554 calls added, like t.insert() reaching TableClause.insert. We read 44 sampled changes and every small bucket. They read right, except 12 self.op() sites that move to the typing-only overload SQLCoreOperations.op and one Enum.copy that moves to TypeEngine.copy. - 3 of the 901 extends are wrong: sqlalchemy's Sequence[_T], Tuple[int, ...] and MutableSet[_T] from typing link to its own classes of those names. The plain form does this on main (28 class X(Enum) there), and none of the 3 brought a call edge. - Controls express and gin: full dumps byte-identical. - Index cost with PR #3001 on top, sqlalchemy, median of 3 interleaved: 23.7 s on main, 25.2 s here, with 1,143 more edges. Original PR #2998 by @JosefAschauer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9bfdd09238 |
feat(cli): codegraph cycles lists the files that import each other in a circle (#3099)
New `codegraph cycles [path] [--json]` prints the groups of files that all reach each other through imports. It exits 1 when there are any and 0 when there are none, so `codegraph cycles || exit 1` works as a pre-commit or CI check. The command stays a thin wrapper over CodeGraph.findCircularDependencies(), but the method itself had to change. It had no callers, and on real repos its walk wasn't fit to print: - It reported one list per back edge, so the number depends on the walk: 1,490 overlapping lists on excalidraw, 87 on gin, 44 on flask. - It left out files. A walk that meets a visited file stops there, so a file whose loop closes through one is never listed (now pinned by a test: a>b, b>a, a>c, c>b). - It counted every edge kind, and most of the graph's edges aren't imports. An interface's call fans out to a test's fake, so src/extraction/parse-pool.ts "depended" on its own test. Two Next.js pages that link to each other were a cycle. gin's files were a 55-file knot, though Go forbids import cycles. With exit 1 those are failures nobody can fix. The method now takes the strongly connected components of the file import graph, one group per knot. The Tarjan pass moves from the viewer's map into src/graph/file-cycles.ts and both surfaces use it, so the viewer's "Circular imports between files" panel, which was reading every edge too, now reports the same groups. Dependent counts on the map keep their own edge list. Validation: new cli-cycles and file-cycles suites, plus cycles in the read-commands suite (mid-rebuild, and old schema indexes); 4 of the 10 findCircularDependencies and finder tests fail with main's method and all 15 pass with this one. Against the TypeScript compiler's own module resolver over the same files (kit oracle): excalidraw 3 groups, 369 files, all identical; codegraph's src/ 3 of 4 identical, and the fourth needs src/resolution/name-matcher.ts, which at 1.6 MB is over the default size limit and so isn't parsed. flask reports 2 groups and gin none. It runs in under a second on each. Type-only imports count, as they aren't told apart in the graph. The MCP tool from PR #889 is left out on purpose: a new tool is rarely picked up, and this needs no agent steering. Reimplements PR #1012 by @jiangyq9, with the exit code from the CLI in PR #889 by @goutham80808. Fixes #888 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
47526d2078 |
feat(serve): --no-telemetry turns telemetry off for one MCP server entry
A team that shares an MCP config had no way to opt one server entry out of telemetry without an environment variable, which not every client lets a shared config set. Issue 1908 asked for a flag. serve --no-telemetry sets CODEGRAPH_TELEMETRY=0 for the process, the same chokepoint --no-watch uses for CODEGRAPH_NO_WATCH, so there is no new telemetry code path: telemetry reads the variable on every check, and the background daemon a server spawns inherits its environment. A daemon that is already running keeps its own setting until it exits, which TELEMETRY.md states; codegraph telemetry off covers every process. Changes on top of the contributor's commit: the test spawns the server with the WASM runtime flags, as AGENTS.md requires on Windows (without them the re-exec'd grandchild keeps the temp dir open past the child's exit), and the changelog bullet states the limit and credits the author. Validated with real proxy-to-daemon runs on Windows against opted-in private homes, reading the telemetry queue each daemon left on exit: - a flagged client starts the daemon: nothing recorded; - control, no flag: the tool call is recorded; - a flagged client joins a running unflagged daemon: its call is recorded, the documented limit; - an unflagged client joins a flagged daemon: nothing recorded; - codegraph telemetry off with a daemon running: nothing recorded. Original PR #1923 by @danusha2345. Fixes #1908 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7889da6697 |
feat(status): show files parsed beside files found, and add --strict (#3096)
A language's file count says what the files are, not whether anything came of them, so a language with nothing in the graph could read as healthy. `codegraph status` and `codegraph_status` now show how many of a language's files yielded symbols when that isn't all of them, and warn about a language where none did. `status --json` gains filesByLanguage, parsedFilesByLanguage and unparsedLanguages. `status --strict` exits 1 unless the index is finished, up to date and every language that should yield symbols did. The count stays out of getStats(), which every explore runs. A plain EXISTS probe per file would have doubled getStats() on a 14,569-file index, from about 40 ms to about 80 ms. The query status runs instead takes 4.3 ms there, because the stored node count settles nearly every row, and only status runs it. The gate covers more than a zero count because a grammar that can't load no longer leaves one: since PR 2335 nothing is stored for such a file, the language drops out of the rows, and the only trace is pending files. A gate checking zeroes alone would pass that. Text changes: a language row gains " (N with symbols)" only when the two numbers differ, and a warning line appears for a language with no symbols. Every other line, and every existing JSON field, is unchanged. Across 115 real indexes, 9 repos have a code language with no symbols, all of them stubs in languages with at most 3 files, such as ripgrep's .nvim.lua. A file-count threshold would have hidden a real failure in a small smoke-test project, so the rule stays plain and the warning names the files instead. Reimplements PR 1658 by @ferrine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f4c4dae91c |
feat(mcp): the server gives hosts its title and icon (#3095)
`initialize` now answers with serverInfo.title "CodeGraph" and one embedded SVG in serverInfo.icons, on the direct path and from the daemon proxy, which share SERVER_INFO. A host that shows them can list CodeGraph by name and logo instead of a plain `codegraph`. The icon is the site's favicon (site/public/favicon.svg) on a light tile. The bare mark is dark ink on nothing, and on a dark background only its two white dots show (checked in a browser at 16 to 64 px). It is spelled out in the source instead of as a base64 blob, so a reader can see it is three lines and three circles. `sizes` is left off the icon on purpose. It's optional and says nothing about an SVG, and @modelcontextprotocol/sdk 1.18.0 typed it as a string where the spec settled on an array, so a host on that SDK would fail the whole handshake on the array form. Left out from PR #879, on purpose: `_meta.icons` and icons on every tool. Those add about 1 KB per tool to each tools/list for no host we could find that needs them; Codex reads serverInfo.icons. What each host shows, read from its source or binary on 2026-10-30: - Codex: keeps serverInfo.title and .icons and sends them to its app (McpServerStatus.serverInfo); the terminal UI's /mcp doesn't read them. - opencode: reads serverInfo.title into the server list; its dialogs still print the config name, and it doesn't read icons. - Claude Code 2.1.296: connects fine with these fields (checked with `claude mcp list` in a sandbox home), and no read of title or icons turns up in its binary, so it keeps showing the config name. - Cursor: closed source and not on this machine. Its docs mention no server title or icon, so this is unverified. Validation: 3 tests in mcp-initialize fail on main's session and pass here; mcp-daemon now pins the proxy's serverInfo to the same object. Reimplements PR #879 by @fedosov. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
22cf7d3aa1 |
fix(watch): a watch callback that throws no longer ends the host's process (#3092)
A program that uses CodeGraph as a library passes callbacks to watch(). FileWatcher.flush() runs from a timer with no caller to catch anything, and it called onSyncError and onDegraded (through degrade()) in its catch block. A throw from either left flush() as an unhandled rejection, which ends the host process with exit 1. A throw from onDegraded also skipped the stop() that degrade() owes, so a watcher reporting itself degraded kept retrying. A throw from onSyncComplete sat inside the try, so it counted a sync that worked as a failed one and called onSyncError with an error that was no sync failure. All three now go through one guard, callHost(), which logs the throw as a warning and changes nothing else. The CLI and MCP server are not affected: their callbacks only write to stderr. Only a host's own callback can reach this. Validation: - Three new tests in watcher.test.ts fail on main and pass here (64/64 in the file). Putting each guard back one at a time fails exactly its own test. - A host script on the public API (watch() with a throwing onSyncComplete and onSyncError, then a real file edit): main exits 1, this branch logs the throw and keeps running, exit 0. PR #3074 listed ten lint findings; this keeps the two watcher call sites' purpose and leaves out the rest, which change no behaviour: - flushOrdered(): flushChain's body catches everything, so it cannot reject. - stop() from timers and signals: nothing stop() awaits can reject today, and a forced rejection already exits (exit 1, one fatal line). - await on getCodeGraph and the upgrade functions: plain values, nothing else runs in that microtask. - { cause } on the rebuild and daemon-lock errors: the rebuild message already carries the reason, and nothing reads or prints cause. Reimplements PR #3074 by @johnhldiver-beep. Refs issue 2643. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
17d5426d63 |
feat(files): list a project's files by the package that owns them (#3090)
Nothing could answer "which package owns this file": manifests are not source, so they have no row in the index, and `--format grouped` groups by language. `codegraph files --format package` and `codegraph_files` with `format: "package"` now group files by the nearest package.json or composer.json above them that names a package, so a monorepo splits into its workspace members with no configuration. The answer is read from disk when asked, so it stays current without a re-index. A manifest that names nothing doesn't stop the walk, and files nothing claims get a section of their own, last. Each directory is resolved once and shares its parent's answer. Reads are bounded by the source size limit and stay inside the project. Opt-in only: the default tree output and the `--json` shape are unchanged, and `--json` keeps ignoring `--format` as it always has. Validation: 31 new tests, including the real CLI. On vuejs/core the default listing and `--json` are byte-identical to main. The walk over babel/babel's 28,653 files in 13,484 directories takes a median 460 ms, on the query worker pool for `codegraph_files`. Reimplements PR 2863 by @LuisAlbertoMK. Fixes #1871 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cd04b21a31 |
fix(python): a base's type argument is no supertype, so Repo.save() is not User.save
The Python branch of declaredSupertypes read every word of a class's base list, so class Repo(Base[User]) declared both Base and User. A call on the class or on a receiver named after it, Repo.save() or repo.save(), walks those supertypes for the method (classStrategyMethod to inheritedClassMethod), and when Base declares no save it reached User.save at 0.85 or 0.8. In class Mixed(Base[Optional[User]], Mixin) the type argument also came ahead of Mixin, so Mixed.save() reached User.save instead of Mixin.save. Bracketed type arguments, nested ones and quoted forward references too, are now dropped before the names are read. The base itself is still read and searched. Other languages are untouched. Validation: - The new test file: red on main (the type-argument and later-base cases fail, the case that a base's own method is still reached passes), green here. We added the Mixed and quoted forward-reference cases to the contributor's test. - A/B against main on 7 Python repos, 670,226 edges on main: requests, flask, litestar, mypy, polar's server, django unchanged, sqlalchemy 1 edge removed, 0 added, 0 retargeted. That edge is a.references(b) in relationships.py. Its local a named a test's class a(TypedColumns), and TypedColumns' type argument "Column[Any]" reached Column.references: the right method by chance, by the path this fixes. Its twin b.references(a) two lines down has no edge in either arm. - Controls express and gin: full dumps byte-identical. - Kernel parity on flask and requests: 43/43 in both arms (the kernel is not touched). Index cost on sqlalchemy, median of 3 interleaved: 29.5 s on main, 25.7 s here, so nothing measurable. Original PR #3000 by @JosefAschauer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3f66df358e |
docs: add a contributing guide and a documentation index (#3081)
A new contributor had nothing to start from: the README covers using CodeGraph, and the build and test rules live in AGENTS.md, written for agents. CONTRIBUTING.md now covers building from source, testing a change, preparing a pull request and what happens to it. docs/README.md indexes the guides and design notes, and the README links to both. Every relative link and heading anchor was checked against main. Reimplements PR 1931 by @mixxer. Also folds in the fork-and-upstream setup, `npm run cli`, `npm run test:watch` and the version rule from PR 306 by @marcelovani, and the commit format, the single-test command, the bug-report list and the `/add-lang` pointer from PR 665 by @ymstar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a0c6c1b17c |
fix(liquid): a variable named with a hyphen is indexed under its whole name (#3080)
Liquid allows hyphens in variable names (`{% assign form-field-label = … %}`,
`{% capture icon-cart %}`), but the extractor read assign and capture
names as \w+. PR #2814, which added capture, made such a name get no
node rather than a cut-short one, so hyphenated variables were missing
from search and codegraph_explore.
Both declarations now read a name as a word character followed by word
characters and hyphens. A capture's bare name is matched lazily up to
whitespace or the tag's end, so in `{% capture title-%}` the hyphen
still belongs to the -%} whitespace control and the name is `title`,
while `{% capture title-text-%}` reads `title-text`. A name that goes on
with a dot (`ghost.name`, `ghost-name.x`) still gets no node, in both
tags. Liquid isn't extracted by the native kernel, so there is no
kernel side.
Validation: extraction.test.ts gains 5 capture forms (bare, tight -%},
trimmed, quoted, a {% liquid %} line) and an assign test (braced,
trimmed with no spaces, a {% liquid %} line); they fail 6 of 6 with
main's extractor and pass with the fix, and the cut-short guard now
covers dotted names in both tags. A/B, each arm built from its own
checkout, on PR #2814's theme set (Shopify/dawn 05222537,
Shopify/horizon dfad90d8, Shopify/skeleton-theme a7a655e7,
ChumsInc/chums-impulse-dev 4fd6f08e, agencecinq/cinquante-et-un
ecb0035a, noclocks/shopify-devstore-theme e1747695, panoply/syncify
98d75fd0, Shopify/theme-tools bb33f49c) and controls expressjs/express
0dee2c52 and psf/requests 3c7baaa0: all ten byte-identical, as none of
them uses a hyphenated name. On three themes that do (Zoront22/LOS
58dc2dfc, lucidnz/bootstrapify-1 b36ab7ff, ibarake/bittergracedc
e36fa5db) the fix adds 4, 25 and 39 variable nodes, each with its one
contains edge: exactly the 68 hyphenated declarations a separate scan
finds, each checked to sit on a capture or assign tag naming it.
Nothing is removed or retargeted, no unresolved ref changes, and file
rows differ only in their node counts. Index time, median of six
alternating runs: horizon 3.60 vs 3.64 s, bittergracedc 28.39 vs
28.24 s.
Reimplements the part of PR #2504 by @Puma7 that PR #2814 left out.
Refs #2498
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
a3e9e760b1 |
fix(explore): Flow reaches a named symbol a shorter route gets to first (#3079)
The named walk behind codegraph_explore's Flow (and the viewer's /api/flow) kept one route per node and replaced it only with a better route of the same length. In entry -> mid1 -> deep -> leaf beside entry -> mid2 -> leaf, leaf was reached first through the unnamed mid2, so the route through the named deep was turned away for being longer, and the query "entry deep leaf" got a Flow that dropped deep or leaf. On django, SQLCompiler.as_sql pre_sql_setup get_select ran as_sql -> get_qualify_sql -> get_select and left out pre_sql_setup (issue #2970). A node now keeps every route to it that no other route to it beats: one that got there no later, through as many named symbols, ending on no more unnamed hops. That is the same-depth rule of PR #2453 widened, so a longer route through more named symbols survives, while a longer route through fewer is still turned away and can't win the deepest-sink choice; that choice and the longest-chain ranking across seeds are unchanged. A route that beats one waiting at its own depth takes its place in the queue, so nodes are visited in the order they were reached, as before. A route never passes a node twice: with several routes per node, a cycle among named symbols (ping -> pong -> ping) would count them again and out-rank the flow it repeats. Each node's callees are read once per search, across seeds. The explore-exact-target fixture's Flow now runs through every symbol its query names, as django's does. Its compiler.py stays a per-symbol view only while the named and spine bodies outgrow the file's share, which django's do (18,390 chars against 8,895, measured on django/db at 11af25c0) and the miniature's no longer did once the oversized unnamed bridge left the spine, so its get_select grows to django's proportions; no explore render rule changes. Validation: new flow-named-longer-route.test.ts (issue repro through resolveNamedSymbolFlow and codegraph_explore, the bridge budget, no cycles) and the updated explore-exact-target fixture: 3 assertions fail with main's walk, all 11 pass with the fix; 64 other Flow tests pass. Explore A/B, both arms on one index per repo (expressjs/express 0dee2c52, gin-gonic/gin 0f09c3a9, django/db of django 11af25c0, excalidraw d3a9c2a1), 290 queries: 152 graph-derived #2970-shape queries all changed their lead chain, each to one more named symbol (37/37, 55/55, 60/60); of 125 random named-chain controls 11 changed and of 32 hand-written flow queries 1 (django's), each gaining a named symbol; no query lost one, no chain repeats a node, and every query whose Flow is unchanged returns a byte-identical answer. Largest answer 24,997 -> 24,998 chars (cap 25,000). getCallees reads per query set drop 35-50% (gin 657 -> 410, django 1207 -> 752, excalidraw 1407 -> 951); explore time unchanged within noise. Reimplements PR #3003 by @johnhldiver-beep. Fixes #2970 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
910e9bd86f |
fix(site): agent lists include GitHub Copilot, and Codex notes its IDE extension (#3066)
The site's agent lists stopped at Kiro, though the installer configures GitHub Copilot in VS Code, the Copilot CLI and JetBrains IDEs, and the README lists it everywhere. Copilot is now in every list: Integrations, Installation (the detected agents and the restart line), Introduction, Quickstart, Indexing and the landing page. The Integrations page's Codex CLI entry now says the Codex IDE extension reads the same config: $CODEX_HOME/config.toml (default ~/.codex/config.toml) for a global install, and .codex/config.toml, which Codex uses only in a trusted project, for a local one, as src/installer/targets/codex.ts writes them. Installation said "Cursor and Kiro get the MCP config only", which, with more agents listed, read as if every other agent gets an instructions-file section. It now names the four that do (Claude Code, Codex CLI, Gemini CLI and opencode); the others get the MCP config only. Validation: site npm ci and astro build pass (21 pages); the built pages carry the new entries. Reimplements the site half of #549 by @ashishpatel26. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
03c10ada87 |
ci(site): move the site deploy off the Node 20 action runtime (#3048)
The "Deploy site to GitHub Pages" workflow still used actions/checkout@v4, withastro/action@v3 (setup-node v4 and upload-pages-artifact v3 inside) and actions/deploy-pages@v4, all on GitHub's deprecated Node 20 action runtime. It now uses actions/checkout@v6, withastro/action@v6 and actions/deploy-pages@v5, all on Node 24, pinned by major tag as release.yml pins its actions. The tags resolve today to checkout v6.1.0, withastro/action v6.1.4 and deploy-pages v5.0.1. checkout stays on v6 to match release.yml; v7 mainly blocks fork checkouts in pull_request_target. Changed defaults, read from each release's notes and action.yml: - checkout v5 and v6: Node 24 (runner 2.327.1 or newer), and persisted credentials go to a separate file. The breaking change in v6.1.0 concerns only pull_request_target and workflow_run checkouts, which this job doesn't do. - withastro/action v4 to v6: the node-version default went from 20 to 24; this workflow sets 22 and keeps it. Install and build are still npm install and npm run build in site/. A new Astro build cache (node_modules/.astro, on by default) is restored before the build and saved after. The upload is upload-pages-artifact v5 with include-hidden-files: true, which packs the same files v3 did, and the built site has no dotfiles anyway. - deploy-pages v5: Node 24 and polling backoff; inputs unchanged. Nothing else changes: triggers, permissions, concurrency, the site path and node-version 22. The YAML parses, and the site builds locally with Node 22 (21 pages). The workflow was not run, because a run deploys the live site. Raised by PR 881 (@Dashsoap). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0d7480f83b |
fix(cpp): an overload chosen by arguments that call it again no longer overflows the stack
On main, indexing darts-clone failed in "Resolving refs" with "Maximum call
stack size exceeded", and the whole run was lost. darts.h writes
`extras(extras(id).prev()).set_next(extras(id).next())` on a class with a
const and a non-const `extras(id)`. Choosing between them types the
arguments; typing `extras(id).prev()` types `extras(id)`, whose overload is
chosen by the arguments of the call the reference is at, the outer one, so
the same arguments are typed again. cppArgumentType restarts
cppExpressionType's depth at 0 on each lap, so the depth bound never trips.
cppArgumentTypeChoice now keeps the calls whose arguments it is typing
(`file:line:column:name`), and a choice that comes back to the same call
takes the first tied overload, the answer it already gives when no
argument's type is known. A try/finally clears the entry.
Validation on main
|
||
|
|
fa03fa6bb4 |
fix(callers): list every call site of a caller, and a function that calls itself (#3077)
getCallers keeps one row per caller (issue #1086) with the caller's first edge only, and seeds its reported set with the symbol itself, so codegraph callers showed a caller that calls the symbol on two lines once, at the line its own declaration starts on, and left a recursive call out, though the index stores every one of those edges and each is a line to edit when the symbol changes. The CLI also merged edges on source, target and kind, which dropped a second call site from --json. A new GraphTraverser.getCallSites (CodeGraph.getCallSites) reads the direct callers or callees with every edge: one row per neighbour in the order getCallers / getCallees list them, every site kept, and the symbol's own row, flagged recursive, when it calls itself. getCallers and getCallees are unchanged, so ranking, explore's Flow and blast radius, impact and the viewer read exactly as before; a test pins that every non-recursive row of getCallSites equals getCallers' row and first edge. src/graph/call-sites.ts holds the one wording of a site list (9, 10, +N more) the surfaces share. - codegraph callers prints a caller at its call-site lines (up to 10, then +N more), a recursive row as "(calls itself)", and --json gives each row lines and recursive, with every site in edges. Callees keep the callee's declaration line, where a call leads. - codegraph_callers / codegraph_callees read the same rows (10 sites per caller), and codegraph_node's trail shows callers at their sites, 3 at most, and the symbol once among its callers when it calls itself. Validation: new callers-call-sites.test.ts (7 tests) fails 7 of 7 with main's sources and passes with the fix; 11 neighbouring callers, trail and lookup test files (217 tests) pass. Before/after on one index per repo (expressjs/express 0dee2c52, excalidraw d3a9c2a1, gin-gonic/gin 0f09c3a9): trails changed for 108/253, 3630/4180 and 557/1395 symbols, nearly all callers moving from the declaration line to the call sites; recursive rows newly shown 2/20/7. Budget: trail median 115->117, 354->360, 185->187 chars, largest growth +131/+212/+91 chars, and the busiest caller rows (67, 300 and 100 sites) stay one capped entry. codegraph_callers on each repo's 15 busiest callees: largest answer 1025->1358, 1882->2127, 1790->1805 chars. Not in this change: the issue's own recursion, child.walk() on a for...of element over children: TreeNode[], is not linked at all on main (the receiver's element type isn't inferred), so it needs a resolution fix of its own; this.walk() and a plain recursive call are. Reimplements PR #2994 by @johnhldiver-beep. Fixes #2909 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
59eb0d2820 |
fix(resolution): an import no longer lands on a same-named method of an exported object
An exported object literal's methods (`export const d = { f() {} }`) are
indexed as exported nodes named `f` (qualified name `d::f`), and the
per-file export index kept the first exported node per name. So
`import { f } from './m'` could link to `d.f` instead of the module's own
`export function f`, and which one won depended only on declaration order.
A JS/TS member, a node whose qualified name differs from its name, now
ranks behind the module's own bindings wherever an import is matched to an
export: a named import (the module's own `f`, then its export clause, then
its re-exports, then a member), an export clause's local name,
`export default f` and `module.exports = f`. A member is never dropped:
`export const { f } = d` binds `f` with no node of its own, and `d::f` is
its only answer. The default-import guess no longer picks a member.
Validation on main
|
||
|
|
1c40e34141 |
fix(mcp): a projectPath naming a file resolves through its directory
An agent that passed the file it was looking at as `projectPath` got
`isError: true` ("Path is not a directory"), while a path that doesn't
exist at all walked up to the project's `.codegraph/` fine. An error teaches
an agent to stop calling codegraph, and this mistake is recoverable.
`execute()` now takes an existing file to stand for its directory, right
after the length check, and rewrites `args.projectPath` there, so the
project gate, the worker dispatch, the worktree notice and getCodeGraph all
see that directory. A path that can't be stat'd walks up as before.
Path safety: the directory goes through the same checks as any
projectPath, the sensitive-folder refusal on the path itself and on the
root its up-walk lands on, so a file reaches exactly what its directory
reaches and never more. `/etc/hosts` is refused as `/etc` is, a file in
`~/.ssh` as `~/.ssh` is, and `win.ini` in the Windows folder as that folder
is. A symlink inside a project that points at a file in a sensitive folder
is answered from the link's own folder, as a symlinked folder already is;
nothing in the sensitive folder is opened.
Validation on main
|
||
|
|
5526e33d1d |
fix(status): recommend a re-index when package.json alone adds or drops a framework
Since PR #2941, a sync records the frameworks that came or went since the
last full index, and `codegraph status` recommends a re-index. But a sync
detects frameworks again only when it extracts a file, so adding `express`
or `@sveltejs/kit` to package.json with no source file edited recorded
nothing: status said the index was up to date while an unchanged Express
app's routes, or the `goto('/login')` in an unchanged +layout.svelte,
stayed unlinked until a full index.
A full index now also stamps the resolver's framework set, and
getFrameworkChangesSinceIndex() adds the difference between that stamp and
the set the resolver detects now (it detects on every open) to what syncs
recorded. So a fresh `codegraph status` sees a manifest-only change whether
or not a sync ran. An index built before this has no stamp, so that half
starts at its next full index. A long-running server's resolver keeps its
old set until it extracts a file, as before.
Validation on main
|
||
|
|
063708ba53 |
fix(pascal): Delphi chained calls and method pointers follow declared types (#3065)
Four Delphi bugs that PR 2550 by @OleksiyPenkov found, reimplemented on
main's Pascal scope code rather than as a second resolver:
- A call through a chain of members and methods recorded only the outer
method's bare name, which name matching gave to any namesake: SynEdit's
Canvas.Font.Assign(Font) went to TProposalColumn::Assign, and
TLogger2.Create.Write('y') linked nothing, a bare Write being a System
name. The extractor now writes such a call as the typed chain
pascal-scope already resolves for paren-less names ({call} for a
statement, a new {args} mode for a last link written with its
arguments). Each link is typed by its declaration: field and property
types, function results, a local's declared type, through pointer and
class reference types (PVirtualNode = ^TVirtualNode, TSynLogClass =
class of TSynLog) and explicit dereferences (a^.Copy). A constructor
called on a class gives an instance of it, a unit name, dotted ones
too, qualifies its declarations, and a chain called with arguments may
end at data that holds a routine. There is no bare fallback: a chain
through a type from outside the project links nothing, and in a with
body only a chain from Self or a typed local is read.
- A method given to a target was a method pointer only when the target
was named On.../FOn...: Callback := FProvider.Value with Callback: TGet
recorded a call, and FOnGet := @FProvider.Value nothing. A value given
to an assignment now carries its target, whose declared type decides
(routine types written out, of object, reference to, project aliases,
the RTL's routine type names; a class, a record or a System value type
holds no routine); the name decides only where the declaration can't
be read. A function that returns a routine type is called for it, and
@Obj.Method is a reference anywhere.
- A nested routine's locals leaked into the routine around it: the name
matcher's Pascal receiver scan read every declaration above the call.
It now steps over routines nested elsewhere, reads the calling
routine's own name lists (Doc, Doc2: TDocVariantData), which main typed
only by luck through a nested routine's parameter, and follows pointer
and class reference types. Where the tree has errors, the source-text
scopes give the outer routine its own names back after a nested one.
- A nested routine declared forward is in scope from that declaration.
Validation:
- New __tests__/pascal-reference-boundaries.test.ts, 41 tests, 24 of
them ported from PR 2550: 22 fail on main's sources, the other 19 pin
what main already got right. pascal-chained-calls and pascal-for-in-var
pinned the bare outer-method names and now expect the typed chains
(4 tests, failing on main). Twenty mutations, one per rule, each fail
a test.
- A/B against main
|
||
|
|
fd57eaaa45 |
fix(mcp): a tools/call whose arguments aren't an object gets invalid params, not a crash (#3071)
A tools/call request passes its arguments as an object. MCPSession took
params.arguments || {} without checking that, so a string or number went
straight to the tool handler, where the session-view check
('_cgExploreSession' in args) threw. The call then came back as a tool
result flagged isError: "This is an internal codegraph error, retry the
call once; if it persists, continue without codegraph". That reports a
client's malformed request as codegraph's own malfunction, with advice to
stop using it. An array went through as if it were the arguments. The
stdio proxy, serving in process while the daemon is unavailable, had the
same unchecked read.
Both now read the arguments through one helper, toolCallArguments(), and
answer anything but an object with a JSON-RPC invalid-params error
(-32602, "Tool arguments must be an object"), as a call with no tool
name already is; no tool runs, and the proxy starts no engine for it.
Missing, null or another empty value still means no arguments.
Validation: new mcp-tool-call-arguments.test.ts covers the session (the
direct stdio server and every daemon client) and the real proxy entry
point in a child process with the daemon unavailable: red on main (2 of
3 failed; the well-formed control passed), green with the fix.
explore-session-state and proxy-shutdown pass. Driving serve --mcp over
stdio: on main, codegraph_explore, codegraph_node and codegraph_search
with a string argument returned isError "internal codegraph error" and
codegraph_status accepted [1], 42 and true; with the fix those six get
-32602, while "", null, {} and absent arguments run as before.
Reimplements PR #2986 by @kjgggggg.
Fixes #2981
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
de0a4eb362 |
fix(vue): a self-closing <script /> no longer swallows the next script (#3070)
The Vue extractor read a self-closing `<script src="./legacy.js" />` block
as an opening tag running on to the next `</script>`, as the Astro and
Svelte extractors did until PR #3063 (PRs #2995 and #2997) landed. The
`<script setup>` after it was parsed with whatever lay in between and
contributed nothing, and a `<template>` in between lost its calls and its
components too. One copy of the same pattern sat in each of 9 places in 5
files, and that fix reached the 6 copies in Astro, Svelte and the viewer.
sfc-script.ts, which the Vue, Svelte and Astro extractors already share,
now exports the script pattern and the script-or-style pattern, built from
@maxmilian's regexes, plus sfcBlockLineRanges, the line ranges a template
scan skips. A self-closing tag has no body, so it covers nothing and isn't
blanked, and calls in its attributes are read. All 9 sites use them: each
extractor's script blocks, Astro's covered ranges, both Svelte template
scans, Vue's template-components scan, the Vue template-calls blanking
and the viewer's highlight regions. The pair also accepts a closing tag
written with a space, `</script >`, as the template-calls blanking always
did; the viewer finds a block's body from its opening tag for that reason.
Validation on main
|
||
|
|
cb7fb61e76 |
fix(docs): agent and command lists match the code; worktree warning drops init -i (#3067)
The warning for a git worktree answered from another checkout's index
still said to run "codegraph init -i", a flag that has done nothing
since init started indexing by default. Its one-line form is prepended
to every read-tool result and codegraph_status embeds the long form, so
both told the agent to build the index, which the server instructions
rule out ("Indexing is the user's decision ... don't run it yourself").
The CLI's codegraph status now says: Run "codegraph init" in this
worktree. What an agent reads (the read-tool notice, which also reaches
it through the prompt hook and the explore/node CLI, and
codegraph_status) says the user can run "codegraph init" there.
Docs that no longer matched the code:
- AGENTS.md listed 4 of the 11 installer targets, called a new one "a
5th agent", said the contract suite covers "all 4" targets, and listed
11 of the 24 subcommands. It now lists every target and subcommand,
and names the hidden ones (serve --mcp, prompt-hook, and ui/web unless
CODEGRAPH_UI=1).
- README's Supported Agents section said no instructions file is
written, but Claude Code, Codex CLI, Gemini CLI and opencode get the
short CodeGraph section. Its Codex entry now says the Codex IDE
extension reads the same config: $CODEX_HOME/config.toml (default
~/.codex/config.toml) for a global install, and .codex/config.toml,
used only in a trusted project, for a local one.
- README's WSL2 "Transport closed" entry named only the in-process
fallback. The daemon also moves its socket to the temp directory when
the project's filesystem can't host one.
- init -i and -iv leftovers in internal docs, scripts, the add-lang
skill and the repo's own Cursor rule, which now also leaves indexing
to the user. Two stay on purpose: scripts/agent-eval/audit.sh can
audit a published version older than 0.9.8, where init built the
index only with -i (now commented there), and
docs/benchmarks/codegraph-ab-matrix.md records a 0.9.4 run.
Validation:
- worktree-detection.test.ts: on main 61776927's worktree.ts and
tools.ts, 4 of its 20 tests fail (the CLI wording, the agent wording,
the read-tool notice and codegraph_status). With only the worktree.ts
fix, the codegraph_status test still fails. All 20 pass here, and so
do mcp-nested-repo-projectpath.test.ts's 13.
- Full suite on this patch over
|
||
|
|
f67eafcbc5 |
fix(search): a pasted call like leaf(entry) finds both names instead of nothing (#3069)
Full-text search dropped the FTS5 syntax characters ( ) ^ " ' * : from a query before quoting each word, so the words on either side of one were glued together: leaf(entry) became the single prefix term "leafentry"*, which no symbol has, and the LIKE and fuzzy fallbacks don't recover it. codegraph query, the codegraph_search tool and searchNodes() all came back empty for a pasted call like render(props). The index's unicode61 tokenizer splits stored text at anything but letters and digits (a signature's leaf(entry) is stored as leaf and entry), so the query now splits there too: each of those characters becomes a space, the treatment :: already gets (issue #173). Validation: new search-special-characters.test.ts (8 tests) fails 8 of 8 on main and passes with the fix; fts5-fallback, search-field-filters, search-query-parser, cli-query-command and symbol-lookup pass. A query A/B on expressjs/express 0dee2c52, both arms over one index: 12 control queries without such a character between words are byte-identical; 10 pasted calls changed, 7 of them from no results to results led by the function they name (res.send(body), res.json(obj), View.prototype.lookup(name)). Reimplements PR #2987 by @kjgggggg. Fixes #2971 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cc17978174 |
fix(rust): a module a #[path] attribute loads is the module that declares it (#3064)
A Rust module loaded with `#[path = "…"]` was found by its file's directory:
fast-mlsirm's `#[path = "../../../tests/unit/scaling_tests.rs"] mod tests;`
in `crates/core/src/scaling.rs` made `tests/unit/scaling_tests.rs`'s
`use super::*` walk to a `tests/unit` module that doesn't exist, so its
`predict_rating_multi(1)` linked to nothing, or to a same-named function of
another crate; its `crate::` had no crate, and the parent's private items
looked out of its reach. From the declaring side, a path through such a
module (`utils::mock_event(…)` under `#[path = "./utils.rs"] mod utils;`)
reached nothing.
The fix reads each file's `#[path]` declarations as the Rust Reference
reads the attribute (from the declaring file's directory, or inside an
inline `mod` from that module's own; a path that leaves the project, by
escapesRoot, or that a `cfg_attr` sets, names no file):
- rustSubmodule: a module the module declares once with a `#[path]` is
the file it names, whatever else loads that file too (a helper several
tests load is each test's); cfg alternatives that name a file each stay
unknown, and a path's 2015 crate-root fallback no longer reaches past
such a module to a namesake at the crate root.
- rustPathClaim: a file the regular rules don't place is the module of the
one `#[path]` declaration that loads it, if exactly one does. Placed
(rustPlacedByName) are crate roots, files with a `fn main` or
`#![no_main]` of their own (trybuild cases, fuzz targets), and files a
plain `mod name;` declares in the module owning their directory or in an
inline module of one above it (dioxus's `mod components { pub mod home; }`
in main.rs). Only for a file left over are the project's Rust files that
write `path =` read (a byte check first), once per resolver. With a
claim, rustSuperModule is the declaring module, rustFileModule owns the
file's directory (rustc reads every `#[path]` file as a mod.rs),
rustCrateRootModule is the declaring file's crate, and rustModuleWithin,
isRustItemVisibleFrom and isVisibleAcrossFiles see the file inside the
declaring module, so `use super::*` brings in its private items.
- rustCrateGraph: `[[example]]`, `[[test]]` and `[[bench]]` paths are
target roots, as `[lib]`/`[[bin]]` ones are since PR #3014, and such an
example is its own crate's root (dioxus's
`examples/07-fullstack/handling_errors.rs`).
Validation:
- rust-path-super-glob.test.ts (PR 1892's case, credited) and
rust-path-attribute-modules.test.ts (7 cases): on main all 8 fail; all
pass. Every rust-* suite passes.
- A/B against main, native kernel: fast-mlsirm (its Rust files) +2,796
links (1,839 calls, 427 references, 334 constructions, 196 imports), 71
retargeted (exposure_tests.rs's `Lcg::next_f64` from cdm_tests.rs's
private `Lcg` to exposure.rs's, the declaring module's helpers instead
of bifactor_grm.rs's), 87 reaching the same item through the declaring
module (confidence 0.4 to 0.9), 0 removed; dioxus +27 (each headless
test's `utils::mock_event(…)` into the helper five tests load); tokio +2
(`use super::AtomicU64` in the files atomic_u64_as_mutex.rs loads);
ripgrep, axum, clap, serde, gleam, express and flask unchanged.
- Cost, native kernel, medians of 3 interleaved runs: fast-mlsirm CPU 15.3
-> 15.8 s, wall 4.2 -> 5.2 s, peak RSS 825 -> 882 MB; tokio CPU 26.1 ->
26.7 s, wall 10.4 -> 11.2 s, peak RSS 1,023 -> 1,028 MB; dioxus CPU 34.8
-> 35.6 s, wall 10.3 -> 11.2 s, peak RSS 1,125 -> 1,120 MB; gleam CPU
24.1 -> 23.8 s, wall 6.6 -> 6.7 s, peak RSS 1,027 -> 1,023 MB.
Builds on PR #1892 by @seonghobae.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
ed6b88e53a |
fix(js): an export default line inside a template literal no longer names the default (#3068)
statedDefaultExport reads a JS/TS file's own export default statement
by scanning comment-stripped source for line-initial `export default`
heads. Strings are deliberately not masked there (a masker that loses
step on nested templates blanked real statements), and a head counted
when it named a node of the file, the first such head winning. A code
generator's template text breaks that: in
export function App() {} export function Main() {}
export const template = `
export default App
`;
export default Main;
the template's line names the real App, so `import X from './gen'`
bound to App instead of Main (issue #2968). A template holding
`export default () => …` also made a module whose real default is an
expression read as anonymous, which keeps every guess away.
templateLiteralTextAt (strip-comments.ts) scans the source once for
template-literal text: it follows `${…}` nesting (a template inside an
interpolation, a brace inside a regex there), and skips comments,
quoted strings and regex literals, which may hold a backtick; a `/`
after a value divides, anywhere else it opens a regex. A head inside
template text is now skipped. When some template never closes the scan
vouches for nothing and every head counts, as before. Nothing else
changes: a head outside template text is read exactly as it was.
Validation: default-export-declaration.test.ts gains the issue's file,
a template returned by a function behind a nested template, one passed
at module scope and one in a class field, a template's anonymous
default above an expression default, and controls (a regex and a string
holding a backtick, a division after a parenthesis, and a file whose
scan can't close a template); strip-comments.test.ts tests the scan
directly. Red with main's resolver: 2 of 10 failed; green: all pass.
Checked against tree-sitter on every line-initial head of 4,191
JS/TS files in 16 corpora (outline, excalidraw, vben, bitwarden apps,
mastodon, shadcn docs, zod, bulletproof-react, express, astro, react,
trezor-suite, this repo's src, tests and ui): of 4,149 real export
default statements the scan puts none inside template text, and all
322 heads it does place there are template text. A/B on excalidraw
d3a9c2a1, vben 61c07936, astro e3bec3d2 (packages/astro/src and
packages/integrations), with gin-gonic/gin 0f09c3a9 and psf/requests
3c7baaa0 as controls, both arms on main
|
||
|
|
617769279d |
fix(astro,svelte): a self-closing <script /> no longer swallows the next script
A self-closing `<script ... />`, such as Astro's
`<script is:inline set:html={code} />` or a third-party
`<script src="/analytics.js" />` in Svelte's `<svelte:head>`, matched the
extractors' script regex as an opening tag, so the match ran on to the next
`</script>`. The real script that followed was parsed with the markup in
between, failed with "parse produced no symbols", and contributed nothing.
The covered-range regexes had the same shape, so the template scans also
skipped the markup between the two tags, and a self-closing `<style />` hid
the markup after it.
The opening-tag pattern now matches a `/>` ending on its own as an empty
tag: in the Astro extractor's script blocks and covered ranges, the Svelte
extractor's script blocks and both of its covered-range scans, and the
viewer's highlight regions.
With a fix of ours: a self-closing tag has no body, so the covered-range
scans skip it, and a call in its attributes, like
`set:html={serializeJsonLd(schema)}`, is read as on main. Matched as a
range, such a tag hid those calls even where no script followed it.
Validation on main
|
||
|
|
4b0f72b7a7 |
fix(sync): a skipped file that now indexes re-links its importers
A file over the source-size limit, or one whose parse failed, is tracked with no nodes and its error recorded, so the imports of it fail. When an edit lets it be indexed, sync stored it as a modified file, and only an added file is looked up by what it can be imported as: an unchanged importer's import of the file itself stayed failed, and so did what a barrel passes on from it, until a full re-index. Named imports and calls bound, since they wait by name. A re-indexed file whose stored row had no nodes and recorded errors now joins addedFilePaths once the store gives it nodes, so the added-file retries and the importer and barrel walks run for it. It still counts as modified. Deciding after the store leaves a file that changes but stays over the limit, or still fails, alone, so it does not re-run those retries on every sync. Review changes: a test for the barrel shape, the one behind most of the real-repo divergence below, and the CHANGELOG entry beside the one for files saved with no symbols. Validation: 5 tests; on main's source 4 fail and the stays-over-the-limit control passes. 9 fixture edits, each compared with a fresh index of the same tree, in a later process and in the same process: main diverged in 7 of each, this in 1, a barrel growing over the limit, where only the edges' resolvedBy differs (same targets), as the PR noted. excalidraw with packages/math/src/point.ts over the limit at index time and put back: main was 1,506 edges short of a fresh index, identical here over nodes, edges, refs and files. Original PR #3022 by @inth3shadows. Fixes #3016 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2d98c1b69a |
fix(sync): a call left with one candidate links after a delete or a rename
x.go() with go in two classes stays unlinked while both exist. A sync that deleted one of them, or renamed it away, never retried that call: the failed-reference retry looks refs up by the names the changed files carry now, which a deleted file or a renamed-away name no longer has, and a sync that only deletes files skips the retry. Failed refs under the names a sync's definitionDelta lost or gained now go back to pending for the sweep, next to the CG-33 rebind that does the same for resolved edges, under the same per-name ceiling. CODEGRAPH_NO_REBIND=1 turns both off. Review changes: the CHANGELOG entry beside the sync entries for calls, with credit. Validation: 6 tests; on main's source the 4 delete and rename tests fail and the 2 still-ambiguous controls pass. 10 fixture edits (Python, TypeScript, JavaScript, Go, Java), each compared with a fresh index of the same tree, in a later process and in the same process: main diverged in 6 of each, this in none. excalidraw with excalidraw-app/data/LocalData.ts deleted (its `save` was one of two): main was 38 edges short of a fresh index, identical here over nodes, edges, refs and files; renaming BinaryHeap's `pop` away re-opens 28 refs that fail again, as in a fresh index. Original PR #3004 by @inth3shadows. Fixes #3006 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e431491972 |
fix(mcp): a session whose host stops reading its output shuts down cleanly (#3060)
Issue #2991: a host can stop reading an MCP stdio server's output while it keeps the server's stdin open and stays alive. The next response the server wrote then failed (EPIPE), and with no listener on stdout Node handed the error to the last-resort fatal handler, which exits 1 at once. That skipped the shutdown that stops the engine, releases the writer lock and waits for query workers still starting, which on Windows can crash a process that exits under them. Stdin, the host's PID and the liveness watchdog all still look healthy, so nothing else noticed. A new treatStdoutFailureAsShutdown, the twin of the stdin teardown from issue #799, routes a stdout 'error' (and the 'close' after it) into the owner's own shutdown, at most once, destroys stdout so later writes are dropped, and keeps its listener so a write queued before the failure cannot escape during the shutdown. Backpressure is left alone. It is wired into the direct-mode transport and into the local-handshake proxy, whose shutdown closes only this session's daemon connection and stops the engines it owns. The legacy socket proxy (runProxy) has had no caller since PR #580 and is left as it is. Validation: __tests__/mcp-stdout-failure.test.ts runs a real direct-mode serve --mcp, and the local proxy with an in-process engine and with a stand-in shared daemon. After the test destroys its end of stdout and sends one more request, main exits 1 in all three, the server with "Uncaught exception: Error: EPIPE"; with the fix all three exit 0, the server's writer.pid is released, the proxy's engine stop runs, and the daemon closes only the proxy's connection and still answers its other client. __tests__/stdout-teardown.test.ts covers the guard itself. proxy-shutdown, stdin-teardown and mcp-initialize pass. Fixes #2991 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
664492551d |
fix(sync): a barrel whose re-export points elsewhere re-links its importers
`c.ts` imports `foo` through `index.ts`, which re-exports it from `a.ts`.
Change the clause to `./b` and sync, and `bar` kept calling `a.ts:foo`
where a fresh index gives `b.ts:foo`. The barrel declares nothing of
`foo`, so the definition delta has nothing to rebind; no file was added
for the walks below a barrel or the added-files pass; the binding never
failed, so the retries never see it; and the name-matched-import pass
reads only bindings a name match took. `export *` moved the same way, and
a scoped sync of the barrel alone too.
The orchestrator now reports the modified ES modules whose `export … from`
statements changed, read off their import nodes (named by the module, with
the statement as signature) before and after the store. Their importers'
bindings through them go back for the sweep, up through the modules that
pass them on in turn, with the failures parked under them and the edges
into the members of what the bindings pointed at (`w.run()` with
`w: Widget`). It is the importer walk the removed-files pass uses, run
after the name-matched-import pass.
Review changes: the trigger compares the `export … from` statements
themselves. The original compared the modules a file imports and counted a
module that lost any import, so dropping a plain import from excalidraw's
`types.ts` (218 importers, no re-exports) re-opened 2,186 refs for the same
graph; it now re-opens none. Comparing statements also catches a name
dropped from a clause whose module stays (`export { foo, qux } from './a'`
to `export { foo } from './a'`), which the module set missed. Plus tests
for both, and the doc comments the new code sat between moved back.
Validation: 9 tests; on main's source 7 fail (the 2 controls pass), and
on the original trigger the 2 new ones fail. 15 fixture edits, each
compared with a fresh index, in a later process and in the same process:
main diverged in 8 of each, this in none. excalidraw with
`export * from "./utils"` in its math barrel (116 importers) retargeted to
`./constants`: main was 224 edges off, identical here over nodes, edges,
refs and files. Fresh indexes are identical to main's.
Original PR #2985 by @danusha2345.
Fixes #2978
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
d139865f9f |
fix(db): processes opening an index that needs a migration all open it (#3058)
Issue #2980: opening an index whose schema is behind read its version, then ran each pending migration in a deferred transaction. When several processes opened it at the same moment, those that read the version just before another one migrated applied the same migration again, and their open failed: on this Windows machine with "UNIQUE constraint failed: schema_versions.version", since the version was already recorded, and in the report with SQLITE_BUSY, which a deferred transaction that has already read gets at once when it comes to write. Each migration now runs in a transaction that takes the write lock up front (BEGIN IMMEDIATE, a new option of the adapter's transaction()), so a process that finds another one migrating waits it out on busy_timeout, and reads the version again inside it, skipping a migration already applied. The version the caller read stays a floor, as before: a database built from schema.sql records only v1 and the current version, and tests replay migrations above a version they rewound to. An open of a current index takes no lock and waits for nothing, as before. Validation: __tests__/migration-concurrent-opens.test.ts. A second process holds the write lock with version 16 recorded but uncommitted, so the open reads 15: main fails with the UNIQUE error after waiting 1.1 s; the fix waits, opens and leaves the migration to the other process. Three processes released at once from a stdin barrier on an index with 150k unresolved references: main fails in round 0, the fix opens all three in both rounds. On excalidraw's real index, 8 rounds of three openers: main 7 rounds with failures, the fix 0. The single process and the current-index cases pass on both. Fixes #2980 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
80f6b50504 |
fix(sync): a tsconfig, jsconfig or go.mod change re-resolves the imports it governs
The resolver read tsconfig / jsconfig `paths` and `baseUrl` and go.mod's
module path once and kept the maps for its lifetime ("callers re-create the
resolver if config changes", which none did), and the watcher follows source
files only. So after an edit to one of them a long-running MCP server kept
resolving imports by the old mapping, and so did later processes: a sync
re-resolves only the changed files, and a config-only edit changes none.
The index now carries a snapshot of the config files resolution read: every
tsconfig / jsconfig the alias loaders opened, `extends` chains included,
every go.mod, and the root's candidates whether present or not. Every sync
reads them again and, when one differs, drops the resolver's alias and
module maps with the memos resolved through them, re-opens every import the
file governs (the ES-module family for a tsconfig / jsconfig, Go for a
go.mod) for the sweep, as a moved workspace map does, and marks synthesis
for a rebuild. An index an older engine stamped records a snapshot without
re-opening anything.
Review changes: each file's digest covers what resolution reads from it, a
go.mod's module path and a tsconfig / jsconfig's `extends`, `baseUrl` and
`paths`, not its bytes. A dependency bump in go.mod or a `strict` flag in
tsconfig had re-opened every import of the language for the same graph
(caddy: 48,945 refs, sync 3.8 s against 0.9 s; excalidraw: 88,860 refs,
9.2 s against 1.1 s). Plus a test for that, the ReferenceResolver class doc
comment back on its class, and the CHANGELOG entry beside the workspace-map
one.
Validation: 7 tests; on main's source the 5 behaviour tests fail, and the
new one fails on the byte digest. 11 fixture edits, each compared with a
fresh index of the same tree, in a later process and in the same process:
main diverged in 7 and 9, this in none. Real repos, sync against a fresh
index over nodes, edges, refs and files: excalidraw with `@excalidraw/math`
retargeted was 27 edges off on main and is identical here; caddy with its
module path renamed was 6,511 edges off and is identical here. Fresh indexes
are identical to main's. A one-file sync on excalidraw, median of 5
interleaved runs: main 3,834 ms, this 3,269 ms.
Original PR #2984 by @danusha2345.
Fixes #2977
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
29ce0149cf |
fix(lock): only one process takes over a stale write lock (#3056)
Issue #2979: FileLock.acquire() read the lock file, saw a dead PID, unlinked the file and created its own with an exclusive create. Two processes that read the same dead PID could both succeed: the first one replaced the stale lock, then the second one, acting on what it had read a moment earlier, unlinked the first one's live lock and created its own. Both returned from acquire() and wrote to the index as its only writer. The same steal happened in two narrower ways: a lock file read in the instant between its exclusive create and the write of its PID was empty, so it counted as stale, and a PID the process may not signal (EPERM: another user's process, or an elevated one on Windows) counted as dead. The fix keeps the lock file and its format (a bare PID) and changes how it is taken: - A free lock costs one create, as before. The lock file is created with its PID already in it: a private file hard-linked into place, which fails if the lock exists. Where hard links are missing it falls back to the exclusive create, and an empty lock younger than 10 s counts as held, since its creator is still writing its PID. - A stale lock (dead PID, malformed, or empty and old) is replaced only by the process holding <lock>.break, itself a FileLock, after reading the lock again there, the way Mercurial breaks its locks. A break lock a crashed process left is broken the same way, one level up, at most three levels. - EPERM from a signal-0 probe means the holder is alive. Validation: __tests__/filelock-stale-takeover.test.ts runs two real processes with a barrier: the slow one stops right after reading the dead PID and resumes only once the other holds the lock. On main both held it, 5 runs of 5; with the fix the slow one is refused and the lock stays the holder's. The other cases cover the slow one getting in once the lock is free, a live and a dead break lock, the empty-lock window, EPERM (mocked, and for real on Windows with the System process), and no leftover files. Fixes #2979 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
bc9291afe6 |
fix(java): a call on a value a type holds in a static field reaches the method of the field's type (#3055)
A Java call written through a type's static field, Consts.HELPER.go() or Holder.SHARED.run(), was resolved by the method's name alone: the receiver Type.FIELD said nothing of its type, so the call went to a same-named method of whatever class the name matcher found, and a field of a library class (Intent.ACTION_VIEW.equals(...), System.out.print()) could reach a project method of that name. An interface's constants were not indexed at all (neither engine read a constant_declaration), so a constant a type declares in an interface, or inherits from one, had no declaration to read. A Java Type.FIELD.m() (Outer.Inner.FIELD.m(), a package written in front) now finds the type as the calling file names it (javaTypeRef), then the field, the type's own or one it inherits (five levels up), and calls the method on the value the field's declaration gives it, read with the field's own file's imports: an enum constant's own override, else the enum's method; the method of the anonymous class the field is initialized with (a CREATOR's createFromParcel); the class a new X(...) initializer builds; else the type the declaration writes. A Kotlin object's INSTANCE or Companion reaches the object's member. A library owner, or a field of a library type, links nothing. A receiver whose head is a local or a field links as before. Both engines index an interface's constant_declaration as a static final constant. Validation: - java-static-field-receivers.test.ts: 9 tests, 7 fail on main |
||
|
|
7021de5c30 |
fix(rust): calls through a type a path or a use names reach that type's own function (#3025)
A Rust call through a type, `sim::items::ItemPool::for_def(…)` into another
crate of the workspace or `ItemPool::for_def(…)` under `use
sim::items::ItemPool;`, was matched by its qualified name alone, so a
namesake type's function in another crate or module could take it (a
workspace's `client` crate's `ItemPool::for_def`). The module walk stopped
at the type, a `use` of a `pub const` or `static` (`use sim::RANK_COUNT;`)
linked nothing, and a type rustfmt wrapped below its path (`h:
&scheduler::\n Handle`) linked by the file's `use` of its last name.
The fix, on the module walk:
- rustPathTarget reads `Type::member` calls: the type is the item the
path's modules name, or the one the `use` that binds its name names
(rustUseItemsAt), and rustTypeMember finds its member: an enum's variant;
else the type's own (inherent) function or `const` in the type's crate,
a trait impl's only when it is the one of that name; a trait's own
declared method. A type alias, `Self`, a type the module declares itself
and a type path in any other position are left as they were.
- Module items include `variable`s (a module-level `pub const`/`static`).
- matchRustUseBoundName reads a qualifier written on the line above a
type (rust-scopes' blanked code, so a commented-out path is none) and
resolves the whole path.
- Memoized per (type, member) and per path.
- Manifest paths stay inside the project: a workspace member that leaves
it (`../sibling`, an absolute path) is neither read nor listed
(escapesRoot, in expandMembers), and rustCrateGraph drops a
`[lib]`/`[[bin]]` path that leaves it, as it already did a dependency's.
Validation:
- rust-cross-crate.test.ts (PR 2631's 9 cases, credited),
rust-type-member-paths.test.ts and rust-manifest-paths.test.ts: on main
10 of 15 fail; all pass. Every rust-* suite passes. On main the workspace
map read `../outside/Cargo.toml`, `../vendor/x/Cargo.toml` and
`/abs/crate/Cargo.toml` and listed `../vendor`; now only the project's.
- A/B against main, native kernel, 7 Rust repos (every site the same
as against PR #3014's own commit; re-checked on
|
||
|
|
cdfa31be54 |
fix(ruby): a call on an instance variable that initialize types links to its class's method (#3053)
Both extractors record a Ruby call on an instance variable as
`@writer.flush`, and no strategy read the variable's type, so every such
call stayed unresolved: 2,780 in activesupport and 3,239 in actionpack on
main, for example.
ruby-constant-paths now resolves `@name.m` written in an instance method of
a class (not a class method, not a module) by the class's `initialize`.
When every assignment to `@name` there starts a statement and is a lone
`Const.new(...)`, `A::B.new` or `::Foo.new ...` of one constant
(arguments with or without parentheses, a block or a statement modifier
may follow), the class is the one Ruby's constant lookup finds for it from
`initialize`, and the method is that class's own instance method or the
first its ancestry declares. Anything else leaves the variable untyped for
the other strategies, as before: a conditional, two classes, `.tap`,
`writer || Foo.new`, `@a, @b = ...`, a chain continued on the next line. A
class the project doesn't declare (`Logger.new($stdout)`) links nothing.
Validation, kernel engine, main
|
||
|
|
6d3804cf57 |
fix(resolution): C, C++ and Objective-C rank as one language, and a lone candidate is judged by family (#3052)
Name matching compared a candidate's raw language tag with the reference's: the single-candidate confidence in matchByExactName and matchFuzzy, and findBestMatch's same-language skip and +50/-80 score. So a call whose name had several definitions in another of C, C++ and Objective-C scored -80 for each and linked to nothing (an Objective-C file's _glfwPlatformSetTls(), a C++ call into Dear ImGui's imgui.h, which reads as C), and the one definition of a name across TSX and TS, or C and C++, was a 0.5 cross-language guess. The single-candidate confidence now compares language families (sameLanguageFamily). Ranking treats C, C++ and Objective-C as one language (sameRankingLanguage), the very language worth a point that only breaks a tie; other families keep the language itself, because ranking them by family moved excalidraw's Store::scheduleCapture calls to the App.tsx method beside them and gave okhttp 83 wrong Java-to-Kotlin Builder picks. Validation: - __tests__/name-match-language-family.test.ts, 8 tests (default and wasm): 6 fail on the base main; the pin that a .ts call keeps its .ts target over a .tsx namesake beside it passes on both; all pass with the fix. 24 related test files (cross-language, React Native, Expo, Fabric, frameworks, mixed TS/JSX, Objective-C, resolution): 746 pass. - A/B, main vs fix: glfw +4 calls from Objective-C into C, imgui +93 from C++ into imgui.h (read as C) and imstb_rectpack.h, all to the definitions meant; excalidraw, okhttp, leveldb and jq gain or lose no edge (43, 9, 97 and 30 unique-name links go from 0.5 to 0.9 with the same targets); flask and gin byte-identical. - Index time, median of 3 interleaved: imgui 7.9 s main, 7.7 s fix; excalidraw 9.2 s main, 9.4 s fix. Reimplements PR #1662 by ferrine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |