Skip to content

feat: port the colby v1.6.2 graph semantics, framework resolvers and synthesis - #322

Draft
sunerpy wants to merge 183 commits into
mainfrom
feat/v0.55-upstream-v1.6.2
Draft

sunerpy wants to merge 183 commits into
mainfrom
feat/v0.55-upstream-v1.6.2

Conversation

@sunerpy

@sunerpy sunerpy commented Oct 10, 2026

Copy link
Copy Markdown
Owner

Summary

Draft: the codegraph-rs v0.55.0 program. One pull request ports the colby
range f4ddf508..b635dd46 (v1.6.1 to main of 2026-10-07; v1.6.2 is its first
180 commits), fixes the Rust-only defects that audit found, and clears every
carry-forward DEFER. The row-by-row ledger is
docs/upstream-sync/V1_6_2_AUDIT_2026-10-10.md. The pull request is squashed
into one commit and released by release-please as v0.55.0.

The branch lands in waves, and CI must pass at the end of each wave:

  • W0 (this push): the audit ledger, golden re-extraction in the test suite
    (golden_reextract, scripts/regen-goldens.sh), a real-pipeline probe
    harness, a cross-language sync-versus-fresh matrix, 13 new golden corpora
    (C#, Java, Swift, Objective-C, PHP, Vue, Svelte, Python bases, CommonJS,
    routers, servers, mobile, synthesis), bench --ab and bench --graph-diff,
    and bounded steps in the viewer live-events test.
  • W1-00 (this push): schema 9 (synthesis_inputs,
    idx_edges_synthesis_site), and the extraction version moves once, 21 → 22.
    A v0.54.0 index reads as outdated and is rebuilt once by codegraph sync,
    a daemon's catch-up or the first MCP access. upgrade_from_v054 covers both
    paths with a real v0.54.0 index.
  • W1 to W10 (later pushes): Rust-only defects, extraction parity, resolution
    seams and per-language families, sync convergence, MCP/CLI/viewer, framework
    resolvers and synthesis, VB.NET (trial), codegraph context, Lua loaders,
    performance strategies, docs.

Owner decisions of 2026-10-10: squash merge; COBOL OWNER-EXCLUDED; VB.NET
through a revertible trial; refusing a newer schema split into a follow-up
pull request.

Verification (this push)

  • cargo test -p codegraph-rs --locked --test golden_reextract --test sync_convergence --test probes --test upgrade_from_v054
    and cargo test -p codegraph-bench --locked --test equivalence: pass
    (33 corpora re-extract byte-identically; the matrix runs 49 cases and keeps 7
    known gaps ignored until W5).
  • cargo test --workspace --locked --no-fail-fast: pass.
  • The pre-push hook (make pre-ci) passed on this head.

equivalence.rs only proved that each committed colby.db agrees with its own
JSON; nothing in the suite ran the extractor, so a change that moved
extraction output passed CI until someone ran the out-of-repo regeneration
helper by hand.

- tests/golden_reextract.rs indexes every re-indexable corpus from
  crates/codegraph-bench/fixtures/ with the binary under test, dumps it
  through the same canonicalizer as `bench --gen-golden`, and compares the
  five artifacts byte for byte with reference/golden/. A failure names the
  changed artifacts, the canonical row diff and the regeneration command.
  A guard test keeps the corpus list in step with reference/golden/.
- scripts/regen-goldens.sh runs the shared recipe: --check (writes
  nothing), --write <corpus...> (only the named corpora) and
  --mini-transplant (mini's schema rebuild). Scratch directories come from
  mktemp and are removed on exit.
- equivalence.rs generates each corpus's two byte-drift tests from one
  macro, keeping the existing test names, and fails when a committed corpus
  has no test pair.
- docs/equivalence.md describes the recipe once, adds the regeneration log
  the v0.55.0 program records changes in, and each corpus section now says
  what it guards plus the one regeneration command.

Every corpus re-extracts byte-identically on this tree; a tampered
go/nodes.json makes the gate fail with a Tier-1 node diff.
…s and CommonJS corpora

The goldens had no corpus for C#, Java, Swift, Objective-C, PHP, Vue or
Svelte, and the Python corpora had no class with bases, so whole families of
extraction never reached the equivalence oracle.

Each new corpus is a small source tree under crates/codegraph-bench/fixtures/
with its golden under reference/golden/, generated by
`scripts/regen-goldens.sh --write`. They capture today's output on purpose,
misses included, so the corrections that follow show up as reviewable diffs
attributed to one change each. Both golden suites now cover them, and
docs/equivalence.md says what each one exercises and logs the addition.
Four multi-project corpora give the framework resolvers and synthesis
passes a baseline before they land: routers (React Router, Next.js, Expo
Router, Vue Router, Nuxt, SvelteKit, Angular, TanStack), servers (Express,
NestJS, Flask, Django, FastAPI, Spring, Laravel, Rails, ASP.NET, Vapor,
axum, actix-web, Gin, Play, Drupal), mobile (React Native and Expo native
modules, a Swift/Objective-C bridge) and synthesis (emitters, handler
tables, re-renders, stores, Go embedding, C function pointers, Kotlin
expect/actual, MyBatis, Celery, Spring events).

Each project carries the manifest detection reads, and projects share
symbol names on purpose, so the cross-project misses in today's output (an
Angular @component bound to a React file's Component) are in the golden and
their later removal shows up as a diff.
…n version to 22

Schema 9 adds the `synthesis_inputs` table and the partial
`idx_edges_synthesis_site` index the synthesis stage uses to decide when a
sync must refresh synthesized edges. The extraction version moves once,
from 21 to 22, ahead of every graph-semantic change in this release, so an
index written by v0.54.0 reads as outdated and is rebuilt exactly once.

Every golden corpus regenerates only `schema.sql` and `colby.db`; the
canonical JSON is unchanged (golden_reextract proves it), and mini is
transplanted with `scripts/regen-goldens.sh --mini-transplant`.

`upgrade_from_v054` plants a real v0.54.0 index (fixture built by the
released binary, provenance in its README) and upgrades it through both
`codegraph sync` and the daemon catch-up sync, asserting schema 9, the
extraction stamp 22 and a canonical graph equal to a fresh init.
Add the `probes` test binary (crates/codegraph-cli/tests/probes). A probe
writes a small tree into a temporary directory, runs the built
`codegraph init` with CODEGRAPH_NO_DAEMON=1 and CODEGRAPH_NO_WATCH=1 so
scanning, extraction, framework detection and resolution run as they do
for a user, and reads the published index back through an immutable=1
SQLite connection, which leaves no sidecar for a later sync to refuse.

`support.rs` provides the Project builder, edit/sync helpers that stamp
explicit mtimes instead of sleeping, row queries over nodes, edges,
unresolved references and files, a selector notation shared with the
failure output, and the assert_edge!, assert_no_edge! and
assert_unresolved! macros, whose failures list the project's whole
non-contains edge list. Smoke probes pin a TypeScript import call, an
unresolved external import, a Python method call and a sync re-read.
Add the `sync_convergence` test binary. Each case indexes a `before`
tree with the built `codegraph init`, applies one edit (add, modify,
delete or rename files), and syncs it through two drivers: the CLI
`codegraph sync`, and the watcher path, `sync_changed_paths` with
exactly the changed paths (or the full reconcile the real watcher runs
for a control file). A fresh `init` of the final tree in a separate
directory is the oracle; both drivers must match it under
`canonicalize_db` + `diff_canonical`, by full natural keys. Cases name
the edges their before and fresh indexes must hold, the on-disk trees
must equal the oracle's, and failures print a readable row diff.

The cases cover every language with an extractor and every change kind:
an added import target, a body-only edit (#2288 same-named methods and
Java overloads), a rename, a deleted module, a heritage edit, a
same-named competitor, control files (codegraph.json, config.toml,
.gitignore, tsconfig.json, package.json) and a C header that turns into
C++. Gates fail when a language with an extractor or a change kind has
no running case.

Known gaps are ignored with their reason and fail under --ignored:
W5-01 #2392, W5-02 #2403 and W5-03 #2450, plus four unscheduled gaps
this matrix found: an Erlang remote call and a MyBatis cross-mapper
include are not retried when their target appears, and a tsconfig.json
`paths` or package.json framework change does not re-resolve unchanged
files.

The probe `file:` selector now matches a file node by path, since the
embedded extractors key theirs by a hash.
The live-events test once held a Windows CI job for 40 minutes: the
sync by another writer and the final search had no time bound, and the
#[tokio::test] runtime's drop waits forever for a stuck spawn_blocking
task, which the viewer uses for every request.

Each test now runs on its own runtime through `run`, which bounds the
test body, propagates a panic from it, and shuts the runtime down on a
helper thread with a limit of its own. Every step that can block has a
named limit: indexing the fixture, starting the viewer, connecting to
/api/events, each awaited event, each request, the sync by another
writer, closing the hub, dropping the last client and the teardown.
Blocking steps run on the blocking pool so a stuck one never stalls the
runtime. A hang fails with the step's name (or, for the backstop, the
last step started). No assertion changed.
`bench --ab` times a cold `init` of a baseline and a candidate codegraph
binary on pinned corpora. Every run copies the corpus into a fresh project
without `.git` or an index directory, sets RAYON_NUM_THREADS (the CLI has no
thread flag), CODEGRAPH_NO_DAEMON and CODEGRAPH_NO_WATCH, removes
CODEGRAPH_DIR, and records the wall time, the wait4 peak RSS and the
canonical fingerprint of the database. Runs alternate between the binaries
and the first run of each cell is a discarded warm-up. The harness stops
without a report when a run fails, is killed or times out, leaves no
database or one that fails PRAGMA quick_check or canonicalization, or when
one binary produces two different graphs at any thread count. The JSON
report records the CPU, kernel, rustc, both binaries' SHA-256, every run,
median and MAD, candidate/baseline ratios and the regression-budget verdict;
--report-md renders it as a table.

`bench --graph-diff` compares two canonical graphs, each a database or a
golden directory, and prints the removed and added rows grouped by surface,
language, kind and resolvedBy. It exits 0 when the graphs are identical, 1
when they differ and 2 when an input cannot be loaded. A database is read
from a private copy, so the input never gains SQLite sidecars.

The corpus registry gains express-medium and the release-pinned leveldb,
gson, Newtonsoft.Json, Alamofire, redis and kong corpora, a tag field, and
--corpora-root for --list-corpora and --fetch-corpora. sha2, already in the
lock graph through codegraph-core, hashes the binaries and graphs.
docs/benchmark.md documents both commands, the report fields, the regression
budget and every pinned corpus.
V1_6_2_AUDIT_2026-10-10.md records the 268 upstream commits from colby
v1.6.1 to main of 2026-10-07 row by row: disposition, Rust target, the
v0.55.0 program commit that carries each row and its status, together with
the Rust-only defects and pre-existing extraction gaps the audit found and
the owner decisions of 2026-10-10. COBOL is OWNER-EXCLUDED, and refusing a
newer schema is recorded as PLANNED for its own follow-up pull request.

The ledger's discovery boundary moves to b635dd46, and docs-check follows
it; the tracked release stays v1.6.1 until the ported rows ship.
…egistry

`index --force` lists every registered stdio MCP server on stderr before
it rebuilds. The pre-heal ordering test drains the child's pipes only after
both lease barriers, so on a machine with many registered servers, and
pipes capped at two pages once the user's pipe budget is spent, the list
filled stderr and parked the command before its second lease. The test now
gives the command an empty registry of its own.
The single extraction-version move to 22 must be exactly one step past the version the preceding release wrote, which is 21 for v0.54.0.
@codecov

codecov Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.31203% with 210 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/codegraph-cli/src/main.rs 82.99% 75 Missing ⚠️
...degraph-extract/src/embedded/vue_template_calls.rs 90.33% 66 Missing ⚠️
.../codegraph-extract/src/embedded/vue_options_api.rs 92.47% 21 Missing ⚠️
crates/codegraph-extract/src/lang/c_preparse.rs 96.09% 18 Missing ⚠️
crates/codegraph-extract/src/embedded/vue.rs 97.02% 5 Missing ⚠️
crates/codegraph-core/src/file_class.rs 98.11% 3 Missing ⚠️
crates/codegraph-daemon/src/lease_keeper.rs 98.59% 3 Missing ⚠️
crates/codegraph-daemon/src/project_service.rs 90.62% 3 Missing ⚠️
crates/codegraph-daemon/src/session.rs 83.33% 3 Missing ⚠️
crates/codegraph-extract/src/commonjs_requires.rs 97.45% 3 Missing ⚠️
... and 6 more

❌ Your patch check has failed because the patch coverage (94.31%) is below the target coverage (95.00%). You can increase the patch coverage or adjust the target coverage.

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #322      +/-   ##
==========================================
+ Coverage   95.06%   95.55%   +0.49%     
==========================================
  Files         198      279      +81     
  Lines      109293   164465   +55172     
==========================================
+ Hits       103895   157161   +53266     
- Misses       5398     7304    +1906     
Files with missing lines Coverage Δ
crates/codegraph-core/src/types.rs 100.00% <ø> (ø)
crates/codegraph-daemon/src/control.rs 97.04% <100.00%> (+0.37%) ⬆️
crates/codegraph-daemon/src/install.rs 100.00% <100.00%> (ø)
crates/codegraph-daemon/src/lib.rs 93.08% <100.00%> (+0.31%) ⬆️
crates/codegraph-daemon/src/proxy.rs 96.39% <100.00%> (+0.51%) ⬆️
crates/codegraph-daemon/src/spawn.rs 95.62% <ø> (-1.10%) ⬇️
crates/codegraph-extract/src/embedded/astro.rs 98.83% <100.00%> (+1.00%) ⬆️
crates/codegraph-extract/src/embedded/mod.rs 100.00% <100.00%> (ø)
crates/codegraph-extract/src/embedded/shopify.rs 100.00% <100.00%> (ø)
crates/codegraph-extract/src/embedded/svelte.rs 93.68% <100.00%> (+0.66%) ⬆️
... and 77 more

... and 74 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The upgrade test built its immutable URI from the raw path, which on Windows keeps the \\?\ verbatim prefix and backslashes that SQLite cannot open. It now uses the same file: URI form as the probe harness.
dump_golden wrote its log beside the output directory, so --write left reference/golden/<corpus>.gen.log next to the named corpus and removed that path on exit. Both logs now live in scratch directories of their own.
…s W0-06 requires

The corpora lacked three shapes the plan asks them to pin at v0.54.0
semantics: a PHP `static::` call, a CommonJS `require().member`, and a
Flutter app whose `pubspec.yaml` triggers detection. Each lives in a new
file, so the regenerated goldens only add rows: `bench --graph-diff`
against the previous goldens reports 0 removed rows for all three.
The UI-family, extraction-gap, carry-forward, planning-finding and Rust-only-defect tables now carry the disposition and Rust target columns the ledger format requires, and the not-needed section is a row ledger.
--mini-transplant still logged to a path beside its scratch directory and removed it on exit; the log now lives in a scratch directory of its own, like the other two.
…ture

The script-tests loop ignored a failing script, so ci-gate.test.sh had silently stopped at its stale needs anchor since the UI job joined CI Success (#298): mutations C to K never ran. The anchor now carries the ui job and the loop stops on the first failing script; all five harnesses pass.
…tarts

A cold `serve --mcp` answers its handshake direct and spawns the shared
daemon fire-and-forget, but never connected to it. With no client the
daemon idle-exited after CODEGRAPH_DAEMON_IDLE_TIMEOUT_MS (300 s by
default) and the session's index silently stopped syncing.

The session now keeps a passive lease on that daemon from a background
thread, the same client connection an explicit projectPath service holds,
so the handshake still never waits on the daemon. When the lease stops
being live (the daemon died, was replaced, or the index was re-created) it
starts or re-attaches a daemon with backoff: CODEGRAPH_DAEMON_RETRY_MS
(default 5000, 0 disables) doubling up to CODEGRAPH_DAEMON_RETRY_MAX_MS
(default 300000), as upstream #2293 does for its degraded proxy. It never
starts a daemon for a project whose index is gone.
A settled burst used to sync exactly the paths its events named, however
many, and the notify callback forwarded only the event kind and paths, so a
backend's sign of dropped events was lost and the index could stay stale
after a mass rename or a branch switch (#269).

The watcher now runs one full project reconcile instead when the burst
named more than 500 paths (upstream #1397's SCOPED_SYNC_MAX_PENDING), when
an event carries notify's Rescan flag (an inotify queue overflow, an
FSEvents drop) or names no path (upstream #2025's filename == null), and on
Windows, whose backend can lose events silently, when an idle drift
sentinel (CODEGRAPH_WATCH_SENTINEL_MS, default 60000, 0 disables) finds
changes no event reported; it then also registers the root watch again.
Until a lost-event reconcile commits, the watcher reports RECOVERING with
the cause, and the RECOVERING banner now names its reason instead of
assuming lock contention.

As promised on #269, the MCP instructions, the codegraph_status
description and the skill now say what happens after a large burst and
that renames never need a rebuild; the initialize golden follows the
instructions.
A daemon keeps running the code it started with. After an upgrade the
project's daemon could be an older build than the session that found it:
the proxy saw a version mismatch, the session fell back to serving reads,
and the old daemon kept the writer slot for good (upstream #2343, #2346).

A session that meets a daemon of another version now reads its version from
the hello and replaces it only when it is an older plain X.Y.Z release: it
asks it to drain over the project-bound control channel, so nothing is
signalled by pid, then starts a daemon from its own install and proxies to
it, or attaches through the cold-start lease keeper if the new daemon is
slower than the usual poll. A newer release, a prerelease, an unknown
version, or a daemon that does not acknowledge the drain is never stopped;
the session then serves reads and says so on stderr.

Every daemon also records its executable at start and drains and exits once
the file is replaced or removed (CODEGRAPH_DAEMON_INSTALL_CHECK_MS, default
30000, 0 disables), and a cold-start session whose own install changed stops
starting daemons. A test-hooks feature lets test builds of the daemon claim
another version and leave control frames unanswered.
A content-hash comparison calls a file up to date even when its stored row
holds none of its symbols, so `status` said "Index is up to date" while a
file contributed nothing to the graph (upstream #2336, the status half of
#2346).

`status --json` adds `index.filesNeedingReindex` (rows stored with no nodes
and no recorded reason, which a hash-based sync never revisits) and
`index.filesWithParseErrors` (a recorded parse failure and no symbols from
it); files over the size limit and file-level-only languages are empty on
purpose and count in neither. Human output names up to three files of each
group and no longer calls such an index up to date. `files --json` carries
each file's recorded errors. Every existing field is unchanged.
`serve --mcp` launched in an unindexed directory with `.git` searches
downward for indexed sub-projects. The walk had a depth and candidate cap
but no limit on how much it read: launched in /tmp (~40K entries) it never
answered `initialize` within 30 s.

The scan now stops after 10,000 directory entries (counted, so where it
stops is deterministic) or 500 ms, whichever comes first, and a scan that
stopped early adopts nothing, since it cannot know the child it found is
the only one; the tool guidance and the stderr notice say it stopped. The
MCP servers no longer run the scan in their constructors, before
`initialize` is answered: their first `tools/list` or tool call runs it,
and reports the candidates it finds once.
The daemon runs detached, with no console of its own, so on Windows every
console program it starts without CREATE_NO_WINDOW opens a visible console
that flashes once per call. The git pending-status path already set the
flag; `git_command` (repository and hook discovery) and the viewer's
`git config user.name` did not (upstream #2280).

`codegraph_watch::hide_console_window` now sets the flag in one place, and
every git call, and the viewer's browser opener, goes through it. Because
std cannot report a command's creation flags back, a static source guard
(like upstream's) checks every `Command::new(` in the crates the daemon runs:
each must hide its console or say in a `console-window-guard:` comment why
it needs not.
KNOWN_DIFFS listed C++ receiver-type inference, Java/Kotlin field-receiver
inference, the batched and conformance passes and the per-language import
refinements as deferred, though all of them are ported. equivalence.md said
`ns::compute()` resolves through the qualified-name matcher, while the cpp
golden records it as an exact-name match, and called the missing Rust
struct-literal instantiation upstream's own shape, which it is not.

A dated UPSTREAM.md entry records the corrections, the KEEP-RUST status of
#226's stricter receiver-word gate, and the #1728 disposition; the entries
it corrects stay as written.
- Read <root>/.git/info/exclude ahead of the root .gitignore in
  RootGitignore, so the scan and the watch policy prune what git's
  exclude-standard prunes (upstream #1728). A .gitignore rule wins a
  conflict, as in git. Only a real .git directory at the project root is
  read, through the bounded stat-first reader; a .git file, a link, an
  ancestor repository and core.excludesFile are never followed.
- The watcher treats the exclude file as a control file: per-directory
  backends watch .git/info, and an edit reloads the scope and runs one
  full reconcile.
- The pending-status git record's scope fingerprint includes the exclude
  file, so a record made before an edit is not trusted after it.
- Sync matrix: a .git/info/exclude control-file case. The root .gitignore
  case now hides legacy/ rather than vendor/, which the default ignores
  already drop, so it moves an edge instead of passing vacuously.
- An import node is a statement, not a definition (upstream #965,
  0a91d0f5). Exact name matching no longer offers one, the file's own or
  another file's, and one named like a definition no longer makes that
  definition look ambiguous. The fuzzy fallback already considers
  callables only. The qualified-name matcher is unchanged here: its
  imports-to-own-statement edges belong to the #2392 target gate (W3-01).
- Goldens: csharp, erlang, java, mobile, php, python, python_bases,
  routers, ruby, rust, servers, swift, synthesis, typescript, vue lose
  every imports edge exact matching bound to an import node, and the refs
  stay unresolved. PHP `use App\Field;` now binds namespace App\Field;
  Ruby `require_relative "logger"` falls through to the fuzzy match
  class Logger. Logged in docs/equivalence.md.
- Probe pr0965_vue_import_never_binds_a_svelte_import_node, unit tests for
  the exact and fuzzy paths, and a PHP AddCompetitor sync-matrix case.
- tier2_false_edges: the phantom node:path import now binds nothing and
  stays parked as unresolved (renamed to
  phantom_import_binds_nothing_and_stays_unresolved); the Rust supertype
  fixture keeps only its in-repo `use` edge, its `crate`/`std` self-edges
  gone.
- The out-of-repo import gate (imports and supertypes) no longer counts a
  JS specifier as local because it starts with a path alias's prefix: a
  catch-all "*" alias has an empty prefix that every package starts with
  (upstream #2437, 2c807f5c). An alias now makes the import local only
  when resolve_import_path maps it to a file the index holds, so an alias
  landing in node_modules or on a directory does not either. Resolving
  an import still honors the prefix, so "*": ["src/*"] aliases resolve.
- ResolutionContext gains is_indexed_file, answered from the store or
  the snapshot's known files, never from the filesystem.
- Probes pr2437_catch_all_alias_keeps_package_imports_external and
  pr2437_alias_into_node_modules_keeps_the_import_external, unit tests,
  and a Tsx AddCompetitor sync-matrix case; the matrix gains
  no_edge_after for an edge a fresh index must not hold.
- No golden changes (scripts/regen-goldens.sh --check: 32 identical).
- detect_language_for_source (and detect_language_of for a bounded read)
  give a file's language as extraction parses it: a .h header holding C++
  or Objective-C is that language. index, sync, migration and framework
  extraction record and use it, so a header's file row agrees with its
  nodes (G15). Framework extraction reads a C header whenever a resolver
  applies to C, C++ or Objective-C.
- Goldens: cpp plain_derived.h and ue_actor.h become cpp, objc
  Sources/Base.h becomes objc; files.json only. Logged in
  docs/equivalence.md.
- c_family_correctness pins the sniffed language for headers and the
  agreement of every node; the c_header_flips_to_cpp sync-matrix case
  now requires the fresh index to record shape.h as cpp, via the new
  file_language_after expectation.
Port upstream #2178 (a) (W4-SOL-01). A Solidity call written without a
receiver reaches a function of the contract, interface or library around
it or of one it inherits through its `is` clause, or a free function;
never another contract's override, and outside any contract no member.
The chain is read from each declaration's head, constructor arguments
dropped and a dotted base by its last name, every declaration of a name
counted. OpenZeppelin's `_msgSender()` in Context-derived contracts went
to ERC2771Context's override 83 times; solmate's WETH `_burn` went to
ERC1155's.

Sync records each Solidity file's types and bases in a
`solidity_heads:<path>` row under the `solidity_heads_version` marker. A
type that came, went or inherits otherwise moves the chains of the types
below it: in unchanged files, the calls naming a method of a type that
entered or left such a chain are retried at their sites. An index
without the marker re-resolves every Solidity file once and rebuilds the
rows.

KEEP-RUST: a `//` comment in a head drops only the rest of its own line;
upstream joins the head's lines first and loses the bases below it.
….hrl

Port upstream #2178 (b) (W4-ERL-01). An Erlang call written without its
module reaches the function of its own module, else another module's
only through `-import(Mod, [f/N])` (nothing when `Mod` declares no
`f/N`), else one a `.hrl` declares; with neither, nothing the project
declares. cowboy's `-import(req_SUITE, [do_get/3])` went to
compress_SUITE's `do_get/3`. A call written in a `.hrl` keeps reaching
any module's function of its name and arity.

Sync touches an Erlang function's name with its arity (`do_get/3`) when
the function comes, goes or changes: a bare call is parked under that
name, which no node carries and the tail lookup never scans, so a call
waiting for the module it imports from, or a header, to declare the
function was never retried.
…stay in their theme

Port upstream #2393 and #2396 (b635dd46 form).

- A Shopify theme is a directory holding a theme folder and a marker
  (`layout/theme.liquid` or `config/settings_schema.json`). JSON under
  `templates/` or `sections/` is Liquid only inside a theme found under
  the project root, which `ExtensionOverrides::load_for_paths` now
  carries into every scan, sync and watcher decision. A marker counts
  wherever it is, ignored or not.
- A section or snippet reference made inside a theme resolves to that
  theme's own file or stays unresolved; outside any theme it resolves by
  path as before.
- New sync lane `shopify_themes`: a row of the theme roots the indexed
  Liquid files sit in and a version marker. A theme root that came or
  went hands its JSON templates (folders of any case) to the per-path
  pass and re-resolves its Liquid files whole; the moved roots are
  recorded before the pass so a stopped sync leaves them to the next.
  Both drivers converge with a fresh index (upstream needs a full sync).
- The watcher hands marker events to the sync whatever the ignore rules,
  watches an ignored `config/` or `layout/` itself, and counts a marker
  folder removed or moved away as its marker gone. `status`'s git fast
  path declines when a marker is a candidate, git may hide one, or a
  recorded theme lost its markers.
- Probes, unit tests, failure injection, real-watcher and git fast path
  tests, a migration test, and nine sync-matrix cases; docs updated.
Port upstream #2289 (b635dd46 form).

- `.inc` is PHP's include extension and Pascal's too. A PHP open tag
  anywhere keeps an include PHP; otherwise a line shape only Pascal
  writes (a compiler directive, a `;`-closed routine header, `unit` or
  `uses`, a `const`, `var` or `type` section, a `begin` ... `end;` pair,
  Lazarus' `{%MainUnit}`) makes it Pascal; anything else stays PHP.
- The decision reads the file's own content, for the file row and the
  extraction alike, so a full index, a sync and a fresh index agree.
- The patterns follow upstream's JavaScript reading: ASCII `\w`, the
  `(?!:)` lookahead written as the class it stands for, `$` before `\r`;
  they compile once into a `RegexSet`.
- A codegraph.json `.inc` mapping turns the content reading off; as for
  any built-in extension, the mapping leaves `.inc` PHP. A file over the
  size limit is never read, so its extension decides.
- Unit tests, probes and two sync-matrix cases; docs updated.
…ans cannot use

The W4 full A/B put two shared-path regressions on regexes compiled per
reference in name_matcher.rs. veto_js_local_shadow read the call's
receiver, a regex compiled per call, for every language's name match,
though only a JavaScript or TypeScript function scope can veto; it now
returns at once outside that family and reads the function's local scope
before the receiver. infer_local_receiver_declaration compiled the
language's receiver patterns for every method call (three quarters of a
Newtonsoft resolution once C# and Java gained type arguments); every
language with patterns now compiles them only once a line passes a
necessary condition of them (line_may_declare), and a language without
patterns returns at once. csharp_keyword_local_type filters its candidate
lines the same way.

Each prefilter is a necessary condition of the patterns it guards, so
resolution is unchanged: the goldens and the full index of the ten
benchmark corpora and of 29 more downloaded ones are identical. Cold init
with 4 threads, fast profile, median of two runs on the same machine:
Newtonsoft 16.4 -> 9.1 s, gson 4.1 -> 3.1 s, redis 2.9 -> 2.3 s,
Alamofire 0.65 -> 0.54 s; typescript-large at 32 threads 12.7 -> 11.9 s.
Peak RSS is unchanged.
bench --ab took a run's peak RSS from wait4's ru_maxrss, and Linux carries
the forking process's high-water mark into the child across exec. Once
the harness had canonicalized the typescript-large index it held 414 MiB,
and every later cell read 414.2 MiB, whatever it really used.

On Linux the harness now polls VmHWM in /proc/<pid>/status every
millisecond while the child runs; other Unix systems keep ru_maxrss. The
report names the source, and docs/benchmark.md describes it. A test runs a
`sleep` while the harness holds 256 MiB and expects a few MiB, where
ru_maxrss reads 265804 KiB.
KNOWN_DIFFS.md recorded what the port has not ported, but not where it
reads a shape differently from colby by decision. A new section lists those
KEEP-RUST decisions with the module that carries each: the OD-8 resolution
rules (#226 receiver words, #1496/G14 nested receivers, the P15 Rust call
shapes, #2218, the C# extension methods), the extraction differences
(always-on value references, Kotlin/PHP property types, C/C++ alias
signatures, the C/C++ pre-parse, CFML fragment ids, Dart positions), the
walks read in path order where colby reads index order, the places the
source is read as written (Java anonymous classes, Solidity heads, Erlang
import lists, C/C++ macro visibility, JS stores, PHP class names, Dart
libraries, Shopify markers, Pascal includes), and the sync lanes colby's
sync has no counterpart for. Differences of mechanism that give colby's
result are left out.
…patterns

The W4 A/B rerun with the fixed harness left two overruns, and profiles
of frame-pointer builds traced both to regexes compiled per reference or
per name.

typescript-large held about 100 MiB more than v0.54.0: #2226's
js_code_binds_name compiled a declaration and a parameter pattern for
every name it was asked about and kept up to 1024 pairs, with their
per-thread search caches, for the whole index. The declaration check now
reads the code by hand, as the pattern's left-to-right matches did, and
the parameter pattern compiles, uncached, only when a `(` opens a list
that could hold the name. At 32 threads whole-file string blanking built
character and offset arrays, 16 bytes a character: strip_c_style and
blank_string_contents now scan bytes, every delimiter being ASCII, and
module facts read a transient blanked copy instead of keeping one per
file.

leveldb's init spent 62% of its samples compiling the C++ receiver
scan's patterns. The receiver's word is read without a pattern, the
declarator and rebinding patterns compile only once a line could match,
an ASCII line uses the declarator spelled over ASCII (no UTF-8 automata
to build), and the alias readers compile theirs only behind literal
checks. JS-family receiver lines are admitted only for `= new` and
`: Type`.

Each reader is tested against the pattern or the character version it
replaces, and the full index of the ten benchmark corpora and of 29
downloaded ones, and every golden, is identical. Against a v0.54.0 build
of the same profile: typescript-large peak RSS 1.06x at 32 threads and
1.05x at 4 (the release A/B had 1.35x and 1.41x), init 0.95x and 0.84x;
leveldb init 0.95x and 0.77x (the A/B had 1.19x and 1.45x).
RUSTSEC-2026-0367, -0368 and -0369 (published 2026-10-09 and -10) report
that zip 6.0.0's extraction follows symlinks out of the destination and
keeps SUID/SGID bits. zip came in only through self_update 0.44, which
required zip ^6; 1.3.1 requires zip ^9, the fixed line.

Since 1.0 each backend, the progress bar and the tar.gz decoder are
features of their own, so the feature list names github, progress-bar
and compression-tar-gz. The update path's API moved: the tag setter is
release_tag, get_latest_release returns the listing, newest first, and
the status predicate is is_updated. quick-xml now comes only with the s3
backend, so it leaves the build, and the RUSTSEC-2026-0194/0195
exceptions in .cargo/audit.toml go with it.

cargo audit reports no advisory. A musl build of this tree updated
itself from the v0.54.0 release, through asset selection, download,
tar.gz extraction and replacement, to the release's own binary.
67cb578 reads VmHWM inside the Unix wait loop, but imported the reader
for every target, so Windows Clippy failed on the unused import under
-D warnings. The import now carries the loop's #[cfg(unix)].
Since c221085 a named const constructor or a redirecting factory is a
method node, and the lookups that resolve a call through a value took it
for an instance member: `x.value()` on a parameter typed `Foo`, or
`build().value()` where `build` returns one, called `Foo.value`'s
constructor instead of the `value()` an extension on `Foo` brings, and
with no such member the matcher's fallbacks still found it, reading a
receiver `foo` as the class `Foo` or scoring a receiver `fooValue` by
its words. Upstream's dartMemberOf has the same reading.

A constructor is now reached only by the type path, a call written
through the class (`Foo.value(2)`). dart_member_of (chain links, the
typing of a chain's links, `call`, extension members) skips constructors,
and match_method_call's value strategies, resolve_method_on_type and the
receiver-as-class and word-scoring fallbacks, take their candidates
through callable_on_a_value. The Dart row of docs/languages.md says what
is now built.

A probe covers the parameter, local and chain shapes, the type path, and
receivers `b`, `bar`, `barValue` and `dynamic bar` of a type with no
member of the name; two matrix cases add and remove the extension. The
goldens, the Dart corpora and the benchmark corpora index identically.
…ferent

A Dart call through a value of a type no longer reaches the type's named
constructor, where colby's member lookup and receiver guesses take it.
Record the rule and the modules that carry it in KNOWN_DIFFS.md's
KEEP-RUST list. It is no owner decision, so the Resolution heading drops
its OD-8 tag and each owner-decided bullet names OD-8 itself, as the
Extraction bullets already do.
…s later

Port upstream #2403's tests (W5-02). The behavior already landed with
W5-01: this port computes a parked reference's tail when it looks the
reference up, and a reference written as a path (`snippets/price.liquid`,
a JSON template's `sections/404.liquid`) waits under its file's stem,
which an added file's import path keys carry, so the file it names sends
the referencing file down the whole-file lane. Upstream parks it under
the file name and rewrote the tails an older index stored (schema v13);
this port stores no tail, so that half has nothing to port.

The probes add upstream's several-themes fixture with four files added
later (the links match upstream's and a fresh index of the same files,
and a deleted snippet's reference waits for it and links again) and the
one-theme sync case; the store test maps upstream's referenceNameTail
cases onto this port's keys. The matrix drops the ignore of
liquid_render_of_an_added_snippet and adds a JSON template's section
added later. Without the path-reference branch of the import retry, the
two probes and the two matrix cases fail.
…e appears

Port upstream #2450's behavior (W5-03). A binding a module declares under
another name (`import { Widget as W } from './widgets/Widget'`) is
written under its own, so an edit that gives the module `Widget` touched
no name the binding's references wait under: sync kept them unresolved,
or on a namesake they had been matched to by name, where a fresh index
links them to the module's declaration. Upstream parks such references
under #2422's `module:` keys and adds an index for them; this port stores
no tail and #2422 belongs to W7-23, so the behavior is ported with a
lookup of its own.

Store::importers_with_unbound_bindings reads, for a JavaScript-family
module, the importers whose statement of it holds a binding the import
did not resolve: an unresolved `imports` reference within the
statement's lines other than its path, or an `imports` edge there whose
resolvedBy is not `import` (matched by its name, in the module or
elsewhere). Its joins are pinned to start from the module's file node.
Sync reads it in sync_one before an edited module's old nodes go, since
the edges into them go with them, and sends those importers down the
whole-file lane after the import retry. An added module is the import
retry's (W5-01) and a removed one the removal lane's; both already
converge, as do default imports, whose module's surface moves.

The probes port upstream's fixture of ten importers and their modules:
added later, restored after a delete, given their exports by an edit,
and a binding its module never declares, each matching a fresh index of
the same files. The store test pins which statements count. The matrix
drops the ignore of typescript_aliased_named_import_of_an_added_file and
adds an export gained (TypeScript, and TSX through a tsconfig alias), an
export lost, and two namesakes the binding leaves once the module gains
the export. Without the lane, read after re-extraction, or with either
binding test weakened, the new cases fail. Fresh indexes are unchanged:
the 32 goldens are identical.
…ames appears

W5-05 (owner 2026-10-11, the two gaps W0-04 found). An Erlang remote call
is parked as `util::double/1` and a MyBatis include of another mapper's
fragment as `app.CommonMapper::userColumns`, names no node carries.
W5-05a's tail retry already reaches them by `double` and `userColumns`,
but skips a tail more than 500 parked references share. A sync now also
looks them up by the whole qualified name of every node whose candidate
changed, on both sides of the change, and of a removed file's nodes. A
node's id holds no qualified name, so a mapper namespace or an Erlang
`-module` renamed in place keeps every id while the qualified names
move; reading them from the candidate change catches that too. The
lanes that look targets up by node name keep the bare names.

The matrix drops the two ignores and adds the module added past the tail
ceiling, the module and the mapper namespace renamed in place past it,
and the module and the mapper deleted. Without the qualified names the
three ceiling cases fail; taking them only from nodes whose id came or
went, the two in-place cases do. Fresh indexes are unchanged: the 32
goldens are identical.
…d names

W5-07 (found by W4-PHP-01's plan gate, introduced with W3-06's #2180).
A call through a class name (`User.create()`) reaches the method the
class inherits, walking at most five levels up the supertypes each
declaration's head names, read from source; no sync lane recorded those
heads, so a head changed in one file left such a call in an unchanged
file at its old target, or unresolved, where a fresh index moves it.

Every full index, migration and sync now keeps a `class_heads:` row per
file declaring a type such a walk reads, with what each declaration's
own head writes (class_heads::declaration_heads, which the walk's
readers now share: the per-declaration Dart reader and the three-line
head reader are factored out unchanged). A sync seeds the types whose
entries came, went or changed; the edges to methods named like those of
a seed's supertypes, old or new, at most five levels up re-resolve at
their sites, cross-file and same-file, and the calls parked through a
seed or a type at most four levels below it are retried at their sites
through Store::unresolved_refs_by_receivers, however many there are.
The class_heads_version marker follows the other lanes' protocol; an
index without it re-resolves every such file once.

The matrix drops the six W5-07 ignores and adds the base change in
Ruby, Kotlin, Scala, Objective-C and Pascal, Dart and TypeScript calls
the walk does not read, a deleted base, a middle class's base, a base
two levels above the changed head, a call parked until the middle
class's new base has the method, and a base pushed past five levels.
Without the lane, its receiver retries, its same-file edges, its depths,
its bootstrap or the migration's marker, the matching cases and watch
and migration tests fail. Fresh indexes are unchanged: the 32 goldens
are identical.
…pendencies, C# project files or Cargo manifests change

Resolution reads control files that are no source files: the JS/TS path
alias configs along their extends chains, the workspace manifests and the
folders a workspace glob lists, the package.json and Deno manifests above
each JavaScript-family file, the C# project files above each C# file, the
Cargo.toml files above each Rust file, and the frameworks it detects. A
sync re-resolved only the files it re-extracted, so a change to one of
those left every unchanged file on its old result.

Each reader now reports what it read or probed in the same traversal.
Every index, migration and sync records a control_inputs:{js,csharp,rust}
row of input digests and a frameworks_detected row. A sync re-reads a
kind's inputs on a CLI sync, or when the watcher hands it a recorded
input, a control file of the kind, or a folder coming into a workspace
glob's listing, or when a file of the kind came, went or changed
language. A kind whose inputs moved re-resolves every file of its
languages whole; a framework that came or went re-extracts every file.
The control_inputs_version marker is withdrawn and restored with the
others and read before the per-path pass; frameworks_version is withdrawn
only by a sync that re-extracts for a framework change. The watcher hands
control-file and recorded-input events to the sync though they are no
source files.
The Dart library model reads each package's name from the nearest
pubspec.yaml and its path: dependencies from pubspec.yaml and
pubspec_overrides.yaml, which are no source files the lane compared. A
package renamed, a folder becoming a package or a path: dependency left
unchanged files' imports where they were.

Every index, migration and sync now keeps a dart_pubspec:<dir> row with
both texts for each folder above a Dart file that holds one, and the
dart_libraries_version marker moves to 2 so older indexes rebuild both
kinds of row once. A sync re-reads the manifests on a CLI sync, when the
watcher hands it a path named like one, or when a Dart file came or went;
when they moved, the before and after library models each read their own
state's manifests from a fixed snapshot, so the existing comparison
re-resolves only what the change moved instead of every Dart file. The
watcher hands manifest events to the sync, ignored or not.
…a name many times

On the whole TypeScript checkout (39,330 files, tests included) a one-line
edit made `codegraph sync` take 81.6 s against 4.3 s for v0.54.0, and a fresh
index 422 s against 185 s. Name matching read the same nodes again for every
candidate of a name and every class a supertype walk reached, and the test
corpus repeats names thousands of times. Every change below keeps the graph
as it was.

- A candidate's enclosing function is looked up in its own file only, through
  a per-file index of its functions and methods kept for the pass, after a
  project-wide check that some function or method has the qualified name.
- A call through a class name walks the supertypes once per language and
  class name: the walk is shared by every method name, taken one class
  further only when the visited part has no match, a supertype name met again
  adds nothing, the methods of a declaration's file are indexed by owner and
  name, and a declaration no method of the name belongs to is skipped. During
  a sync the class-head rows of files it neither rebuilds nor removes stand in
  for their source.
- A local declaration's scope comes from the same per-file index; the
  SourceFacts memo is gone.
- The LRU caches keep recency by timestamp (O(log n) instead of a linear
  queue) and are read with borrowed keys.
- A name's candidates are kept for the pass, each with its scope and its
  object-literal owner read once; an object path's holders are grouped by
  path once per name; a class's file is read for a method only when it holds
  a method of that name.
- Receiver inference reads cached lines and compiles a field's patterns only
  once the field is found; nodes are read with cached statements and column
  indexes.
- A sync's retries by name read the parked references in one pass instead of
  four, and its pubspec check reads only the Dart file paths.

Whole TypeScript checkout, release builds against v0.54.0, interleaved, first
run dropped: single-file sync 1.114x at 32 threads and 1.116x at 4, init
0.89x and 0.68x, peak RSS 1.04x and 1.05x. Fresh indexes match the previous
commit's canonical graph on 10 registry corpora, 29 downloaded corpora and
the whole TypeScript checkout; goldens unchanged.
refresh_dependent_refs extracts every dependent file of a sync in parallel
(each extraction reads only its own file and the project scope), then works
out each file's replacement in the dependents' order, the first error in
that order stopping it before anything is written, and writes every
replacement in one IMMEDIATE transaction through the new
Store::replace_resolution_states. A commit per dependent cost more than the
writes themselves. Each replacement only touches its own file's edges and
references, so one transaction gives what one per file gave; a store test
compares the two.
resolve_lua_require scanned the whole file list for every require, and for
every file built a `/suffix` string to compare. lua_scope::lua_require_matches
now keeps, per resolution pass, each suffix's matching files in the file
list's order, so every require of a module reads the same list once; the
choice by the longest shared prefix with the requiring file is unchanged.
A context test compares the memo with the scan for each suffix, twice.
retryable_failed_imports read every parked import and path reference as a
whole row before choosing the few a sync retries. It now reads only each
candidate's id, name and kind, borrowed from the row, buckets them under the
same three keys and per-key ceiling, and then reads the chosen rows whole by
id, in id order: the same rows in the same order.
After a sync, cmd_sync opened the index read-only twice, once for the
file_complete diagnostic events and once for the extraction warnings, and
each read-only open copies the whole database into memory. SyncOutcome now
carries each changed path's file row as the sync left it (None for a removed
file), read by the writer just before it returns, in an incremental sync and
in a migration rebuild alike, and cmd_sync reports from those rows. The
events' fields and order and the warnings are unchanged; an outcome without
rows still falls back to opening the index.
…without re-extracting them

A one-line edit to a C# file that many files reference re-extracted all of
them in memory, only to recover the references at the sites that pointed
into it. Schema 10 keeps those references instead:

- `resolved_refs` has the columns of `unresolved_refs`, the id it had there,
  a cascade on the source node, and indexes on the site and the file path.
  Migration 10 creates it empty.
- Resolution moves each row it takes out of `unresolved_refs` there, in the
  same transaction (an id listed twice moves once); re-extraction, site and
  file replacements, and node deletion clear both tables alike. So a file's
  rows in the two tables are every reference of its last extraction.
- `resolved_refs_version` (`1:` and a digest of the extension overrides,
  max_file_size, the Godot DSL config, the extraction version and the
  detected frameworks) vouches for that. Only a full index writes it; a sync
  reads it before its first change, withdraws it at that change and writes it
  back only when it held at the start and the digest did not move, so a
  migrated index, a sync that failed partway and changed inputs fall back to
  extraction until the next full index.
- While it holds, a sync re-resolves an unchanged dependent from its stored
  rows, read only at the refreshed sites unless the file goes whole; a site
  with no stored row sends the file whole, as before. `project.godot`, whose
  extraction reads every `.tscn` and `.gd.uid`, is always extracted.
- A Current writer open runs pending migrations under its exclusive lease,
  so a schema-9 index of the current extraction version gains the table on
  its first incremental sync.

Every golden corpus regenerates `schema.sql` and `colby.db` (and
`reference/golden/colby.schema.sql`); the canonical JSON is unchanged. A
schema-9 fixture built by a release build of 3878d52 drives the upgrade
tests through `codegraph sync` and the watcher's sync.
… read-only

A read-only open without sidecars read the whole database into a Vec and
then copied it into the sqlite3_malloc64 buffer it hands to
sqlite3_deserialize, two copies of the index per open. It now allocates the
SQLite buffer at the file's length and reads the file straight into it; the
buffer is freed if the read fails before SQLite owns it. A file that grows
during the read fails it ("database changed while it was read") and one that
shrinks fails read_exact, where the old read could return a torn image. The
FREEONCLOSE | READONLY deserialization and the WAL header normalization are
unchanged; mmap is not used, since a checkpoint or auto-vacuum can shorten the
file under a mapping.
oxfmt wraps the provenance command as it wraps the v0.54.0 fixture's README.
Moving each resolved reference into resolved_refs with its own INSERT ...
SELECT and DELETE made a full index measurably slower than before schema 10
(newtonsoft: 0.15 -> 0.43 s CPU in delete_resolved_unresolved_refs). The
ids now move a chunk of at most 500 at a time, one INSERT OR IGNORE ...
SELECT ... WHERE id IN (...) and one DELETE ... WHERE id IN (...) per chunk,
in the same transaction: the same rows with the same ids, and an id listed
twice, in one chunk or two, still moves once because its first move deleted
the row. A store test moves 3,000 rows across chunks with repeats and
compares both tables with the row-by-row result.
The control-input digest stood in a file's size and modification time for
its text once it passed 64 MiB, so a tsconfig.json, package.json, C# project
file or Cargo.toml that large, replaced with both kept, went unnoticed while
its reader, which has no size limit, read the new text: a fresh index used
the new aliases and the sync kept the old edges. The plan and the docs
promise every input is read whole with no size or time shortcut; the digest
now always is. A watch test replaces a 64 MiB tsconfig.json keeping its size
and mtime and checks a full sync and a watcher's sync against a fresh index.
e3839db made the control-input digest read every file whole; the W5 goal
review asked for the convergence matrix to hold that, as every sync change
here adds a case. The harness gains Op::Replace, a rewrite that keeps the
file's modification time, and the case replaces a 64 MiB + 1 byte
tsconfig.json with one of the same length aliasing another folder: both
drivers must match a fresh index. With the size and mtime shortcut restored,
both drivers diverge.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant