Compare commits

...
Author SHA1 Message Date
github-actions[bot] 15fb007c53 release: v1.6.4-rc.7 2026-04-27 07:14:16 +00:00
Gergő Magyar 09d78cadec fix(ingestion): skip empty scope extraction (#1100) 2026-04-27 07:29:37 +01:00
Gergő Magyar 9e62f7c121 fix(ci): allow expected legacy parity failures (#1099)
Made-with: Cursor
2026-04-27 06:59:18 +01:00
ManniX-ITA acef549791 test(csharp): companion fixture for #1066 frozen-bucket regression (#1085)
The csharp-large-cache-miss-resolution fixture added in #1082 reproduces
the freeze contract failure via tree-sitter cache-miss reparse on >32 KB
files. This adds a complementary trigger for the same root cause that
does not depend on file size: a small-file pair where the importer
locally declares a class with the same simple name as a sibling reached
through `using`.

Pre-#1082 path: scope-extractor pre-populates (and freezes) `User` in
the importer's Module bindings, then populateCsharpNamespaceSiblings'
namespace-import loop calls push() on the frozen array and throws
"Cannot add property N, object is not extensible", aborting the whole
scopeResolution phase.

Post-#1082 the augmentation channel keeps both bindings visible; the
local `Collision.App.User` shadows the namespace-imported one per
origin precedence, so `Program.Run -> new User()` resolves to the
local class.

Three assertions:
- scopeResolution completes (no throw on the colliding bucket).
- both `User` declarations are detected across the two namespaces.
- `Program.Run -> User` constructor edge points at App/Program.cs
  (not Models/User.cs), verifying origin:local shadows origin:namespace.

Verified: full csharp.test.ts suite green (207/207). tsc --noEmit clean.

Refs: #1066, #1082, #1083 (closed as superseded).
2026-04-27 06:40:26 +01:00
12 changed files with 218 additions and 12 deletions
+2 -1
View File
@@ -11,7 +11,8 @@ name: Scope Resolution Parity
# TWICE on every PR:
#
# 1. `REGISTRY_PRIMARY_<LANG>=0` — legacy DAG path (guarantees we haven't
# broken the old path while migrating).
# broken the old path while migrating). Known legacy gaps may be skipped
# through the resolver test helper's expected-failure list.
# 2. `REGISTRY_PRIMARY_<LANG>=1` — registry-primary path (guarantees the
# new path carries the same behavior — the parity gate).
#
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "gitnexus",
"version": "1.6.3",
"version": "1.6.4-rc.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gitnexus",
"version": "1.6.3",
"version": "1.6.4-rc.7",
"hasInstallScript": true,
"license": "PolyForm-Noncommercial-1.0.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gitnexus",
"version": "1.6.3",
"version": "1.6.4-rc.7",
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
"author": "Abhigyan Patwari",
"license": "PolyForm-Noncommercial-1.0.0",
@@ -13,8 +13,11 @@
* `emitScopeCaptures`. Returns `undefined`; zero work done. This is
* the state of every language today — `ParsedFile` production stays
* dormant until a language migrates.
* 2. Invokes the hook + feeds its output to `ScopeExtractor.extract`.
* 3. **Swallows exceptions from either side.** A failure here returns
* 2. Short-circuits empty / whitespace-only files. There is no scope
* content to extract, and some tree-sitter queries do not match an
* otherwise valid empty root node.
* 3. Invokes the hook + feeds its output to `ScopeExtractor.extract`.
* 4. **Swallows exceptions from either side.** A failure here returns
* `undefined` and emits a warning via `onWarn`; legacy parsing on
* the same file continues unaffected by the scope-extraction miss.
* Scope-based resolution is the new path under construction — it
@@ -41,6 +44,7 @@ export function extractParsedFile(
cachedTree?: unknown,
): ParsedFile | undefined {
if (provider.emitScopeCaptures === undefined) return undefined;
if (sourceText.trim().length === 0) return undefined;
try {
const captures = provider.emitScopeCaptures(sourceText, filePath, cachedTree);
return extractScope(captures, filePath, provider);
@@ -0,0 +1,18 @@
using Collision.Models;
namespace Collision.App
{
public class User
{
public string GetName() { return "app"; }
}
public class Program
{
public void Run()
{
var u = new User();
u.GetName();
}
}
}
@@ -0,0 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>Collision</RootNamespace>
</PropertyGroup>
</Project>
@@ -0,0 +1,7 @@
namespace Collision.Models
{
public class User
{
public string GetName() { return "models"; }
}
}
@@ -1,11 +1,12 @@
/**
* C#: heritage resolution via base_list + ambiguous namespace-import refusal
*/
import { describe, it, expect, beforeAll } from 'vitest';
import { describe, expect, beforeAll } from 'vitest';
import path from 'path';
import {
FIXTURES,
CROSS_FILE_FIXTURES,
createResolverParityIt,
getRelationships,
getNodesByLabel,
getNodesByLabelFull,
@@ -14,6 +15,8 @@ import {
type PipelineResult,
} from './helpers.js';
const it = createResolverParityIt('csharp');
// ---------------------------------------------------------------------------
// Heritage: class + interface resolution via base_list
// ---------------------------------------------------------------------------
@@ -2485,3 +2488,50 @@ describe('C# large-file + frozen-bucket regression (issue #1066)', () => {
expect(['import-resolved', 'global']).toContain(save!.rel.reason);
});
});
// ---------------------------------------------------------------------------
// Issue #1066 companion regression: small-file trigger for the same
// frozen-bucket failure. Where csharp-large-cache-miss-resolution exercises
// the path through tree-sitter cache-miss reparse on >32 KB files, this
// fixture trips the same `Object.freeze` contract on the populator's
// namespace-import loop in a single small file pair: the importer locally
// declares a class with the same simple name as a sibling reachable through
// `using`, so the extractor pre-populates (and freezes) `User` in the
// importer's Module bindings before populateNamespaceSiblings tries to
// append the cross-file `Collision.Models.User`. Pre-#1082 the populator
// pushed onto the frozen array → "Cannot add property N, object is not
// extensible" → whole scopeResolution phase aborted. Post-#1082 the
// augmentation channel keeps both bindings visible to readers, with the
// local `Collision.App.User` taking precedence per origin ordering.
// ---------------------------------------------------------------------------
describe('C# frozen-binding collision via using-import (issue #1066 companion)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'csharp-frozen-binding-collision'),
() => {},
);
}, 60000);
it('completes scopeResolution without throwing on the colliding bucket', () => {
expect(getNodesByLabel(result, 'Class')).toEqual(expect.arrayContaining(['User', 'Program']));
});
it('detects both User declarations across the two namespaces', () => {
const users = getNodesByLabelFull(result, 'Class').filter((n) => n.name === 'User');
expect(users.length).toBe(2);
const paths = users.map((u) => u.properties.filePath).sort();
expect(paths).toEqual(['App/Program.cs', 'Models/User.cs']);
});
it('resolves Program.Run -> local Collision.App.User constructor (origin:local shadows namespace)', () => {
const calls = getRelationships(result, 'CALLS');
const ctor = calls.find(
(c) => c.source === 'Run' && c.target === 'User' && c.targetLabel === 'Class',
);
expect(ctor).toBeDefined();
expect(ctor!.targetFilePath).toBe('App/Program.cs');
});
});
+45 -1
View File
@@ -2,10 +2,54 @@
* Shared test helpers for language resolution integration tests.
*/
import path from 'path';
import { it as vitestIt } from 'vitest';
import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js';
import type { PipelineOptions } from '../../../src/core/ingestion/pipeline.js';
import type { PipelineResult } from '../../../src/types/pipeline.js';
import type { GraphRelationship } from '../../../src/core/graph/types.js';
import type { GraphRelationship } from 'gitnexus-shared';
const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, ReadonlySet<string>>> = {
csharp: new Set([
'emits the using-import edge App/Program.cs -> Models/User.cs through the scope-resolution path',
]),
};
type ResolverParityEnv = Readonly<Record<string, string | undefined>>;
type VitestIt = typeof vitestIt;
type CallableIt = (name: string, ...args: unknown[]) => unknown;
export function resolverParityFlagName(languageSlug: string): string {
return `REGISTRY_PRIMARY_${languageSlug.toUpperCase().replace(/-/g, '_')}`;
}
export function isLegacyResolverParityRun(
languageSlug: string,
env: ResolverParityEnv = process.env,
): boolean {
const value = env[resolverParityFlagName(languageSlug)]?.trim().toLowerCase();
return value === '0' || value === 'false' || value === 'no';
}
export function isLegacyResolverParityExpectedFailure(
languageSlug: string,
testName: string,
env: ResolverParityEnv = process.env,
): boolean {
if (!isLegacyResolverParityRun(languageSlug, env)) return false;
return LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES[languageSlug]?.has(testName) ?? false;
}
export function createResolverParityIt(languageSlug: string): VitestIt {
const wrapped = ((name: string, ...args: unknown[]) => {
const runner = isLegacyResolverParityExpectedFailure(languageSlug, name)
? vitestIt.skip
: vitestIt;
return (runner as unknown as CallableIt)(name, ...args);
}) as VitestIt;
Object.assign(wrapped, vitestIt);
return wrapped;
}
export const FIXTURES = path.resolve(__dirname, '..', '..', 'fixtures', 'lang-resolution');
export const CROSS_FILE_FIXTURES = path.resolve(
@@ -67,6 +67,27 @@ describe('extractParsedFile', () => {
});
describe('provider HAS migrated', () => {
it('returns undefined without warning for whitespace-only source', () => {
const warnings: string[] = [];
let called = false;
const provider = fakeProvider({
emitScopeCaptures: () => {
called = true;
throw new Error('should not inspect empty source');
},
});
for (const src of ['', ' \n\t']) {
const result = extractParsedFile(provider, src, 'pkg/__init__.py', (msg) => {
warnings.push(msg);
});
expect(result).toBeUndefined();
}
expect(called).toBe(false);
expect(warnings).toEqual([]);
});
it('threads emitScopeCaptures output through ScopeExtractor', () => {
const provider = fakeProvider({
emitScopeCaptures: () => [moduleScopeMatch()],
@@ -56,15 +56,23 @@ function findDef(file: ParsedFile, name: string) {
describe('Python scopes — module / class / function', () => {
it('case 01: minimal module produces a single Module scope', () => {
// Empty source produces a zero-range module node; the central
// extractor treats zero-range scopes as malformed (and rightly so —
// they collide with sibling-overlap detection on subsequent reparses).
// Real Python files always have at least a newline.
const f = parse('pass\n');
expect(f.scopes).toHaveLength(1);
expect(f.scopes[0]!.kind).toBe('Module');
});
it('case 01a: empty and whitespace-only files are skipped without warnings', () => {
for (const src of ['', ' \n\t']) {
const warnings: string[] = [];
const parsed = extractParsedFile(pythonProvider, src, 'pkg/__init__.py', (msg) => {
warnings.push(msg);
});
expect(parsed).toBeUndefined();
expect(warnings).toEqual([]);
}
});
it('case 01b: large cache-miss files use the adaptive tree-sitter buffer', () => {
const padding = 'x'.repeat(600 * 1024);
const f = parse(`# ${padding}\ndef after_padding():\n return 1\n`);
@@ -0,0 +1,47 @@
import { describe, expect, it } from 'vitest';
import {
isLegacyResolverParityExpectedFailure,
isLegacyResolverParityRun,
resolverParityFlagName,
} from '../../integration/resolvers/helpers.js';
const csharpNamespaceRootImportTest =
'emits the using-import edge App/Program.cs -> Models/User.cs through the scope-resolution path';
describe('resolver parity expected legacy failures', () => {
it('uses the same env var convention as the parity workflow', () => {
expect(resolverParityFlagName('csharp')).toBe('REGISTRY_PRIMARY_CSHARP');
expect(resolverParityFlagName('c-plus-plus')).toBe('REGISTRY_PRIMARY_C_PLUS_PLUS');
});
it('recognizes only legacy parity runs', () => {
expect(isLegacyResolverParityRun('csharp', { REGISTRY_PRIMARY_CSHARP: '0' })).toBe(true);
expect(isLegacyResolverParityRun('csharp', { REGISTRY_PRIMARY_CSHARP: 'false' })).toBe(true);
expect(isLegacyResolverParityRun('csharp', { REGISTRY_PRIMARY_CSHARP: '1' })).toBe(false);
expect(isLegacyResolverParityRun('csharp', {})).toBe(false);
});
it('matches configured expected failures only during the legacy run', () => {
expect(
isLegacyResolverParityExpectedFailure('csharp', csharpNamespaceRootImportTest, {
REGISTRY_PRIMARY_CSHARP: '0',
}),
).toBe(true);
expect(
isLegacyResolverParityExpectedFailure('csharp', csharpNamespaceRootImportTest, {
REGISTRY_PRIMARY_CSHARP: '1',
}),
).toBe(false);
expect(
isLegacyResolverParityExpectedFailure(
'csharp',
'detects exactly 3 classes and 2 interfaces',
{
REGISTRY_PRIMARY_CSHARP: '0',
},
),
).toBe(false);
});
});