The repo-local Temporal env-helper allow-list matched user-supplied
names with a raw, case-sensitive map lookup, while the built-in names
went through strings.EqualFold. A capitalisation mismatch between
.gortex/temporal-allowlist.yaml and the call site failed silently: the
dispatch fell back to the hidden "heuristic" tier when the callee
carried an "env" marker, and lost its env attribution entirely when it
did not. Both doc comments already described the intended behaviour —
case-insensitive matching over lower-cased keys — so only the code had
drifted.
Lower-case the keys where the map is built and lower-case the callee at
lookup, restoring parity with the built-in list. Cover the whole opt-in
chain (env gate, allow-list file, indexer options, Go extractor) with a
case-mismatch regression test, and document the feature: the gate and
the file shape were previously undocumented outside source comments.
Closes#524
Review coverage sweep over call shapes found a real gap: the
method_type_params stamp lived inside the extension-only branch, so an
explicit Pack<int>(5) could not split an ordinary generic/non-generic
overload pair — the type-arg filter saw two non-generic candidates and
declined, leaving declaration order to win. Hoist the stamp out of the
extension block; rides the same csharp@12 salt.
Also pins named arguments and out-var arguments as counted call shapes
(both already handled by csharpCallArgCount — argument nodes either
way), each targeting the second-declared overload so the pins fail
under declaration order.
Arity narrowing existed only inside the extension binder; every other C#
overload set bound to whichever declaration came first — at 0.95
ast_resolved when the receiver was typed. Both halves of the evidence
were already in the graph (edge arg_count, candidate param_count /
param_required / param_variadic); nothing consulted them.
- csharpNarrowMethodByApplicability / csharpMethodAcceptsArgCount: the
extension window's [required, declared] range with ordinary-method
semantics — no this-slot discount, and param_count == 0 is a real
arity, not missing evidence. Extension candidates are exempt (their
own binder adjudicates them). A filter that would empty a set keeps
it: narrowing can turn a refusal into a bind, never a bind into a
refusal.
- resolveMethodCall narrows rawCandidates + candidates up front, so the
exact-type passes and every fallback below see only invocable
overloads. Pass 2's uniqueness guard now also converges when arity
makes an overload set unique across directories.
- resolveFunctionCall narrows after the extension routing, covering the
same-file pick and the locality cascade.
- The extractor stamps arg_count / type_arg_count on receiverless calls
too (the scope rules that bound them never consulted arity — the
exact premise #559 retires). csharp salt 11 -> 12.
Tests drive zzet's measured repros through the real extractor +
resolver: both declaration orders for the typed receiver, the
receiverless same-file set, zero-arity, optional-parameter and
params-tail windows.
These declarations are their own grammar rules and matched no query, so the
members were absent and call edges made inside their bodies were dropped for
lack of an enclosing function. They now emit with the cross-language
constructor spelling, work identically inside extensions, and recover the
call attribution. A protocol init requirement stays unemitted.
A field declaration inside a class body was dropped entirely, enum bodies
emitted only the type, and setters were named 'set foo' so a search for the
property name could never match. Fields now emit one node per bound name
with the member conventions the other languages use, enum constants follow
the shared Enum.constant identity, and accessors carry the bare property
name with an accessor stamp for disambiguation.
* origin/main: (142 commits)
Exempt Python dunders from dead-code analysis
store_sqlite: normalize native separators in generated dir columns
store_sqlite: space WAL auto-checkpoints at 8k pages for index-write bursts
store_sqlite: three-phase seeded reindex benchmark
Update internal/semantic/tstypes/fact_spool.go
ci(deps): bump github/codeql-action/upload-sarif
store_sqlite: guard the json transport against unmarshalable payloads
resolver: scope page_load to the pass clock
resolver: split commit_apply interior timings
store_sqlite: bind resolved-conversion updates as one json_each relation
resolver: attribute pass interior time and churn shapes
tstypes: true up spool capacity hints and page-cap/deadline comments
resolver: note razor marker removals; make retention counters mean what they log
fix(indexer): use incremental contract cleanup on untrack
fix(mcp): cache index health scans
fix(agents): register the OpenCode MCP server at user scope
feat(agents): install the Gortex sub-agents for Codex CLI
style: gofmt the init wizard test
test(agents): regenerate the render goldens for the widened fence
fix(uninstall): remove Claude Code's per-community skills too
...
# Conflicts:
# docs/agents.md
# internal/mcp/overlay_view.go
NodeText, WrapNode and WrapTree had no callers left: extractors slice
src[start:end] directly and the shim Node/Tree values are constructed
inside tsitter itself. The per-language complexity wrappers and the
Temporal env-helper configurator were likewise unreachable, so the
stranded EnvHelperConfigurable interface goes with them.
A definition written outside its declaring scope — `void ns::Cls::run() {}`
in the .cpp for a header's class — declares itself with a
qualified_identifier, a shape no function_definition pattern admitted. The
definition emitted no node at all, and every call in its body went with it:
a deferred call needs an enclosing function range to attach to. In a
translation unit whose class lives in a header, that is the whole file.
Admit a qualified declarator in each function_definition pattern and read
the qualifier structurally. The trailing segment decides the shape: a type
owner yields a method keyed on the owner (member_of the class when this
file declares it, so `A::run` and `B::run` no longer collide), and an
all-namespace qualifier yields a free function carrying the qualifier as
scope_ns. Destructors, operators, pointer and reference returns, and
out-of-line template members reach the same walk.
Folds cppQualifiedCallName and lastIdentifier into that one walk — a
qualified callee and a qualified declarator are the same shape. lastIdentifier
scanned direct children only, so once a name nested it returned the first
segment rather than the last (`ns::Cls::bar` gave `ns`).
Both defects shipped in the generic-call work and both turn a previously
missing edge into a confidently wrong one, which is the worse failure.
C#: a static-form extension call passes the receiver as its first argument,
so the arity window must not discount the `this` slot. That distinction
rides on the receiver's spelling, which the extractor stamped only after
the local / parameter / builtin lookups missed — and a namespace-qualified
receiver takes the chain-walker branch before reaching it, so it arrived
carrying no spelling at all. The binder read `Lib.BagExt.Add(bag)` as
extension form, subtracted a slot the argument list had already filled, and
bound the two-parameter overload; `BagExt.Add(bag)`, the same call written
unqualified, bound the correct one. A qualified receiver the chain walker
could not type now carries the same evidence, and the comparison is against
the trailing segment so both spellings agree.
Go: the generic-call patterns match `Zero[int]()`, but Go spells indexing a
func-valued map or struct field identically. The safety argument for that
was that a package-scope name is a func, a var or a type and never two —
which is true, and covers neither a func-valued STRUCT FIELD (it lives in
the type's namespace, free to share a name with another type's method) nor
a LOCAL shadowing a package function. Both were bound: `t.handlers[k]()`
landed on an unrelated `Other.handlers`, and a shadowed `Handler[k]()`
landed on the package's `Handler` at the ast_resolved tier.
The fix is the property the impostors cannot fake. A real instantiation can
only target a declaration that DECLARES type parameters, so the extractor
marks the call shapes it could not disambiguate and the resolver binds them
only to a generic declaration. That runs at the shared candidate lookup, so
every name tier inherits it, and it narrows only — an unmarked edge is
untouched and an empty result leaves the call unresolved, which is the right
answer for an index or a conversion.
Both extractor versions are bumped: existing indexes carry the wrong edges
and only a re-extraction removes them.
Two losses in the same dispatch, the second far larger than the first.
Adding type arguments reroutes ANY Swift call away from `call_expression`:
`lowerGeneric<Int>()`, `obj.doThing<Int>()` and `MyType<Int>(x: 1)` all parse
as a `constructor_expression` whose constructed type swallows the whole
dotted path. Nothing in the query matched one, so every generic call — free
function, member, or initializer — emitted nothing. The constructed type's
own `type_identifier` children give the path back; the ones nested in a
`type_arguments` list are the arguments and are excluded.
The member branch was then gated to factory chains only, on the stated belief
that the bare-identifier pattern stayed "authoritative for ordinary
obj.method()". It never was: that pattern needs a `simple_identifier` as a
DIRECT child of the call, and a member call nests it under a
`navigation_expression`. So every plain `obj.method()` and `self.method()`
emitted no edge either — Swift was recording free calls and factory chains
and nothing in between. Removing the gate restores what every other language
already does.
The C++ call query matched two callee shapes — a bare `identifier` and a
`field_expression` with a `field_identifier` — and C++ has five. The three it
missed emitted no call edge at all:
generic<int>() -> function: (template_function …)
obj.m<int>() / ptr->m<int>() -> field: (template_method …)
ns::helper() / std::move(x) -> function: (qualified_identifier …)
The last one is not about templates and is the larger of the two losses:
every namespace-qualified free call in the language was dropped, so a
codebase that qualifies consistently recorded almost no free calls at all.
Rust already carries the equivalent `scoped_identifier` pattern; C++ now
matches it, binding on the trailing name the call actually names.
The call patterns pinned `function:` to `(identifier)` / `(selector_expression)`,
and a Go call spelling ONE type argument matches neither — through two
different mechanisms, so fixing only the obvious one leaves half the calls
missing:
Zero[int]() obj.M[int]() -> function: (index_expression …)
OneArg[int](1) obj.N[int](1) -> type_conversion_expression, not a
call_expression at all
Two or more type arguments were never affected: `F[string, int](a, b)` keeps
a plain callee with a sibling `type_arguments` field, which is why the gap
looked like it did not exist.
Both shapes are genuinely ambiguous in the grammar — an index_expression
callee is also how `handlers[key]()` parses, and a conversion to a generic
type is spelled exactly like a one-argument generic call. Widening the
patterns therefore emits an unresolved stub for those too. That is safe for a
reason specific to Go rather than luck: a package-scope name is a func, a var
or a type and never two of them, so a stub named after a variable or a type
finds no function candidate and stays unresolved — and an unresolved target
is a placeholder that reverse walks ignore. The resolver test asserts exactly
that, alongside a real cross-file generic call that does resolve.
extractCall switched on the callee node and handled only `identifier` and
`field_expression`, falling through to `default: return` for anything else.
tree-sitter-scala wraps any callee carrying explicit type arguments in a
`generic_function` node, so `f[T]()`, `obj.m[T](x)` and `Future[Int] { … }`
hit that default and emitted nothing at all — no edge, not even an
unresolved stub. `query callers` on such a method then answers
`total_edges: 0` with the `likely_unused` caveat, making a resolution
failure indistinguishable from dead code.
Unwrapping `generic_function` to its real callee lets the two existing cases
apply unchanged, so a generic call is recorded exactly like its plain
spelling — which is what the test asserts, pairing each shape with and
without type arguments.
Applicability compared argument counts against the EXTENSION-form window,
where the receiver fills the `this` slot. An extension invoked through its
declaring class (`BagExt.Add(bag)`) passes that receiver as the first
argument instead, so every parameter has to be covered by the argument list.
Reading one form as the other shifts the window by one, which does not
merely lose a bind: with a sibling overload present it leaves the WRONG
candidate as the sole survivor, and narrowing then binds it.
The resolver had no way to tell the forms apart, so the extractor now stamps
a bare receiver's spelling — but only in the one case nothing else explains
it, after the local, parameter and builtin lookups have all missed. That
restriction is what makes the signal trustworthy: reaching it means no value
in scope carries the name, so a receiver equal to a candidate's declaring
class is a type reference rather than a variable shadowing it. Receivers the
extractor did type are values by construction and carry no such stamp.
Beyond removing the misbinding this also resolves static-form calls
correctly, which previously stayed unresolved on any overloaded set.
The function-as-value capture treats a `(` after an identifier as proof the
identifier is a callee, and leans on that to skip a declaration's own name.
A generic declaration puts the type parameters in between —
`AddFooDecorator<TInterface, TImpl>(` — so the `(` is not where the rule
looks and the method's own name was captured as a function reference. The
placeholder edge carried the DECLARATION line, and the gate then bound it to
whichever same-name sibling it found first, minting an edge between two
overloads anchored at a line where no call exists. Reported as the second
symptom of issue #534.
Same root cause as the dropped call edges — an identifier followed by `<` —
but the byte rule cannot simply learn `<`: in JavaScript, TypeScript and
every C-family language the capture serves, `f < g` is a comparison. The
tree answers exactly and for all of them at once: a node that fills its
parent's `name` field where that parent also declares `parameters` is a
declaration however it is spelled.
Once generic invocations emit edges, the reporter's fixture still resolved
to nothing. Both `AddFooDecorator` overloads extend `IServiceCollection` and
both live in `Lib.Extensions`, so neither the receiver-type tiers nor the
namespace-visibility tie-break could separate them, and the binder's
ambiguity refusal dropped all three call sites. Overloaded extension methods
over one interface are the standard .NET DI registration shape, so this hit
far more than the reported fixture.
C# separates such a set by APPLICABILITY, which is exact rather than
heuristic: an overload is a candidate only if the call's argument count fits
its parameter list and its type-parameter count matches any explicitly
spelled type arguments. Neither fact was in the graph, so both are now
recorded — argument and type-argument counts on the member-call edge, and
`param_count` / `param_required` / `param_variadic` on the method node.
Node-level arity rather than the existing KindParam nodes because those
carry no default-value marker and cannot answer "how many MUST be supplied".
Applicability runs before the visibility tie-break, matching §12.8.10.3
(each scope level considers only applicable candidates). It narrows only:
missing evidence, an unreadable stamp, or a filter that would empty the set
all leave the candidates untouched, so this can turn a refusal into a bind
but never a bind into a different one. Where it does narrow, the winner must
still clear the visibility veto — the pool-unique rule's waiver is scoped to
sets applicability never touched, so an overload the call site cannot see is
refused rather than promoted by arity.
Two traps the fixtures pin. The vendored grammar does not resolve a `params`
entry into a parameter node, so a naive count claims one parameter for a
two-parameter method; a list that cannot be read in full now yields no
evidence at all, leaving the method universally applicable instead of
wrongly excluded. And a defaulted parameter has no equals-value wrapper —
only a bare `=` token — so optional parameters were invisible.
Also fixes parameter positions: a discard (`Foo(int _, string name)`) emits
no node but still occupies its slot, and skipping the slot with the node
renumbered every parameter after it.
The C# call-site query pinned every invoked name to an `identifier`.
A call that spells its type arguments — `services.AddFooDecorator<I, T>()`,
`GetRequiredService<T>()`, `AddSingleton<TI, TImpl>()` — parses that name as
a `generic_name` instead, so it matched no pattern at all and produced no
call edge: not an unresolved stub, nothing. The method-access patterns had
the same constraint, so the call earned no reference edge either.
The result was that a generic method could be called from everywhere and
still answer `query callers` with `total_edges: 0` and the `likely_unused`
caveat — a resolution failure indistinguishable from dead code. Generic
invocation is the dominant shape in .NET, so this covered a large fraction
of every indexed C# repo. Rust hit this same class of bug (its query carries
a note about 5,301 lost turbofish sites); the C# query is now equally
explicit about it.
Each affected pattern gains an alternation that accepts either spelling and
captures the inner identifier, so the callee name stays bare and every
downstream tier is unchanged. The extractor version is bumped so
already-indexed files re-extract instead of keeping the edge-less graph, and
Razor templates are salted with C# since they are extracted by it.
The test pairs each call shape with and without type arguments and asserts
they behave identically, which is the property that was actually violated.
Four attribution gaps survived receiver-aware call extraction. Measured on
a Godot project of ~490 files, 175 of 1002 targets with callers missed at
least one calling file.
Static methods and autoload callers. Neither was a distinct defect — the
minimal shapes from the report already resolved. What did not:
- A call outside every func was dropped entirely. `var hp = Stats.roll()`
and `@onready var ui = Hud.build()` are a script's whole initialisation
layer, and Godot puts a lot there. Attribute them to the file, the same
rule Python applies to module-level calls.
- `self.tick()` in a script with no `class_name` — which every autoload
entry point is — has no nameable receiver, so the member shape carried
no evidence and the call settled on the name-only tier find_usages
suppresses by default. The file declares the callee, so emit the
free-function shape and let the same-file tier bind it exactly.
- `const Persist = preload("res://…")` is how a static-utility script
with no `class_name` is reached, and the alias is file-local: the same
name one directory over means a different script. Record the loaded
path on the const and bind through it.
Same-named methods cross-linking. The dangerous one: a call states its
receiver, and when no member of that receiver was found a weak tier still
answered with whatever same-named method sat nearest the caller. With
`apply` in several classes, `Carry.apply`'s caller list held the calls
belonging to `Items` and `Effects` — the count looks reassuring while
pointing at code the change cannot reach. A receiver gate now demotes a
provable mismatch to the speculative tier, mirroring the C# gate. Only a
provable one: inherited dispatch survives by walking the receiver's
declared base chain, and a base that cannot be read as one in-repo class
name, an autoload or alias receiver on its own script, and a receiver the
calling file declares itself all decline to judge.
Signal wiring. Godot passes the method itself — `pressed.connect(_on_x)`,
`Callable(self, "_on_x")`, and the scene's own `[connection … method=…]`
block. None is a call, so rename rewrote the definition and the ordinary
call sites and left the wiring pointing at a name that no longer exists:
the project still parses, nothing errors anywhere, and the button simply
stops working. All three shapes are now usages, so rename rewrites them —
including in the `.tscn`, bound through the target node's script rather
than by name, which is right only while the handler name is unique and
`_on_pressed` never is.
The receiver gate runs whole-graph, so it is gated on the resolve having
seen a GDScript node at all; nothing else it reads can move a verdict.
An interface declaration's base list never reached emitCSharpBaseList,
so a derived interface had no hierarchy edge in the base graph:
inherited members could not resolve through it and dispatch families
never spanned interface inheritance. Every base of an interface is an
interface and the relation is inheritance, so each entry emits
EdgeExtends, bypassing the class-path discrimination.
The bulk index walk resolves every file's language before reading any of
them, then reuses that answer to pick the extractor. For a contested
extension the walk can only report the extension's default, so `.h`
always resolved to C: every header-only C++ library was parsed by the C
extractor, which cannot see past `template <…>` and dropped the file's
free template functions, class members and namespaces.
Re-detect at parse time for the extensions whose default is a guess —
the bytes are already in hand there, so it costs no extra read and gets
the full content rather than a bounded prefix. sniffAmbiguous is now
gated on the same set, so an uncontested extension can never be
re-typed from content.
A free template function is a function_definition wrapped in a
template_declaration, and the extractor only ever matched the inner
definition. Its symbol therefore started below the `template <…>`
header — a line-anchored lookup on the header found nothing — and an
explicit specialization, whose declarator is `name<Args>` rather than a
bare identifier, produced no symbol at all.
Template-wrapped definitions at file or namespace scope now emit from
the template_declaration, so the span covers the header, and the
declarator name is resolved through the specialization form as well.
Members declared inside a class or struct body are not free functions
and keep their existing path unchanged.
A named function expression carries a name the grammar exposes on the
expression itself, but only function_declaration was matched, so
`const x = function foo() {}`, `module.exports = function foo() {}`,
`{ key: function foo() {} }` and `export default flag && function foo() {}`
produced no symbol at all. Both extractors now match the named
expression form and emit it through the same path as a declaration
(same kind, spans, signature and file-defines edge). Anonymous
function expressions have no name field and stay unemitted.
The JavaScript and TypeScript extractors keep separate queries and
walkers, so the pattern and the shared emit helper are added to each;
the TypeScript one covers the TSX grammar as well.
A Dart call whose chain head is a Capitalized identifier —
`TiledAtlas.fromTiledMap(1)` — names the type the callee lives on, so
the call edge now carries that identifier as receiver_type. The
resolver already prefers a receiver-typed candidate, so such a call
binds to the member on that exact type instead of any same-named symbol
elsewhere in the graph.
Scope is deliberately narrow: only a two-segment chain qualifies (in
`Foo.bar.baz()` the head types `bar`, not `baz`), lowercase heads are
left alone rather than guessed at through local-variable inference, and
an import-alias head keeps its extern attribution unstamped because a
library prefix is not a type.
Every dotted Dart constructor now emits a member symbol the way an
ordinary method does: KindMethod, bare constructor name, owner-qualified
id, an EdgeMemberOf to the enclosing type, and a span that reaches
through the body when there is one.
Two grammar shapes were being missed. A body-less constructor (named,
const, redirecting factory) hangs off a plain `declaration` rather than
`method_signature`, so the class-body walk never saw it; the walk now
visits both wrappers in the same pass. And a constructor signature's
`name` field points at the leading TYPE identifier, so name extraction
read `TiledAtlas` instead of `fromTiledMap` and the unnamed-constructor
guard dropped the node; the name is now the identifier after the dot.
Unnamed constructors stay unemitted — `Foo()` constructs the class and
must not be shadowed by a phantom member.
`.tscn` / `.tres` / `project.godot` were claimed by the generic
signature-only forest grammar, which emits no edges, so
`[ext_resource type="Script" path="res://…"]` produced nothing. In Godot
a script is normally reached through the scene that instantiates it, so
a script's blast radius stopped at its own callers and missed every
scene that mounts it.
A hand-written extractor now reads all three formats: scene nodes become
symbols keyed by their NodePath, `[ext_resource]` and `[autoload]`
entries become references, and a `script =` / `instance=ExtResource(…)`
assignment attributes to the scene node carrying it. Both Godot 4
(quoted string ids) and Godot 3 (bare integer ids) dialects parse.
A new resolver pass binds those `res://` references — and GDScript's
`preload("res://…")`, which never resolved either — onto the indexed
file, matching by path suffix because `res://` is rooted at the Godot
project rather than the repository, and refusing when two projects in a
workspace make the path ambiguous.
Detection matched only the exact `Dockerfile` / `Containerfile`
basename and the `.dockerfile` extension, so the common per-purpose
convention — `Dockerfile.perf`, `Dockerfile.ci`, `Containerfile.dev` —
fell through every lookup and was never indexed at all.
Registry entries now accept a third form, `<Stem>.*`, matched against a
basename of that stem with any suffix. It is consulted after the
extension lookups, so a suffix that is itself a registered extension
still wins and `Dockerfile.md` stays markdown.