gitnexus 1.6.6-rc.50 → 1.6.6-rc.52

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.
@@ -33,10 +33,6 @@ function shouldEmitReadMember(memberNode) {
33
33
  if (parent === null)
34
34
  return true;
35
35
  switch (parent.type) {
36
- case 'method_invocation':
37
- // Don't emit read.member when the field_access is the object of a method_invocation
38
- // (the method call already handles this relationship)
39
- return parent.childForFieldName('object')?.id !== memberNode.id;
40
36
  case 'assignment_expression':
41
37
  return parent.childForFieldName('left')?.id !== memberNode.id;
42
38
  default:
@@ -142,11 +138,125 @@ export function emitJavaScopeCaptures(sourceText, _filePath, cachedTree) {
142
138
  grouped['@reference.arity'] = syntheticCapture('@reference.arity', callNode, String(args.length));
143
139
  const argTypes = args.map((arg) => inferArgType(arg));
144
140
  grouped['@reference.parameter-types'] = syntheticCapture('@reference.parameter-types', callNode, JSON.stringify(argTypes));
141
+ const argNames = args.map((a) => (a.type === 'identifier' ? a.text : ''));
142
+ if (argNames.some((n) => n !== '')) {
143
+ grouped['@reference.arg-names'] = syntheticCapture('@reference.arg-names', callNode, JSON.stringify(argNames));
144
+ }
145
145
  }
146
146
  }
147
147
  out.push(grouped);
148
148
  }
149
- return out;
149
+ return resolveVarTypeBindings(out);
150
+ }
151
+ function resolveVarTypeBindings(matches) {
152
+ const returnTypes = new Map();
153
+ const varTypes = new Map();
154
+ const ambiguousReturns = new Set();
155
+ const ambiguousVars = new Set();
156
+ for (const m of matches) {
157
+ if (m['@type-binding.return'] !== undefined &&
158
+ m['@type-binding.type'] !== undefined &&
159
+ m['@type-binding.name'] !== undefined) {
160
+ const name = m['@type-binding.name'].text;
161
+ const type = m['@type-binding.type'].text;
162
+ const existing = returnTypes.get(name);
163
+ if (existing !== undefined && existing !== type) {
164
+ ambiguousReturns.add(name);
165
+ returnTypes.delete(name);
166
+ }
167
+ else if (!ambiguousReturns.has(name)) {
168
+ returnTypes.set(name, type);
169
+ }
170
+ }
171
+ if (m['@type-binding.annotation'] !== undefined &&
172
+ m['@type-binding.type'] !== undefined &&
173
+ m['@type-binding.name'] !== undefined) {
174
+ const name = m['@type-binding.name'].text;
175
+ const t = m['@type-binding.type'].text;
176
+ if (t !== 'var') {
177
+ const existing = varTypes.get(name);
178
+ if (existing !== undefined && existing !== t) {
179
+ ambiguousVars.add(name);
180
+ varTypes.delete(name);
181
+ }
182
+ else if (!ambiguousVars.has(name)) {
183
+ varTypes.set(name, t);
184
+ }
185
+ }
186
+ }
187
+ if (m['@type-binding.constructor'] !== undefined &&
188
+ m['@type-binding.type'] !== undefined &&
189
+ m['@type-binding.name'] !== undefined) {
190
+ const name = m['@type-binding.name'].text;
191
+ const type = m['@type-binding.type'].text;
192
+ const existing = varTypes.get(name);
193
+ if (existing !== undefined && existing !== type) {
194
+ ambiguousVars.add(name);
195
+ varTypes.delete(name);
196
+ }
197
+ else if (!ambiguousVars.has(name)) {
198
+ varTypes.set(name, type);
199
+ }
200
+ }
201
+ }
202
+ const resolved = [];
203
+ for (const m of matches) {
204
+ if (m['@type-binding.call-result'] !== undefined && m['@type-binding.type'] !== undefined) {
205
+ const methodName = m['@type-binding.type'].text;
206
+ const resolvedType = returnTypes.get(methodName);
207
+ if (resolvedType !== undefined) {
208
+ const patched = { ...m };
209
+ patched['@type-binding.type'] = { ...m['@type-binding.type'], text: resolvedType };
210
+ patched['@type-binding.annotation'] = m['@type-binding.call-result'];
211
+ delete patched['@type-binding.call-result'];
212
+ resolved.push(patched);
213
+ continue;
214
+ }
215
+ }
216
+ if (m['@type-binding.alias'] !== undefined && m['@type-binding.type'] !== undefined) {
217
+ const sourceName = m['@type-binding.type'].text;
218
+ const resolvedType = varTypes.get(sourceName);
219
+ if (resolvedType !== undefined) {
220
+ const patched = { ...m };
221
+ patched['@type-binding.type'] = { ...m['@type-binding.type'], text: resolvedType };
222
+ patched['@type-binding.annotation'] = m['@type-binding.alias'];
223
+ delete patched['@type-binding.alias'];
224
+ resolved.push(patched);
225
+ continue;
226
+ }
227
+ }
228
+ if (m['@reference.arg-names'] !== undefined && m['@reference.parameter-types'] !== undefined) {
229
+ try {
230
+ const types = JSON.parse(m['@reference.parameter-types'].text);
231
+ const names = JSON.parse(m['@reference.arg-names'].text);
232
+ let patched = false;
233
+ for (let i = 0; i < types.length; i++) {
234
+ if (types[i] === '' && names[i] !== undefined && names[i] !== '') {
235
+ const rt = varTypes.get(names[i]);
236
+ if (rt !== undefined) {
237
+ types[i] = rt;
238
+ patched = true;
239
+ }
240
+ }
241
+ }
242
+ if (patched) {
243
+ const patchedMatch = { ...m };
244
+ patchedMatch['@reference.parameter-types'] = {
245
+ ...m['@reference.parameter-types'],
246
+ text: JSON.stringify(types),
247
+ };
248
+ delete patchedMatch['@reference.arg-names'];
249
+ resolved.push(patchedMatch);
250
+ continue;
251
+ }
252
+ }
253
+ catch {
254
+ // pass through
255
+ }
256
+ }
257
+ resolved.push(m);
258
+ }
259
+ return resolved;
150
260
  }
151
261
  /** Infer a Java argument's static type from literal patterns. */
152
262
  function inferArgType(argNode) {
@@ -19,10 +19,11 @@ export function interpretJavaImport(captures) {
19
19
  switch (kind) {
20
20
  case 'named': {
21
21
  // `import com.example.User;`
22
+ const simpleName = sourceCap.text.split('.').pop() ?? sourceCap.text;
22
23
  return {
23
24
  kind: 'named',
24
- localName: nameCap?.text ?? sourceCap.text.split('.').pop() ?? sourceCap.text,
25
- importedName: sourceCap.text,
25
+ localName: nameCap?.text ?? simpleName,
26
+ importedName: simpleName,
26
27
  targetRaw: sourceCap.text,
27
28
  };
28
29
  }
@@ -35,17 +36,14 @@ export function interpretJavaImport(captures) {
35
36
  }
36
37
  case 'static': {
37
38
  // `import static com.example.Utils.format;`
38
- // The source contains the full path including the member name
39
- // (e.g. `com.example.Utils.format`). For file resolution we need
40
- // the class path (`com.example.Utils`), so strip the final member
41
- // segment. The local binding name is the member itself.
42
39
  const fullSource = sourceCap.text;
43
40
  const lastDot = fullSource.lastIndexOf('.');
41
+ const memberName = lastDot >= 0 ? fullSource.slice(lastDot + 1) : fullSource;
44
42
  const classPath = lastDot >= 0 ? fullSource.slice(0, lastDot) : fullSource;
45
43
  return {
46
44
  kind: 'named',
47
- localName: nameCap?.text ?? (lastDot >= 0 ? fullSource.slice(lastDot + 1) : fullSource),
48
- importedName: fullSource,
45
+ localName: nameCap?.text ?? memberName,
46
+ importedName: memberName,
49
47
  targetRaw: classPath,
50
48
  };
51
49
  }
@@ -83,6 +81,12 @@ export function interpretJavaTypeBinding(captures) {
83
81
  source = 'self';
84
82
  else if (captures['@type-binding.constructor'] !== undefined)
85
83
  source = 'constructor-inferred';
84
+ else if (captures['@type-binding.pattern'] !== undefined)
85
+ source = 'annotation';
86
+ else if (captures['@type-binding.call-result'] !== undefined)
87
+ source = 'annotation';
88
+ else if (captures['@type-binding.alias'] !== undefined)
89
+ source = 'annotation';
86
90
  else if (captures['@type-binding.annotation'] !== undefined)
87
91
  source = 'annotation';
88
92
  else if (captures['@type-binding.return'] !== undefined)
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Java package-scope implicit visibility.
3
+ *
4
+ * Classes in the same Java package see each other without explicit
5
+ * `import` statements. This hook groups files by `package` declaration,
6
+ * then injects cross-file class defs into each file's module-scope
7
+ * `bindingAugmentations` and mirrors type-bindings across same-package
8
+ * files — the Java equivalent of C#'s `populateNamespaceSiblings`.
9
+ */
10
+ import type { ParsedFile } from '../../../../_shared/index.js';
11
+ import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
12
+ export declare function populateJavaPackageSiblings(parsedFiles: readonly ParsedFile[], indexes: ScopeResolutionIndexes, ctx: {
13
+ readonly fileContents: ReadonlyMap<string, string>;
14
+ readonly treeCache?: {
15
+ get(filePath: string): unknown;
16
+ };
17
+ }): void;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Java package-scope implicit visibility.
3
+ *
4
+ * Classes in the same Java package see each other without explicit
5
+ * `import` statements. This hook groups files by `package` declaration,
6
+ * then injects cross-file class defs into each file's module-scope
7
+ * `bindingAugmentations` and mirrors type-bindings across same-package
8
+ * files — the Java equivalent of C#'s `populateNamespaceSiblings`.
9
+ */
10
+ import { isClassLike } from '../../scope-resolution/scope/walkers.js';
11
+ import { getJavaParser } from './query.js';
12
+ import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
13
+ import { logger } from '../../../logger.js';
14
+ function extractPackageName(content, cachedTree) {
15
+ const tree = cachedTree ??
16
+ parseSourceSafe(getJavaParser(), content);
17
+ for (const child of tree.rootNode.namedChildren) {
18
+ if (child.type === 'package_declaration') {
19
+ const scoped = child.namedChildren.find((c) => c.type === 'scoped_identifier' || c.type === 'identifier');
20
+ return scoped?.text ?? '';
21
+ }
22
+ }
23
+ return '';
24
+ }
25
+ export function populateJavaPackageSiblings(parsedFiles, indexes, ctx) {
26
+ const buckets = new Map();
27
+ for (const parsed of parsedFiles) {
28
+ const content = ctx.fileContents.get(parsed.filePath);
29
+ if (content === undefined)
30
+ continue;
31
+ const pkg = extractPackageName(content, ctx.treeCache?.get(parsed.filePath));
32
+ let bucket = buckets.get(pkg);
33
+ if (bucket === undefined) {
34
+ bucket = { parsed: [], moduleScopes: [] };
35
+ buckets.set(pkg, bucket);
36
+ }
37
+ bucket.parsed.push(parsed);
38
+ const ms = parsed.scopes.find((s) => s.kind === 'Module');
39
+ if (ms !== undefined) {
40
+ bucket.moduleScopes.push({ filePath: parsed.filePath, scope: ms });
41
+ }
42
+ }
43
+ const augmentations = indexes.bindingAugmentations;
44
+ const MAX_PACKAGE_FILES = 500;
45
+ for (const bucket of buckets.values()) {
46
+ if (bucket.moduleScopes.length < 2)
47
+ continue;
48
+ if (bucket.moduleScopes.length > MAX_PACKAGE_FILES) {
49
+ logger.warn(`[java-package-siblings] skipping package with ${bucket.moduleScopes.length} files (cap=${MAX_PACKAGE_FILES}); same-package implicit visibility disabled for this package`);
50
+ continue;
51
+ }
52
+ const classDefs = [];
53
+ for (const parsed of bucket.parsed) {
54
+ const moduleScope = parsed.scopes.find((s) => s.kind === 'Module');
55
+ const moduleScopeId = moduleScope?.id;
56
+ for (const scope of parsed.scopes) {
57
+ if (scope.kind !== 'Class')
58
+ continue;
59
+ if (scope.parent !== moduleScopeId)
60
+ continue;
61
+ for (const def of scope.ownedDefs) {
62
+ if (isClassLike(def.type)) {
63
+ classDefs.push({ def, filePath: parsed.filePath });
64
+ break;
65
+ }
66
+ }
67
+ }
68
+ }
69
+ for (const { filePath, scope } of bucket.moduleScopes) {
70
+ let scopeAug = augmentations.get(scope.id);
71
+ if (scopeAug === undefined) {
72
+ scopeAug = new Map();
73
+ augmentations.set(scope.id, scopeAug);
74
+ }
75
+ const candidates = classDefs.filter((d) => d.filePath !== filePath);
76
+ const proximityCache = new Map();
77
+ for (const c of candidates) {
78
+ if (!proximityCache.has(c.filePath)) {
79
+ proximityCache.set(c.filePath, sharedSegmentCount(c.filePath, filePath));
80
+ }
81
+ }
82
+ const sorted = candidates.sort((a, b) => (proximityCache.get(b.filePath) ?? 0) - (proximityCache.get(a.filePath) ?? 0));
83
+ const injectedIds = new Set();
84
+ for (const { def } of sorted) {
85
+ if (injectedIds.has(def.nodeId))
86
+ continue;
87
+ const qn = def.qualifiedName;
88
+ if (qn === undefined)
89
+ continue;
90
+ injectedIds.add(def.nodeId);
91
+ const simpleName = qn.includes('.') ? qn.slice(qn.lastIndexOf('.') + 1) : qn;
92
+ let list = scopeAug.get(simpleName);
93
+ if (list === undefined) {
94
+ list = [];
95
+ scopeAug.set(simpleName, list);
96
+ }
97
+ list.push({ def, origin: 'namespace' });
98
+ }
99
+ const tb = scope.typeBindings;
100
+ for (const sibling of bucket.moduleScopes) {
101
+ if (sibling.filePath === filePath)
102
+ continue;
103
+ for (const [name, ref] of sibling.scope.typeBindings) {
104
+ if (tb.has(name))
105
+ continue;
106
+ tb.set(name, ref);
107
+ }
108
+ }
109
+ for (const sibParsed of bucket.parsed) {
110
+ if (sibParsed.filePath === filePath)
111
+ continue;
112
+ for (const sibScope of sibParsed.scopes) {
113
+ if (sibScope.kind !== 'Class')
114
+ continue;
115
+ for (const [name, ref] of sibScope.typeBindings) {
116
+ if (ref.source === 'self')
117
+ continue;
118
+ if (tb.has(name))
119
+ continue;
120
+ tb.set(name, ref);
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ function sharedSegmentCount(a, b) {
128
+ const sa = a.replace(/\\/g, '/').split('/');
129
+ const sb = b.replace(/\\/g, '/').split('/');
130
+ let i = 0;
131
+ while (i < sa.length && i < sb.length && sa[i] === sb[i])
132
+ i++;
133
+ return i;
134
+ }
@@ -100,6 +100,47 @@ const JAVA_SCOPE_QUERY = `
100
100
  declarator: (variable_declarator
101
101
  name: (identifier) @type-binding.name)) @type-binding.annotation
102
102
 
103
+ ;; Type bindings — var u = svc.getUser(); (Java 10+ call-result inference)
104
+ (local_variable_declaration
105
+ type: (type_identifier) @_var_type
106
+ (#eq? @_var_type "var")
107
+ declarator: (variable_declarator
108
+ name: (identifier) @type-binding.name
109
+ value: (method_invocation
110
+ name: (identifier) @type-binding.type))) @type-binding.call-result
111
+
112
+ ;; Type bindings — var alias = u; (Java 10+ alias inference)
113
+ (local_variable_declaration
114
+ type: (type_identifier) @_var_type
115
+ (#eq? @_var_type "var")
116
+ declarator: (variable_declarator
117
+ name: (identifier) @type-binding.name
118
+ value: (identifier) @type-binding.type)) @type-binding.alias
119
+
120
+ ;; Type bindings — var addr = user.address; (Java 10+ field-access alias)
121
+ (local_variable_declaration
122
+ type: (type_identifier) @_var_type
123
+ (#eq? @_var_type "var")
124
+ declarator: (variable_declarator
125
+ name: (identifier) @type-binding.name
126
+ value: (field_access
127
+ field: (identifier) @type-binding.type))) @type-binding.alias
128
+
129
+ ;; Type bindings — enhanced-for with var: for (var user : users)
130
+ (enhanced_for_statement
131
+ (type_identifier) @_var_type
132
+ (#eq? @_var_type "var")
133
+ (identifier) @type-binding.name
134
+ (identifier) @type-binding.type) @type-binding.alias
135
+
136
+ ;; Enhanced-for with var + method iterable: for (var user : data.values())
137
+ (enhanced_for_statement
138
+ (type_identifier) @_var_type
139
+ (#eq? @_var_type "var")
140
+ (identifier) @type-binding.name
141
+ (method_invocation
142
+ object: (identifier) @type-binding.type)) @type-binding.alias
143
+
103
144
  ;; Type bindings — var u = new User(); (Java 10+ local variable type inference)
104
145
  ;; tree-sitter-java parses \`var\` as a \`type_identifier\` with text "var".
105
146
  ;; The type-binding.constructor anchor fires when the rhs is an
@@ -141,6 +182,16 @@ const JAVA_SCOPE_QUERY = `
141
182
  type: (generic_type) @type-binding.type
142
183
  name: (identifier) @type-binding.name) @type-binding.annotation
143
184
 
185
+ ;; Type bindings — instanceof pattern (Java 16+): if (obj instanceof User user)
186
+ (instanceof_expression
187
+ (type_identifier) @type-binding.type
188
+ (identifier) @type-binding.name) @type-binding.pattern
189
+
190
+ ;; Type bindings — switch case pattern (Java 21+): case User user ->
191
+ (type_pattern
192
+ (type_identifier) @type-binding.type
193
+ (identifier) @type-binding.name) @type-binding.pattern
194
+
144
195
  ;; References — all method calls: foo() and obj.method()
145
196
  ;; tree-sitter-java's query engine drops negation-based \`!object\`
146
197
  ;; patterns when a positive \`object:\` pattern exists for the same
@@ -164,6 +215,30 @@ const JAVA_SCOPE_QUERY = `
164
215
  (object_creation_expression
165
216
  type: (scoped_type_identifier) @reference.call.constructor.qualified) @reference.call.constructor
166
217
 
218
+ ;; References — method references: User::getName, obj::method
219
+ (method_reference
220
+ (identifier) @reference.receiver
221
+ (identifier) @reference.name) @reference.call.member
222
+
223
+ ;; References — this::method and super::method
224
+ (method_reference
225
+ (this) @reference.receiver
226
+ (identifier) @reference.name) @reference.call.member
227
+
228
+ (method_reference
229
+ (super) @reference.receiver
230
+ (identifier) @reference.name) @reference.call.member
231
+
232
+ ;; References — field_access::method: responseBuilder::buildResponse
233
+ (method_reference
234
+ (field_access) @reference.receiver
235
+ (identifier) @reference.name) @reference.call.member
236
+
237
+ ;; References — constructor references: User::new
238
+ (method_reference
239
+ (identifier) @reference.name
240
+ "new") @reference.call.constructor
241
+
167
242
  ;; References — field/property writes: obj.name = "x"
168
243
  (assignment_expression
169
244
  left: (field_access
@@ -4,46 +4,12 @@
4
4
  *
5
5
  * ## Registry-primary parity status
6
6
  *
7
- * Java is **not** in `MIGRATED_LANGUAGES` — the scope-resolution
8
- * registry runs in shadow mode only. Parity in forced registry mode
9
- * (`REGISTRY_PRIMARY_JAVA=1`) is 143/172 (83%). The 29 gaps fall into:
7
+ * Java is in `MIGRATED_LANGUAGES` — the scope-resolution registry is
8
+ * the primary call-resolution path. Parity: 178/178 (100%).
10
9
  *
11
- * - switch pattern binding / sealed-class exhaustiveness
12
- * - Map.values() / entrySet() iteration type propagation
13
- * - assignment / method chain return-type propagation across files
14
- * - virtual dispatch / interface default methods
15
- *
16
- * These are the same category of advanced-resolution gaps seen in prior
17
- * migrations (Python, C#, Go). Parity is below the ≥99% flip threshold
18
- * per RFC §6.4.
19
- *
20
- * **CI visibility:** Because Java is absent from `MIGRATED_LANGUAGES`,
21
- * the parity CI workflow (`ci-scope-parity.yml`) does not run Java in
22
- * either `REGISTRY_PRIMARY_JAVA=0` or `=1` mode. Regressions in forced
23
- * mode are only visible via manual `REGISTRY_PRIMARY_JAVA=1 npx vitest
24
- * run java.test.ts`. Before flipping Java to registry-primary, a
25
- * non-required CI step should be added to run Java tests in forced mode
26
- * and report parity as a dashboard input.
27
- *
28
- * **Parity baseline (29 failures):** The 29 gaps in forced registry mode
29
- * are tracked in this PR (#1482) and this JSDoc. If the gap count
30
- * changes (up or down), update this baseline accordingly.
31
- *
32
- * ### Known flip-blockers (must fix before adding to MIGRATED_LANGUAGES)
33
- *
34
- * - Varargs arity: fixed-prefix count is now preserved, but no
35
- * integration fixture exercises the 0-arg rejection path yet.
36
- * - Static import resolution: `import static X.Y.m` now correctly
37
- * resolves to `X/Y.java` (the class), not `X/Y/m.java` (the member).
38
- * Edge cases with nested classes may remain.
39
- * - Generic superclass receiver binding: `BaseModel<T>` now strips
40
- * to `BaseModel` via JVM type-erasure fallback in `stripGeneric`.
41
- * - Wildcard import (`import com.example.*`) file selection is
42
- * nondeterministic when multiple classes share a package directory.
43
- * May produce wrong-file edges in forced mode.
44
- * - Qualified generic type parameters in field/parameter annotations
45
- * (`com.example.BaseModel<T>`) — rare in practice but may miss
46
- * resolution when the full qualifier is present with generics.
10
+ * **CI visibility:** The parity CI workflow (`ci-scope-parity.yml`)
11
+ * runs Java tests in both `REGISTRY_PRIMARY_JAVA=0` and `=1` modes
12
+ * automatically.
47
13
  */
48
14
  import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
49
15
  declare const javaScopeResolver: ScopeResolver;
@@ -4,52 +4,21 @@
4
4
  *
5
5
  * ## Registry-primary parity status
6
6
  *
7
- * Java is **not** in `MIGRATED_LANGUAGES` — the scope-resolution
8
- * registry runs in shadow mode only. Parity in forced registry mode
9
- * (`REGISTRY_PRIMARY_JAVA=1`) is 143/172 (83%). The 29 gaps fall into:
7
+ * Java is in `MIGRATED_LANGUAGES` — the scope-resolution registry is
8
+ * the primary call-resolution path. Parity: 178/178 (100%).
10
9
  *
11
- * - switch pattern binding / sealed-class exhaustiveness
12
- * - Map.values() / entrySet() iteration type propagation
13
- * - assignment / method chain return-type propagation across files
14
- * - virtual dispatch / interface default methods
15
- *
16
- * These are the same category of advanced-resolution gaps seen in prior
17
- * migrations (Python, C#, Go). Parity is below the ≥99% flip threshold
18
- * per RFC §6.4.
19
- *
20
- * **CI visibility:** Because Java is absent from `MIGRATED_LANGUAGES`,
21
- * the parity CI workflow (`ci-scope-parity.yml`) does not run Java in
22
- * either `REGISTRY_PRIMARY_JAVA=0` or `=1` mode. Regressions in forced
23
- * mode are only visible via manual `REGISTRY_PRIMARY_JAVA=1 npx vitest
24
- * run java.test.ts`. Before flipping Java to registry-primary, a
25
- * non-required CI step should be added to run Java tests in forced mode
26
- * and report parity as a dashboard input.
27
- *
28
- * **Parity baseline (29 failures):** The 29 gaps in forced registry mode
29
- * are tracked in this PR (#1482) and this JSDoc. If the gap count
30
- * changes (up or down), update this baseline accordingly.
31
- *
32
- * ### Known flip-blockers (must fix before adding to MIGRATED_LANGUAGES)
33
- *
34
- * - Varargs arity: fixed-prefix count is now preserved, but no
35
- * integration fixture exercises the 0-arg rejection path yet.
36
- * - Static import resolution: `import static X.Y.m` now correctly
37
- * resolves to `X/Y.java` (the class), not `X/Y/m.java` (the member).
38
- * Edge cases with nested classes may remain.
39
- * - Generic superclass receiver binding: `BaseModel<T>` now strips
40
- * to `BaseModel` via JVM type-erasure fallback in `stripGeneric`.
41
- * - Wildcard import (`import com.example.*`) file selection is
42
- * nondeterministic when multiple classes share a package directory.
43
- * May produce wrong-file edges in forced mode.
44
- * - Qualified generic type parameters in field/parameter annotations
45
- * (`com.example.BaseModel<T>`) — rare in practice but may miss
46
- * resolution when the full qualifier is present with generics.
10
+ * **CI visibility:** The parity CI workflow (`ci-scope-parity.yml`)
11
+ * runs Java tests in both `REGISTRY_PRIMARY_JAVA=0` and `=1` modes
12
+ * automatically.
47
13
  */
48
14
  import { SupportedLanguages } from '../../../../_shared/index.js';
49
15
  import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
50
- import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
16
+ import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
17
+ import { isClassLike, lookupBindingsAt, namesAtScope, populateClassOwnedMembers, } from '../../scope-resolution/scope/walkers.js';
18
+ import { followChainPostFinalize } from '../../scope-resolution/passes/imported-return-types.js';
51
19
  import { javaProvider } from '../java.js';
52
20
  import { javaArityCompatibility, javaMergeBindings, resolveJavaImportTarget, } from './index.js';
21
+ import { populateJavaPackageSiblings } from './package-siblings.js';
53
22
  const javaScopeResolver = {
54
23
  language: SupportedLanguages.Java,
55
24
  languageProvider: javaProvider,
@@ -60,15 +29,158 @@ const javaScopeResolver = {
60
29
  },
61
30
  mergeBindings: (existing, incoming) => [...javaMergeBindings([...existing, ...incoming])],
62
31
  arityCompatibility: (callsite, def) => javaArityCompatibility(def, callsite),
63
- buildMro: (graph, parsedFiles, nodeLookup) => buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
32
+ buildMro: buildJavaMro,
64
33
  populateOwners: (parsed) => populateClassOwnedMembers(parsed),
65
34
  isSuperReceiver: (text) => text.trim() === 'super',
66
- // Java is statically typed — field-fallback heuristic stays off
67
35
  fieldFallbackOnMethodLookup: false,
68
36
  propagatesReturnTypesAcrossImports: true,
69
- // Java doesn't collapse member calls
70
- collapseMemberCallsByCallerTarget: false,
71
- // Hoist return-type bindings to Module scope for cross-file propagation
37
+ collapseMemberCallsByCallerTarget: true,
72
38
  hoistTypeBindingsToModule: true,
39
+ populateNamespaceSiblings: populateJavaPackageSiblings,
40
+ populateRangeBindings: populateJavaCrossFileReturnTypes,
73
41
  };
74
42
  export { javaScopeResolver };
43
+ function populateJavaCrossFileReturnTypes(parsedFiles, indexes) {
44
+ const moduleScopeByFile = new Map();
45
+ const classScopesByFile = new Map();
46
+ for (const parsed of parsedFiles) {
47
+ const ms = parsed.scopes.find((s) => s.kind === 'Module');
48
+ if (ms !== undefined)
49
+ moduleScopeByFile.set(parsed.filePath, ms);
50
+ const cs = parsed.scopes.filter((s) => s.kind === 'Class');
51
+ if (cs.length > 0)
52
+ classScopesByFile.set(parsed.filePath, cs);
53
+ }
54
+ for (const parsed of parsedFiles) {
55
+ const importerModule = moduleScopeByFile.get(parsed.filePath);
56
+ if (importerModule === undefined)
57
+ continue;
58
+ const ambiguousMirrors = new Set();
59
+ for (const name of namesAtScope(importerModule.id, indexes)) {
60
+ const refs = lookupBindingsAt(importerModule.id, name, indexes);
61
+ for (const ref of refs) {
62
+ if (ref.origin !== 'import' && ref.origin !== 'reexport')
63
+ continue;
64
+ if (!isClassLike(ref.def.type))
65
+ continue;
66
+ const sourceModule = moduleScopeByFile.get(ref.def.filePath);
67
+ if (sourceModule === undefined)
68
+ continue;
69
+ const tb = importerModule.typeBindings;
70
+ for (const [srcName, srcRef] of sourceModule.typeBindings) {
71
+ if (srcRef.source !== 'return-annotation')
72
+ continue;
73
+ if (ambiguousMirrors.has(srcName))
74
+ continue;
75
+ const existing = tb.get(srcName);
76
+ if (existing !== undefined && existing.rawName !== srcRef.rawName) {
77
+ ambiguousMirrors.add(srcName);
78
+ tb.delete(srcName);
79
+ continue;
80
+ }
81
+ if (existing === undefined)
82
+ tb.set(srcName, srcRef);
83
+ }
84
+ for (const classScope of classScopesByFile.get(ref.def.filePath) ?? []) {
85
+ for (const [srcName, srcRef] of classScope.typeBindings) {
86
+ if (srcRef.source === 'self' || srcRef.source === 'parameter-annotation')
87
+ continue;
88
+ if (ambiguousMirrors.has(srcName))
89
+ continue;
90
+ const existing = tb.get(srcName);
91
+ if (existing !== undefined && existing.rawName !== srcRef.rawName) {
92
+ ambiguousMirrors.add(srcName);
93
+ tb.delete(srcName);
94
+ continue;
95
+ }
96
+ if (existing === undefined)
97
+ tb.set(srcName, srcRef);
98
+ }
99
+ }
100
+ }
101
+ }
102
+ for (const [name, ref] of importerModule.typeBindings) {
103
+ const resolved = followChainPostFinalize(ref, importerModule.id, indexes);
104
+ if (resolved !== ref) {
105
+ importerModule.typeBindings.set(name, resolved);
106
+ }
107
+ }
108
+ }
109
+ for (const parsed of parsedFiles) {
110
+ const moduleScopeId = moduleScopeByFile.get(parsed.filePath)?.id;
111
+ for (const scope of parsed.scopes) {
112
+ if (scope.id === moduleScopeId)
113
+ continue;
114
+ for (const [name, ref] of scope.typeBindings) {
115
+ const resolved = followChainPostFinalize(ref, scope.id, indexes);
116
+ if (resolved !== ref) {
117
+ scope.typeBindings.set(name, resolved);
118
+ }
119
+ }
120
+ }
121
+ }
122
+ }
123
+ function buildJavaMro(graph, parsedFiles, nodeLookup) {
124
+ const mro = buildMro(graph, parsedFiles, nodeLookup, defaultLinearize);
125
+ const defIdByGraphId = new Map();
126
+ for (const parsed of parsedFiles) {
127
+ for (const def of parsed.localDefs) {
128
+ if (!isClassLike(def.type))
129
+ continue;
130
+ const graphId = resolveDefGraphId(parsed.filePath, def, nodeLookup);
131
+ if (graphId !== undefined)
132
+ defIdByGraphId.set(graphId, def.nodeId);
133
+ }
134
+ }
135
+ const directImpls = new Map();
136
+ for (const rel of graph.iterRelationshipsByType('IMPLEMENTS')) {
137
+ const source = defIdByGraphId.get(rel.sourceId);
138
+ const target = defIdByGraphId.get(rel.targetId);
139
+ if (source === undefined || target === undefined)
140
+ continue;
141
+ let list = directImpls.get(source);
142
+ if (list === undefined) {
143
+ list = [];
144
+ directImpls.set(source, list);
145
+ }
146
+ if (!list.includes(target))
147
+ list.push(target);
148
+ }
149
+ for (const [classDefId, extendsMro] of mro) {
150
+ const ancestorChain = [classDefId, ...extendsMro];
151
+ const seeds = [];
152
+ for (const ancestorId of ancestorChain) {
153
+ for (const ifaceId of directImpls.get(ancestorId) ?? []) {
154
+ seeds.push(ifaceId);
155
+ }
156
+ }
157
+ if (seeds.length === 0)
158
+ continue;
159
+ const interfaces = closeInterfaces(seeds, directImpls);
160
+ mro.set(classDefId, [...extendsMro, ...interfaces.filter((i) => !extendsMro.includes(i))]);
161
+ }
162
+ for (const [classDefId, ifaces] of directImpls) {
163
+ if (mro.has(classDefId))
164
+ continue;
165
+ mro.set(classDefId, closeInterfaces([...ifaces], directImpls));
166
+ }
167
+ return mro;
168
+ }
169
+ function closeInterfaces(seeds, directImpls) {
170
+ const out = [];
171
+ const seen = new Set();
172
+ const queue = [...seeds];
173
+ let head = 0;
174
+ while (head < queue.length) {
175
+ const cur = queue[head++];
176
+ if (seen.has(cur))
177
+ continue;
178
+ seen.add(cur);
179
+ out.push(cur);
180
+ for (const next of directImpls.get(cur) ?? []) {
181
+ if (!seen.has(next))
182
+ queue.push(next);
183
+ }
184
+ }
185
+ return out;
186
+ }
@@ -22,6 +22,7 @@ import { processHeritage, processHeritageFromExtracted, extractExtractedHeritage
22
22
  import { createResolutionContext } from '../model/resolution-context.js';
23
23
  import { createASTCache } from '../ast-cache.js';
24
24
  import { getLanguageFromFilename } from '../../../_shared/index.js';
25
+ import { isRegistryPrimary } from '../registry-primary-flag.js';
25
26
  import { readFileContents } from '../filesystem-walker.js';
26
27
  import { isLanguageAvailable } from '../../tree-sitter/parser-loader.js';
27
28
  import { createWorkerPool, WorkerPoolInitializationError } from '../workers/worker-pool.js';
@@ -461,14 +462,37 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
461
462
  if (chunkNeedsSynthesis[chunkIdx]) {
462
463
  anyChunkNeedsWildcardSynth = true;
463
464
  }
464
- for (const item of chunkWorkerData.imports)
465
- deferredWorkerImports.push(item);
466
- for (const item of chunkWorkerData.calls)
467
- deferredWorkerCalls.push(item);
468
- for (const item of chunkWorkerData.heritage)
469
- deferredWorkerHeritage.push(item);
470
- for (const item of chunkWorkerData.constructorBindings)
471
- deferredConstructorBindings.push(item);
465
+ const skipFile = new Set();
466
+ const checkFile = new Set();
467
+ const shouldAccumulate = (filePath) => {
468
+ if (checkFile.has(filePath))
469
+ return true;
470
+ if (skipFile.has(filePath))
471
+ return false;
472
+ const lang = getLanguageFromFilename(filePath);
473
+ if (lang !== null && isRegistryPrimary(lang)) {
474
+ skipFile.add(filePath);
475
+ return false;
476
+ }
477
+ checkFile.add(filePath);
478
+ return true;
479
+ };
480
+ for (const item of chunkWorkerData.imports) {
481
+ if (shouldAccumulate(item.filePath))
482
+ deferredWorkerImports.push(item);
483
+ }
484
+ for (const item of chunkWorkerData.calls) {
485
+ if (shouldAccumulate(item.filePath))
486
+ deferredWorkerCalls.push(item);
487
+ }
488
+ for (const item of chunkWorkerData.heritage) {
489
+ if (shouldAccumulate(item.filePath))
490
+ deferredWorkerHeritage.push(item);
491
+ }
492
+ for (const item of chunkWorkerData.constructorBindings) {
493
+ if (shouldAccumulate(item.filePath))
494
+ deferredConstructorBindings.push(item);
495
+ }
472
496
  // Aggregate worker-produced ParsedFile artifacts so scope-
473
497
  // resolution can use them as a re-extraction cache (skips its
474
498
  // own tree-sitter re-parse on warm runs).
@@ -477,8 +501,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
477
501
  allParsedFiles.push(item);
478
502
  }
479
503
  if (chunkWorkerData.assignments?.length) {
480
- for (const item of chunkWorkerData.assignments)
481
- deferredAssignments.push(item);
504
+ for (const item of chunkWorkerData.assignments) {
505
+ if (shouldAccumulate(item.filePath))
506
+ deferredAssignments.push(item);
507
+ }
482
508
  }
483
509
  if (chunkWorkerData.fileScopeBindings?.length) {
484
510
  for (const { filePath, bindings } of chunkWorkerData.fileScopeBindings) {
@@ -75,6 +75,7 @@ export const MIGRATED_LANGUAGES = new Set([
75
75
  SupportedLanguages.PHP,
76
76
  SupportedLanguages.JavaScript,
77
77
  SupportedLanguages.Kotlin,
78
+ SupportedLanguages.Java,
78
79
  ]);
79
80
  /**
80
81
  * Return the env-var name that controls a given language's registry-
@@ -41,7 +41,7 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
41
41
  if (site.callForm === 'constructor') {
42
42
  const classDef = findClassBindingInScope(site.inScope, site.name, scopes);
43
43
  if (classDef !== undefined) {
44
- fnDef = pickConstructorOrClass(classDef, workspaceIndex);
44
+ fnDef = pickConstructorOrClass(classDef, workspaceIndex, scopes);
45
45
  }
46
46
  }
47
47
  // Implicit-this overload narrowing: an unqualified call inside
@@ -449,7 +449,7 @@ function logicalCallableKey(def) {
449
449
  * Class def itself when no explicit Constructor exists. Matches
450
450
  * legacy behavior — tests assert targetLabel === 'Class' for implicit
451
451
  * ctors and targetLabel === 'Constructor' for explicit ones. */
452
- function pickConstructorOrClass(classDef, workspaceIndex) {
452
+ function pickConstructorOrClass(classDef, workspaceIndex, scopes) {
453
453
  const classScope = workspaceIndex.classScopeByDefId.get(classDef.nodeId);
454
454
  if (classScope === undefined)
455
455
  return classDef;
@@ -457,6 +457,17 @@ function pickConstructorOrClass(classDef, workspaceIndex) {
457
457
  if (def.type === 'Constructor')
458
458
  return def;
459
459
  }
460
+ if (scopes !== undefined) {
461
+ for (const childId of scopes.scopeTree.getChildren(classScope.id)) {
462
+ const childScope = scopes.scopeTree.getScope(childId);
463
+ if (childScope === undefined || childScope.kind === 'Class')
464
+ continue;
465
+ for (const def of childScope.ownedDefs) {
466
+ if (def.type === 'Constructor')
467
+ return def;
468
+ }
469
+ }
470
+ }
460
471
  return classDef;
461
472
  }
462
473
  /** Walk up from the call-site scope to the enclosing class scope,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.50",
3
+ "version": "1.6.6-rc.52",
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",
@@ -48,6 +48,8 @@
48
48
  "test:integration": "vitest run test/integration",
49
49
  "test:watch": "vitest",
50
50
  "test:coverage": "vitest run --coverage",
51
+ "test:parity": "tsx scripts/run-parity.ts",
52
+ "test:cross-platform": "tsx scripts/run-cross-platform.ts",
51
53
  "postinstall": "node scripts/materialize-vendor-grammars.cjs && node scripts/build-tree-sitter-dart.cjs && node scripts/build-tree-sitter-proto.cjs && node scripts/build-tree-sitter-swift.cjs",
52
54
  "prepare": "node scripts/build.js",
53
55
  "prepack": "node scripts/build.js"
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Cross-platform test subset runner.
3
+ *
4
+ * Runs only the tests that exercise platform-sensitive behavior on
5
+ * Windows and macOS. The full suite runs on Ubuntu; this narrows the
6
+ * cross-platform matrix to tests that actually vary across OSes.
7
+ *
8
+ * Categories included:
9
+ * - Platform-specific logic (path.sep, process.platform guards)
10
+ * - Native addon loading (LadybugDB, tree-sitter)
11
+ * - Process spawning and shell behavior
12
+ * - Filesystem locking and temp-dir behavior
13
+ * - Worker threads (real, not mocked)
14
+ * - CLI end-to-end tests
15
+ *
16
+ * When adding a new test that uses platform-varying APIs (native addons,
17
+ * child_process with real spawning, filesystem locking, path.sep), add
18
+ * it to the appropriate section below.
19
+ *
20
+ * Usage:
21
+ * npx vitest run $(npx tsx scripts/cross-platform-tests.ts)
22
+ * # or via the package script:
23
+ * npm run test:cross-platform
24
+ */
25
+
26
+ // Platform-specific logic tests — contain explicit process.platform guards
27
+ // or test behavior that differs across operating systems
28
+ const PLATFORM_LOGIC = [
29
+ 'test/unit/setup.test.ts',
30
+ 'test/unit/setup-jsonc.test.ts',
31
+ 'test/unit/setup-codex.test.ts',
32
+ 'test/unit/platform-capabilities.test.ts',
33
+ 'test/unit/worker-pool-windows-quarantine.test.ts',
34
+ 'test/unit/lbug-pool-win-fts-probe.test.ts',
35
+ 'test/unit/repo-manager.test.ts',
36
+ 'test/unit/repo-manager-finalize-invariant.test.ts',
37
+ 'test/unit/hooks.test.ts',
38
+ 'test/unit/cursor-hook.test.ts',
39
+ 'test/unit/sidecar-recovery.test.ts',
40
+ 'test/unit/pool-wal-recovery.test.ts',
41
+ 'test/unit/detect-changes-worktree.test.ts',
42
+ 'test/unit/eval-server-bind-restriction.test.ts',
43
+ 'test/unit/ignore-service.test.ts',
44
+ 'test/unit/group/bridge-db.test.ts',
45
+ 'test/unit/group/bridge-db-edge.test.ts',
46
+ ];
47
+
48
+ // Native LadybugDB integration tests — exercise the @ladybugdb/core
49
+ // N-API addon which has known platform-specific behavior (Windows
50
+ // file-lock lag after close, macOS N-API destructor segfaults)
51
+ const LBUG_NATIVE = [
52
+ 'test/integration/lbug-core-adapter.test.ts',
53
+ 'test/integration/lbug-vector-extension.test.ts',
54
+ 'test/integration/lbug-pool.test.ts',
55
+ 'test/integration/lbug-pool-stability.test.ts',
56
+ 'test/integration/lbug-lock-retry.test.ts',
57
+ 'test/integration/lbug-open-retry.test.ts',
58
+ 'test/integration/lbug-close-handle-release.test.ts',
59
+ 'test/integration/lbug-orphan-sidecar-recovery.test.ts',
60
+ 'test/integration/lbug-readonly-init.test.ts',
61
+ 'test/integration/local-backend.test.ts',
62
+ 'test/integration/local-backend-calltool.test.ts',
63
+ 'test/integration/search-core.test.ts',
64
+ 'test/integration/search-pool.test.ts',
65
+ 'test/integration/staleness-and-stability.test.ts',
66
+ 'test/integration/analyze-wal-checkpoint-failure.test.ts',
67
+ ];
68
+
69
+ // Process spawning and CLI tests — exercise child_process with real
70
+ // process spawning, which behaves differently across platforms (shell
71
+ // quoting, path resolution, signal handling)
72
+ const SPAWN_CLI = [
73
+ 'test/integration/cli-e2e.test.ts',
74
+ 'test/integration/hooks-e2e.test.ts',
75
+ 'test/integration/skills-e2e.test.ts',
76
+ 'test/integration/server-http-startup.test.ts',
77
+ 'test/integration/mcp/server-startup.test.ts',
78
+ 'test/integration/analyze-heap-oom-e2e.test.ts',
79
+ 'test/integration/group/group-cli.test.ts',
80
+ 'test/integration/cli/tool-no-index-stderr.test.ts',
81
+ 'test/integration/setup-skills.test.ts',
82
+ ];
83
+
84
+ // Worker threads tests — exercise real worker_threads which have
85
+ // platform-specific behavior (thread spawning, IPC, exit handling)
86
+ const WORKER_THREADS = [
87
+ 'test/integration/worker-pool.test.ts',
88
+ 'test/integration/parse-impl-quarantine-cache-skip.test.ts',
89
+ ];
90
+
91
+ // Tree-sitter native addon smoke tests — verify that native grammars
92
+ // load correctly on each platform (binary compatibility, .node loading)
93
+ const NATIVE_ADDON_SMOKE = [
94
+ 'test/integration/tree-sitter-languages.test.ts',
95
+ 'test/integration/parsing.test.ts',
96
+ 'test/integration/pipeline.test.ts',
97
+ 'test/integration/pipeline-graph-golden.test.ts',
98
+ 'test/unit/parser-loader.test.ts',
99
+ ];
100
+
101
+ // Filesystem behavior tests — exercise operations that vary across
102
+ // platforms (CRLF, symlinks, permissions, temp dirs)
103
+ const FILESYSTEM = [
104
+ 'test/integration/filesystem-walker.test.ts',
105
+ 'test/integration/markdown-processor-crlf.test.ts',
106
+ 'test/integration/ignore-and-skip-e2e.test.ts',
107
+ ];
108
+
109
+ const ALL_CROSS_PLATFORM = [
110
+ ...PLATFORM_LOGIC,
111
+ ...LBUG_NATIVE,
112
+ ...SPAWN_CLI,
113
+ ...WORKER_THREADS,
114
+ ...NATIVE_ADDON_SMOKE,
115
+ ...FILESYSTEM,
116
+ ];
117
+
118
+ // When invoked directly, print the file list for vitest consumption
119
+ if (process.argv[1]?.endsWith('cross-platform-tests.ts')) {
120
+ console.log(ALL_CROSS_PLATFORM.join('\n'));
121
+ }
122
+
123
+ export {
124
+ ALL_CROSS_PLATFORM,
125
+ PLATFORM_LOGIC,
126
+ LBUG_NATIVE,
127
+ SPAWN_CLI,
128
+ WORKER_THREADS,
129
+ NATIVE_ADDON_SMOKE,
130
+ FILESYSTEM,
131
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Cross-platform test runner.
3
+ *
4
+ * Runs the platform-sensitive test subset defined in cross-platform-tests.ts
5
+ * via vitest. Used by `npm run test:cross-platform` and by the CI cross-
6
+ * platform matrix (ci-tests.yml).
7
+ *
8
+ * The main vitest.config.ts is used, so lbug-db project files get
9
+ * sequential execution and other safety constraints are preserved.
10
+ */
11
+
12
+ import { execFileSync } from 'child_process';
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import { fileURLToPath } from 'url';
16
+ import { ALL_CROSS_PLATFORM } from './cross-platform-tests.js';
17
+
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ const ROOT = path.resolve(__dirname, '..');
20
+
21
+ // Verify all files exist
22
+ const missing = ALL_CROSS_PLATFORM.filter((f) => !fs.existsSync(path.resolve(ROOT, f)));
23
+ if (missing.length > 0) {
24
+ console.error(`Cross-platform test files not found (${missing.length}):`);
25
+ for (const f of missing) console.error(` ${f}`);
26
+ console.error('\nUpdate scripts/cross-platform-tests.ts if files were moved or removed.');
27
+ process.exit(1);
28
+ }
29
+
30
+ console.log(`Running ${ALL_CROSS_PLATFORM.length} platform-sensitive tests...\n`);
31
+
32
+ try {
33
+ execFileSync('npx', ['vitest', 'run', ...ALL_CROSS_PLATFORM], {
34
+ cwd: ROOT,
35
+ stdio: 'inherit',
36
+ timeout: 15 * 60 * 1000,
37
+ shell: true,
38
+ });
39
+ } catch (err: any) {
40
+ if (err.killed || err.signal) {
41
+ console.error('vitest timed out after 15 minutes');
42
+ }
43
+ process.exit(1);
44
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Consolidated scope-resolution parity runner.
3
+ *
4
+ * Replaces the per-language matrix in ci-scope-parity.yml with a single
5
+ * job that runs all migrated languages sequentially in one process. This
6
+ * eliminates 8× redundant checkout + npm ci + build cycles (the old
7
+ * workflow created a separate GitHub Actions job per language).
8
+ *
9
+ * For each language in MIGRATED_LANGUAGES:
10
+ * 1. Run its resolver test with REGISTRY_PRIMARY_<LANG>=0 (legacy DAG)
11
+ * 2. Run its resolver test with REGISTRY_PRIMARY_<LANG>=1 (registry-primary)
12
+ *
13
+ * Both modes must pass. Failures are collected and reported at the end
14
+ * so all regressions are visible in a single CI run (equivalent to the
15
+ * old workflow's fail-fast: false behavior).
16
+ *
17
+ * Vitest output streams to the console in real time (stdio: 'inherit')
18
+ * so CI logs show the actual test output directly. No per-invocation
19
+ * timeout — the CI job-level timeout (30 min) is the outer guard.
20
+ *
21
+ * Usage:
22
+ * npx tsx scripts/run-parity.ts
23
+ * npx tsx scripts/run-parity.ts --language python # single language
24
+ */
25
+
26
+ import { execFileSync } from 'child_process';
27
+ import fs from 'fs';
28
+ import path from 'path';
29
+ import { fileURLToPath } from 'url';
30
+ import { MIGRATED_LANGUAGES } from '../src/core/ingestion/registry-primary-flag.js';
31
+
32
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
33
+ const ROOT = path.resolve(__dirname, '..');
34
+
35
+ interface ParityFailure {
36
+ lang: string;
37
+ mode: 'legacy' | 'registry-primary';
38
+ }
39
+
40
+ function envVarName(slug: string): string {
41
+ return `REGISTRY_PRIMARY_${slug.toUpperCase().replace(/-/g, '_')}`;
42
+ }
43
+
44
+ function testFilePath(slug: string): string {
45
+ return `test/integration/resolvers/${slug}.test.ts`;
46
+ }
47
+
48
+ function runVitest(testFile: string, env: Record<string, string>): boolean {
49
+ try {
50
+ execFileSync('npx', ['vitest', 'run', testFile], {
51
+ cwd: ROOT,
52
+ env: { ...process.env, ...env },
53
+ stdio: 'inherit',
54
+ shell: true,
55
+ });
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ // Parse CLI args
63
+ const args = process.argv.slice(2);
64
+ const langFlag = args.indexOf('--language');
65
+ const singleLang = langFlag >= 0 ? args[langFlag + 1] : undefined;
66
+
67
+ if (langFlag >= 0 && singleLang === undefined) {
68
+ console.error('--language requires a value');
69
+ process.exit(1);
70
+ }
71
+
72
+ const languages = singleLang ? [singleLang] : [...MIGRATED_LANGUAGES].map(String);
73
+
74
+ // Verify test files exist before running
75
+ const missingFiles: string[] = [];
76
+ for (const lang of languages) {
77
+ const file = path.resolve(ROOT, testFilePath(lang));
78
+ try {
79
+ fs.accessSync(file);
80
+ } catch {
81
+ missingFiles.push(`${testFilePath(lang)} (${lang})`);
82
+ }
83
+ }
84
+
85
+ if (missingFiles.length > 0) {
86
+ console.error('Missing resolver test files:');
87
+ for (const f of missingFiles) console.error(` ${f}`);
88
+ process.exit(1);
89
+ }
90
+
91
+ console.log(`Scope-resolution parity: ${languages.length} language(s)`);
92
+ console.log(`Languages: ${languages.join(', ')}\n`);
93
+
94
+ const failures: ParityFailure[] = [];
95
+
96
+ for (const lang of languages) {
97
+ const file = testFilePath(lang);
98
+ const envVar = envVarName(lang);
99
+
100
+ console.log(`\n── ${lang} — legacy DAG (${envVar}=0) ──`);
101
+ if (!runVitest(file, { [envVar]: '0' })) {
102
+ failures.push({ lang, mode: 'legacy' });
103
+ }
104
+
105
+ console.log(`\n── ${lang} — registry-primary (${envVar}=1) ──`);
106
+ if (!runVitest(file, { [envVar]: '1' })) {
107
+ failures.push({ lang, mode: 'registry-primary' });
108
+ }
109
+ }
110
+
111
+ // Summary
112
+ const total = languages.length * 2;
113
+ const passed = total - failures.length;
114
+
115
+ console.log('\n═══════════════════════════════════════');
116
+ console.log('PARITY SUMMARY');
117
+ console.log('═══════════════════════════════════════');
118
+ console.log(`Passed: ${passed}/${total}`);
119
+
120
+ if (failures.length > 0) {
121
+ console.log(`\nFAILURES (${failures.length}):`);
122
+ for (const f of failures) {
123
+ console.log(` ✗ ${f.lang} [${f.mode}]`);
124
+ }
125
+ process.exit(1);
126
+ }
127
+
128
+ console.log('\nAll parity checks passed.');