AS-BUILT contract: ordinary symbol IDs use
codegraph_core::node_id::generate_node_id; tree-sitter file nodes use the literalfile:{file_path}; content hashes usehash_content. Framework resolvers are active extensions, not an empty seam: the registry currently contains NestJS, React, Vue, Godot, and Tauri candidates, each gated by project detection. Framework nodes with an explicitly documented compatibility ID are validated by their own corpora and do not change the ordinary symbol-ID formula.
This document defines the byte-level and semantic parity contract between the
Rust port and the pinned upstream TypeScript reference. The current authoritative
fixture is crates/codegraph-bench/fixtures/mini/; the live reference outputs are
stored under reference/golden/mini/.
The complete committed corpus registry is the set of directories under
reference/golden/. Extraction corpora contain canonical nodes.json,
edges.json, refs.json, files.json, and schema.sql; mcp/ contains
structural protocol fixtures. Do not maintain an unchecked fixed corpus count in
this document—tests enumerate the committed directories and recipes below.
The symbol-node helper computes:
sha256("{filePath}:{kind}:{name}:{line}") -> hex -> first 32 chars
id = "{kind}:{hash32}"
Rust mirrors this in codegraph_core::node_id::generate_node_id().
Inputs are part of the compatibility contract:
filePath: project-relative path with/separators, for examplesrc/app.ts.kind: the serializedNodeKind::as_str()value, for examplefunction,class,method, orimport.name: the exact extracted name. Import nodes use the module specifier, for example./math.line: 1-based start line. The tree-sitter call site passesnode.startPosition.row + 1.
Same-kind, same-name declarations on one line (a getter/setter pair) hash to
the same id. Since extraction version 15 the first one, in extraction order,
keeps it, and every later declaration at a different column appends
:{column}, its zero-based UTF-16 column — the column unit upstream uses, not
tree-sitter's byte column (NodeIdAllocator, upstream #1349). Revisiting the
same declaration yields the same id, so a declaration that collides with
nothing keeps exactly the id above.
Tree-sitter file nodes do not call generateNodeId(). The tree-sitter file-node special case uses the literal ID:
file:{filePath}
The mini golden data verifies this for all three file nodes, for example
file:src/app.ts. Non-file nodes in the same golden set, including imports, use
the hashed {kind}:{32hex} form.
Some custom extractors call generateNodeId(..., 'file', ..., 1) for their own
file-like nodes; that is a separate custom-extractor path and is not the
tree-sitter file node represented in the mini golden.
The content hash (hashContent)
stores a full lowercase SHA-256 hex digest of the file content in
files.content_hash.
Rust mirrors this in codegraph_core::node_id::hash_content(). The test fixture
hashes are cross-checked against:
sqlite3 reference/golden/mini/colby.db \
"select path,content_hash from files order by path;"Tier-1 fields must match the reference output byte-for-byte and are allowed to fail tests on any mismatch:
nodesrows, excluding inherently time-varyingupdated_at.- Node IDs, including the
file:{path}tree-sitter file-node special case. files.content_hashvalues.- SQLite schema and FTS5 schema/triggers/indexes captured from
.schema.
Tier-2 data may be compared as unordered multisets when insertion order or rowid allocation is not semantically stable:
edgeskeyed by(source, target, kind)plus relevant metadata.unresolved_refskeyed by(from_node_id, reference_name, reference_kind)and source location.
Tier-3 output can differ only when the difference is intentionally documented in
KNOWN_DIFFS.md:
- Query output formatting.
- MCP response formatting and summaries.
- Other presentation-layer or non-deterministic fields that preserve semantics.
Node IDs are Tier-1 deterministic. Given the same relative path, serialized
NodeKind, extracted name, and 1-based start line, Rust must produce exactly the
same bytes as the reference. The golden test in crates/codegraph-core/src/node_id.rs
loads all 13 real nodes from reference/golden/mini/colby.nodes.json and proves
that every ID reproduces.
The executable oracle lives in crates/codegraph-bench/src/oracle/ and is the
library entry point for later cross-implementation runs. Later tasks should call:
codegraph_bench::oracle::assert_equivalent(rust_db, golden_dir)For the current mini fixture:
cargo test -p codegraph-bench --test equivalence -- --nocaptureCanonical fixture files are committed under reference/golden/<corpus>/:
nodes.jsonedges.jsonrefs.jsonfiles.jsonschema.sql
Every corpus except mini is re-indexable and is regenerated with one recipe:
copy its source corpus from crates/codegraph-bench/fixtures/<corpus>/ into a
clean directory, index it with the current binary
(CODEGRAPH_NO_DAEMON=1 CODEGRAPH_NO_WATCH=1 codegraph init), commit the produced
database as reference/golden/<corpus>/colby.db, and dump the canonical
artifacts from that same database with bench --gen-golden. Never hand-write a
golden. scripts/regen-goldens.sh runs exactly that recipe:
cargo build --locked --release -p codegraph-rs -p codegraph-bench
scripts/regen-goldens.sh --check # every corpus; exit 1 on any difference
scripts/regen-goldens.sh --check cpp go # only the named corpora
scripts/regen-goldens.sh --write cpp # rewrite colby.db + artifacts of cpp only
scripts/regen-goldens.sh --mini-transplant # mini; see "Mini schema rebuild" below
scripts/regen-goldens.sh --mini-resolve # mini; see "Mini resolution regeneration" below--check writes nothing into the repository; --write writes only under
reference/golden/<named corpus>/. Both use target/release/codegraph and
target/release/bench unless CODEGRAPH_BIN / BENCH_BIN say otherwise, and
work in mktemp scratch directories that are removed on exit.
The same comparison runs in the test suite:
cargo test -p codegraph-rs --locked --test golden_reextract indexes every
re-indexable corpus with the test build of the binary and fails, naming the
changed artifacts and the canonical row diff, when extraction or resolution
output moved without its golden being regenerated. equivalence.rs (below)
only proves that each committed colby.db agrees with its own JSON; it never
runs the extractor.
To dump a single database by hand (for example while investigating a diff):
cargo run -p codegraph-bench --bin bench -- \
--gen-golden reference/golden/mini/colby.db reference/golden/miniEvery intentional golden change is recorded here with the commit, the corpora and artifacts it changed, why the graph change is intended, and the command that regenerated it. Older regenerations are described in each corpus section.
| Commit | Corpus | Artifacts | Intent | Command |
|---|---|---|---|---|
test(bench): add C#, Java, Swift, ObjC, PHP, Vue, Svelte, Python-bases and CommonJS corpora |
csharp, java, swift, objc, php, vue, svelte, python_bases, commonjs | all (new corpora) | baseline capture of the current extraction and resolution, including the misses later commits correct | scripts/regen-goldens.sh --write csharp java swift objc php vue svelte python_bases commonjs |
test(bench): add router, server, mobile and synthesis corpora |
routers, servers, mobile, synthesis | all (new corpora) | baseline capture of today's framework and synthesis output ahead of the framework resolvers and synthesis passes | scripts/regen-goldens.sh --write routers servers mobile synthesis |
test(bench): seed the PHP, CommonJS and mobile corpora with the shapes W0-06 requires |
php, commonjs, mobile | all | baseline capture of a static:: call, require().member and a Flutter pubspec.yaml app; each adds rows only, no existing row changes |
scripts/regen-goldens.sh --write php commonjs mobile |
feat(store): add schema 9 for synthesis inputs and move the extraction version to 22 |
every corpus, plus reference/golden/colby.schema.sql |
schema.sql, colby.db |
schema 9 adds the synthesis_inputs table and the idx_edges_synthesis_site index; the canonical JSON of every corpus is unchanged |
scripts/regen-goldens.sh --write <every re-indexable corpus>, then scripts/regen-goldens.sh --mini-transplant |
perf(store): keep resolved references so sync re-resolves dependents without re-extracting them |
every corpus, plus reference/golden/colby.schema.sql |
schema.sql, colby.db |
schema 10 adds the resolved_refs table and its two indexes, and each reference resolution takes out of unresolved_refs now moves there, so every colby.db also holds its resolved references; the canonical JSON of every corpus is unchanged |
scripts/regen-goldens.sh --write <every re-indexable corpus>, then scripts/regen-goldens.sh --mini-transplant |
fix(watch): rescan the project when the watcher may have lost events |
mcp | initialize.json |
the server instructions now say that a burst of changes, or a sign that the operating system dropped file events, makes the watcher re-scan the whole project, that responses can start with the RECOVERING banner until that catch-up commits, and that renames never need a rebuild (#269); the fixture's instructions string gains that one sentence, the rest of the fixture is unchanged, and no graph artifact moves |
edited with crates/codegraph-mcp/src/instructions.rs; cargo test -p codegraph-mcp --locked --test golden_mcp --test rmcp_parity requires the instructions byte-identical |
fix(resolve): never offer import statements as name-match candidates |
csharp, erlang, java, mobile, php, python, python_bases, routers, ruby, rust, servers, swift, synthesis, typescript, vue | edges.json, refs.json (ruby: edges.json only), colby.db |
an import statement is not a definition (upstream #965): every imports edge exact name matching bound to an import node goes, all but one to the file's own statement, and its ref stays unresolved; PHP use App\Field; binds namespace App\Field instead, and Ruby require_relative "logger" falls through to the fuzzy match class Logger |
scripts/regen-goldens.sh --write csharp erlang java mobile php python python_bases routers ruby rust servers swift synthesis typescript vue |
fix(extract): store the sniffed language on the file row |
cpp, objc | files.json, colby.db |
a .h header promoted to C++ or Objective-C by its content records that language on its file row, as its nodes already did (G15): plain_derived.h and ue_actor.h become cpp, Sources/Base.h becomes objc; no node, edge or reference changes |
scripts/regen-goldens.sh --write cpp objc |
feat(extract): extract C# base lists |
csharp, servers | edges.json (csharp), refs.json (servers), colby.db |
every base_list entry is an extends ref (G1, upstream b712e4de): csharp Store : BaseStore, IStore gains extends BaseStore and implements IStore; servers' UsersController : ControllerBase parks the out-of-repo base as unresolved |
scripts/regen-goldens.sh --write csharp servers |
feat(extract): emit instantiations for object creation and literal construction |
csharp, go, java, php, rust, servers, synthesis | edges.json, refs.json, nodes.json/files.json (java), colby.db |
object creation and literal construction yield instantiates (G2, upstream INSTANTIATION_KINDS): Java/C#/PHP object_creation_expression, Go composite_literal (package qualifier kept), Rust struct_expression, Scala instance_expression; a Java anonymous class new Runnable() { ... } becomes class <Runnable$anon@24>, which extends Runnable and owns run, so run's call to helper now leaves run; out-of-repo types (ArrayList, gin.H) and PHP new self()/new static() park unresolved |
scripts/regen-goldens.sh --write csharp go java php rust servers synthesis |
feat(extract): type-annotation references for Rust and Go |
go, rust, servers, synthesis | edges.json, refs.json, colby.db |
parameter and return types of Rust and Go functions and methods are references (G3, upstream TYPE_ANNOTATION_LANGUAGES), with upstream's shared builtin set skipped (Go int, error, ...): rust make_bits references union Bits, go ComputePay references Payroll; Self, Option, Result and framework types (Router, HttpResponse, gin Context) park unresolved |
scripts/regen-goldens.sh --write go rust servers synthesis |
feat(extract): type-annotation references for Java, Kotlin and Scala |
java, kotlin, mobile, servers, synthesis | edges.json, refs.json, colby.db |
parameter and return types are references (G3): Java through the grammar's fields (a field's type too, a qualified name by its last segment), Kotlin through kotlin-ng's unnamed parameter and return types (an extension receiver is not one), Scala through every curried parameter list, the return type and type-parameter bounds: java Main::build references Square, servers' UserController::userService references UserService; framework and standard types (Action, Promise, ApplicationEventPublisher, Map, Result) and Kotlin type parameters (T) park unresolved |
scripts/regen-goldens.sh --write java kotlin mobile servers synthesis |
feat(extract): type-annotation references for C#, Swift, Dart and PHP |
csharp, mobile, servers | edges.json, refs.json, colby.db |
annotated types are references (G3): C# type positions only (a property's type, a method's returns, a field's declaration type, each parameter's type; predefined_type skipped, a qualified name by its last segment), Swift parameter and return types, Dart signatures, PHP parameter and return hints (primitives and pseudo-types skipped); a Swift composition continued on an & line keeps its types (#2108's residue). csharp Store::Map and Store::map reference TypeMap; User::Make's Store binds the same-named constructor Store::Store until C# type positions take only type candidates (#2121, W4-CS-01); framework types (IActionResult, BuildContext, vapor's Request) park unresolved |
scripts/regen-goldens.sh --write csharp mobile servers |
feat(extract): Objective-C message sends and interface supertypes |
mobile, objc | edges.json, refs.json, colby.db |
a message send is a call named by its full selector (G5, upstream 61153f96): a self/super receiver leaves the bare selector, so [self ping] and [super ping] reach Base::ping; another receiver is written before it (Base.new, greeter.greet:), a class receiver also references the class (it replaces the instantiates the receiver-named call used to get), and a unary message to a class factory keeps the chain (Greeter.alloc().init); @interface Sub : Base <Doer> extends Base and implements Doer |
scripts/regen-goldens.sh --write mobile objc |
feat(extract): Python class bases |
python_bases | edges.json, refs.json, colby.db |
each identifier or dotted base of a Python class is an extends ref (G6, upstream v1.0.1); metaclass= and subscripted generics are not: User extends Base and Mixin, Admin extends User, and the metaclass's own base type parks unresolved |
scripts/regen-goldens.sh --write python_bases |
feat(extract): PHP extends and implements |
php, servers | edges.json, refs.json, colby.db |
a PHP base_clause is extends and a class_interface_clause is implements, every listed name by its last \ segment (G7): Controller extends App::Base, FirstName and Registry extend App\Field::Base, FirstName implements Renders; framework bases (TestCase, laravel Controller, drupal ControllerBase) park unresolved |
scripts/regen-goldens.sh --write php servers |
feat(extract): delegate Vue SFC scripts to the TypeScript extractor |
routers, vue | all, colby.db |
each <script> block is extracted by the TypeScript or JavaScript extractor at its file lines (G8, upstream vue-extractor.ts): function bodies' calls (go calls helper and increment), top-level constants (msg, nuxt route, id) and their initializers' calls, and import nodes now appear; a function's qualified name is the TypeScript extractor's (go, not App.vue::go), node ids unchanged. The block's top-level imports lose their ref until the next commit folds the block into the file |
scripts/regen-goldens.sh --write routers vue |
fix(extract): fold SFC scripts into the component's file node |
routers, svelte, vue | all, colby.db |
an SFC has one file node, file:<path>, holding its component (G9, upstream #2268); the component holds what a script holds at its top level; a per-instance script's (<script setup>, a Svelte instance script) top-level calls, a top-level constant's initializer's included, are the component's, while imports and a module-level script's calls stay with the file: svelte Widget calls doSetup and formatLabel, vue App calls helper, App.vue imports Counter.vue, helper and useCounter. New file: nodes only, every existing node id unchanged (OD-5) |
scripts/regen-goldens.sh --write routers svelte vue |
feat(extract): Go struct and interface embedding |
synthesis | edges.json, colby.db |
a Go struct's unnamed field and an interface's lone named type are extends refs (G10, upstream 1244c621, #2397), pkg.T by T and T[X] by T, predeclared types skipped: MemStore extends base, Store extends Getter |
scripts/regen-goldens.sh --write synthesis |
fix(extract): keep the receiver of Ruby method calls |
ruby, servers | edges.json, refs.json, colby.db |
a Ruby call with a receiver is named receiver.method (G11, upstream #2147's extraction half): self/super leave the bare name, a constant receiver is also a reference, Foo.new stays an instantiation. Formatter.shout now resolves by qualified name and references Formatter; message.upcase and rails Rails.application.routes.draw keep their receivers; @logger.log no longer resolves by its bare name to Logger::log and parks unresolved, as upstream leaves an instance-variable receiver |
scripts/regen-goldens.sh --write ruby servers |
fix(extract): keep the scope of C++ qualified calls |
cpp | edges.json, colby.db |
a C++ call keeps the scope it is written with (G13): ns::compute() is recorded as ns::compute and binds ns::compute by its qualified name (it bound by exact name before), and simulator::ManifestStartup::Apply(1) by its full path (the partial ManifestStartup::Apply match before); a call from the global scope, ::f(), is recorded as ::f, which the resolver leaves unresolved. Two edges change their resolvedBy or confidence; nodes, refs and files are unchanged |
scripts/regen-goldens.sh --write cpp |
feat(extract): same-file value references for TypeScript, JavaScript, Go and Python |
mobile | edges.json, colby.db |
a symbol has a references edge, {"valueRef": true}, to a distinctively named constant or variable of its own file that it reads (upstream #895, #897): rn listen reads PICTURE_TAKEN and expo-modules hello reads HelloModule. Edges only: nodes, refs and files are unchanged, and every other corpus is identical |
scripts/regen-goldens.sh --write mobile |
feat(extract): static member reads |
csharp, swift | refs.json, edges.json, colby.db |
a read through a type, a static member or an enum value, references the type (G4, upstream STATIC_MEMBER_LANGS): csharp new Store(Console.Out) parks Console unresolved, and swift Client::run references enum API through API.DependencyController.GetRoute.query(). Nodes unchanged; the other corpora hold no such read |
scripts/regen-goldens.sh --write csharp swift |
feat(extract): same-file value references for PHP, Scala, Rust, Ruby, C and Pascal |
cpp, synthesis | nodes.json, edges.json, files.json, colby.db |
value references reach six more languages (upstream #897), and the values they read become nodes: a C file-scope initialized, pointer or array declaration (cpp plain_header_negatives.h header_text, synthesis c/src/ops.c ops, both constants; the header stays C), a Ruby constant, a Rust module const/static, a PHP file-scope const, a Pascal unit const; a Scala object value is a constant, no longer a field (OD-5, node id changes; no corpus has one). Neither new constant is read by name, so no value reference is added |
scripts/regen-goldens.sh --write cpp synthesis |
feat(extract): same-file value references for Java, C#, Kotlin, Swift and Dart |
servers, swift | nodes.json, edges.json, files.json, colby.db |
value references reach the last five languages (upstream #897), and the values they read become nodes: a Swift stored property (servers vapor Package.swift let package a constant; swift HomeView::title, Client::cache, Cache::imageCachedType fields), a Kotlin property, a Dart static_final_declaration. A Java static final and a C# const / static readonly field is a constant, no longer a field (OD-5, node id changes; no corpus has one). No value reference is added: none of these is read by a distinctive name |
scripts/regen-goldens.sh --write servers swift |
fix(extract): C++ scoped reads reference the type they read through |
cpp | edges.json, colby.db |
a C++ read through a capitalized scope references it (G4): tree-sitter-cpp writes every scope as a namespace_identifier, which upstream's static-member pass never reads, so its C++ reads linked nothing. run_manifest now references simulator::ManifestStartup through simulator::ManifestStartup::Apply(1) (the inner path's scope; the call itself stays the call's). Nodes and refs unchanged |
scripts/regen-goldens.sh --write cpp |
fix(resolve): an imports reference binds the file it names, never its own import statement |
arkts, mini, mobile, objc, python_bases, routers, servers, svelte, typescript, vue | edges.json, refs.json (routers: edges.json only), colby.db |
an imports reference never binds its own import statement (upstream #2392): all 34 qualified-name edges from a file to its own import node go (17 TS/JS/TSX, 6 Python, 4 Objective-C, 3 Vue, and one each for ArkTS, Dart, Go and Svelte); a JS-family module path names a file (#2263), so 16 of them become import edges to the file the path resolves to, and 18 park unresolved: ./missing.js, ../foo, Foundation/Foundation.h, package:flutter/material.dart, three iOS headers, Python .models/.user/.base/django.urls/django.http/., github.com/gin-gonic/gin, and the Vue and Svelte relative paths, which upstream's JS path rule does not cover either; two file-path edges (0.7) become import edges (0.9): routers' ../views/Home.vue keeps its target, and typescript's ./collision.js moves from collision.js to collision.ts, the file viaCollision() already resolved to; mini also gains the unresolved function_ref this.value, see Mini resolution regeneration |
scripts/regen-goldens.sh --write arkts mobile objc python_bases routers servers svelte typescript vue, then scripts/regen-goldens.sh --mini-resolve |
fix(resolve): out-of-repo and namespace receivers never reach S2/S3 |
commonjs, routers, servers | edges.json, refs.json, colby.db |
a name the file binds from a package outside the repository names nothing in it (upstream #2120, #2134): commonjs request(createApp()) and servers express() no longer bind the const request = require('supertest') / const express = require('express') binding they call through, and routers' Angular @Component(...) (from @angular/core) no longer binds, as a call and as a decorator, the React app's Component function in Team.tsx (a cross-language 0.5 guess); all four park unresolved |
scripts/regen-goldens.sh --write commonjs routers servers |
fix(resolve): fuzzy matching ignores case only where the language does |
ruby | edges.json, refs.json, colby.db |
the last-resort fuzzy match compares names exactly outside PHP, Pascal and CFML (upstream #2139), and a fuzzy instantiates never names a method (#2141 c): ruby's require_relative "logger" no longer fuzzy-matches class Logger (the fallback the never offer import statements row above left it) and parks unresolved, as upstream's does |
scripts/regen-goldens.sh --write ruby |
fix(resolve): S1 walks declared supertypes; S3 skips dispatched handlers, test doubles and the caller |
python_bases | edges.json, refs.json, colby.db |
a call through a class name reaches the class method the class inherits, by the supertypes its declaration head names (upstream #2180): python_bases' User.create() in pkg/service.py, the classmethod User inherits from Base, now resolves to Base::create (qualified-name, 0.8) instead of staying unresolved; the Strategy 3 rules of #2175, #2197 and #2221 move nothing in the corpora |
scripts/regen-goldens.sh --write python_bases |
feat(resolve): a call reaches the overload its arguments fit |
csharp, swift | edges.json, colby.db |
a call reaches the same-owner overload its argument count fits, defaults and varargs counted, and in Swift the one its argument labels spell (upstream #2212, #2213, #2238, #2241): csharp Log(message) => Log(message, 0) and swift store(key:)'s store(key: key, value: 0) move from the caller itself to the two-parameter overload, and swift cache.store(key: "a") in Client::run from store(key:value:) to store(key:); only the targets move |
scripts/regen-goldens.sh --write csharp swift |
feat(resolve): follow functions destructured from call results |
commonjs, synthesis, vue | refs.json, edges.json (vue), colby.db |
a module-scope destructuring declaration's initializer runs (upstream #2357): commonjs const { setCharset } = require('../lib/app') and const { default: Service } = require('./service') and synthesis const { EventEmitter } = require('events') gain the file's require call ref (unresolved, like every require), and vue's <script setup> const { count, increment } = useCounter(0) its component's import edge to useCounter; a bare call through a name destructured from a call's result reaches what the callee returns under that key (upstream #2163): vue's increment() in go moves from unresolved to useCounter::increment (instance-method) |
scripts/regen-goldens.sh --write commonjs synthesis vue |
feat(extract): CommonJS requires import their files |
commonjs, servers | edges.json, colby.db |
a local require('./x') is a file-level imports edge to that file (upstream #2263): commonjs gains eight (require('../') from test/ reaches the root index.js, #2189) and servers' require('./routes/users') one; calls through require bindings follow CommonJS exports (upstream #2189, #2433): require('./util').wrap in chain.js moves from the local constant to util.js::wrap, createApp() through require('../') to index.js's app (its module.exports = app), and setCharset and new Service() keep their targets, now resolved by import |
scripts/regen-goldens.sh --write commonjs servers |
feat(extract): every named object literal owns its members |
typescript | edges.json, colby.db |
a named object literal owns its function members, exported or not, in a function or an IIFE, or hung on a path, and a module-scope literal's other values run where it is written (upstream #2363, #693): typescript namespace.ts's nested: { run() { helper(); } } value was walked by nothing, and its helper() call is now the constant api's (one calls edge, exact-match); no node or reference changes, as every other literal in the corpora is a module-scope one whose members already had nodes |
scripts/regen-goldens.sh --write typescript |
feat(extract): Vue Options API members and template calls |
vue | all, colby.db |
an Options API component's functions are method nodes of the component (upstream #2140): vue Counter.vue gains data, doubled, bump and mounted with their contains edges; bump now makes the helper() call the file made, and mounted's this.bump(), unresolved from the file, reaches bump. Template calls (upstream #2357 b) add none here: {{ total }} reads a value and @click="bump" names a handler |
scripts/regen-goldens.sh --write vue |
feat(resolve): Python imports follow package re-exports and module members |
python_bases | edges.json, refs.json, colby.db |
a Python name imported from a package follows the package's named and star re-exports to its definition, three modules deep, and so does a module member used through an imported module (upstream #2247, #2248): python_bases' from .models import User in pkg/__init__.py and from pkg.models import User in pkg/service.py, both re-exported by pkg/models/__init__.py's from .user import *, keep their imports edges to User, now bound by import instead of exact-match (0.9 both), and models.User() after from pkg import models, unresolved before, now instantiates User (import, 0.85); parenthesized from-imports and pkg.mod.Cls through import pkg.mod (#2291 b, c) move nothing in the corpora |
scripts/regen-goldens.sh --write python_bases |
feat(resolve): pytest fixtures type parameters; Django managers; keyword arguments are not bindings |
python_bases | edges.json, colby.db |
a pytest test's parameter is the class its in-reach fixture returns (upstream #2228): python_bases' test_admin(admin) in tests/test_service.py takes Admin() from the admin fixture of tests/conftest.py, so its admin.run() call keeps its target Admin::run but is typed (instance-method 0.9) instead of guessed from the receiver's name (0.8); the Django manager and keyword-argument rules (#2175, #2169 Python parts) move nothing in the corpora |
scripts/regen-goldens.sh --write python_bases |
feat(extract): types defined in variable declarations |
cpp | nodes.json, edges.json, files.json, colby.db |
a struct, union or enum defined in a declaration's type is a node (upstream #2446), and an unnamed one takes the name of the first variable the declaration declares: union_agg.c:8 union { int q; } anon_var; is union anon_var, contained by its file; no other node, edge or reference changes |
scripts/regen-goldens.sh --write cpp |
feat(resolve): typedef and using-alias receivers |
cpp | nodes.json, colby.db |
a C or C++ type_alias node records its declaration as written in signature (KEEP-RUST, owner 2026-10-12: upstream records no aliased type; a sync compares it to tell an alias that names another type): attr_macro.c's UINT32 gains typedef unsigned int UINT32. Receivers declared through an alias (upstream #2399) move no golden edge: no corpus calls through one |
scripts/regen-goldens.sh --write cpp |
feat(resolve): base classes by C++ name lookup |
cpp, synthesis | edges.json, refs.json (synthesis), colby.db |
a C or C++ base is resolved only by C++ name lookup (upstream #2407): every cpp extends edge keeps its target and is now qualified-name at 0.9 (it was exact-match 0.9 or qualified-name 0.95); and every entry resolves imports and supertypes before the rest (upstream #1789's prerequisite phase), so synthesis's Go Use(s Store)'s s.Get("a") reaches Getter::Get through Store's embedded Getter, its parked s.Get ref gone |
scripts/regen-goldens.sh --write cpp synthesis |
fix(resolve): Java bare calls, outside imports and type names by package |
java | edges.json, refs.json, colby.db |
a bare Java call reaches only a method of a class around it, of a supertype their heads name, or of a static import (upstream #2126): java's MainTest.checks calls verify(service) under import static org.mockito.Mockito.verify;, which no longer reaches app.b::EmailVerificationService::verify (the calls edge is gone and the reference stays parked, MainTest.java:16); outside imports (upstream #2195) and type-name visibility (upstream #2236) move nothing else in the corpora |
scripts/regen-goldens.sh --write java |
feat(extract): enum constant bodies |
java | nodes.json, edges.json, files.json, colby.db |
a Java enum constant or Kotlin enum entry with a body of its own declares its members under the constant (upstream #2194): java's Main.Op gains Op::ADD::apply and Op::SUB::apply (contained by ADD and SUB), ADD::apply calls Op::combine, and Main.java's node count goes from 15 to 17 |
scripts/regen-goldens.sh --write java |
fix(resolve): C# type positions, namespace-scoped type names and block namespaces |
csharp, servers | nodes.json, edges.json, refs.json, files.json (csharp), colby.db |
a block namespace X { … } qualifies only its body (upstream #2220): csharp's Nested.cs gains the Outer.Inner, JetBrains.Annotations and Second namespace nodes, Deep and its methods become Outer.Inner::Deep… (Shallow::Call instantiates Outer.Inner::Deep), Guard is JetBrains.Annotations::Guard and Two Second::Two (Nested.cs's node count goes from 9 to 12); the usings outside a block namespace are their file's, so Store.cs's four and servers' UsersController.cs's import nodes lose the namespace prefix and its file node holds them and their references; a C# type position names only a type (upstream #2121), so User::Make's Store return and parameter types reference the class App::Store, not its constructor; namespace visibility (upstream #2216) moves nothing else in the corpora |
scripts/regen-goldens.sh --write csharp servers |
feat(resolve): calls through declared members use the member type |
csharp, servers | nodes.json, colby.db |
a C# declaration without an access modifier is public in an interface and internal in a namespace or the file (upstream #2171): csharp's IStore::Save and servers' Greeter change visibility; calls through declared members, the receiver-word scorer's owner-only words and the standard-name rule move no edge in the corpora |
scripts/regen-goldens.sh --write csharp servers |
feat(extract): C# accessor bodies, initializers and target-typed new |
csharp | edges.json, colby.db |
a C# property's accessor bodies and expression body, and each field declarator's initializer, are walked as that member (upstream #2345, #2348): csharp's Store::Count (get { return Measure(); }) and Store::Size (=> Measure()) call Store::Measure, the field seed (= Seed()) calls BaseStore::Seed, and map (TypeMap map = new();, a target-typed new()) instantiates App.Support::TypeMap; a constant is no longer a type position's candidate, which moves no edge in the corpora |
scripts/regen-goldens.sh --write csharp |
feat(extract): CFML container tags, cfinterface, cfscript-in-tag re-parse and cfquery |
cfml | nodes.json, edges.json, colby.db |
a tag <cfcomponent> spans to its close tag (upstream #2296's tag walk): cfml's Widget.cfm::Widget ends at line 5, not 1; a bare-script extends is recorded at its attribute value, as upstream records it: Gadget.cfs::Gadget's extends edge to Base moves from column 0 to 18. The corpus has no <cfscript>, <cfquery>, <cfinterface>, container-wrapped function, component path or typed receiver, so nothing else moves |
scripts/regen-goldens.sh --write cfml |
fix(resolve): Swift call shapes, type paths, typed properties and labels |
swift | edges.json, colby.db |
a Swift call through a type path keeps the path and lands on the type it names (upstream #2118): swift's Client::run still calls API::DependencyController::GetRoute::query, now by the type-path rule at confidence 0.9 instead of 0.85. The corpus's bare min(1, 2) reaches no project member before or after, and cache.store(…) already reached Cache::store, so nothing else moves |
scripts/regen-goldens.sh --write swift |
feat(extract): Go type aliases, defined types and standalone doc comments |
go, synthesis | go: nodes.json, colby.db; synthesis: nodes.json, edges.json, files.json, colby.db |
a type declared on its own takes the comment above type (upstream #2438): go's generator.go::Emitter gains its docstring; a type A = B alias is a node that references the type it names (upstream #2417) and a defined type references the types it is defined from (upstream #2441): synthesis's go/store/store.go gains type_alias Alias (contained by its file) and the references of Alias and Defined to MemStore. Nothing else moves |
scripts/regen-goldens.sh --write go synthesis |
fix(resolve): PHP bare calls are functions; namespace-qualified names resolve exactly |
php | edges.json, refs.json, colby.db |
a PHP call written without a receiver is a function call (upstream #2103, #2141): php's Controller::show call 'label' => trans('title') moves from the method LocaleDefinition::trans to the function trans in src/Util/helpers.php; a class name written with a namespace resolves to that class (upstream #2286): Field\FirstName::make() under use App\Field; now reaches App\Field::FirstName::make (import, 0.95) and leaves refs.json. The corpus's other class names already named their visible class, so nothing else moves |
scripts/regen-goldens.sh --write php |
fix(resolve): case-insensitive name prefilter for PHP, Pascal and CFML |
php | edges.json, refs.json, colby.db |
PHP, Pascal and CFML names ignore case, so a name in another case passes the pre-filter (upstream #2141): php's Controller::show call formatprice(300) now reaches the function formatPrice (fuzzy, 0.5) and leaves refs.json. The cfml corpus has no name written in another case, so nothing else moves |
scripts/regen-goldens.sh --write php |
feat(resolve): Dart getter reads, type positions and initializer calls |
mobile | edges.json, colby.db |
a const constructor call calls the class it names (W4-DART-03; KEEP-RUST: this grammar writes the class in the call's type, where the call's name was never read): the Flutter main's runApp(const HelloApp()) instantiates HelloApp at the const, where it referenced HelloApp as a type at the name; nodes, refs and files are unchanged, and the dart corpus, whose extension type has no getter read, initializer or body type, is identical |
scripts/regen-goldens.sh --write mobile |
feat(resolve): type the later links of Dart call chains |
dart, mobile | nodes.json, colby.db |
a Dart function's or method's return type is the last name of the type its signature writes (W4-DART-03b; KEEP-RUST: this grammar holds it in the signature's return_type field, where the extractor never looked, so every Dart return type was empty): the dart corpus's extension type getters MetersT::km and Widget::half return double, mobile's greeting returns String and HelloApp::build returns Widget; edges, refs and files are unchanged, as neither corpus writes a later link of a call chain |
scripts/regen-goldens.sh --write dart mobile |
The canonicalizer strips inherently unstable timestamp columns
(nodes.updated_at, files.modified_at, files.indexed_at), parses JSON text
columns before re-serializing them with deterministic key order, asserts all
stored paths are relative / paths, ignores edges.id and
unresolved_refs.id, and normalizes .schema text with the same rules used by
crates/codegraph-store/tests/schema_parity.rs.
A second golden fixture, reference/golden/godot/, guards Godot-specific
extraction that the mini fixture cannot reach — there are no .gd/.tscn/
project.godot files in mini. It captures the framework-resolver output for:
- F1 — an autoload call (
GameFlow.return_to_map()) resolving to the unique same-namedfuncin the bound script (aframework-resolvedCallsedge), alongside the coexisting singleton-constant edge. - F2 — signal-handler connections (
.connect(_on_pressed.bind(button))and.connect(Callable(self, "_on_input"))) resolving to the handlerfuncs (Callsedges). - F3 — a
.tscnExtResourcescript attachment (main.tscn→stage_manager.gd), captured as ascript_attachunresolved-ref subkind. - UID-form autoloads — a sidecar-UID SCRIPT autoload
(
EffectManager="*uid://…"resolved througheffect_manager.gd.uid→effect_manager.gd, with anEffectManager.apply_effect()F1 method edge) and a header-UID SCENE autoload (ComboUi="*uid://…"resolved throughcombo_ui.tscn'suid=header, registration-only). Both emit anAutoload-subkind UNRESOLVED ref; the.gd.uidsidecar is NOT indexed (it maps toLanguage::Unknown, so it is neither a file record nor a node).
The minimal source corpus lives at crates/codegraph-bench/fixtures/godot/
(project.godot, game_flow.gd, stage_manager.gd, main.tscn,
effect_manager.gd, effect_manager.gd.uid, combo_ui.tscn).
Regenerate it with scripts/regen-goldens.sh --write godot (the shared recipe in Regenerating goldens).
The extraction and --gen-golden steps are both byte-stable: re-running the
index or the dump reproduces identical nodes.json/edges.json/refs.json/
files.json/schema.sql. The generated_golden_matches_committed_godot_fixture
and upstream_db_is_self_equivalent_to_godot_golden tests in
crates/codegraph-bench/tests/equivalence.rs enforce this.
Two properties this recipe does NOT claim, for every fixture below as well:
colby.dbis not byte-reproducible. SQLite's header carries a change counter, so a freshly indexed database differs from the committed one in the first page even when every row matches. Only the--gen-goldenartifacts are compared byte-for-byte; the.dbis an input to that dump, not a golden.schema.sqlrecords.schemastatement ORDER, which can shift. The order reflects how the current binary creates its objects. Regenerating a fixture whose committedschema.sqlwas produced by an earlier binary can therefore reorder statements (e.g.idx_edges_identity) with no schema change. Always regenerateschema.sqlfrom the database you are committing — steps 3 and 4 do exactly that, which keeps the pair self-consistent — and review an order-only diff as expected rather than as drift.
The schema normalization helper is replicated inside codegraph-bench rather
than extracted into codegraph-store to avoid changing store source during the
parallel CRUD work. It preserves .schema statement order, strips optional
IF NOT EXISTS from CREATE TABLE/INDEX/VIRTUAL TABLE/TRIGGER, trims line
whitespace, removes blank lines, joins statements with ;\n, and enforces a
final ;\n.
A third golden fixture, reference/golden/ruby/, guards Ruby receiver.method
extraction (upstream #1110) that the other fixtures cannot reach — there are no
.rb files in mini/godot. It captures the four receiver-bearing-call edge
shapes:
- instance-method call —
@logger.log(message)is recorded with its receiver,@logger.log(G11), and stays unresolved: an instance variable carries no type the resolver reads, as upstream leaves it. - class-method call —
Formatter.shout(message)is recorded asFormatter.shoutand resolves to it by qualified name; the constant receiver is also aReferencesedge toFormatter. Const.newconstruction —Logger.newrecorded as anInstantiatesedge to the receiver classLogger, not aCallsedge tonew.- bare
include—include Greetingstill records anImplementsedge (regression guard: the receiver.method path must not disturb it).
The minimal source corpus lives at crates/codegraph-bench/fixtures/ruby/
(service.rb, logger.rb).
Regenerate it with scripts/regen-goldens.sh --write ruby (the shared recipe in Regenerating goldens).
Like the Godot fixture, both the index and the dump are byte-stable, and the
generated_golden_matches_committed_ruby_fixture and
upstream_db_is_self_equivalent_to_ruby_golden tests in
crates/codegraph-bench/tests/equivalence.rs enforce it.
The dedicated reference/golden/python/ fixture guards Python bare
class-as-value references plus aliased imports (#1626) without changing the
shared mini corpus. Its seven indexed files live at
crates/codegraph-bench/fixtures/python/ and retain the original six positive
References edges:
- same-file class values in a direct return, assignment RHS, registry-pair value, call argument, and list literal;
- one cross-file
ImportedClassvalue. The import syntax is present, but real Python nodes are not marked exported, so import Gate 3b is unreachable. This edge is intentionally resolved by Gate 3a's unique cross-file name match at confidence 0.8.
It also pins two negative boundaries: a tuple return does not recurse into
TupleA/TupleB, and a bare handler parameter remains an unresolved
function_ref rather than resolving to a same-named method. The undefined
register(...) calls used by the argument and method shapes legitimately remain
as two unresolved calls rows.
The added alias files pin three import-resolution contracts:
import top_level as top_alias; top_alias.top_func()binds the receiver to moduletop_level, never to a global same-named callable;from pkg import module as mod_alias; mod_alias.func()prefers the existing module filepkg/module.py;from imported_types import ImportedClass as ImportedAlias; return ImportedAliaspreserves member-import semantics: both the import statement and the aliased class-as-value reference resolve to the class through the import (named Python imports use the absolute-module and top-level-definition fallback of upstream #1820, because Python symbols are never marked exported).
Missing/duplicate/ambiguous module aliases remain unresolved, and a claimed module alias never falls through to global bare-name matching.
Since extraction version 14 the pkg/__init__.py file node carries its module
docstring (upstream #1905): a bare string literal first in a module, class or
function body is that node's docstring, joined after any preceding comment.
Regenerate it with scripts/regen-goldens.sh --write python (the shared recipe in Regenerating goldens).
As with every fixture, compare only nodes.json, edges.json, refs.json,
files.json, and schema.sql byte-for-byte. colby.db itself is not a
byte-reproducibility contract because SQLite updates its header change counter.
Regenerate schema.sql from the database being committed; statement ordering
may differ between binary versions, but its normalized statement set must not.
The tests generated_golden_matches_committed_python_fixture and
python_db_is_self_equivalent_to_python_golden enforce database/artifact
self-equivalence.
The dedicated reference/golden/kotlin/ fixture guards Kotlin callable
signatures (#1495) without changing a shared corpus. Its single source file,
crates/codegraph-bench/fixtures/kotlin/signatures.kt, pins five positive
shapes: an explicit return, an inferred return, a generic return, a multiline
class method with a nullable generic return, and an extension function. The
Processor primary constructor is the negative boundary: the class signature
stays null and no constructor method is synthesized.
A Kotlin property's declared type is a References from the property node
(W2-03d), which upstream does not read: a deliberate difference (KEEP-RUST,
OD-8, owner decision 2026-10-11). Processor's val id: Int is a constructor
parameter, which mints no property node, so the fixture is unchanged;
kotlin_property_types_are_references in type_references.rs and the sync
case kotlin_property_type_of_an_added_class pin the behavior.
Regenerate it with scripts/regen-goldens.sh --write kotlin (the shared recipe in Regenerating goldens).
As with every fixture, only the five text artifacts are byte-compared;
colby.db is not byte-reproducible. Compare schema.sql by normalized statement
set when an older binary changes statement ordering. The tests
generated_golden_matches_committed_kotlin_fixture and
kotlin_db_is_self_equivalent_to_kotlin_golden enforce database/artifact
self-equivalence.
reference/golden/scala/ guards the Scala 3 inheritance and companion-object
fixes from upstream #1823 and #1824. Its two-file corpus under
crates/codegraph-bench/fixtures/scala/ pins all of these as one contract:
- the
tree-sitter-scala0.26.2 grammar accepts a class with multiple constructor parameter lists whose parent constructor also has multiple lists; - one
extends A(...) with B with Cclause emits three separate, bare-nameExtendsedges; - a Scala
objectis amodule, while its same-named companion remains atrait; the inheritance edge resolves to the trait even when the object appears first in the file.
Regenerate it with scripts/regen-goldens.sh --write scala (the shared recipe in Regenerating goldens).
generated_golden_matches_committed_scala_fixture and
scala_db_is_self_equivalent_to_scala_golden pin the canonical artifacts and
database-to-artifact agreement.
reference/golden/dart/ guards upstream #1784 / PR #1865. The focused Dart 3
corpus at crates/codegraph-bench/fixtures/dart/extension_type.dart proves an
extension type MetersT is class-like and owns MetersT::km and
MetersT::report as methods with their complete spans; Widget::half is the
ordinary-class control. This prevents either the old top-level-function shape
or the later complete member drop from returning unnoticed.
Since feat(extract): Dart mixins, implements, mixin applications and member docs, a mixin application class A = B with M; is named A rather than
<anonymous>, so its node id changes; the formula does not, the name is one of
its inputs (OD-5). No committed corpus holds a mixin application, a with or
implements list, a member dartdoc or a generic call, so no golden changed
(scripts/regen-goldens.sh --check: every corpus identical); the extractor
tests inheritance_clauses, dart_member_docs and dart_generic_calls pin
the behavior.
Regenerate it with scripts/regen-goldens.sh --write dart (the shared recipe in Regenerating goldens).
generated_golden_matches_committed_dart_fixture and
dart_db_is_self_equivalent_to_dart_golden enforce the same two-layer
self-equivalence as the other re-indexable corpora.
A fourth golden fixture, reference/golden/cpp/, guards C++ base_class_clause
inheritance extraction (upstream #1043) that the other fixtures cannot reach —
there are no .cpp/.hpp files in mini/godot/ruby. It captures the
general C++ inheritance shapes plus templated-base stripping:
- single public base —
class D : public Baseresolving toBase(anExtendsedge; thepublicaccess specifier is skipped). - templated base (stripped) —
class T : public Container<int>resolving toContainer(template args stripped to the base name). - multiple inheritance —
class Both : public Container<char>, public Plainemitting twoExtendsedges (toContainerandPlain). - struct base —
struct S : Container<double>resolving toContainer(struct inheritance goes through the same path as class inheritance). ::-qualified templated base —class Q : public ns::Tpl<int>recording anExtendsref tons::Tpl(qualified head kept, template args stripped). Since the C++ namespace-prefix work (Release D) storesTpl's qualified name asns::Tpl, this ref now RESOLVES to a realExtendsedge (Q→ns::Tpl) inedges.jsoninstead of remaining an unresolved ref.- plain derived declarations in
.h—plain_derived.his promoted from the extension-default C parser to C++ by a full-source, lexically maskedclass/struct Derived : Baseprobe. It covers access modifiers,virtual, qualified names, and templated bases.plain_header_negatives.hpins the false-positive boundary: comments, strings, character literals, preprocessor text, bitfields, labels, and ternaries do not promote C source.
Three further files exercise the Release D C++ extraction gains:
- namespace prefix +
ns::fn()resolution —namespaced.cppdefinesnamespace ns { void compute() {} }(qualified namens::compute) and callsns::compute()fromrun_namespaced. The extractor records the call with the scope it is written with,ns::compute(G13), so it resolves to aCallsedge through the qualified-name matcher (resolvedBy: qualified-name). - template-argument call stripping —
templated_call.cppdefinestemplate <typename T> void process(T)and callsprocess<int>(0); the<int>template args are stripped at extraction so the call links toprocess. - Unreal-Engine reflection-macro recovery +
.hC++ detection —ue_actor.his a lean UE header whose only C++ signal isclass ENGINE_API UFoo : public UObjectplus line-leadingGENERATED_BODY()/UPROPERTY(...)/UFUNCTION(), a member-levelENGINE_API, and no explicitpublic:. Content sniffing reclassifies the.hto C++, and the offset-preserving pre-parse blanking recovers theUFooclass + itsExtends UObjectclause (both dropped before).
Five further files exercise the Batch B C/C++ gains:
-
C leading attribute macros (upstream #1311) —
attr_macro.cis the one.cfile in this corpus (extension-mapped toLanguage::C, so it guards the C walker, not the C++ one). It#defines an attribute macro + aVOIDmacro,typedefsUINT32, and declares four functions: two behind the macro (GoodNamewith a macro return type,LostNamewith a typedef'd one), one without it (NoAttr, the control), and one pointer-returning (PtrRet). tree-sitter's C grammar reads the macro as the type and the real return type as the declarator, so before the fix these indexed under the RETURN TYPE's name (VOID/UINT32); the golden now pins all four under their real names with their real return types. The blank fires ONLY because#define SEC_ATTR __attribute__((section(".init")))is visible IN THIS FILE — the pass demands same-file#defineproof that a leading token is attribute-like, so this fixture also pins that evidence requirement, not just the macro's name. -
namespaced out-of-line method + fully-qualified call (upstream #1310) —
namespaced_member.hppdeclaresnamespace simulator { class ManifestStartup }with a staticApply, andnamespaced_member.cppdefines it OUT OF LINE inside the same namespace block, then calls it through the fully-qualified path (simulator::ManifestStartup::Apply(1)) from a function OUTSIDE the namespace. The receiver qualifier is spelled relative to the namespace, so the golden pins the method atsimulator::ManifestStartup::Apply(matching the class node'ssimulator::ManifestStartup) plus theCallsedgerun_manifest → simulator::ManifestStartup::Applyresolved byqualified-name— the edge that a namespace-less qualifier loses. The call is recorded by its full path, an exact qualified-name match. -
out-of-line template method receivers (upstream #1309) —
template_method.cppdeclarestemplate <typename T> class Boxwithget/setand defines both OUT OF LINE (template <typename T> T Box<T>::get()). The receiver qualifier carries<T>, which the class node never spells, so the golden pins both methods atBox::get/Box::set(template args stripped) plus thecontainsedges fromclass Boxto each — the link that aBox<T>::qualifier breaks.
A further file exercises the Batch B explicit-operator gain (upstream #1268):
- explicit operator calls —
operators.cppdefinesstruct Vec2withoperator+/operator[]/ a plainget, then calls each through the EXPLICIT syntax (a.operator+(b),a.operator[](3),p->operator+(b)) plus one plaina.get()control. tree-sitter-cpp strands theoperator_namein an ERROR child, so before the fix the extractor emitted the bare receiver (a) and no edge existed; the golden now carries fourCallsedges resolved byinstance-methodat confidence 0.9 (Vec2::operator+twice,Vec2::operator[],Vec2::get).
Seven further files exercise the Tier 3 aggregate work — MSVC COM interface
(upstream #1519) and first-class union nodes (upstream PR #1516). This corpus
is the C-FAMILY corpus, not a strictly-C++ one: it already held a .c file, and
Tier 3 adds a .mm file whose extension maps to the ObjC spec, so cpp/ names the
family rather than the dialect.
- MSVC COM
interfacepositives —com_interface.hppcarriesstruct SControlas the control plus threeinterfaceshapes (IWidget : IBasewith a base clause,IPlainwithout one, andINewlineBracewith the brace on the next line). The keyword is not C++, so tree-sitter reads each as afunction_definition: the container became afunction, its memberRuna freefunctioninstead of amethod, and the base clause vanished. The offset-preserving pre-parse rewrites the line-leading keyword tostruct+ 3 spaces (9 bytes → 9 bytes, so every line and column is unchanged), and the golden now pins all three asstructwithIWidget::Runamethodand TWOextends IBaserefs.int interface_count = 0;is the mid-line negative in the same file. - MSVC COM
interfacenegatives —neg_interface.hppholds the five shapes the guard must DECLINE, and pins them by extraction-INVARIANCE rather than by "the substitution did not fire": a block comment containinginterface INegComment;, a C++/CLIinterface class, aninterface struct, a raw string containing a fullinterface INegGhost {…};declaration, and a#definecontinuation line carryinginterface INegMacro. The golden pins exactly THREE nodes, the sharpest value beingINegCli'sdocstring— the verbatiminterface INegComment;. A comment-blind substitution rewrites that string tostruct INegComment;, which is how a leaking guard is caught.INegCli/INegStructKwstayingfunctionis pre-existing garbage from the C++/CLI misparse and is deliberately preserved. - C union —
union_agg.c(the second.cfile, so the C walker is under test) coversunion Namedplus bothtypedef unionRE-KINDS (AnonUandNamedUweretype_aliasbefore and areunionnow — node ids change), withstruct Ctl/AnonSas controls, the negativesunion Fwd;andstruct FwdS;(bodiless forward declarations, unindexed), andunion { int q; } anon_var;, the unnamed union defined in a declaration, which isunion anon_var: it takes its variable's name (upstream #2446; it minted nothing before).typedef union NamedTag { … } NamedU;mints exactly ONE node namedNamedU; the tag never becomes a node. - C++ union member method —
union_agg.cppgivesunion WithMethodandstruct SWithMethoda SAME-NAMEDread_field, plus one call on each receiver. Before the fix the union's member leaked to file scope as a freefunction, sow.read_field()boundSWithMethod::read_field— a FALSE edge to the wrong type, not merely a missing one. The golden pins each call on its own receiver's member, so receiver type has to decide. - ObjC union —
union_agg.mmis the repo's ONLY Objective-C fixture (.mmmaps to the ObjC spec). It pinsunion ObjcUbesidestruct ObjcS. Calls→Instantiatespromotion —instantiate_agg.cpphasReg r = Reg();where the union is the only node of that name (measured pre-fix: a DANGLINGcalls Regref, because no union node existed to bind), withCtl c = Ctl();as the control. The golden pinsinstantiates mk_union -> union:Regand NO survivingcallsedge.Instantiatescandidate ranking —instantiate_rank.cpphasValue v{1};competing against a same-namedvoid Value(). Measured pre-fix it boundfunction:Value, the WRONG target; the golden pins the union winning, withPacket p{2};as the control. Kept in a SEPARATE file from the promotion case on purpose: one file mixing both mechanisms would make either revert-proof ambiguous.- C designated-initializer macros —
designated_macro.ckeepsRESET_CONFIG(.field = VALUE)bounded to its argument list so the followingafter_reset/final_valuedeclarations remain independent functions. - C++ raw-string opacity —
raw_string.cppproves annotation/macro text inside a raw string is never blanked while a genuine annotation whose argument contains a raw string still balances on its real closer. - C++ pure virtual ownership —
pure_virtual.cpppins= 0declarations as abstract methods owned byAbstractStore, with overrides, prototypes, data fields, and function-pointer fields as controls. The same rule adds the four previously missing abstractPingmembers to the existingcom_interface.hppcontrols; no pre-existing node or edge is removed. - C++ constructor owner/arity proof —
constructor_resolution.cppemits aCallsedge only when one lexically owned constructor accepts the local initializer's positional arity. It pins default/braced/value positives plus aggregate-only, overload-ambiguous, namespace-ambiguous, and explicit-owner controls. Internal constructor probes are consumed after resolution and never remain as dangling call references.
Why the union/struct members are named read_field and not get. get is the
only duplicated symbol name in this corpus already (operators.cpp:5 Vec2::get
and template_method.cpp:12 Box::get), and one of those carries a live
2-candidate calls edge. Naming the new members get would take that candidate set
to four and put an existing, unrelated golden edge at risk of re-pointing — drift in
files this change has no business touching. read_field keeps the property that
matters (two same-named members inside the new file, so receiver type must
disambiguate) and drops the one that only adds risk.
The minimal source corpus lives at crates/codegraph-bench/fixtures/cpp/
(attr_macro.c, base.hpp, com_interface.hpp, derived.cpp,
instantiate_agg.cpp, instantiate_rank.cpp, namespaced.cpp,
namespaced_member.cpp, namespaced_member.hpp, neg_interface.hpp,
operators.cpp, constructor_resolution.cpp, plain_derived.h, plain_header_negatives.h,
pure_virtual.cpp, raw_string.cpp, designated_macro.c,
template_method.cpp, templated_call.cpp, ue_actor.h, union_agg.c,
union_agg.cpp, union_agg.mm — 23 files). The inheritance base
classes live in a .hpp file (not .h,
which maps to Language::C by extension); ue_actor.h deliberately uses .h to
guard the content-based C++ reclassification, and attr_macro.c uses .c so the
C walker (not the C++ one) is the thing under test.
Regenerate it with scripts/regen-goldens.sh --write cpp (the shared recipe in Regenerating goldens).
Like the Ruby fixture, both the index and the dump are byte-stable, and the
generated_golden_matches_committed_cpp_fixture and
cpp_db_is_self_equivalent_to_cpp_golden tests in
crates/codegraph-bench/tests/equivalence.rs enforce it.
reference/golden/rust/ is the repository's Rust corpus. It retains the
original unit-struct/union contracts and now also pins generic impl ownership
and self.field.method() resolution:
- unit structs are indexed (upstream #1513 / PR #1514) —
lib.rsdeclaresstruct UnitStruct;besideBraceStruct { … }andTupleStruct(u8), and gives all three animpl Greet for. A bodilessstruct NAME;is a COMPLETE definition in Rust, not a forward declaration, so before the fix the unit struct produced no node at all — and itsimplementsedge went with it, leavingUnitStruct::greetan orphan naming a type the graph did not contain. The golden pins exactly threestructnodes and fourimplementsedges (three structs + the union). unionis a first-class kind (upstream PR #1516) —union Bitscarries an inherentimpl(raw) and a traitimpl(greet). The golden pinsunion:Bitswithcontains union:Bits -> method:raw,contains union:Bits -> method:greetandimplements union:Bits -> trait:Greet.- cross-file resolution —
consumer.rscallsu.greet()/b.greet()on a&UnitStructand a&Bits, soimplementsand method resolution have to work across files rather than only within one. - implementing-type ownership —
impl_ownership.rscovers generic, lifetime, reference, qualified, inherent, and trait impls. Methods qualify under the implementing type, not the trait; only trait impls emitImplements. Tuple,dyn, raw-pointer, and primitive impls remain ordinary functions because there is no single safe owner. - validated self/field calls —
self_field.rspinsself.inner.run()/reference/Boxwrappers to the unique project field type.Option,Vec,Mutex, generic, external, and ambiguous field types stay unresolved. A directself.method()keeps the receiver and resolves only to the enclosing method owner's exact qualified member, including split inherent impls and trait impls. The corpus includes same-named decoys and a missing owner method; the latter stays inrefs.jsonand never falls through to a global same-name guess. Genuineself.run()recursion keeps its self-edge, now at qualified-name confidence rather than proximity confidence.
Rust instantiation. A struct expression is an instantiation, as upstream
records it: Bits { i: 0 } in make_bits instantiates the union Bits, and
Own { .. } in Own::from instantiates Own. The corpus carries
no Cargo.toml: it is indexed, not compiled, and a manifest inside the
workspace tree could confuse cargo.
The four source files are lib.rs, consumer.rs, impl_ownership.rs, and
self_field.rs.
Regenerate it with scripts/regen-goldens.sh --write rust (the shared recipe in Regenerating goldens).
generated_golden_matches_committed_rust_fixture and
rust_db_is_self_equivalent_to_rust_golden enforce byte-stability.
reference/golden/lua/ guards #1616 function expressions in assignments and
table fields. Its single handlers.lua source pins:
local localFn = function ... endas oneFunctionand no duplicateVariable;M.assignedFn = functionandM["bracketFn"] = functionas methods qualifiedM::assignedFn/M::bracketFn;- nested constructor fields as
M.callbacks::onStartandM.callbacks::onStop, while a computed key remains dynamic and emits no callable; - every body
helper()call attributed to its synthesized callable; localFn(),M.assignedFn(), andM:assignedFn()resolving to the intended target, with dot and colon calls sharingM::assignedFn.
Regenerate it with scripts/regen-goldens.sh --write lua (the shared recipe in Regenerating goldens).
generated_golden_matches_committed_lua_fixture and
lua_db_is_self_equivalent_to_lua_golden enforce byte-stability.
reference/golden/go/ byte-pins files.generated — the content-header
generated-file detection ported from upstream #1500 (16e1749) plus the
Wrangler double-by fix (57e0854). It is the repo's only Go corpus, and the
only corpus where files.generated is anything other than 0.
Go is the language that forces content detection to exist: its convention for a
generated file is a comment banner, not a filename suffix, so a machine-written
payroll.go sitting beside hand-written use-cases is invisible to the path-only
is_generated_file. Renaming is not an option — payroll.go is a legal, ordinary
Go filename.
Six files pin BOTH values of the flag, three each way:
| fixture | generated |
what it guards |
|---|---|---|
payroll.go |
1 | pattern 1, Go's codified Code generated … DO NOT EDIT. — the #1500 defect itself |
worker_types.go |
1 | pattern 6 / CG-25, Wrangler's TWO by clauses (Generated by Wrangler by running …) |
api.pb.go |
1 | the PATH signal still writes 1 with no banner present |
payroll_usecase.go |
0 | the must-not-demote side: hand-written code beside a generated sibling |
nightly.go |
0 | pattern 6 PRECISION — one by clause (generated by running the ETL job) is ordinary prose |
generator.go |
0 | the comment-line fence: the banner is a const string in the function BODY, so a generator's own source is not flagged |
payroll.go and payroll_usecase.go both define ComputePay, which is what makes
the ranking effect observable: without the content signal the generated definition
wins on name overlap alone.
Regenerate it with scripts/regen-goldens.sh --write go (the shared recipe in Regenerating goldens).
generated_golden_matches_committed_go_fixture and
go_db_is_self_equivalent_to_go_golden enforce byte-stability.
reference/golden/mini/ is the one corpus with no re-indexable provenance — it
is upstream-derived, and its files.modified_at is a JS Date.now()
(typeof(modified_at) = real, versus integer in every corpus we produce). So the
recipe above cannot regenerate it: re-indexing the fixture would replace upstream's
data with ours.
An in-place Store::open migration is also not sufficient when a schema
migration adds an index. CREATE INDEX APPENDS a sqlite_master row and never
reorders an existing one, and the golden schema.sql is a sqlite_master dump
compared as a strict string, so a migrated corpus accumulates its indexes in
migration order while a freshly-created one carries them in BASE_SCHEMA order.
That is why mini and godot used to carry idx_edges_identity LAST while the
other corpora carried it alphabetically.
For a schema migration, rebuild mini on the fresh schema and transplant its rows.
scripts/regen-goldens.sh --mini-transplant does exactly this: a fresh init over
crates/codegraph-bench/fixtures/mini/ creates a database from the current
BASE_SCHEMA, its freshly-extracted rows are replaced with mini's committed rows
(an ATTACH of reference/golden/mini/colby.db), the result is committed as
colby.db, and the JSON + schema goldens are re-derived from it:
scripts/regen-goldens.sh --mini-transplantTwo constraints in that transplant are load-bearing, and the script keeps both:
- The
filesinsert MUST use an explicit column list.SELECT *fails withtable files has N columns but 8 values were suppliedonce a column is added, and the omitted column correctly takes itsDEFAULT, which is exactly the migrated-but-not-re-extracted semantics the goldens should pin. - Do NOT transplant
schema_versions.codegraph_store::test_support:: finalize_current_test_fixturecallsStore::openon a COPY of this database from seven call sites. Copyingmini's rows would leaveMAX(version)behind the current schema on a database that already HAS the new column, sorun_pending_migrationswould fire theALTER TABLEagainst it and every one of those tests would die onduplicate column name: …. Keeping the fresh rows leaves nothing pending, soStore::openis a no-op on the copy.
Verify the rebuild preserved provenance and normalised the order:
sqlite3 reference/golden/mini/colby.db 'SELECT DISTINCT typeof(modified_at) FROM files;' # real
sqlite3 reference/golden/mini/colby.db 'SELECT COUNT(*) FROM nodes;' # 13
sqlite3 reference/golden/mini/colby.db 'SELECT COUNT(*) FROM edges;' # 21
diff reference/golden/mini/schema.sql reference/golden/cpp/schema.sql # byte-equalThat last diff is the real proof: the rebuilt mini schema is now the same text
as a freshly-created corpus's, so every corpus schema.sql shares one hash.
A change that moves how mini resolves (its edges or unresolved references)
regenerates those two tables from Rust output while its nodes, files and project
metadata stay upstream-derived (decision OD-4 of the v0.55 program).
scripts/regen-goldens.sh --mini-resolve does that: the same fresh init as the
transplant, then mini's committed nodes, files and project metadata replace the
fresh ones, and the fresh edges and unresolved_refs stay. It refuses to run
unless the fresh node ids equal the committed ones, so every regenerated edge
and reference points at a node the golden keeps. Each regenerated row is listed
here with its reason:
fix(resolve): an imports reference binds the file it names, never its own import statement:imports file:src/app.ts -> import:4ec14e7f…(qualified-name, 0.95: the file's own./mathstatement) is gone, andimports file:src/app.ts -> file:src/math.ts(import, 0.9) replaces it (upstream #2392 and #2263). The first Rust regeneration of mini's references also adds the unresolvedfunction_refthis.valueatsrc/math.ts:9:26fromCounter::increment: the argument inadd(this.value, step)is a function-as-value candidate, and athis.<member>candidate is always kept (upstreamflushFnRefCandidates). It binds nothing, sincevalueis a field, and mini's committed rows came from an upstream build that did not record it.
The final OD-4 parity run (2026-10-20) built upstream b635dd46 with Node
22.23.2 (npm ci --ignore-scripts, tsc, npm run copy-assets), ran its
codegraph init -y on a copy of crates/codegraph-bench/fixtures/mini/ with
telemetry and the update check off, and compared that database with this golden
through bench --graph-diff. The compared copy first gained an empty
reference_subkind column, which the canonical form reads and upstream does not
have. Nodes, files and unresolved references are identical, so both rows above
match upstream, which records this.value too. Every edge has the same
endpoints, kind, position, confidence and resolvedBy. The differences that
remain are KEEP-RUST:
- Upstream stamps a resolved edge with the reference that produced it:
refName, plusrefKindwhen the reference kind is not the edge kind (upstream #1240,resurrect-ref.ts). Of mini's 21 edges the 11 that are notcontainsedges carry it, and Python'sGreeter()recordsrefKind: "calls"on itsinstantiatesedge. Rust writes no such stamp: it keeps the reference as aresolved_refsrow (schema 10), which a sync reads to resolve it again. unresolved_refsholds only parked references here, so it has nostatusorname_tailcolumn and none of upstream'sidx_unresolved_statusandidx_unresolved_failed_*partial indexes (upstream #1240, #2392, #2450): a sync looks up the parked references a change can satisfy by their names. It hasreference_subkind(migration 6) instead, and theresolved_refstable with its two indexes (migration 10) has no upstream counterpart.- There is no
name_segment_vocabtable (upstream #1136): explore derives identifier segments at query time, recorded as a design substitution indocs/upstream-sync/UPSTREAM.md. idx_nodes_kindindexeskindalone. Upstream's migration 10 (#1988) widens it to(kind, file_path, start_line, id)for its synthesis refresh. Resolution here orders candidates itself (upstream CG-33), and the synthesis stage belongs to the follow-up pull request (W7-14 to W7-18).
A fifth golden fixture, reference/golden/metal/, guards Metal Shading Language
support (upstream #1121 / cc89146). MSL ≈ C++14 and rides the existing
tree-sitter-cpp grammar — .metal maps to Language::Cpp with no new
Language variant. It guards the .metal-gated [[attribute]] blank: MSL's
post-declarator attributes (float4 position [[position]];) otherwise misparse a
struct field into a spurious extends edge from the struct to the field's own
type. The corpus (crates/codegraph-bench/fixtures/metal/shader.metal) defines
float4/float2 structs, a VertexIn struct whose fields carry
[[position]]/[[user(locn0)]] attributes on those self-defined types, and a
vertex_main function that calls a tint helper. The golden must show:
shader.metalwith"language": "cpp";VertexIn/float4/float2as ordinary structs with noExtendsedge (the attribute blank prevents the spuriousVertexIn extends float4);- the intra-shader
vertex_main→tintCallsedge.
The [[attribute]] blank fires ONLY for .metal files; a .cpp/.hpp with a
regular [[nodiscard]] attribute is byte-identical through pre-parse (proven by
the metal_attribute_blanked_only_for_dot_metal unit test in lang/cpp.rs).
A sixth golden fixture, reference/golden/cuda/, guards CUDA support (the
CUDA-language parts of upstream #1172 / e1a8d88). CUDA ≈ C++ + dialect tokens
and likewise rides tree-sitter-cpp — .cu/.cuh map to Language::Cpp with
no new Language variant. It guards the CUDA pre-parse blank (execution-space
specifiers + <<<grid, block>>> launch configs, offset-preserving and
brace-balance-checked) and macro-defined-kernel name recovery. The corpus
(crates/codegraph-bench/fixtures/cuda/kernel.cu) defines a __global__ void add_kernel, a templated __global__ scale_kernel, a
DEFINE_FLASH_FORWARD_KERNEL(my_kernel, …) macro-defined kernel, and a launch
host function with a plain launch and a templated launch. The golden must show:
kernel.cuwith"language": "cpp";add_kernel,scale_kernel,my_kernel,launchas functions — the macro kernel under its real namemy_kernel, NOTDEFINE_FLASH_FORWARD_KERNEL;- host→kernel
Callsedgeslaunch→add_kernelandlaunch→scale_kernel(the<<<…>>>blank restores the call; the templated launch rides the already-landed template-argument strip).
The CUDA blank fires for .cu/.cuh files OR any C/C++-family file whose content
carries a strong CUDA marker (__global__/__device__/__constant__/
cudaStream_t), so CUDA living in .h/.hpp headers is recognized.
Regenerate both fixtures with scripts/regen-goldens.sh --write metal cuda (the shared recipe in Regenerating goldens).
The generated_golden_matches_committed_{metal,cuda}_fixture and
{metal,cuda}_db_is_self_equivalent_to_{metal,cuda}_golden tests in
crates/codegraph-bench/tests/equivalence.rs enforce byte-stability.
A seventh golden fixture, reference/golden/arkts/, guards ArkTS (HarmonyOS /
OpenHarmony .ets) extraction (the extraction slice of upstream #1186 /
9915221). Unlike Metal/CUDA, ArkTS is a new Language::ArkTs variant backed
by a dedicated tree-sitter-arkts grammar — a TypeScript-superset fork that
understands the ArkUI @Component struct syntax tree-sitter-typescript cannot
parse. .ets maps to Language::ArkTs; plain .ts stays TypeScript. The corpus
(crates/codegraph-bench/fixtures/arkts/component.ets) has an import, a global
function helper, a function driver that calls helper, a @Component struct MyView with a build() method, and a plain class Model. The golden must show:
component.etswith"language": "arkts";MyViewas aNodeKind::Structwith itsbuildmethod as a member (via the existingextract_structpath — no walker change);helper/driverfunctions, theModelclass, and the../fooimport node, whoseimportsreference stays unresolved: no file answers../foo, and an import never binds its own statement (upstream #2392);- the
driver→helperCallsedge (plaincall_expression); - the
MyView::build→helperCallsedge from line 12.tree-sitter-arkts0.3 parses the call inside the@Component struct'sbuild()method; 0.2 did not, so this edge first appeared when #280 moved the grammar from 0.2 to 0.3 and the fixture was regenerated with the recipe below. Onlyedges.jsonandcolby.dbchanged.
The ArkUI dynamic-dispatch / callback-synthesizer bridges are DEFERRED — the
port has no callback synthesizer. So ARKTS_SPEC uses call_types = ["call_expression"] only (no arkui_component_expression component-instantiation
edges) and does NOT override extract_modifiers (the decorator hook). Adding the
variant is byte-neutral for colby.schema.sql (language is a stored TEXT value,
not DDL) and for the six existing goldens (none holds a .ets file).
Regenerate it with scripts/regen-goldens.sh --write arkts (the shared recipe in Regenerating goldens).
The generated_golden_matches_committed_arkts_fixture and
arkts_db_is_self_equivalent_to_arkts_golden tests in
crates/codegraph-bench/tests/equivalence.rs enforce byte-stability.
An eighth golden fixture, reference/golden/solidity/, guards Solidity (.sol)
extraction (upstream #1170 / 1441933). Solidity is a new Language::Solidity
variant backed by a dedicated tree-sitter-solidity grammar. .sol maps to
Language::Solidity. The corpus (crates/codegraph-bench/fixtures/solidity/) has
an IERC20.sol interface and a Token.sol that imports it, declares a file-level
error and a file-level constant, and a contract Token is IERC20 carrying a
state variable, an event, an enum, a struct, a modifier, a constructor,
fallback/receive, and a transfer function guarded by the modifier that
emits the event, plus a library Math. What it guards:
- both
.solfiles with"language": "solidity"; contract Token/library MathasNodeKind::Class,interface IERC20asNodeKind::Interface,struct HolderasNodeKind::Struct,enum StatusasNodeKind::Enumwith itsActive/Closedmembers (bare-textenum_value);- functions/modifiers/methods, including the synthetic
constructor/fallback/receivemethod names (nameless grammar nodes); - state variable / struct member /
event/errorasNodeKind::Fieldname nodes (direct-namefield, novariable_declarator), including the file-levelUnauthorizederror and the file-levelMAX_SUPPLYconstant; - the
./IERC20.solimport node +importsedge; is-inheritance emitted as anExtendsref, promoted by the EXISTING resolver to anImplementsedgeToken → IERC20(interface target, present in-corpus);emit/headermodifier_invocationCallsedges (transfer → Transfer,transfer → onlyOwner), resolved to same-file targets.
Because the fixture is fully self-contained, every ref resolves in-corpus, so
refs.json is empty and edges.json holds only RESOLVED edges — the expected
post-resolution state. No FrameworkResolver impl is involved; the
Extends → Implements promotion is the same path Java/C# use
(resolver.rs:1231-1247). Adding the variant is byte-neutral for
colby.schema.sql (language is a stored TEXT value, not DDL) and for the seven
existing goldens (none holds a .sol file).
Regenerate it with scripts/regen-goldens.sh --write solidity (the shared recipe in Regenerating goldens).
The generated_golden_matches_committed_solidity_fixture and
solidity_db_is_self_equivalent_to_solidity_golden tests in
crates/codegraph-bench/tests/equivalence.rs enforce byte-stability.
A ninth golden fixture, reference/golden/nix/, guards Nix (.nix) extraction
(upstream #1190 / 7f32513, the extraction slice only). Nix is a new
Language::Nix variant backed by a dedicated tree-sitter-nix grammar.
.nix maps to Language::Nix. Because Nix is an expression language with no
C-family class/struct/method/enum node kinds, NIX_SPEC has all-empty
type-sets and the extraction is driven by the Language::Nix-guarded
visit_nix_node walker extension. The corpus
(crates/codegraph-bench/fixtures/nix/) has a top-level lambda
{ pkgs, lib }: …, a let … in, a returned attrset with bindings, an
import ./foo.nix, a pkgs.callPackage ./bar.nix { }, an inherit lib;, an
imports = [ ./foo.nix ./bar.nix ] module list, and a curried build = { src }: … lambda. What it guards:
- all three
.nixfiles with"language": "nix"; - a
bindingwhose value is a lambda →NodeKind::Functionwith a formatted curried-param signature (build→{ src },double→(x)); - a non-lambda
bindingand eachinherited name →NodeKind::Variable; import ./foo.nix,callPackage ./bar.nix { }, and the literalimports-list paths →NodeKind::Importnodes +Importsrefs;- an
apply_expressioncall →Callsref, deduped across curried levels (pkgs.mkDerivation,pkgs.callPackage,stdenv.mkDerivation).
The imports/callPackage path refs to ./foo.nix / ./bar.nix resolve
in-corpus (both files exist), so refs.json retains only the three unresolved
Calls refs — the module-system option-path synthesizer, lexical-scope
resolution gates, callback synthesizer, and import-resolver module-list wiring
that upstream bundles with the same commit are DEFERRED, so no new Nix
resolve code binds anything. Adding the variant is byte-neutral for
colby.schema.sql (language is a stored TEXT value, not DDL) and for the eight
existing goldens (none holds a .nix file).
Regenerate it with scripts/regen-goldens.sh --write nix (the shared recipe in Regenerating goldens).
The generated_golden_matches_committed_nix_fixture and
nix_db_is_self_equivalent_to_nix_golden tests in
crates/codegraph-bench/tests/equivalence.rs enforce byte-stability.
A tenth golden fixture, reference/golden/terraform/, guards Terraform/OpenTofu
(HCL) extraction (upstream #1173 / 6c24f4b, the extraction slice only).
Terraform is a new Language::Terraform variant backed by a dedicated
tree-sitter-hcl grammar (.tf/.tfvars/.tofu → Language::Terraform).
HCL is intentionally generic — every top-level construct is a block
distinguished only by its first identifier child — so TERRAFORM_SPEC has
all-empty type-sets and extraction is driven by the Language::Terraform-guarded
visit_terraform_node walker extension. The corpus
(crates/codegraph-bench/fixtures/terraform/main.tf) is a single deterministic
file with a terraform {} settings block, a provider "aws", a
variable "region", a locals block, a data "aws_ami" "ubuntu", a
resource "aws_s3_bucket" "b", a module "vpc", and two output blocks. What
it guards:
- the
.tffile with"language": "terraform"; - block-type dispatch:
resource/data→NodeKind::Class(qualifiedT.N/data.T.N),module→NodeKind::Module(module.M),variable/output→NodeKind::Variable(var.V/output.O,is_exported),provider→NodeKind::Namespace(provider.P),localsattributes →NodeKind::Constantper attribute (local.k); - plain attribute-expression traversal refs
(
var.X/local.X/module.M/data.T.N/<type>.<name>) →References, with built-in heads (each/count/self/path/terraform) skipped.
The plain traversal refs with a unique same-file target resolve via the existing
generic qualified-name matcher: var.region ×3 → variable "region",
aws_s3_bucket.b → the resource, module.vpc → the module (each an EDGE, absent
from refs.json). The undeclared aws_kms_key.logs stays the sole unresolved
refs.json row. The module-boundary TerraformResolver, emitModuleWiring's
:-scoped refs (module.M:file/:var.X/:output.X), the .tfvars
top-level-assignment var.X ref, and the module.M:output.<out> scoped half of
qualifyReference are all DEFERRED — the port keeps its single
GodotResolver — so no :-scoped ref is emitted. Adding the variant is
byte-neutral for colby.schema.sql (language is a stored TEXT value, not DDL)
and for the nine existing goldens (none holds a .tf/.tfvars/.tofu file).
Regenerate it with scripts/regen-goldens.sh --write terraform (the shared recipe in Regenerating goldens).
The generated_golden_matches_committed_terraform_fixture and
terraform_db_is_self_equivalent_to_terraform_golden tests in
crates/codegraph-bench/tests/equivalence.rs enforce byte-stability.
reference/golden/erlang/ retains the original Erlang extraction contract
(upstream #1165 / 6511722) and now guards arity-qualified identity and
resolution (upstream 41c1075). Erlang is form-based — a function's name lives
on its function_clause, the grammar emits one fun_decl per clause, and
-spec/type bodies also contain call nodes — so extraction remains in the
Language::Erlang-guarded walker extension rather than the C-family type sets.
The seven-file corpus under crates/codegraph-bench/fixtures/erlang/ pins:
- display names remain bare (
get,job,header), while qualified names aremodule::function/arity, such asstore::get/1,store::get/2,multi::job/1, andmulti::job/2; - adjacent clauses merge only when both name and arity match (
m::f/1remains one function), while same-name/different-arity definitions remain distinct; - exports and specs select the exact arity;
- local and remote calls carry arity, so
store:get/2resolves while the deliberatestore:get/3call stays unresolved instead of binding a sibling; fun store:get/1resolves as a function-valueReferencesedge;- the statically safe
erlang:spawn(single, work, Args)MFA form resolvessingle::work/1;multi::jobstays unresolved when the runtime argument list does not prove whether/1or/2is intended; - commas inside
<<Name, Value>>do not inflatebinary_arg/1or its call arity; - the original module, record, macro, include, recursive-call, and
type-position negatives in
m.erlremain intact.
The unresolved set intentionally includes store::get/3, external library
calls such as lists::map/2 and maps::get/3, other::h/0, the runtime
erlang::spawn/3 calls themselves, and ambiguous multi::job. Variable-module
dispatch, dynamic MFA targets, behaviour callback contracts, and
.app/.app.src resource wiring remain outside this corpus. The CLI and MCP
node lookup normalize Erlang's source spelling mod:fn/3 to the stored
mod::fn/3 form.
Regenerate it with scripts/regen-goldens.sh --write erlang (the shared recipe in Regenerating goldens).
The generated_golden_matches_committed_erlang_fixture and
erlang_db_is_self_equivalent_to_erlang_golden tests in
crates/codegraph-bench/tests/equivalence.rs enforce byte-stability.
A twelfth golden fixture, reference/golden/cfml/, guards CFML / ColdFusion
extraction. CFML is a Language::Cfml variant backed by the
dual-grammar tree-sitter-cfml crate (.cfc/.cfm/.cfs →
Language::Cfml). A file's dialect is picked by a first-token sniff
(is_bare_script_cfml): script files parse with the bundled cfscript grammar
and drive the generic type-set dispatch; tag files parse with the cfml tag
grammar and are walked by the Language::Cfml-guarded tag walker
(walk_cfml_program). The corpus (crates/codegraph-bench/fixtures/cfml/) has
three deterministic files — a script Base.cfc, a tag Widget.cfm, and a
bare-script Gadget.cfs. What it guards:
- all three files with
"language": "cfml"; Base.cfc(script) →NodeKind::ClassBase(named from the FILE — the cfscriptcomponentis unnamed) +NodeKind::MethodBase::ping;Widget.cfm(tag) →NodeKind::ClassWidget(from thenametag-attr, spanning to its close tag on line 5) +NodeKind::MethodWidget::doThing(accesspublic, returntypevoid), and a tagextends="Base"→Extends;Gadget.cfs(bare script) →NodeKind::ClassGadget(from the FILE) +NodeKind::Propertyx+NodeKind::MethodGadget::doThing, and a script-styleextends="Base"(component_attribute) →Extends, recorded at the attribute value;- both
extends Baserefs RESOLVE to theBase.cfccomponent (edges);Gadget.doThing'shelper()call → an unresolvedhelperref.
The corpus holds none of the other CFML shapes languages.md
describes: <cfinterface>, tag expressions, the <cfscript> and <cfquery>
bodies read in place with their own grammars, scope-prefixed and typed
receivers (upstream #1155), component-path inheritance (upstream #1152 / #1154:
FW/1's relative ../base, ColdBox's dotted coldbox.system.web.Controller),
the extends-chain scope of a bare call and the untyped-receiver gate. Those
are guarded by the CFML probes, the walker's unit tests and the
sync-convergence matrix, not by this golden. A <cfscript> or <cfquery> body
nested past the walker's depth limit fails its whole file with the depth error,
as a deep tag tree does (crates/codegraph-extract/tests/deep_nesting.rs).
Adding the variant was byte-neutral for colby.schema.sql (language is a
stored TEXT value, not DDL) and for the eleven goldens before it (none holds a
.cfc/.cfm/.cfs file).
Regenerate it with scripts/regen-goldens.sh --write cfml (the shared recipe in Regenerating goldens).
The generated_golden_matches_committed_cfml_fixture and
cfml_db_is_self_equivalent_to_cfml_golden tests in
crates/codegraph-bench/tests/equivalence.rs enforce byte-stability.
The dedicated reference/golden/typescript/ fixture guards TypeScript export
aliases, JavaScript-family import resolution, object-literal namespaces, and
inherited path aliases without changing the shared mini corpus. It has 16
indexed source files plus tsconfig.json and
config/tsconfig.base.json; source lines are contractual because node IDs
include the declaration line.
The original runAll function retains six positive Calls edges, in source
order:
viaConstAlias()→ the localconstTargetfunction behindexport const constAlias = constTarget;viaNamedAlias()→ the localnamedTargetfunction behindexport { namedTarget as namedAlias };defaultExportAlias()→ the localdefaultTargetfunction behindexport default defaultTarget;viaJsSpecifier()from./js_target.js→jsTargetinjs_target.ts;viaCollision()from./collision.js→collisionTargetincollision.ts, ahead of the realcollision.jsfile;viaExtensionless()→extensionlessTargetinextensionless.ts, preserving the existing extensionless behavior.
Two negative rules make false positives visible: viaMissing remains the only
unresolved direct-import call and import pair in refs.json, with no Calls
edge; and no
Calls edge may target either the exported Constant constAlias or the
JavaScript collisionTarget in collision.js.
Each module specifier is a file-level imports edge to the file it names,
found the way the call resolution finds it: ./collision.js reaches
collision.ts, as viaCollision() does. ./missing.js names no file, so its
imports reference is unresolved too; an import never binds its own statement
(upstream #2392 and #2263).
The expanded corpus also pins:
- local
api.run()/api.stop()and importedapi.run()/api.stop()to direct callable members of one exported object literal; - source-range containment prevents the unrelated top-level
runfunction from becoming the target, while nestedapi.nested.run()remains unresolved; since extraction version 14 it is retained as the qualified call siteapi.nested.run(upstream #1862), which only a framework resolver may bind; - the nested literal's
run()is no member ofapi, but the value holding it runs where the literal is written, so itshelper()call is a call from the constantapi(upstream #2363, #693), beside the callsapi::runandapi::stopmake themselves; tsconfig.jsonextends the JSONC/trailing-commaconfig/tsconfig.base.json; the declaring config'sbaseUrlresolves@fixture/aliasedtosrc/aliased.ts;- extensionless imports consider
.xsjsand.xsjslibafter ordinary.js/.jsx/.mjs/.cjscandidates and before index candidates, solegacy_helpers.xsjslibresolves whilelegacy_priority.jsstill wins over the same-name.xsjsfile. callable_semantics.tspins extraction version 13: generator declarations, TypeScript interface method/property signatures, direct declarator-bound handlers, ReactuseCallback/useEffectEventhandlers, and the supported curried-wrapper shape own their body calls.useMemoand array callbacks stay anonymous negatives. Its helper name is globally unique so adding the fixture cannot change confidence or target selection in the original 14 files.commonjs_handlers.jspinsexports.name = functionandmodule.exports.name = functionas exported callables with body ownership; non-callable assignments and a laterhandlers.onSave = …remain ordinary values rather than fabricated CommonJS exports.
The v13 regeneration was accepted only after filtering the two new paths out of all four canonical JSON artifacts and proving the original 14-file rows byte-for-byte unchanged. This is the required review shape for future corpus growth: an additive fixture must not silently perturb old resolution confidence.
Regenerate it with scripts/regen-goldens.sh --write typescript (the shared recipe in Regenerating goldens).
The generated_golden_matches_committed_typescript_fixture and
typescript_db_is_self_equivalent_to_typescript_golden tests in
crates/codegraph-bench/tests/equivalence.rs enforce artifact/database
self-equivalence. As for every fixture below the Godot caveats, do not compare
colby.db bytes; compare the four JSON artifacts byte-for-byte and compare
schema.sql as a normalized statement set when statement order differs.
These nine corpora pin languages and shapes the older corpora never exercised. Each was captured with the extraction and resolution as they stood when the corpus was added, deliberately including what that code got wrong, so every later correction shows up as a diff attributable to one change:
csharp: abase_listwith a class base and an interface, target-typednew(), property accessor bodies and expression-bodied members, field initializers, nested and sibling block namespaces,using static, a partial class split across declarations, and an overload that delegates to its sibling. It declares no extension method, so the rule that a call on an outside type reaches the project's one in-scope extension method for it (W3-05c; KEEP-RUST, OD-8: upstream resolves no extension method) moves nothing here; the probepr2169_a_csharp_call_on_an_outside_type_reaches_the_projects_extension_methodand the sync casecsharp_extension_method_call_body_editpin it. What it cannot tell apart: two outside types with one simple name from different namespaces (System.Threading.TimerandSystem.Timers.Timer), imported by different files, as every typed resolution by simple name. An extension call's overloads compete only when theirthistypes andwhereclauses agree token by token (spacing and comments aside, the method's own type parameters numbered by first use); two spellings of one type (intandSystem.Int32, an alias) keep the target the name match chose, as does an overload choice that needs argument types.java:extends/implements, object creation and an anonymous class, enum constants with their own bodies, a static import, two same-namedFieldclasses in different packages, test-only imports of Mockito helpers, and thepick(int)/pick(String)overload pair.swift: a protocol and its conformance, an extension of a struct, nested types reached through a type path (API.DependencyController.GetRoute.query()) beside a same-named path elsewhere, an overload family distinguished by argument labels, and a#Previewblock.objc:@interface Sub : Base <Doer>, message sends toself,superand a class, and a plain C call inside a method.php: namespaces withuseimports,extends/implements,$this/self/static/parentcalls, calls written after=>in an array literal, a case-variant call to a global function, and a namespace-qualified class name (Field\FirstName::make()). It declares no class property, so W2-03d's propertyfieldnodes (as upstream'sextractField) and their type references (KEEP-RUST, OD-8: upstream reads no property type) move nothing here;php_class_properties_are_field_nodes,php_property_types_are_referencesand the sync casephp_property_type_of_an_added_classpin them.vue: a<script setup>component calling an imported composable and a local function from the template, and an Options API component withdata,computed,methodsand a lifecycle hook.svelte: a component with a module script and an instance script whose top-level statement calls an imported function.python_bases: a class with two bases and a metaclass keyword, a classmethod reached through a subclass,super()calls, a pytest fixture feeding a test parameter, and a package that re-exports a class through*, whichfrom pkg.models import Userandmodels.User()follow to its definition (upstream #2247, #2248).commonjs:require('../'),module.exports = X,exports.default, a destructured{ default: X }require,require().member, avarlist declaring two requires, and a test that requires an out-of-repo package.
Regenerate any of them with scripts/regen-goldens.sh --write <corpus> (the
shared recipe in Regenerating goldens); golden_reextract
and the golden_corpora! pairs in equivalence.rs pin them like every other
corpus.
Four multi-project corpora give the framework resolvers and the synthesis passes
a baseline before they land. Each project directory carries the manifest that
framework detection reads (package.json, pom.xml, pubspec.yaml,
composer.json, Gemfile, *.csproj, Package.swift, Cargo.toml, go.mod,
build.sbt),
and the projects deliberately share symbol names, so name matching that crosses
project boundaries is visible in the golden:
routers: React Router, Next.js (app/andpages/), Expo Router (including a+apifile), Vue Router, Nuxt, SvelteKit (a(group)folder and a parameter matcher), Angular and TanStack Router apps side by side, each with links and programmatic navigation.servers: Express (a mounted router, inline and function-expression handlers, middleware), NestJS (global prefix and URI versioning), Flask (routeandadd_url_rule), Django, FastAPI (router prefix), Spring, Laravel (array and string controllers, a prefix group), Rails (resourceswithonly/except, a namespace), ASP.NET (a controller and a minimal API), Vapor, axum, actix-web, Gin, Play and a Drupalrouting.yml.mobile: React Native native modules (Java and Objective-C halves, an event name held in a constant, arequireNativeComponentview), an Expo module (Swift and Kotlin definitions called throughrequireNativeModule), a Swift class called from Objective-C, and a Flutter app whosepubspec.yamlnames the Flutter SDK.synthesis: an event emitter and a handler table, a React component that re-renders a child, Redux Toolkit and Pinia stores, Go interface embedding with implicit implementations, a type alias and a defined type, a C function-pointer table, Kotlinexpect/actual, a MyBatis mapper, a Celery task, and Spring application events.
At capture time the golden still contains cross-project misses (for example an
Angular @Component decorator bound to a React file's Component); the commits
that scope resolution to its own project remove them, and the diff records it.
Regenerate any of them with scripts/regen-goldens.sh --write <corpus>.
Explicit-path pinning in codegraph_explore, camel/segment explore seeding,
case-insensitive exact-name index seeks, and [indexing].deprioritize are
query/render-time behavior only. They do not modify extraction, resolution,
node IDs, files, nodes, edges, unresolved references, FTS schema, or any
committed golden artifact. That earlier wave required no extraction-version
bump of its own; the current version is 13 because of the later callable
extraction changes above. A golden change attributed only to retrieval or
ranking remains a regression, not expected output.
Planner tests lock exact-name hit, miss, and filtered probes to
idx_nodes_lower_name; search/MCP tests cover Variable/Constant seeds,
last-match-wins ranking rules, explicit-path bounds, pinned-file budgeting, and
same-process config reload. The normal equivalence suite remains the authority
that every extraction golden is byte-stable.
Tier-3 differences are allowlisted by grep-able lines in
docs/upstream-sync/KNOWN_DIFFS.md — the single path
KnownDiffs::repo_doc_path hardcodes
(crates/codegraph-bench/src/oracle/diff.rs):
RULE tier=3 surface=<surface> key=<substring-or-*> justification=<short-token>
Only Tier-3 entries can be allowed. Tier-1 byte mismatches and Tier-2 multiset
mismatches always fail; the differ never weakens those tiers to pass —
KnownDiffs::allows returns false for anything that is not Tier::Tier3, and
parse_rule rejects tier=1 / tier=2 before that, so a Tier-1/Tier-2 rule
cannot even be written down.
The parser is fail-closed: an unparsable document fails every equivalence
assertion instead of being ignored. A RULE line is rejected when a token is
not key=value, a key or value is empty, a field name is outside
tier/surface/key/justification, a field is repeated, tier is anything
other than Tier-3, surface is outside the five surfaces the differ reports
(nodes, files, schema, edges, unresolved_refs), or any of the four
fields is missing. Lines inside a fenced code block are documentation, not
rules — including the template above — and an unterminated fence is an error,
because every RULE after it would otherwise be skipped silently.