gitnexus 1.6.10-rc.166 → 1.6.10-rc.167

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,15 +2,20 @@
2
2
  import { SupportedLanguages } from '../../../../_shared/index.js';
3
3
  export const goClassConfig = {
4
4
  language: SupportedLanguages.Go,
5
- typeDeclarationNodes: ['type_declaration'],
5
+ // `type_spec`, not the enclosing `type_declaration` (#2837): one node per
6
+ // DECLARED TYPE, so a grouped `type ( A struct{…}; B struct{…} )` yields one
7
+ // each instead of one for the whole block. Every Go capture that reaches this
8
+ // extractor is anchored the same way, so the wrapper is never handed over —
9
+ // and accepting it would be worse than rejecting it, because picking one spec
10
+ // out of several with no reference point silently returns the FIRST type's
11
+ // name. A `null` here is loud; the wrong name is not.
12
+ typeDeclarationNodes: ['type_spec'],
6
13
  fileScopeNodeTypes: ['package_clause'],
7
14
  extractName(node) {
8
- const typeSpec = node.namedChildren.find((child) => child.type === 'type_spec');
9
- return typeSpec?.childForFieldName('name')?.text;
15
+ return node.childForFieldName('name')?.text;
10
16
  },
11
17
  extractType(node) {
12
- const typeSpec = node.namedChildren.find((child) => child.type === 'type_spec');
13
- const typeNode = typeSpec?.childForFieldName('type');
18
+ const typeNode = node.childForFieldName('type');
14
19
  if (typeNode?.type === 'struct_type')
15
20
  return 'Struct';
16
21
  if (typeNode?.type === 'interface_type')
@@ -1,5 +1,6 @@
1
1
  import { simpleQualifiedName } from '../../scope-resolution/graph-bridge/ids.js';
2
2
  import { resolveInheritanceBaseInScope } from '../../scope-resolution/scope/walkers.js';
3
+ import { goPackageDir } from './package-clause.js';
3
4
  export function detectGoInterfaceImplementations(parsedFiles, _indexes, _model) {
4
5
  return detectGoInterfaceImplementationsFromIndexes(buildDetectionIndexes(parsedFiles, _indexes));
5
6
  }
@@ -582,12 +583,13 @@ function signatureContextForFile(parsed, indexes) {
582
583
  importQualifiers,
583
584
  };
584
585
  }
586
+ /** The package directory, or `undefined` for a repo-root file.
587
+ *
588
+ * Shares `goPackageDir` with the package-clause resolver rather than repeating
589
+ * its normalize-and-slice (#2837): the two disagree only on how they spell "no
590
+ * directory", so the difference stays here, at the one call site that cares. */
585
591
  function packageQualifierForFile(filePath) {
586
- const normalized = filePath.replace(/\\/g, '/');
587
- const slash = normalized.lastIndexOf('/');
588
- if (slash === -1)
589
- return undefined;
590
- const packageDir = normalized.slice(0, slash);
592
+ const packageDir = goPackageDir(filePath);
591
593
  return packageDir.length === 0 ? undefined : packageDir;
592
594
  }
593
595
  const GO_BUILTIN_TYPES = new Set([
@@ -1,4 +1,8 @@
1
+ import { logger } from '../../../logger.js';
1
2
  import { isClassLike, populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
3
+ import { goPackageDir, inferGoPackageName } from './package-clause.js';
4
+ /** Bound on the sample of no-package-clause paths named in the warning. */
5
+ const SKIPPED_SAMPLE_CAP = 5;
2
6
  /**
3
7
  * Populate `ownerId` on Go Method defs by matching receiver types
4
8
  * extracted from `@type-binding.self` captures against struct defs in
@@ -20,15 +24,32 @@ export function populateGoOwners(parsed) {
20
24
  }
21
25
  export function populateGoWorkspaceOwners(parsedFiles, ctx) {
22
26
  const filesByPackage = new Map();
27
+ // A file with no resolvable package clause is dropped from ownership
28
+ // resolution entirely — its methods never attach to a struct declared in a
29
+ // sibling file. That used to be a bare `continue` with no trace, which is the
30
+ // same false-safe silence #2813 was filed about. Report it once, bounded
31
+ // (mirrors the fan-out-cap warning in scope-resolution/pipeline/run.ts).
32
+ let skippedCount = 0;
33
+ const skippedSample = [];
23
34
  for (const parsed of parsedFiles) {
24
- const pkgName = inferPackageName(ctx.fileContents.get(parsed.filePath) ?? '');
25
- if (pkgName === null)
35
+ const pkgName = inferGoPackageName(ctx.fileContents.get(parsed.filePath) ?? '');
36
+ if (pkgName === null) {
37
+ // Count everything, retain only the sample — a misrouted vendored tree
38
+ // would otherwise accumulate one path reference per file to print five.
39
+ skippedCount += 1;
40
+ if (skippedSample.length < SKIPPED_SAMPLE_CAP)
41
+ skippedSample.push(parsed.filePath);
26
42
  continue;
27
- const key = `${packageDir(parsed.filePath)}\0${pkgName}`;
43
+ }
44
+ const key = `${goPackageDir(parsed.filePath)}\0${pkgName}`;
28
45
  const bucket = filesByPackage.get(key) ?? [];
29
46
  bucket.push(parsed);
30
47
  filesByPackage.set(key, bucket);
31
48
  }
49
+ if (skippedCount > 0) {
50
+ logger.warn({ skippedFiles: skippedCount, sample: skippedSample }, 'go: files with no resolvable package clause were excluded from method-owner ' +
51
+ 'resolution (their methods cannot attach to structs declared in sibling files)');
52
+ }
32
53
  for (const bucket of filesByPackage.values()) {
33
54
  populateGoOwnersInPackage(bucket);
34
55
  }
@@ -97,12 +118,3 @@ function populateGoOwnersInPackage(parsedFiles) {
97
118
  }
98
119
  }
99
120
  }
100
- function inferPackageName(sourceText) {
101
- const match = sourceText.match(/^\s*package\s+([A-Za-z_][A-Za-z0-9_]*)/m);
102
- return match?.[1] ?? null;
103
- }
104
- function packageDir(filePath) {
105
- const normalized = filePath.replace(/\\/g, '/');
106
- const idx = normalized.lastIndexOf('/');
107
- return idx === -1 ? '' : normalized.slice(0, idx);
108
- }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Go package-clause resolution — the single derivation of a file's package
3
+ * identity (#2837).
4
+ *
5
+ * Both Go passes that bucket files by package (`populateGoWorkspaceOwners` and
6
+ * `populateGoPackageSiblings`) previously carried their own byte-identical copy
7
+ * of this, spelled as one unanchored multiline regex:
8
+ *
9
+ * sourceText.match(/^\s*package\s+([A-Za-z_][A-Za-z0-9_]*)/m)
10
+ *
11
+ * With the `m` flag that matches the first line ANYWHERE in the file starting
12
+ * with `package <ident>` — comment bodies included. Measured against that exact
13
+ * expression: a header comment containing `package legacy_notes kept for
14
+ * history` yields `legacy_notes`, and an indented ` package helper old name`
15
+ * yields `helper`. A file that mis-infers its own package gets a bucket key no
16
+ * sibling shares, so it is isolated in BOTH passes: its methods never attach to
17
+ * structs declared in sibling files, and it exchanges no same-package bindings.
18
+ * Every field-receiver call in it then resolves to nothing, silently — the same
19
+ * per-file signature #2837 reported.
20
+ *
21
+ * The Go spec makes the correct rule exact rather than heuristic: a source
22
+ * file's first non-comment, non-blank token is `package`. So skip the leading
23
+ * run of whitespace and comments, then require the very next token to be the
24
+ * clause. Anything else is `null` — a truncated read, a misrouted non-Go file,
25
+ * an unparseable header — reported by the caller rather than guessed at.
26
+ *
27
+ * ONE rule governs every leniency below (the `\s+` separator, the shebang skip,
28
+ * CR-only line endings): a file this returns `null` for is dropped from BOTH
29
+ * passes, so refusing a shape the previous regex accepted is a silent
30
+ * regression, not a principled tightening. Be no stricter than the grammar.
31
+ */
32
+ /**
33
+ * The package name declared by this Go source text, or `null` when its first
34
+ * real token is not a package clause.
35
+ *
36
+ * Only the leading run before the clause is skipped — deliberately NOT a
37
+ * whole-file comment strip, which would be O(file) on every Go file and would
38
+ * also have to model string literals to stay correct.
39
+ */
40
+ export declare function inferGoPackageName(sourceText: string): string | null;
41
+ /**
42
+ * The directory half of a Go package key. Go package identity is
43
+ * directory-scoped, so repeated `package main` directories must not see each
44
+ * other's unqualified names.
45
+ */
46
+ export declare function goPackageDir(filePath: string): string;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Go package-clause resolution — the single derivation of a file's package
3
+ * identity (#2837).
4
+ *
5
+ * Both Go passes that bucket files by package (`populateGoWorkspaceOwners` and
6
+ * `populateGoPackageSiblings`) previously carried their own byte-identical copy
7
+ * of this, spelled as one unanchored multiline regex:
8
+ *
9
+ * sourceText.match(/^\s*package\s+([A-Za-z_][A-Za-z0-9_]*)/m)
10
+ *
11
+ * With the `m` flag that matches the first line ANYWHERE in the file starting
12
+ * with `package <ident>` — comment bodies included. Measured against that exact
13
+ * expression: a header comment containing `package legacy_notes kept for
14
+ * history` yields `legacy_notes`, and an indented ` package helper old name`
15
+ * yields `helper`. A file that mis-infers its own package gets a bucket key no
16
+ * sibling shares, so it is isolated in BOTH passes: its methods never attach to
17
+ * structs declared in sibling files, and it exchanges no same-package bindings.
18
+ * Every field-receiver call in it then resolves to nothing, silently — the same
19
+ * per-file signature #2837 reported.
20
+ *
21
+ * The Go spec makes the correct rule exact rather than heuristic: a source
22
+ * file's first non-comment, non-blank token is `package`. So skip the leading
23
+ * run of whitespace and comments, then require the very next token to be the
24
+ * clause. Anything else is `null` — a truncated read, a misrouted non-Go file,
25
+ * an unparseable header — reported by the caller rather than guessed at.
26
+ *
27
+ * ONE rule governs every leniency below (the `\s+` separator, the shebang skip,
28
+ * CR-only line endings): a file this returns `null` for is dropped from BOTH
29
+ * passes, so refusing a shape the previous regex accepted is a silent
30
+ * regression, not a principled tightening. Be no stricter than the grammar.
31
+ */
32
+ /** Leading trivia: whitespace, `//` lines, block comments. Sticky. */
33
+ const LEADING_TRIVIA = /(?:\s+|\/\/[^\n\r]*|\/\*[\s\S]*?\*\/)*/y;
34
+ /** The clause itself, anchored at the first non-trivia byte. `\s+` (not
35
+ * `[ \t]+`) because Go separates tokens by any whitespace: `package\nmain` is
36
+ * legal and tree-sitter parses it without error. */
37
+ const PACKAGE_CLAUSE = /package\s+([A-Za-z_][A-Za-z0-9_]*)/y;
38
+ /**
39
+ * The package name declared by this Go source text, or `null` when its first
40
+ * real token is not a package clause.
41
+ *
42
+ * Only the leading run before the clause is skipped — deliberately NOT a
43
+ * whole-file comment strip, which would be O(file) on every Go file and would
44
+ * also have to model string literals to stay correct.
45
+ */
46
+ export function inferGoPackageName(sourceText) {
47
+ let i = 0;
48
+ // A leading `#!` line: `gorun`-style scripts carry one. Only a FIRST-line
49
+ // `#!` is skipped; a `#` anywhere else still ends the scan.
50
+ if (sourceText.startsWith('#!')) {
51
+ const eol = sourceText.search(/[\n\r]/);
52
+ if (eol === -1)
53
+ return null;
54
+ i = eol + 1;
55
+ }
56
+ // Whitespace (`\s` covers the BOM and every line ending), `//` lines and block
57
+ // comments — the run a Go file may carry before its clause. Sticky, so the
58
+ // header is skipped in place without slicing a copy of the file. An
59
+ // unterminated `/*` or a `//` running to EOF simply leaves `lastIndex` where
60
+ // the clause cannot match, so neither needs its own early return.
61
+ LEADING_TRIVIA.lastIndex = i;
62
+ LEADING_TRIVIA.exec(sourceText);
63
+ PACKAGE_CLAUSE.lastIndex = LEADING_TRIVIA.lastIndex;
64
+ return PACKAGE_CLAUSE.exec(sourceText)?.[1] ?? null;
65
+ }
66
+ /**
67
+ * The directory half of a Go package key. Go package identity is
68
+ * directory-scoped, so repeated `package main` directories must not see each
69
+ * other's unqualified names.
70
+ */
71
+ export function goPackageDir(filePath) {
72
+ const normalized = filePath.replace(/\\/g, '/');
73
+ const idx = normalized.lastIndexOf('/');
74
+ return idx === -1 ? '' : normalized.slice(0, idx);
75
+ }
@@ -1,4 +1,5 @@
1
1
  import { expandGoDotImports } from './expand-wildcards.js';
2
+ import { goPackageDir, inferGoPackageName } from './package-clause.js';
2
3
  /**
3
4
  * O(n²×d) where n = files per package, d = defs per file.
4
5
  * Acceptable for V1 since Go packages are typically small (< 20 files).
@@ -17,9 +18,13 @@ export function populateGoPackageSiblings(parsedFiles, indexes, ctx) {
17
18
  // must not see each other's unqualified names.
18
19
  const packageByFile = new Map();
19
20
  for (const parsed of nonTestFiles) {
20
- const pkgName = inferPackageName(ctx.fileContents.get(parsed.filePath) ?? '');
21
+ // Same derivation as `populateGoWorkspaceOwners` — one shared resolver, so
22
+ // the two passes cannot disagree about a file's package (#2837). The
23
+ // no-clause case is reported there; warning twice for one fact would be
24
+ // noise.
25
+ const pkgName = inferGoPackageName(ctx.fileContents.get(parsed.filePath) ?? '');
21
26
  if (pkgName !== null) {
22
- packageByFile.set(parsed.filePath, `${packageDir(parsed.filePath)}\0${pkgName}`);
27
+ packageByFile.set(parsed.filePath, `${goPackageDir(parsed.filePath)}\0${pkgName}`);
23
28
  }
24
29
  }
25
30
  const filesByPackage = new Map();
@@ -60,15 +65,6 @@ export function populateGoPackageSiblings(parsedFiles, indexes, ctx) {
60
65
  }
61
66
  }
62
67
  }
63
- function inferPackageName(sourceText) {
64
- const match = sourceText.match(/^\s*package\s+([A-Za-z_][A-Za-z0-9_]*)/m);
65
- return match?.[1] ?? null;
66
- }
67
- function packageDir(filePath) {
68
- const normalized = filePath.replace(/\\/g, '/');
69
- const idx = normalized.lastIndexOf('/');
70
- return idx === -1 ? '' : normalized.slice(0, idx);
71
- }
72
68
  function getAugmentationBucket(augmentations, scopeId, name) {
73
69
  let scopeBindings = augmentations.get(scopeId);
74
70
  if (scopeBindings === undefined) {
@@ -3,9 +3,33 @@ import Go from 'tree-sitter-go';
3
3
  const GO_SCOPE_QUERY = `
4
4
  ;; Scopes
5
5
  (source_file) @scope.module
6
+ ;; One Class scope per DECLARED TYPE, not per declaration (#2837).
7
+ ;;
8
+ ;; Capturing the type_declaration made a grouped declaration
9
+ ;; type (
10
+ ;; Decoy struct { ... }
11
+ ;; PickService struct { ... }
12
+ ;; )
13
+ ;; a SINGLE Class scope owning every struct in the block. Downstream that scope
14
+ ;; can name only one owner -- buildWorkspaceResolutionIndex keeps the first
15
+ ;; class-like def it finds -- so the block's structs lost their field type
16
+ ;; bindings, typeOfMemberOnClass found no scope, the compound-receiver fold
17
+ ;; declined, and every s.field.Method() site in the file emitted nothing at
18
+ ;; all. Silent, per-file, and independent of file size: exactly the split
19
+ ;; reported in #2837 that #2829's global fixes could not explain. Measured on
20
+ ;; the go-grouped-type-decl fixture: EVERY struct in a grouped block lost its
21
+ ;; edges, the first one included, and grouped interface blocks produced no
22
+ ;; IMPLEMENTS edges at all.
23
+ ;;
24
+ ;; type_spec is the node Go's own grammar gives one declared type, so one
25
+ ;; capture per type_spec is the granularity the rest of the pipeline already
26
+ ;; assumes. A plain single-type declaration is unaffected in count -- only its
27
+ ;; scope range narrows, from the type keyword to the name.
28
+ ;;
29
+ ;; NOTE: this string is a JS template literal. Backticks are a syntax error.
6
30
  (type_declaration
7
31
  (type_spec
8
- type: [(struct_type) (interface_type)])) @scope.class
32
+ type: [(struct_type) (interface_type)]) @scope.class)
9
33
  (function_declaration) @scope.function
10
34
  (method_declaration) @scope.function
11
35
  (func_literal) @scope.function
@@ -21,14 +45,24 @@ const GO_SCOPE_QUERY = `
21
45
  (communication_case) @scope.block
22
46
 
23
47
  ;; Declarations — struct
48
+ ;;
49
+ ;; Anchored on the type_spec, in lockstep with @scope.class above (#2837). Both
50
+ ;; captures MUST name the same node: the def node and the class-scope node are
51
+ ;; paired by range, so anchoring the def on the enclosing type_declaration while
52
+ ;; the scope sits on the type_spec leaves the def strictly larger than its own
53
+ ;; scope and NOTHING is owned -- measured as every Go field-receiver edge in the
54
+ ;; fixture disappearing, plain declarations included. Keeping both on
55
+ ;; type_declaration is the original bug: a grouped block gave both structs the
56
+ ;; same capture node, so one silently displaced the other.
24
57
  (type_declaration
25
58
  (type_spec name: (type_identifier) @declaration.name
26
- type: (struct_type))) @declaration.struct
59
+ type: (struct_type)) @declaration.struct)
27
60
 
28
61
  ;; Declarations — interface
62
+ ;; Same lockstep requirement as @declaration.struct above (#2837).
29
63
  (type_declaration
30
64
  (type_spec name: (type_identifier) @declaration.name
31
- type: (interface_type))) @declaration.interface
65
+ type: (interface_type)) @declaration.interface)
32
66
 
33
67
  ;; Declarations — function
34
68
  (function_declaration
@@ -132,6 +132,15 @@ export const goProvider = defineLanguage({
132
132
  descriptionExtractor: createLeadingDocDescriptionExtractor({
133
133
  lineCommentPrefixes: ['//'],
134
134
  lineDirectivePrefixes: ['//go:', '// +build', '//nolint', '//line'],
135
+ // #2837: Go type declarations anchor on the `type_spec`, whose
136
+ // `previousNamedSibling` is null for the ordinary `type T struct{…}` form —
137
+ // the godoc comment is a sibling of the enclosing `type_declaration`, one
138
+ // level up. Without this the doc silently vanished for EVERY Go struct and
139
+ // interface (measured: pre-#2837 `desc=YES`, post `none`), taking #2270's
140
+ // description out of both the embedding header and the FTS column.
141
+ // Functions/methods are unaffected — they anchor on their own declaration
142
+ // node, which does carry the comment as a previous sibling.
143
+ wrapperNodeTypes: ['type_declaration'],
135
144
  }),
136
145
  builtInNames: GO_BUILT_INS,
137
146
  // ── RFC #909 Ring 3: scope-based resolution hooks ──────────
@@ -87,3 +87,23 @@ export declare function lookupUnresolvedCallCount(summary: UnresolvedReceiverSum
87
87
  * friends would otherwise return a function.
88
88
  */
89
89
  export declare function lookupExternalCallCount(summary: UnresolvedReceiverSummary | undefined, symName: string): number | undefined;
90
+ /**
91
+ * Report which FILES lost the most call sites to an untyped receiver (#2837).
92
+ *
93
+ * `summarizeUnresolvedReceivers` persists the same drops keyed by member NAME,
94
+ * capped at 500 distinct names, and discards `filePath` — deliberately, because
95
+ * its consumer (`impact()`'s exact-vs-lower-bound verdict) asks "is this
96
+ * symbol's caller count trustworthy", a question about names.
97
+ *
98
+ * That leaves "why does THIS file resolve nothing while its sibling resolves
99
+ * everything" unanswerable from any artifact, which is exactly what #2837 asked
100
+ * for and could not run: comparing the drop records of two files with identical
101
+ * declared shapes separates "receiver never typed" from "typed but no edge
102
+ * emitted", and narrows a per-file split in one run instead of a bisect.
103
+ *
104
+ * A log line rather than a persisted field because nothing queries it — a
105
+ * persisted `byFile` map would be an unread field carrying its own cap and
106
+ * truncation semantics. When a consumer appears, `UnresolvedReceiverSummary` is
107
+ * already a `RepoMeta` field and can carry it with no schema change.
108
+ */
109
+ export declare function logUnresolvedReceiverFiles(outcomes: readonly ResolutionOutcome[]): void;
@@ -12,6 +12,8 @@
12
12
  * report its caller count as a lower bound, while a query about an unrelated
13
13
  * symbol stays exact.
14
14
  */
15
+ import { createLogger } from '../../logger.js';
16
+ import { compareCodeUnits } from '../../../lib/utils.js';
15
17
  /** Cap on distinct member names persisted. Well above what a real repo
16
18
  * produces (the whole point of the signal is that drops are the exception),
17
19
  * but bounded so a pathological repo cannot grow the metadata file without
@@ -30,11 +32,42 @@ export const MAX_UNRESOLVED_RECEIVER_MEMBERS = 500;
30
32
  * `omitted` is the number of distinct names past the cap, so the caller can
31
33
  * report truncation rather than silently losing entries.
32
34
  */
33
- function rankAndCap(counts) {
34
- const ranked = [...counts.entries()].sort(([aName, aCount], [bName, bCount]) => bCount - aCount || aName.localeCompare(bName));
35
- const kept = ranked.slice(0, MAX_UNRESOLVED_RECEIVER_MEMBERS);
35
+ function rankAndCap(counts, cap = MAX_UNRESOLVED_RECEIVER_MEMBERS) {
36
+ const ranked = [...counts.entries()].sort(
37
+ // `compareCodeUnits`, not `localeCompare` (#2787). The tiebreak feeds the
38
+ // `.slice()` below, so locale-sensitive collation would decide WHICH
39
+ // entries survive the cap, not merely how they are listed — and ICU order
40
+ // varies by platform and ICU build, so two runs over one repo could persist
41
+ // different sets. Key-based lookup is unaffected either way.
42
+ ([aName, aCount], [bName, bCount]) => bCount - aCount || compareCodeUnits(aName, bName));
43
+ const kept = ranked.slice(0, cap);
36
44
  return { kept, omitted: ranked.length - kept.length };
37
45
  }
46
+ /**
47
+ * A `receiver-unresolved` drop at a CALL site.
48
+ *
49
+ * ONE predicate, shared by the persisted summary and the per-file diagnostic —
50
+ * the same contract `rankAndCap` states for its comparator. If the two ever
51
+ * disagreed, the diagnostic would rank a different population than the artifact
52
+ * it exists to explain.
53
+ *
54
+ * Call sites only: Case 0's recorder gates on the receiver's punctuation, not on
55
+ * what the reference IS, so property reads (`d.source.kind`) and writes
56
+ * (`x.argtypes = [...]`) arrive alongside lost method calls — measured at 25 of
57
+ * 124 drops on the fixture corpus. Counting them made the consumer's "N call
58
+ * sites invoking X were dropped" literally false. A missing `siteKind` counts as
59
+ * a call: the only emitter always sets it, and erring toward `lower-bound` is
60
+ * the safe direction for an epistemic signal.
61
+ *
62
+ * Receiver ORIGIN is deliberately NOT decided here — the summary routes external
63
+ * drops into their own bucket while the diagnostic excludes them, and collapsing
64
+ * that choice into this predicate would take it away from both callers.
65
+ */
66
+ function isUnresolvedReceiverCall(outcome) {
67
+ return (outcome.kind === 'suppressed' &&
68
+ outcome.reason === 'receiver-unresolved' &&
69
+ (outcome.siteKind === undefined || outcome.siteKind === 'call'));
70
+ }
38
71
  /**
39
72
  * Build the summary, or `undefined` when nothing was dropped — an index with
40
73
  * no unresolved receivers stores no key at all, so `epistemic` keeps its
@@ -47,20 +80,12 @@ export function summarizeUnresolvedReceivers(outcomes) {
47
80
  let totalSites = 0;
48
81
  let externalSites = 0;
49
82
  for (const outcome of outcomes) {
50
- if (outcome.kind !== 'suppressed' || outcome.reason !== 'receiver-unresolved')
83
+ // CALL sites only — see `isUnresolvedReceiverCall`, which the per-file
84
+ // diagnostic shares so the two can never rank different populations.
85
+ if (!isUnresolvedReceiverCall(outcome))
51
86
  continue;
52
87
  if (outcome.name.length === 0)
53
88
  continue;
54
- // CALL sites only. Case 0's recorder gates on the receiver's punctuation, not
55
- // on what the reference IS, so property reads (`d.source.kind`) and writes
56
- // (`x.argtypes = [...]`) are recorded alongside lost method calls — measured at
57
- // 25 of 124 drops on the fixture corpus. Counting them made the consumer's
58
- // "N call sites invoking X were dropped" literally false, and flagged symbols
59
- // whose CALL count was never short. `siteKind` exists to make this separable.
60
- // A missing `siteKind` counts as a call: the only emitter always sets it, and
61
- // erring toward `lower-bound` is the safe direction for an epistemic signal.
62
- if (outcome.siteKind !== undefined && outcome.siteKind !== 'call')
63
- continue;
64
89
  // Routed, not discarded. External-rooted drops (`console.log(...)`,
65
90
  // `fetch(...)`) reach code this index does not contain, so there is no node
66
91
  // an edge could have pointed at and nothing was lost — they must not hedge.
@@ -142,3 +167,69 @@ export function lookupExternalCallCount(summary, symName) {
142
167
  return undefined;
143
168
  return sites;
144
169
  }
170
+ /**
171
+ * Opt-in, following the repo's established diagnostic pattern
172
+ * (`createLogger(name, { debugEnvVar })`). Every repository in every language
173
+ * drops SOME receivers, so emitting this at `info` unconditionally would add a
174
+ * line to every analyze anyone ever runs — the noise ADV-6 flagged. #2837's
175
+ * reporter turns it on deliberately:
176
+ *
177
+ * GITNEXUS_DEBUG_RECEIVER_DROPS=1 gitnexus analyze
178
+ */
179
+ // Built on first use, not at import. `createLogger` is eager where the `logger`
180
+ // singleton is a lazy Proxy — it resolves `pino-pretty`, constructs a SonicBoom
181
+ // destination and registers a `beforeExit` hook. This module is imported by
182
+ // `mcp/local/local-backend.ts` for its lookup helpers, so an eager call would
183
+ // put that on MCP server startup for a diagnostic that is off by default.
184
+ let receiverDropLog;
185
+ const dropLog = () => (receiverDropLog ??= createLogger('receiver-drops', {
186
+ debugEnvVar: 'GITNEXUS_DEBUG_RECEIVER_DROPS',
187
+ }));
188
+ /** Files named in the per-file receiver-drop line. Bounded: this is a signpost
189
+ * pointing at where to look, not an inventory. */
190
+ const UNRESOLVED_RECEIVER_FILE_SAMPLE = 10;
191
+ /**
192
+ * Report which FILES lost the most call sites to an untyped receiver (#2837).
193
+ *
194
+ * `summarizeUnresolvedReceivers` persists the same drops keyed by member NAME,
195
+ * capped at 500 distinct names, and discards `filePath` — deliberately, because
196
+ * its consumer (`impact()`'s exact-vs-lower-bound verdict) asks "is this
197
+ * symbol's caller count trustworthy", a question about names.
198
+ *
199
+ * That leaves "why does THIS file resolve nothing while its sibling resolves
200
+ * everything" unanswerable from any artifact, which is exactly what #2837 asked
201
+ * for and could not run: comparing the drop records of two files with identical
202
+ * declared shapes separates "receiver never typed" from "typed but no edge
203
+ * emitted", and narrows a per-file split in one run instead of a bisect.
204
+ *
205
+ * A log line rather than a persisted field because nothing queries it — a
206
+ * persisted `byFile` map would be an unread field carrying its own cap and
207
+ * truncation semantics. When a consumer appears, `UnresolvedReceiverSummary` is
208
+ * already a `RepoMeta` field and can carry it with no schema change.
209
+ */
210
+ export function logUnresolvedReceiverFiles(outcomes) {
211
+ // Nothing below is observable unless the diagnostic is switched on, so skip
212
+ // the whole tally rather than building it and discarding it at the emit.
213
+ if (!dropLog().isLevelEnabled('debug'))
214
+ return;
215
+ const byFile = new Map();
216
+ let totalSites = 0;
217
+ for (const outcome of outcomes) {
218
+ // External-rooted drops (`fmt.Println`) reach code this index does not
219
+ // contain, so no node existed for an edge to point at and nothing was lost.
220
+ // Counting them here led the ranking with files that call `fmt` the most.
221
+ if (!isUnresolvedReceiverCall(outcome) || outcome.receiverOrigin === 'external')
222
+ continue;
223
+ totalSites += 1;
224
+ byFile.set(outcome.filePath, (byFile.get(outcome.filePath) ?? 0) + 1);
225
+ }
226
+ if (totalSites === 0)
227
+ return;
228
+ const { kept } = rankAndCap(byFile, UNRESOLVED_RECEIVER_FILE_SAMPLE);
229
+ dropLog().debug({
230
+ totalSites,
231
+ filesAffected: byFile.size,
232
+ topFiles: kept.map(([filePath, sites]) => ({ filePath, sites })),
233
+ }, 'receiver-unresolved call sites by file (top offenders; a file far above its ' +
234
+ 'siblings usually means its receivers were never typed, not that it has more calls)');
235
+ }
@@ -3,7 +3,7 @@ export declare const JAVASCRIPT_QUERIES: string;
3
3
  export declare const PYTHON_QUERIES = "\n(class_definition\n name: (identifier) @name) @definition.class\n\n(function_definition\n name: (identifier) @name) @definition.function\n\n(import_statement\n name: (dotted_name) @import.source) @import\n\n; import numpy as np \u2192 aliased_import captures the module name so the\n; import path is resolved and named-binding extraction stores \"np\" \u2192 \"numpy\".\n(import_statement\n name: (aliased_import\n name: (dotted_name) @import.source)) @import\n\n(import_from_statement\n module_name: (dotted_name) @import.source) @import\n\n(import_from_statement\n module_name: (relative_import) @import.source) @import\n\n(call\n function: (identifier) @call.name) @call\n\n(call\n function: (attribute\n attribute: (identifier) @call.name)) @call\n\n; Class attribute type annotations \u2014 PEP 526: address: Address or address: Address = Address()\n; Both bare annotations (address: Address) and annotated assignments (name: str = \"test\")\n; are parsed as (assignment left: ... type: ...) in tree-sitter-python.\n(expression_statement\n (assignment\n left: (identifier) @name\n type: (type)) @definition.property)\n\n; Plain variable assignments without type annotation: x = 5, MAX_SIZE = 100\n; Overlap with @definition.property (typed) is handled by parse-worker dedup.\n(expression_statement\n (assignment\n left: (identifier) @name)) @definition.variable\n\n; Lambda bindings: `f = lambda x: x` binds a CALLABLE, so it emits Function\n; rather than Variable, matching what TS/JS already do for `const f = () => {}`.\n; This aligns the LABEL only \u2014 call resolution runs off the scope-resolution\n; query, which still models the binding as a value, so `f()` does not resolve\n; here yet. Overlap with the assignment pattern above is collapsed by the\n; parse-worker dedup (#2687).\n(expression_statement\n (assignment\n left: (identifier) @name\n right: (lambda))) @definition.function\n\n; Write access: obj.field = value\n(assignment\n left: (attribute\n object: (_) @assignment.receiver\n attribute: (identifier) @assignment.property)\n right: (_)) @assignment\n\n; Write access: obj.field += value (compound assignment)\n(augmented_assignment\n left: (attribute\n object: (_) @assignment.receiver\n attribute: (identifier) @assignment.property)\n right: (_)) @assignment\n\n; Python HTTP clients: requests.get('/path'), httpx.post('/path'), session.get('/path')\n(call\n function: (attribute\n attribute: (identifier) @http_client.method)\n arguments: (argument_list\n (string (string_content) @http_client.url))) @http_client\n\n; Python decorators: @app.route, @router.get, etc.\n; The first positional argument is captured three ways (#2391): a string literal\n; path via @decorator.arg (quote-free, the fast path); a bare constant name or a\n; plus-concatenation via @decorator.arg_expr (resolved cross-file by the constant\n; resolver). The anchored optional alternation pins to the FIRST arg and stays\n; optional, so no-arg decorators (@app.tool(), etc.) and non-path first args still\n; match.\n(decorator\n (call\n function: (attribute\n object: (identifier) @decorator.receiver\n attribute: (identifier) @decorator.name)\n arguments: (argument_list\n .\n [\n (string (string_content)? @decorator.arg) @decorator.arg_str\n (identifier) @decorator.arg_expr\n (binary_operator) @decorator.arg_expr\n ]?))) @decorator\n";
4
4
  export declare const JAVA_QUERIES = "\n; Classes, Interfaces, Enums, Records, Annotations\n(class_declaration name: (identifier) @name) @definition.class\n(interface_declaration name: (identifier) @name) @definition.interface\n(enum_declaration name: (identifier) @name) @definition.enum\n(record_declaration name: (identifier) @name) @definition.record\n(annotation_type_declaration name: (identifier) @name) @definition.annotation\n\n; Anonymous class bodies: new Runnable() { ... } \u2014 no @name capture; the\n; class extractor synthesizes the javac-style Worker$N name (#2550)\n(object_creation_expression (class_body)) @definition.class\n\n; Enum constant bodies: enum E { A { ... } } \u2014 javac's other anonymous\n; shape, synthesized as E$N by the same naming authority (#2555)\n(enum_constant body: (class_body)) @definition.class\n\n; Methods & Constructors\n(method_declaration name: (identifier) @name) @definition.method\n(constructor_declaration name: (identifier) @name) @definition.constructor\n\n; Fields \u2014 typed field declarations inside class bodies\n(field_declaration\n declarator: (variable_declarator\n name: (identifier) @name)) @definition.property\n\n; Imports - capture any import declaration child as source\n(import_declaration (_) @import.source) @import\n\n; Calls\n(method_invocation name: (identifier) @call.name) @call\n(method_invocation object: (_) name: (identifier) @call.name) @call\n(method_reference) @call\n\n; Constructor calls: new Foo()\n(object_creation_expression type: (type_identifier) @call.name) @call\n\n; Local variable declarations inside method bodies\n(local_variable_declaration\n declarator: (variable_declarator\n name: (identifier) @name)) @definition.variable\n\n; Write access: obj.field = value\n(assignment_expression\n left: (field_access\n object: (_) @assignment.receiver\n field: (identifier) @assignment.property)\n right: (_)) @assignment\n\n; \u2500\u2500 Closure bindings (#2693) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n; A name bound to a closure literal IS a callable, so it emits Function rather\n; than a value label \u2014 matching TS/JS and the languages #2687 already covered.\n; The callable node is what callable-value-flow joins the binding to (by file,\n; line and name), which is what makes handler.apply(1) resolve. Overlap with the value\n; rules above is collapsed by the parse-worker dedup, which ranks callable\n; highest (#2687).\n; Anchored on field_declaration / local_variable_declaration \u2014 the SAME nodes\n; the value rules above use \u2014 so the parse-worker dedup (keyed by definition\n; node + name) actually collapses the pair. Anchoring on the inner\n; variable_declarator instead produced a Function AND a Property twin, the exact\n; double-indexing #2687 removed.\n(field_declaration\n declarator: (variable_declarator\n name: (identifier) @name\n value: (lambda_expression))) @definition.function\n(local_variable_declaration\n declarator: (variable_declarator\n name: (identifier) @name\n value: (lambda_expression))) @definition.function\n";
5
5
  export declare const C_QUERIES = "\n; Functions (direct declarator)\n(function_definition declarator: (function_declarator declarator: (identifier) @name)) @definition.function\n(declaration declarator: (function_declarator declarator: (identifier) @name)) @definition.function\n\n; Functions returning pointers (pointer_declarator wraps function_declarator)\n(function_definition declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name))) @definition.function\n(declaration declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name))) @definition.function\n\n; Functions returning double pointers (nested pointer_declarator)\n(function_definition declarator: (pointer_declarator declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name)))) @definition.function\n\n; Structs, Unions, Enums, Typedefs\n(struct_specifier name: (type_identifier) @name) @definition.struct\n(type_definition\n type: (struct_specifier\n body: (field_declaration_list))\n declarator: (type_identifier) @name) @definition.struct\n(union_specifier name: (type_identifier) @name) @definition.union\n(enum_specifier name: (type_identifier) @name) @definition.enum\n(type_definition\n type: (enum_specifier\n body: (enumerator_list))\n declarator: (type_identifier) @name) @definition.enum\n(enumerator name: (identifier) @name) @definition.const\n(type_definition declarator: (type_identifier) @name) @definition.typedef\n\n; Macros\n(preproc_function_def name: (identifier) @name) @definition.macro\n(preproc_def name: (identifier) @name) @definition.macro\n\n; Includes\n(preproc_include path: (_) @import.source) @import\n\n; Calls\n(call_expression function: (identifier) @call.name) @call\n(call_expression function: (field_expression field: (field_identifier) @call.name)) @call\n\n; Variable declarations: int x = 5; or int x;\n(declaration\n declarator: (init_declarator\n declarator: (identifier) @name)) @definition.variable\n";
6
- export declare const GO_QUERIES = "\n; Functions & Methods\n(function_declaration name: (identifier) @name) @definition.function\n(method_declaration name: (field_identifier) @name) @definition.method\n(method_elem name: (field_identifier) @name) @definition.method\n\n; Types\n(type_declaration (type_spec name: (type_identifier) @name type: (struct_type))) @definition.struct\n(type_declaration (type_spec name: (type_identifier) @name type: (interface_type))) @definition.interface\n\n; Imports\n(import_declaration (import_spec path: (interpreted_string_literal) @import.source)) @import\n(import_declaration (import_spec_list (import_spec path: (interpreted_string_literal) @import.source))) @import\n\n; Struct fields \u2014 named field declarations inside struct types\n(field_declaration_list\n (field_declaration\n name: (field_identifier) @name) @definition.property)\n\n; Calls\n(call_expression function: (identifier) @call.name) @call\n(call_expression function: (selector_expression field: (field_identifier) @call.name)) @call\n\n; Const/var declarations\n(const_declaration (const_spec (identifier) @name)) @definition.const\n(var_declaration (var_spec (identifier) @name)) @definition.variable\n(var_declaration (var_spec_list (var_spec (identifier) @name))) @definition.variable\n\n; Short variable declaration: x := 5\n(short_var_declaration left: (expression_list (identifier) @name)) @definition.variable\n\n; Closure bindings: `var f = func(){}` / `f := func(){}` bind a CALLABLE, so\n; they emit Function, not Variable \u2014 the same convention TS/JS already use for\n; `const f = () => {}`. This aligns the LABEL only \u2014 call resolution runs off\n; the scope-resolution query, which still models the binding as a value, so\n; `f()` does not resolve here yet. Overlap with the value patterns above is\n; collapsed by the parse-worker dedup (#2687).\n(var_declaration\n (var_spec\n name: (identifier) @name\n value: (expression_list (func_literal)))) @definition.function\n(var_declaration\n (var_spec_list\n (var_spec\n name: (identifier) @name\n value: (expression_list (func_literal))))) @definition.function\n(short_var_declaration\n left: (expression_list (identifier) @name)\n right: (expression_list (func_literal))) @definition.function\n\n; Struct literal construction: User{Name: \"Alice\"}\n(composite_literal type: (type_identifier) @call.name) @call\n\n; Write access: obj.field = value\n(assignment_statement\n left: (expression_list\n (selector_expression\n operand: (_) @assignment.receiver\n field: (field_identifier) @assignment.property))\n right: (_)) @assignment\n\n; Write access: obj.field++ / obj.field--\n(inc_statement\n (selector_expression\n operand: (_) @assignment.receiver\n field: (field_identifier) @assignment.property)) @assignment\n(dec_statement\n (selector_expression\n operand: (_) @assignment.receiver\n field: (field_identifier) @assignment.property)) @assignment\n";
6
+ export declare const GO_QUERIES = "\n; Functions & Methods\n(function_declaration name: (identifier) @name) @definition.function\n(method_declaration name: (field_identifier) @name) @definition.method\n(method_elem name: (field_identifier) @name) @definition.method\n\n; Types\n;\n; Anchored on the type_spec, NOT the enclosing type_declaration (#2837) \u2014 a\n; grouped type ( A struct{}; B struct{} ) block otherwise gave every match the\n; same capture node, and goClassConfig.extractName resolved all of them to the\n; FIRST spec's name, collapsing the block to one node. Must stay in lockstep\n; with @scope.class / @declaration.struct in languages/go/query.ts, which\n; carries the full rationale. (No backticks here: this is a template literal.)\n(type_declaration (type_spec name: (type_identifier) @name type: (struct_type)) @definition.struct)\n(type_declaration (type_spec name: (type_identifier) @name type: (interface_type)) @definition.interface)\n\n; Imports\n(import_declaration (import_spec path: (interpreted_string_literal) @import.source)) @import\n(import_declaration (import_spec_list (import_spec path: (interpreted_string_literal) @import.source))) @import\n\n; Struct fields \u2014 named field declarations inside struct types\n(field_declaration_list\n (field_declaration\n name: (field_identifier) @name) @definition.property)\n\n; Calls\n(call_expression function: (identifier) @call.name) @call\n(call_expression function: (selector_expression field: (field_identifier) @call.name)) @call\n\n; Const/var declarations\n(const_declaration (const_spec (identifier) @name)) @definition.const\n(var_declaration (var_spec (identifier) @name)) @definition.variable\n(var_declaration (var_spec_list (var_spec (identifier) @name))) @definition.variable\n\n; Short variable declaration: x := 5\n(short_var_declaration left: (expression_list (identifier) @name)) @definition.variable\n\n; Closure bindings: `var f = func(){}` / `f := func(){}` bind a CALLABLE, so\n; they emit Function, not Variable \u2014 the same convention TS/JS already use for\n; `const f = () => {}`. This aligns the LABEL only \u2014 call resolution runs off\n; the scope-resolution query, which still models the binding as a value, so\n; `f()` does not resolve here yet. Overlap with the value patterns above is\n; collapsed by the parse-worker dedup (#2687).\n(var_declaration\n (var_spec\n name: (identifier) @name\n value: (expression_list (func_literal)))) @definition.function\n(var_declaration\n (var_spec_list\n (var_spec\n name: (identifier) @name\n value: (expression_list (func_literal))))) @definition.function\n(short_var_declaration\n left: (expression_list (identifier) @name)\n right: (expression_list (func_literal))) @definition.function\n\n; Struct literal construction: User{Name: \"Alice\"}\n(composite_literal type: (type_identifier) @call.name) @call\n\n; Write access: obj.field = value\n(assignment_statement\n left: (expression_list\n (selector_expression\n operand: (_) @assignment.receiver\n field: (field_identifier) @assignment.property))\n right: (_)) @assignment\n\n; Write access: obj.field++ / obj.field--\n(inc_statement\n (selector_expression\n operand: (_) @assignment.receiver\n field: (field_identifier) @assignment.property)) @assignment\n(dec_statement\n (selector_expression\n operand: (_) @assignment.receiver\n field: (field_identifier) @assignment.property)) @assignment\n";
7
7
  export declare const CPP_QUERIES = "\n; Classes, Structs, Namespaces\n(class_specifier name: (type_identifier) @name) @definition.class\n(class_specifier\n name: (template_type\n (type_identifier) @name\n (template_argument_list) @template-arguments)) @definition.class\n; Out-of-line nested definition: class Outer::Inner { ... } / struct Outer::Inner { ... }.\n; Key the node by the full qualified_identifier text so the def materializes a\n; node that matches the HAS_METHOD owner id (also the full qualified text) and\n; stays distinct from a same-tail type in another scope (#1975, #1978).\n(class_specifier name: (qualified_identifier) @name) @definition.class\n(struct_specifier name: (qualified_identifier) @name) @definition.struct\n(struct_specifier name: (type_identifier) @name) @definition.struct\n(struct_specifier\n name: (template_type\n (type_identifier) @name\n (template_argument_list) @template-arguments)) @definition.struct\n(namespace_definition name: (namespace_identifier) @name) @definition.namespace\n(enum_specifier name: (type_identifier) @name) @definition.enum\n\n; Typedefs and unions (common in C-style headers and mixed C/C++ code)\n(type_definition\n type: (struct_specifier\n body: (field_declaration_list))\n declarator: (type_identifier) @name) @definition.struct\n(type_definition\n type: (enum_specifier\n body: (enumerator_list))\n declarator: (type_identifier) @name) @definition.enum\n(enumerator name: (identifier) @name) @definition.const\n(type_definition declarator: (type_identifier) @name) @definition.typedef\n(union_specifier name: (type_identifier) @name) @definition.union\n\n; Macros\n(preproc_function_def name: (identifier) @name) @definition.macro\n(preproc_def name: (identifier) @name) @definition.macro\n\n; Functions & Methods (direct declarator)\n(function_definition declarator: (function_declarator declarator: (identifier) @name)) @definition.function\n(function_definition declarator: (function_declarator declarator: (operator_name) @name)) @definition.function\n(function_definition declarator: (function_declarator declarator: (qualified_identifier name: (identifier) @name))) @definition.method\n(function_definition declarator: (function_declarator declarator: (qualified_identifier name: (operator_name) @name))) @definition.method\n\n; Functions/methods returning pointers (pointer_declarator wraps function_declarator)\n(function_definition declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name))) @definition.function\n(function_definition declarator: (pointer_declarator declarator: (function_declarator declarator: (qualified_identifier name: (identifier) @name)))) @definition.method\n\n; Functions/methods returning double pointers (nested pointer_declarator)\n(function_definition declarator: (pointer_declarator declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name)))) @definition.function\n(function_definition declarator: (pointer_declarator declarator: (pointer_declarator declarator: (function_declarator declarator: (qualified_identifier name: (identifier) @name))))) @definition.method\n\n; Functions/methods returning references (reference_declarator wraps function_declarator)\n(function_definition declarator: (reference_declarator (function_declarator declarator: (identifier) @name))) @definition.function\n(function_definition declarator: (reference_declarator (function_declarator declarator: (operator_name) @name))) @definition.function\n(function_definition declarator: (reference_declarator (function_declarator declarator: (qualified_identifier name: (identifier) @name)))) @definition.method\n(function_definition declarator: (reference_declarator (function_declarator declarator: (qualified_identifier name: (operator_name) @name)))) @definition.method\n\n; Destructors (destructor_name is distinct from identifier in tree-sitter-cpp)\n(function_definition declarator: (function_declarator declarator: (qualified_identifier name: (destructor_name) @name))) @definition.method\n\n; Function declarations / prototypes (common in headers)\n(declaration declarator: (function_declarator declarator: (identifier) @name)) @definition.function\n(declaration declarator: (function_declarator declarator: (operator_name) @name)) @definition.function\n(declaration declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name))) @definition.function\n(declaration declarator: (reference_declarator (function_declarator declarator: (operator_name) @name))) @definition.function\n\n; Class/struct data member fields (Address address; int count;)\n; Uses field_identifier to exclude method declarations (which use function_declarator)\n(field_declaration\n declarator: (field_identifier) @name) @definition.property\n\n; Pointer member fields (Address* address;)\n(field_declaration\n declarator: (pointer_declarator\n declarator: (field_identifier) @name)) @definition.property\n\n; Reference member fields (Address& address;)\n(field_declaration\n declarator: (reference_declarator\n (field_identifier) @name)) @definition.property\n\n; Inline class method declarations (inside class body, no body: void save();)\n; tree-sitter-cpp uses field_identifier (not identifier) for names inside class bodies\n(field_declaration declarator: (function_declarator declarator: [(field_identifier) (identifier) (operator_name)] @name)) @definition.method\n\n; Inline class method declarations returning a pointer (User* lookup();)\n(field_declaration declarator: (pointer_declarator declarator: (function_declarator declarator: [(field_identifier) (identifier)] @name))) @definition.method\n\n; Inline class method declarations returning a reference (User& lookup();)\n(field_declaration declarator: (reference_declarator (function_declarator declarator: [(field_identifier) (identifier) (operator_name)] @name))) @definition.method\n\n; Inline class method definitions (inside class body, with body: void Foo() { ... })\n(field_declaration_list\n (function_definition\n declarator: (function_declarator\n declarator: [(field_identifier) (identifier) (operator_name) (destructor_name)] @name)) @definition.method)\n\n; Inline class methods returning a pointer type (User* lookup(int id) { ... })\n(field_declaration_list\n (function_definition\n declarator: (pointer_declarator\n declarator: (function_declarator\n declarator: [(field_identifier) (identifier) (operator_name)] @name))) @definition.method)\n\n; Inline class methods returning a reference type (User& lookup(int id) { ... })\n(field_declaration_list\n (function_definition\n declarator: (reference_declarator\n (function_declarator\n declarator: [(field_identifier) (identifier) (operator_name)] @name))) @definition.method)\n\n; Templates\n(template_declaration (class_specifier name: (type_identifier) @name)) @definition.template\n(template_declaration\n (class_specifier\n name: (template_type\n (type_identifier) @name\n (template_argument_list) @template-arguments))) @definition.template\n(template_declaration (function_definition declarator: (function_declarator declarator: (identifier) @name))) @definition.template\n\n; Includes\n(preproc_include path: (_) @import.source) @import\n\n; Calls\n(call_expression function: (identifier) @call.name) @call\n(call_expression function: (field_expression field: (field_identifier) @call.name)) @call\n(call_expression function: (qualified_identifier name: (identifier) @call.name)) @call\n(call_expression function: (template_function name: (identifier) @call.name)) @call\n(binary_expression operator: \"+\" @call.name) @call\n(binary_expression operator: \"<<\" @call.name) @call\n\n; Constructor calls: new User()\n(new_expression type: (type_identifier) @call.name) @call\n\n; Variable declarations: int x = 5; or auto x = 5;\n(declaration\n declarator: (init_declarator\n declarator: (identifier) @name)) @definition.variable\n\n; Lambda bindings: `auto f = [](int x){ \u2026 };` binds a CALLABLE, so it emits\n; Function rather than Variable, matching TS/JS. This aligns the LABEL only \u2014\n; call resolution runs off the scope-resolution query, which still models the\n; binding as a value, so `f()` does not resolve here yet. Overlap with the\n; pattern above is collapsed by the parse-worker dedup (#2687).\n(declaration\n declarator: (init_declarator\n declarator: (identifier) @name\n value: (lambda_expression))) @definition.function\n\n; Structured bindings: auto [a, b] = makePair(); (one @name per bound identifier)\n(declaration\n declarator: (init_declarator\n declarator: (structured_binding_declarator\n (identifier) @name))) @definition.variable\n\n; Structured bindings, reference form: auto& [x, y] = tup;\n(declaration\n declarator: (init_declarator\n declarator: (reference_declarator\n (structured_binding_declarator\n (identifier) @name)))) @definition.variable\n\n; Write access: obj.field = value\n(assignment_expression\n left: (field_expression\n argument: (_) @assignment.receiver\n field: (field_identifier) @assignment.property)\n right: (_)) @assignment\n\n";
8
8
  export declare const CSHARP_QUERIES = "\n; Types\n(class_declaration name: (identifier) @name) @definition.class\n(interface_declaration name: (identifier) @name) @definition.interface\n(struct_declaration name: (identifier) @name) @definition.struct\n(enum_declaration name: (identifier) @name) @definition.enum\n(record_declaration name: (identifier) @name) @definition.record\n(delegate_declaration name: (identifier) @name) @definition.delegate\n\n; Namespaces (block form and C# 10+ file-scoped form)\n(namespace_declaration name: (identifier) @name) @definition.namespace\n(namespace_declaration name: (qualified_name) @name) @definition.namespace\n(file_scoped_namespace_declaration name: (identifier) @name) @definition.namespace\n(file_scoped_namespace_declaration name: (qualified_name) @name) @definition.namespace\n\n; Methods & Properties\n(method_declaration name: (identifier) @name) @definition.method\n(local_function_statement name: (identifier) @name) @definition.function\n(constructor_declaration name: (identifier) @name) @definition.constructor\n(property_declaration name: (identifier) @name) @definition.property\n\n; Primary constructors (C# 12): class User(string name, int age) { }\n(class_declaration name: (identifier) @name (parameter_list) @definition.constructor)\n(record_declaration name: (identifier) @name (parameter_list) @definition.constructor)\n\n; Using\n(using_directive (qualified_name) @import.source) @import\n(using_directive (identifier) @import.source) @import\n\n; Calls\n(invocation_expression function: (identifier) @call.name) @call\n(invocation_expression function: (member_access_expression name: (identifier) @call.name)) @call\n\n; Null-conditional method calls: user?.Save()\n; Parses as: invocation_expression \u2192 conditional_access_expression \u2192 member_binding_expression \u2192 identifier\n(invocation_expression\n function: (conditional_access_expression\n (member_binding_expression\n (identifier) @call.name))) @call\n\n; Constructor calls: new Foo() and new Foo { Props }\n(object_creation_expression type: (identifier) @call.name) @call\n\n; Target-typed new (C# 9): User u = new(\"x\", 5)\n(variable_declaration type: (identifier) @call.name (variable_declarator (implicit_object_creation_expression) @call))\n\n; Local variable declarations\n(local_declaration_statement\n (variable_declaration\n (variable_declarator\n (identifier) @name))) @definition.variable\n\n; Write access: obj.field = value\n(assignment_expression\n left: (member_access_expression\n expression: (_) @assignment.receiver\n name: (identifier) @assignment.property)\n right: (_)) @assignment\n\n; \u2500\u2500 Closure bindings (#2693) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n; A name bound to a closure literal IS a callable, so it emits Function rather\n; than a value label \u2014 matching TS/JS and the languages #2687 already covered.\n; The callable node is what callable-value-flow joins the binding to (by file,\n; line and name), which is what makes handler(1) resolve. Overlap with the value\n; rules above is collapsed by the parse-worker dedup, which ranks callable\n; highest (#2687).\n(variable_declarator\n (identifier) @name\n (lambda_expression)) @definition.function\n";
9
9
  export declare const RUST_QUERIES = "\n; Functions & Items\n(function_item name: (identifier) @name) @definition.function\n(function_signature_item name: (identifier) @name) @definition.function\n\n; Closure bound to a let: let handler = || target(1);\n; Emits the Function NODE. Without it a Rust closure binding had no graph node\n; at all, so it could be neither a call target nor a call source (#2699), which\n; made Rust the one exception to \"a closure bound to a name is a Function node\n; in every language\" (#2687).\n; Anchor note: this channel puts @definition.function on the OUTER\n; let_declaration, which is the OPPOSITE of the scope-resolution channel in\n; languages/rust/query.ts (inner closure_expression, to align with\n; @scope.function). Both match their own channel's convention -- compare the\n; (lexical_declaration (variable_declarator ... (arrow_function))) rule above.\n(let_declaration\n pattern: (identifier) @name\n value: (closure_expression)) @definition.function\n(struct_item name: (type_identifier) @name) @definition.struct\n; A union is materialized as a Struct node (same rationale as the\n; scope-resolution @declaration.struct in languages/rust/query.ts: every\n; registry-primary resolution gate includes Struct but excludes Union, so a\n; Union-labeled node would be an unresolvable orphan). #1934 F71.\n(union_item name: (type_identifier) @name) @definition.struct\n(enum_item name: (type_identifier) @name) @definition.enum\n(trait_item name: (type_identifier) @name) @definition.trait\n(impl_item type: (type_identifier) @name !trait) @definition.impl\n(impl_item type: (generic_type type: (type_identifier) @name) !trait) @definition.impl\n; Scoped inherent impl: impl path::Type { ... }. Key the Impl node by the full\n; scoped_type_identifier text so it matches the owner id (also full text) and\n; stays distinct from a same-tail type in another module (#1975).\n(impl_item type: (scoped_type_identifier) @name !trait) @definition.impl\n(mod_item name: (identifier) @name) @definition.module\n\n; Type aliases, const, static, macros\n(type_item name: (type_identifier) @name) @definition.type\n(const_item name: (identifier) @name) @definition.const\n(static_item name: (identifier) @name) @definition.static\n(macro_definition name: (identifier) @name) @definition.macro\n\n; Use statements\n(use_declaration argument: (_) @import.source) @import\n\n; Calls\n(call_expression function: (identifier) @call.name) @call\n(call_expression function: (field_expression field: (field_identifier) @call.name)) @call\n(call_expression function: (scoped_identifier name: (identifier) @call.name)) @call\n(call_expression function: (generic_function function: (identifier) @call.name)) @call\n\n; Struct literal construction: User { name: value }\n(struct_expression name: (type_identifier) @call.name) @call\n\n; Struct fields \u2014 named field declarations inside struct bodies\n(field_declaration_list\n (field_declaration\n name: (field_identifier) @name) @definition.property)\n\n; Write access: obj.field = value\n(assignment_expression\n left: (field_expression\n value: (_) @assignment.receiver\n field: (field_identifier) @assignment.property)\n right: (_)) @assignment\n\n; Write access: obj.field += value (compound assignment)\n(compound_assignment_expr\n left: (field_expression\n value: (_) @assignment.receiver\n field: (field_identifier) @assignment.property)\n right: (_)) @assignment\n";
@@ -1114,8 +1114,15 @@ export const GO_QUERIES = `
1114
1114
  (method_elem name: (field_identifier) @name) @definition.method
1115
1115
 
1116
1116
  ; Types
1117
- (type_declaration (type_spec name: (type_identifier) @name type: (struct_type))) @definition.struct
1118
- (type_declaration (type_spec name: (type_identifier) @name type: (interface_type))) @definition.interface
1117
+ ;
1118
+ ; Anchored on the type_spec, NOT the enclosing type_declaration (#2837) — a
1119
+ ; grouped type ( A struct{}; B struct{} ) block otherwise gave every match the
1120
+ ; same capture node, and goClassConfig.extractName resolved all of them to the
1121
+ ; FIRST spec's name, collapsing the block to one node. Must stay in lockstep
1122
+ ; with @scope.class / @declaration.struct in languages/go/query.ts, which
1123
+ ; carries the full rationale. (No backticks here: this is a template literal.)
1124
+ (type_declaration (type_spec name: (type_identifier) @name type: (struct_type)) @definition.struct)
1125
+ (type_declaration (type_spec name: (type_identifier) @name type: (interface_type)) @definition.interface)
1119
1126
 
1120
1127
  ; Imports
1121
1128
  (import_declaration (import_spec path: (interpreted_string_literal) @import.source)) @import
@@ -775,20 +775,26 @@ getQualifiedOwnerName) => {
775
775
  }
776
776
  }
777
777
  }
778
- // Go: type_declaration wrapping a struct_type (type User struct { ... })
779
- if (current.type === 'type_declaration') {
780
- const typeSpec = current.children?.find((c) => c.type === 'type_spec');
781
- if (typeSpec) {
782
- const typeBody = typeSpec.childForFieldName?.('type');
783
- if (typeBody?.type === 'struct_type' || typeBody?.type === 'interface_type') {
784
- const nameNode = typeSpec.childForFieldName?.('name');
785
- if (nameNode) {
786
- const label = typeBody.type === 'struct_type' ? 'Struct' : 'Interface';
787
- return {
788
- classId: generateId(label, `${filePath}:${nameNode.text}`),
789
- className: nameNode.text,
790
- };
791
- }
778
+ // Go: the `type_spec` IS the declared type (`type User struct { ... }`, and
779
+ // one per member of a grouped `type ( A struct{…}; B struct{…} )` block).
780
+ //
781
+ // Matched here rather than on the enclosing `type_declaration` (#2837): this
782
+ // walk climbs `node.parent`, so it passes THROUGH the containing spec on its
783
+ // way up from any member, and the structure it already has is the answer.
784
+ // Keying on the wrapper instead meant picking one spec out of several with
785
+ // no reference point — which filed every member of a grouped block under its
786
+ // FIRST struct, so two same-named fields minted one id and first-write-wins
787
+ // dropped the second.
788
+ if (current.type === 'type_spec') {
789
+ const typeBody = current.childForFieldName?.('type');
790
+ if (typeBody?.type === 'struct_type' || typeBody?.type === 'interface_type') {
791
+ const nameNode = current.childForFieldName?.('name');
792
+ if (nameNode) {
793
+ const label = typeBody.type === 'struct_type' ? 'Struct' : 'Interface';
794
+ return {
795
+ classId: generateId(label, `${filePath}:${nameNode.text}`),
796
+ className: nameNode.text,
797
+ };
792
798
  }
793
799
  }
794
800
  }
@@ -14,7 +14,7 @@ import { randomUUID } from 'node:crypto';
14
14
  import { retryRename } from '../storage/fs-atomic.js';
15
15
  import { acquireIndexLock } from '../storage/index-lock.js';
16
16
  import { runPipelineFromRepo } from './ingestion/pipeline.js';
17
- import { summarizeUnresolvedReceivers } from './ingestion/scope-resolution/unresolved-receivers.js';
17
+ import { logUnresolvedReceiverFiles, summarizeUnresolvedReceivers, } from './ingestion/scope-resolution/unresolved-receivers.js';
18
18
  import { resetDegradedParseCounter } from './tree-sitter/safe-parse.js';
19
19
  import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, closeLbugBeforeExit, loadCachedEmbeddings, deleteNodesForFiles, ensureEmbeddingRowDmlSafe, deleteAllCommunitiesAndProcesses, deleteAllInterprocTaintPaths, deleteAllCallSummaries, deleteAllInjects, deleteAllAdvisedBy, deleteSpringAopEvidenceNodes, deleteSpringAutoConfigurationDeclarations, deleteSpringAutoConfigurationSyntheticClasses, queryImportersBatch, loadFTSExtension, wipeLbugDbFiles, LbugWipeError, DELETE_FILES_CHUNK_SIZE, } from './lbug/lbug-adapter.js';
20
20
  import { estimateBufferPool, setBufferPoolSizeHint, resolveNativeSafeStorageDir, } from './lbug/lbug-config.js';
@@ -2218,6 +2218,8 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
2218
2218
  const newFileHashesRecord = {};
2219
2219
  for (const [k, v] of newFileHashes)
2220
2220
  newFileHashesRecord[k] = v;
2221
+ const resolutionOutcomes = pipelineResult.resolutionOutcomes ?? [];
2222
+ logUnresolvedReceiverFiles(resolutionOutcomes);
2221
2223
  // Annotated so the capabilities stamp below is compile-checked against
2222
2224
  // RepoMeta's status unions (tri-review 4669518496 P1/U3) — an unannotated
2223
2225
  // literal widens the vectorSearch.status ternary to `string` and the
@@ -2286,7 +2288,7 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
2286
2288
  // Derived digest of the DDL this run created the tables from (#2798).
2287
2289
  // Git-only: non-git repos never take the incremental path.
2288
2290
  schemaFingerprint: hasGitDir(repoPath) ? SCHEMA_FINGERPRINT : undefined,
2289
- unresolvedReceiverMembers: summarizeUnresolvedReceivers(pipelineResult.resolutionOutcomes ?? []),
2291
+ unresolvedReceiverMembers: summarizeUnresolvedReceivers(resolutionOutcomes),
2290
2292
  analysisFeatures: currentAnalysisFeatures,
2291
2293
  // Always stamped with the live resolved mode (#2331/#2339) — unlike
2292
2294
  // `pdg` below, 'none' is a meaningful value to compare, not an
@@ -225,7 +225,26 @@ import { fileURLToPath } from 'url';
225
225
  // cache would serve entries that are missing those matches entirely — the
226
226
  // exact failure a bump exists to prevent. Verified against origin/main at
227
227
  // a857f4c5a, which is still on 43, so 44 is free. RE-CHECK BEFORE MERGE.
228
- const SCHEMA_BUMP = 44;
228
+ // 44 -> 45: #2837 re-anchors Go's struct/interface captures from the
229
+ // `type_declaration` onto the `type_spec` (`languages/go/query.ts`
230
+ // @scope.class/@declaration.struct/@declaration.interface, and GO_QUERIES
231
+ // @definition.struct/@definition.interface in `tree-sitter-queries.ts`). Every
232
+ // Go file declaring a type therefore emits DIFFERENT capture ranges — measured:
233
+ // 70 fixture digests moved with zero change in capture COUNT — and a grouped
234
+ // `type (...)` block emits nodes it previously did not emit at all. Parse-time,
235
+ // so a warm cache would replay pre-fix ParsedFiles and the fix would be a silent
236
+ // no-op on every incremental analyze while still passing every cold-run test.
237
+ //
238
+ // This branch originally took 44 and it COLLIDED: #2842 above merged first and
239
+ // claimed it. The ninth entry in this ledger, and the third EXACT clash. Worth
240
+ // recording HOW it was caught, because the pin test cannot catch it — both PRs
241
+ // asserted `toBe(44)`, which passes even when main is already 44, so the two
242
+ // capture schemas would have shared one PARSE_CACHE_VERSION and the durable
243
+ // ParsedFile store would have replayed pre-fix ParsedFiles verbatim for one of
244
+ // them. Only comparing against origin/main at MERGE time surfaces it.
245
+ // PR #2840 (Objective-C, draft) still claims 44 as well — it must move too.
246
+ // RE-CHECK AGAINST origin/main IMMEDIATELY BEFORE MERGING.
247
+ const SCHEMA_BUMP = 45;
229
248
  const GITNEXUS_PKG_VERSION = (() => {
230
249
  try {
231
250
  // package.json sits at gitnexus/package.json — two levels up from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.10-rc.166",
3
+ "version": "1.6.10-rc.167",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",