fix(eval): grade real C include provenance (#3611)

* fix(eval): verify independent C repairs and reject broken controls

* fix(eval): replace invalid C task with provenance behavior

* fix(eval): grade real C include provenance

* test(eval): add independent typed C reference repairs

* docs(eval): record C oracle evidence contract

* fix(eval): remove invalid synthetic C oracle

* fix(eval): remove type-invalid C reference

* docs: remove C oracle planning document

* test(eval): tolerate formatting in C oracle mutants

* test(eval): clarify C oracle node and file bindings
This commit is contained in:
Gergő Magyar 2026-10-17 17:03:42 +03:00 • committed by GitHub
parent 22a7608b7c
commit 2251adeeb2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 527 additions and 61 deletions

View file

@ -5,6 +5,7 @@ from __future__ import annotations
import argparse
import hashlib
import os
import re
import shutil
import subprocess
import sys
@ -589,6 +590,9 @@ def scenario_control_base(tmp_path_factory: pytest.TempPathFactory) -> tuple[Pat
ref,
"gitnexus/src",
"gitnexus/vendor",
"gitnexus/test",
"gitnexus/tsconfig.json",
"gitnexus/vitest.config.ts",
"gitnexus/package.json",
"gitnexus-shared/package.json",
],
@ -607,7 +611,7 @@ def scenario_control_base(tmp_path_factory: pytest.TempPathFactory) -> tuple[Pat
pytest.skip("oracle controls need tarfile extraction filters (Python 3.11.4+)")
with tarfile.open(archive) as source:
source.extractall(base, filter="data")
for relative in ("node_modules", "gitnexus/node_modules", "gitnexus-shared/dist"):
for relative in ("node_modules", "gitnexus/node_modules", "gitnexus-shared/node_modules", "gitnexus-shared/dist"):
(base / relative).symlink_to(dependencies / relative, target_is_directory=True)
subprocess.run(["git", "init", "--quiet", str(base)], check=True, capture_output=True)
return base, {task["id"]: task for task in tasks}
@ -633,12 +637,82 @@ def _verify_shipped_oracle(candidate: Path, task: dict[str, object]) -> subproce
)
def _assert_behavioral_rejection(completed: subprocess.CompletedProcess[str], assertion: str = "") -> None:
output = completed.stdout + completed.stderr
assert completed.returncode == 1, output
assert "AssertionError" in output, output
assert assertion in output, output
# An import/setup crash is not evidence that a behavioral control worked.
assert "Failed Suites" not in output, output
assert "Unhandled Errors" not in output, output
def _check_c_provenance_mutants(candidate: Path, base: Path, task: dict[str, object]) -> None:
"""Reject broken behavior, and distinguish module-load errors from evidence."""
interpret = Path("gitnexus/src/core/ingestion/languages/c/interpret.ts")
resolver = Path("gitnexus/src/core/ingestion/languages/c/scope-resolver.ts")
valid_interpret = (candidate / interpret).read_text()
valid_resolver = (candidate / resolver).read_text()
system_guards = re.findall(
r"\bconst\s+parsedImport\s*=\s*context\s*\?\.\s*parsedImport\s*;\s*"
r"if\s*\(\s*parsedImport\s*\?\.\s*kind\s*===\s*'wildcard'\s*&&\s*"
r"'isSystem'\s+in\s+parsedImport\s*&&\s*parsedImport\s*\.\s*isSystem\s*===\s*true\s*\)\s*"
r"\{\s*return\s+null\s*;\s*\}",
valid_resolver,
)
fallbacks = re.findall(
r"\breturn\s+resolveCImportTarget\s*\(\s*targetRaw\s*,\s*fromFile\s*,\s*allFilePaths\s*,?\s*\)\s*;",
valid_resolver,
)
assert len(system_guards) == 1, "expected exactly one C system-include guard"
assert len(fallbacks) == 1, "expected exactly one C resolver fallback"
system_guard, fallback = system_guards[0], fallbacks[0]
late_guard_resolver = valid_resolver.replace(system_guard, "").replace(fallback, system_guard + "\n " + fallback)
assert valid_interpret.count("return parsedImport;") == 1
assert valid_interpret.count("targetRaw: source,") == 1
mutants = [
(interpret, (base / interpret).read_text(), "literal include <stdio.h> must be retained"),
(
interpret,
valid_interpret.replace("return parsedImport;", "return null;"),
'literal include "stdio.h" must be retained',
),
(interpret, valid_interpret.replace("targetRaw: source,", "targetRaw: '',"), "retained header identity"),
(resolver, (base / resolver).read_text(), "include <stdio.h> resolution"),
(resolver, late_guard_resolver, "include <stdio.h> resolution"),
]
for relative, broken, assertion in mutants:
path = candidate / relative
original = path.read_text()
assert broken != original
path.write_text(broken)
try:
_assert_behavioral_rejection(_verify_shipped_oracle(candidate, task), assertion)
finally:
path.write_text(original)
# A missing native-parser module must fail setup, never satisfy a negative
# behavioral control just because Vitest exits nonzero.
parser = candidate / "gitnexus/src/core/ingestion/languages/c/query.ts"
original_parser = parser.read_bytes()
parser.unlink()
try:
broken_setup = _verify_shipped_oracle(candidate, task)
assert "Failed Suites" in broken_setup.stdout + broken_setup.stderr
with pytest.raises(AssertionError):
_assert_behavioral_rejection(broken_setup, "literal include <stdio.h> must be retained")
finally:
parser.write_bytes(original_parser)
@pytest.mark.skipif(os.name == "nt", reason="oracle controls use POSIX dependency symlinks and shell commands")
@pytest.mark.parametrize(
("task_id", "reference"),
[
("trivial-status-json-alias", "trivial-status-json-alias"),
("inv-bug-c-system-include", "inv-bug-c-system-include"),
("inv-feature-c-include-provenance", "inv-feature-c-include-provenance-flag"),
("inv-feature-c-include-provenance", "inv-feature-c-include-provenance-delimiter"),
("inv-feature-list-repos-filter", "inv-feature-list-repos-filter"),
("cross-module-parse-retry", "cross-module-parse-retry"),
("cross-module-parse-retry", "cross-module-parse-retry-pool"),
@ -653,13 +727,37 @@ def test_shipped_oracles_reject_task_base_and_accept_reference_fixes(
base, tasks = scenario_control_base
candidate = _oracle_control_candidate(base, tmp_path / "candidate")
negative = _verify_shipped_oracle(candidate, tasks[task_id])
assert negative.returncode == 1, negative.stdout + negative.stderr
assert "AssertionError" in negative.stdout + negative.stderr
_assert_behavioral_rejection(
negative,
"literal include <stdio.h> must be retained" if task_id == "inv-feature-c-include-provenance" else "",
)
patches = Path(__file__).resolve().parents[1] / "workflow_bench/oracles/reference"
subprocess.run(["git", "apply", str(patches / f"{reference}.patch")], cwd=candidate, check=True)
positive = _verify_shipped_oracle(candidate, tasks[task_id])
assert positive.returncode == 0, positive.stdout + positive.stderr
if task_id == "inv-feature-c-include-provenance":
# A reference passing transpile-only Vitest is not enough: use the same
# typed, authored verification that an evaluated repair must satisfy.
authored = subprocess.run(
["/bin/sh", "-c", str(tasks[task_id]["verify"])],
cwd=candidate,
capture_output=True,
text=True,
check=False,
timeout=180,
)
assert authored.returncode == 0, authored.stdout + authored.stderr
if reference == "inv-feature-c-include-provenance-flag":
# This entry point is already selected by native CI; keep every C
# validity control reachable without changing workflow selection.
# Indent every existing code line differently without matching the
# reference's formatting; the behavioral controls must still work.
resolver = candidate / "gitnexus/src/core/ingestion/languages/c/scope-resolver.ts"
resolver.write_text(re.sub(r"(?m)^([ \t]+)", r"\t\1", resolver.read_text()))
positive = _verify_shipped_oracle(candidate, tasks[task_id])
assert positive.returncode == 0, positive.stdout + positive.stderr
_check_c_provenance_mutants(candidate, base, tasks[task_id])
@pytest.mark.skipif(os.name == "nt", reason="oracle controls use POSIX dependency symlinks and shell commands")

View file

@ -347,11 +347,18 @@ def test_shipped_scenarios_opt_out_the_cross_module_cell_and_rebuild_graph_asset
selected, skipped = select_tasks(tasks, include_expensive=False)
assert [task["id"] for task in selected] == [
"trivial-status-json-alias",
"inv-bug-c-system-include",
"inv-feature-c-include-provenance",
"inv-feature-list-repos-filter",
]
assert skipped == ["cross-module-parse-retry"]
# v1.6.12 predates all four fixes, including the C fix already on main.
assert len(tasks) == 4
c_task = next(task for task in tasks if task["id"] == "inv-feature-c-include-provenance")
assert c_task["class"] == "investigation-feature"
assert "npx tsc --noEmit" in c_task["verify"]
assert "test/unit/scope-resolution/c/c-include-provenance.test.ts" in c_task["verify"]
assert "test/unit/scope-resolution/external-import-conformance.test.ts" in c_task["verify"]
assert not c_task.get("setup")
# v1.6.12 lacks all four requested behaviors, including retained C include provenance.
assert {task["ref"] for task in tasks} == {"c4ecf398de637cf779ab2238e69958cfd5d601ce"}
assert all(not task.get("sandbox_copy") for task in tasks)
assert all(task["sandbox_dependencies"] for task in tasks)

View file

@ -786,6 +786,30 @@ the shared `vitest.config.mts` as an oracle file and pass it explicitly with
`--config`. Prefer `verify` commands that use the repo's own npm scripts (they
carry build pre-hooks).
The C scenario is `inv-feature-c-include-provenance`. At its pinned task
commit, ingestion drops literal system includes before resolution. The task
requires retaining the header identity and enough provenance to prevent
unconfigured system includes from matching unrelated repository headers,
while keeping quoted local and relative includes resolvable. Computed macro
includes remain outside this task's scope. Its hidden oracle parses real C
source and passes the resulting import through the decomposer, interpreter,
and resolver; it does not require a particular metadata field or encoding.
Offline controls must show that the untouched task base fails the intended
behavioral assertion and that two independent, type-correct repairs pass both
the hidden oracle and the complete authored verification command. Negative
controls also reject dropped imports, lost header identity, and false system
header matches. These controls run with the pinned task dependencies through
`test_shipped_oracles_reject_task_base_and_accept_reference_fixes`.
This replaces `inv-bug-c-system-include`, whose oracle constructed an import
state the pinned parser did not produce and constrained the repair's internal
representation. Historical scores for that task are not valid evidence of C
ingestion quality and are not comparable with the replacement task. The new
task ID, prompt, verification command, and oracle bytes produce a new task
digest. Old agent patches are not available for rescoring; keep historical
measurements unchanged and compare only runs with matching task contracts.
## Relation to the SWE-bench harness
The rest of `eval/` benchmarks GitNexus _tools_ inside a litellm agent loop

View file

@ -1,28 +0,0 @@
import { describe, expect, it } from 'vitest';
import { cScopeResolver } from '../gitnexus/src/core/ingestion/languages/c/scope-resolver.js';
describe('hidden oracle: C system headers do not bind to repository decoys', () => {
it.each([
['stdio.h', true, null],
['project.h', true, null],
['stdio.h', false, 'src/stdio.h'],
['util.h', false, 'include/util.h'],
] as const)(
'resolves %s with isSystem=%s from its include form',
(targetRaw, isSystem, expected) => {
const files = new Set(['src/stdio.h', 'include/project.h', 'include/util.h', 'src/main.c']);
const parsedImport = { kind: 'wildcard' as const, targetRaw, isSystem };
// Both spellings name stdio.h. Preserve the parsed include form;
// refusing every header by name would break the quoted local include.
const resolved = cScopeResolver.resolveImportTarget(
parsedImport.targetRaw,
'src/main.c',
files,
undefined,
{ parsedFiles: [], parsedImport },
);
expect(resolved).toBe(expected);
},
);
});

View file

@ -0,0 +1,67 @@
import { describe, expect, it } from 'vitest';
import { getCParser } from '../gitnexus/src/core/ingestion/languages/c/query.js';
import { splitCInclude } from '../gitnexus/src/core/ingestion/languages/c/import-decomposer.js';
import { interpretCImport } from '../gitnexus/src/core/ingestion/languages/c/interpret.js';
import { cScopeResolver } from '../gitnexus/src/core/ingestion/languages/c/scope-resolver.js';
function parseInclude(spelling: string) {
const tree = getCParser().parse(`#include ${spelling}\n`);
const node = tree.rootNode.namedChildren.find((child) => child.type === 'preproc_include');
if (node === undefined) throw new Error('native parser must produce an include node');
const captures = splitCInclude(node);
return captures === null ? null : interpretCImport(captures);
}
// Header identity is public behavior. Bare targets or targets carrying their
// literal delimiters are both valid; no private marker field is prescribed.
function headerIdentity(target: string): string {
if (
(target.startsWith('<') && target.endsWith('>')) ||
(target.startsWith('"') && target.endsWith('"'))
)
return target.slice(1, -1);
return target;
}
const workspaceFiles = new Set([
'src/main.c',
'src/stdio.h',
'include/project.h',
'src/local.h',
'include/util.h',
]);
describe.each([
{ name: 'workspace headers', files: workspaceFiles, resolutionConfig: undefined },
{
name: 'discovered headers',
files: new Set(['src/main.c']),
resolutionConfig: new Set([...workspaceFiles].filter((file) => file.endsWith('.h'))),
},
])('hidden oracle: retained C include provenance ($name)', ({ files, resolutionConfig }) => {
it.each([
['<stdio.h>', 'stdio.h', null],
['<project.h>', 'project.h', null],
['"stdio.h"', 'stdio.h', 'src/stdio.h'],
['"local.h"', 'local.h', 'src/local.h'],
['"../include/util.h"', '../include/util.h', 'include/util.h'],
] as const)('retains and resolves literal %s', (spelling, header, expected) => {
const parsedImport = parseInclude(spelling);
expect(parsedImport, `literal include ${spelling} must be retained`).not.toBeNull();
expect(parsedImport!.kind).toBe('wildcard');
expect(headerIdentity(parsedImport!.targetRaw), 'retained header identity').toBe(header);
const resolved = cScopeResolver.resolveImportTarget(
parsedImport!.targetRaw,
'src/main.c',
files,
resolutionConfig,
{ parsedFiles: [], parsedImport: parsedImport! },
);
const targets = resolved === null ? [] : typeof resolved === 'string' ? [resolved] : resolved;
expect(targets, `include ${spelling} resolution`).toEqual(expected === null ? [] : [expected]);
});
it('does not invent a literal import for a computed macro include', () => {
expect(parseInclude('HEADER_MACRO')).toBeNull();
});
});

View file

@ -1,12 +0,0 @@
--- a/gitnexus/src/core/ingestion/languages/c/scope-resolver.ts
+++ b/gitnexus/src/core/ingestion/languages/c/scope-resolver.ts
@@ -81,7 +81,8 @@
// this process. Runs BEFORE `populateOwners`.
applyCaptureSideChannel: applyCStaticLinkageSideChannel,
- resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig) => {
+ resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig, context) => {
+ if (context?.parsedImport?.kind === 'wildcard' && context.parsedImport.isSystem) return null;
// Augment allFilePaths with .h files discovered via loadResolutionConfig
// since the phase only passes .c files to the C resolver but #include
// targets .h files classified as C++ in language detection.

View file

@ -0,0 +1,146 @@
--- a/gitnexus/src/core/ingestion/languages/c/interpret.ts
+++ b/gitnexus/src/core/ingestion/languages/c/interpret.ts
@@ -8,10 +8,9 @@
const source = captures['@import.source']?.text;
if (source === undefined) return null;
- // System headers (e.g. <stdio.h>) are not resolved to local files
- if (captures['@import.system'] !== undefined) return null;
-
- return { kind: 'wildcard', targetRaw: source };
+ // Preserve the literal spelling in the existing target field.
+ const targetRaw = captures['@import.system'] === undefined ? source : `<${source}>`;
+ return { kind: 'wildcard', targetRaw };
}
/**
--- a/gitnexus/src/core/ingestion/languages/c/scope-resolver.ts
+++ b/gitnexus/src/core/ingestion/languages/c/scope-resolver.ts
@@ -82,6 +82,7 @@
applyCaptureSideChannel: applyCStaticLinkageSideChannel,
resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig) => {
+ if (targetRaw.startsWith('<') && targetRaw.endsWith('>')) return null;
// Augment allFilePaths with .h files discovered via loadResolutionConfig
// since the phase only passes .c files to the C resolver but #include
// targets .h files classified as C++ in language detection.
--- a/gitnexus/test/unit/scope-resolution/external-import-conformance.test.ts
+++ b/gitnexus/test/unit/scope-resolution/external-import-conformance.test.ts
@@ -38,6 +38,10 @@
import { describe, expect, it } from 'vitest';
import type { ParsedFile, ParsedImport, ScopeId, SymbolDefinition } from 'gitnexus-shared';
import { SupportedLanguages } from 'gitnexus-shared';
+import { getCParser } from '../../../src/core/ingestion/languages/c/query.js';
+import { splitCInclude } from '../../../src/core/ingestion/languages/c/import-decomposer.js';
+import { interpretCImport } from '../../../src/core/ingestion/languages/c/interpret.js';
+import type { SyntaxNode } from '../../../src/core/ingestion/utils/ast-helpers.js';
import { SCOPE_RESOLVERS } from '../../../src/core/ingestion/scope-resolution/pipeline/registry.js';
import type { ComposerConfig } from '../../../src/core/ingestion/language-config.js';
import {
@@ -143,6 +147,16 @@
};
}
+function parseCInclude(spelling: string): ParsedImport {
+ const tree = getCParser().parse(`#include ${spelling}\n`);
+ const node = tree.rootNode.namedChildren.find((child) => child.type === 'preproc_include');
+ if (!node) throw new Error('C conformance fixture did not parse');
+ const captures = splitCInclude(node as SyntaxNode);
+ const parsedImport = captures === null ? null : interpretCImport(captures);
+ if (parsedImport === null) throw new Error('C literal include was not retained');
+ return parsedImport;
+}
+
const PHP_FUNCTION_IMPORT = (targetRaw: string): ParsedImport => ({
kind: 'named',
localName: 'imported',
@@ -345,9 +359,10 @@
files: ['src/stdio.h', 'include/util.h', 'src/main.c'],
fromFile: 'src/main.c',
resolutionConfig: undefined,
- external: 'stdio.h',
+ external: '<stdio.h>',
decoy: 'src/stdio.h',
- reachesDecoy: 'util.h',
+ reachesDecoy: '"stdio.h"',
+ parsedImport: parseCInclude,
},
],
[
@@ -418,7 +433,6 @@
const KNOWN_GAPS: ReadonlyMap<SupportedLanguages, string> = new Map<SupportedLanguages, string>([
[SupportedLanguages.Dart, '`package:http/http.dart` -> `lib/http.dart`'],
[SupportedLanguages.Swift, '`Foundation` -> `Sources/Foundation/Thing.swift`'],
- [SupportedLanguages.C, '`stdio.h` -> `src/stdio.h`'],
[SupportedLanguages.CPlusPlus, '`cstdio.h` -> `src/cstdio.h`'],
]);
@@ -463,14 +477,15 @@
: testCase.files.map(
testCase.parsedFile ?? ((filePath) => ({ filePath }) as unknown as ParsedFile),
);
+ const parsedImport = testCase.parsedImport?.(targetRaw);
return resolver.resolveImportTarget(
- targetRaw,
+ parsedImport?.targetRaw ?? targetRaw,
testCase.fromFile,
files,
testCase.resolutionConfig,
{
parsedFiles,
- parsedImport: testCase.parsedImport?.(targetRaw),
+ parsedImport,
},
);
}
--- /dev/null
+++ b/gitnexus/test/unit/scope-resolution/c/c-include-provenance.test.ts
@@ -0,0 +1,48 @@
+import { describe, expect, it } from 'vitest';
+import { getCParser } from '../../../../src/core/ingestion/languages/c/query.js';
+import { splitCInclude } from '../../../../src/core/ingestion/languages/c/import-decomposer.js';
+import { interpretCImport } from '../../../../src/core/ingestion/languages/c/interpret.js';
+import { cScopeResolver } from '../../../../src/core/ingestion/languages/c/scope-resolver.js';
+import type { SyntaxNode } from '../../../../src/core/ingestion/utils/ast-helpers.js';
+
+function parseIncludes(source: string) {
+ return getCParser().parse(source).rootNode.namedChildren
+ .filter((node) => node.type === 'preproc_include')
+ .map((node) => {
+ const captures = splitCInclude(node as SyntaxNode);
+ return captures === null ? null : interpretCImport(captures);
+ });
+}
+
+function headerIdentity(target: string): string {
+ if ((target.startsWith('<') && target.endsWith('>')) ||
+ (target.startsWith('"') && target.endsWith('"'))) return target.slice(1, -1);
+ return target;
+}
+
+describe('C literal include provenance through parsing and resolution', () => {
+ it('retains system and quoted headers, resolving only local includes', () => {
+ const imports = parseIncludes([
+ '#include <sys/net.h>',
+ '#include "api.h"',
+ '#include "../include/common.h"',
+ ].join('\n') + '\n');
+ expect(imports).toHaveLength(3);
+ expect(imports.every((item) => item !== null)).toBe(true);
+ expect(imports.map((item) => headerIdentity(item!.targetRaw)))
+ .toEqual(['sys/net.h', 'api.h', '../include/common.h']);
+ const files = new Set(['src/main.c', 'include/sys/net.h', 'src/api.h', 'include/common.h']);
+ const targets = imports.map((parsedImport) => {
+ const resolved = cScopeResolver.resolveImportTarget(
+ parsedImport!.targetRaw, 'src/main.c', files, undefined,
+ { parsedFiles: [], parsedImport: parsedImport! },
+ );
+ return resolved === null ? [] : typeof resolved === 'string' ? [resolved] : resolved;
+ });
+ expect(targets).toEqual([[], ['src/api.h'], ['include/common.h']]);
+ });
+
+ it('does not treat a computed macro as a literal header path', () => {
+ expect(parseIncludes('#include HEADER_MACRO\n')).toEqual([null]);
+ });
+});

View file

@ -0,0 +1,155 @@
--- a/gitnexus/src/core/ingestion/languages/c/interpret.ts
+++ b/gitnexus/src/core/ingestion/languages/c/interpret.ts
@@ -8,10 +8,13 @@
const source = captures['@import.source']?.text;
if (source === undefined) return null;
- // System headers (e.g. <stdio.h>) are not resolved to local files
- if (captures['@import.system'] !== undefined) return null;
-
- return { kind: 'wildcard', targetRaw: source };
+ // Keep provenance locally without changing immutable shared declarations.
+ const parsedImport: ParsedImport & { isSystem: boolean } = {
+ kind: 'wildcard',
+ targetRaw: source,
+ isSystem: captures['@import.system'] !== undefined,
+ };
+ return parsedImport;
}
/**
--- a/gitnexus/src/core/ingestion/languages/c/scope-resolver.ts
+++ b/gitnexus/src/core/ingestion/languages/c/scope-resolver.ts
@@ -81,7 +81,11 @@
// this process. Runs BEFORE `populateOwners`.
applyCaptureSideChannel: applyCStaticLinkageSideChannel,
- resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig) => {
+ resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig, context) => {
+ const parsedImport = context?.parsedImport;
+ if (parsedImport?.kind === 'wildcard' && 'isSystem' in parsedImport && parsedImport.isSystem === true) {
+ return null;
+ }
// Augment allFilePaths with .h files discovered via loadResolutionConfig
// since the phase only passes .c files to the C resolver but #include
// targets .h files classified as C++ in language detection.
--- a/gitnexus/test/unit/scope-resolution/external-import-conformance.test.ts
+++ b/gitnexus/test/unit/scope-resolution/external-import-conformance.test.ts
@@ -38,6 +38,10 @@
import { describe, expect, it } from 'vitest';
import type { ParsedFile, ParsedImport, ScopeId, SymbolDefinition } from 'gitnexus-shared';
import { SupportedLanguages } from 'gitnexus-shared';
+import { getCParser } from '../../../src/core/ingestion/languages/c/query.js';
+import { splitCInclude } from '../../../src/core/ingestion/languages/c/import-decomposer.js';
+import { interpretCImport } from '../../../src/core/ingestion/languages/c/interpret.js';
+import type { SyntaxNode } from '../../../src/core/ingestion/utils/ast-helpers.js';
import { SCOPE_RESOLVERS } from '../../../src/core/ingestion/scope-resolution/pipeline/registry.js';
import type { ComposerConfig } from '../../../src/core/ingestion/language-config.js';
import {
@@ -143,6 +147,16 @@
};
}
+function parseCInclude(spelling: string): ParsedImport {
+ const tree = getCParser().parse(`#include ${spelling}\n`);
+ const node = tree.rootNode.namedChildren.find((child) => child.type === 'preproc_include');
+ if (!node) throw new Error('C conformance fixture did not parse');
+ const captures = splitCInclude(node as SyntaxNode);
+ const parsedImport = captures === null ? null : interpretCImport(captures);
+ if (parsedImport === null) throw new Error('C literal include was not retained');
+ return parsedImport;
+}
+
const PHP_FUNCTION_IMPORT = (targetRaw: string): ParsedImport => ({
kind: 'named',
localName: 'imported',
@@ -345,9 +359,10 @@
files: ['src/stdio.h', 'include/util.h', 'src/main.c'],
fromFile: 'src/main.c',
resolutionConfig: undefined,
- external: 'stdio.h',
+ external: '<stdio.h>',
decoy: 'src/stdio.h',
- reachesDecoy: 'util.h',
+ reachesDecoy: '"stdio.h"',
+ parsedImport: parseCInclude,
},
],
[
@@ -418,7 +433,6 @@
const KNOWN_GAPS: ReadonlyMap<SupportedLanguages, string> = new Map<SupportedLanguages, string>([
[SupportedLanguages.Dart, '`package:http/http.dart` -> `lib/http.dart`'],
[SupportedLanguages.Swift, '`Foundation` -> `Sources/Foundation/Thing.swift`'],
- [SupportedLanguages.C, '`stdio.h` -> `src/stdio.h`'],
[SupportedLanguages.CPlusPlus, '`cstdio.h` -> `src/cstdio.h`'],
]);
@@ -463,14 +477,15 @@
: testCase.files.map(
testCase.parsedFile ?? ((filePath) => ({ filePath }) as unknown as ParsedFile),
);
+ const parsedImport = testCase.parsedImport?.(targetRaw);
return resolver.resolveImportTarget(
- targetRaw,
+ parsedImport?.targetRaw ?? targetRaw,
testCase.fromFile,
files,
testCase.resolutionConfig,
{
parsedFiles,
- parsedImport: testCase.parsedImport?.(targetRaw),
+ parsedImport,
},
);
}
--- /dev/null
+++ b/gitnexus/test/unit/scope-resolution/c/c-include-provenance.test.ts
@@ -0,0 +1,48 @@
+import { describe, expect, it } from 'vitest';
+import { getCParser } from '../../../../src/core/ingestion/languages/c/query.js';
+import { splitCInclude } from '../../../../src/core/ingestion/languages/c/import-decomposer.js';
+import { interpretCImport } from '../../../../src/core/ingestion/languages/c/interpret.js';
+import { cScopeResolver } from '../../../../src/core/ingestion/languages/c/scope-resolver.js';
+import type { SyntaxNode } from '../../../../src/core/ingestion/utils/ast-helpers.js';
+
+function parseIncludes(source: string) {
+ return getCParser().parse(source).rootNode.namedChildren
+ .filter((node) => node.type === 'preproc_include')
+ .map((node) => {
+ const captures = splitCInclude(node as SyntaxNode);
+ return captures === null ? null : interpretCImport(captures);
+ });
+}
+
+function headerIdentity(target: string): string {
+ if ((target.startsWith('<') && target.endsWith('>')) ||
+ (target.startsWith('"') && target.endsWith('"'))) return target.slice(1, -1);
+ return target;
+}
+
+describe('C literal include provenance through parsing and resolution', () => {
+ it('retains system and quoted headers, resolving only local includes', () => {
+ const imports = parseIncludes([
+ '#include <sys/net.h>',
+ '#include "api.h"',
+ '#include "../include/common.h"',
+ ].join('\n') + '\n');
+ expect(imports).toHaveLength(3);
+ expect(imports.every((item) => item !== null)).toBe(true);
+ expect(imports.map((item) => headerIdentity(item!.targetRaw)))
+ .toEqual(['sys/net.h', 'api.h', '../include/common.h']);
+ const files = new Set(['src/main.c', 'include/sys/net.h', 'src/api.h', 'include/common.h']);
+ const targets = imports.map((parsedImport) => {
+ const resolved = cScopeResolver.resolveImportTarget(
+ parsedImport!.targetRaw, 'src/main.c', files, undefined,
+ { parsedFiles: [], parsedImport: parsedImport! },
+ );
+ return resolved === null ? [] : typeof resolved === 'string' ? [resolved] : resolved;
+ });
+ expect(targets).toEqual([[], ['src/api.h'], ['include/common.h']]);
+ });
+
+ it('does not treat a computed macro as a literal header path', () => {
+ expect(parseIncludes('#include HEADER_MACRO\n')).toEqual([null]);
+ });
+});

View file

@ -3,7 +3,7 @@
# Task fields:
# id: short slug used in reports
# class: task class for cost/quality routing analysis. The suite spans:
# trivial → investigation-bug → investigation-feature → cross-module
# trivial → investigation-feature → cross-module
# repo: path to a git repo that is GitNexus-indexed
# ref: git ref benchmarked (fresh detached worktree per arm per run)
# setup: optional shell command run in the fresh worktree first
@ -29,7 +29,7 @@
# prebuilt .gitnexus directory is never copied. Three declared dependency trees
# are mounted read-only; missing assets fail before model execution.
# Every task starts at the exact v1.6.12 commit, where all four tasks remain
# unsolved. In particular, moving main already contains the C include fix.
# unsolved. The C provenance feature replaces the invalid historical system-include task.
tasks:
# Overhead floor — measured 2026-07-11 (see README calibration): the
@ -67,30 +67,39 @@ tasks:
- source: trivial-status-json-alias.oracle.test.ts
target: trivial-status-json-alias.oracle.test.ts
# Investigation-heavy bug (#2965): requires following the C import marker
# through the shared scope-resolution pipeline without breaking local
# quoted-header resolution.
- id: inv-bug-c-system-include
class: investigation-bug
# Investigation-heavy feature: retain literal include provenance through
# parsing and resolution without binding system headers to repository decoys.
- id: inv-feature-c-include-provenance
class: investigation-feature
repo: ~/GitNexus
ref: c4ecf398de637cf779ab2238e69958cfd5d601ce
sandbox_dependencies: *gitnexus_sandbox_dependencies
prompt: >
Fix the C scope resolver so a system include such as <stdio.h> never
resolves to an unrelated same-named header inside the repository, while
quoted/local includes still resolve. Remove C from the conformance
suite's KNOWN_GAPS and cover the negative and positive cases there.
verify: cd gitnexus && npx tsc --noEmit && npx vitest run test/unit/scope-resolution/external-import-conformance.test.ts
Retain literal C #include records after parsing and interpretation, including
system includes such as <stdio.h>. Preserve each header's identity and enough
provenance to distinguish system from quoted includes. With no configured
system include roots, system includes must never resolve to unrelated
same-named repository headers; quoted sibling and relative includes must
still resolve. Computed macro includes remain unsupported and produce no
parsed import. Cover the real parser, include decomposition, interpretation
and resolver in test/unit/scope-resolution/c/c-include-provenance.test.ts.
Correct the C conformance fixture to distinguish system and quoted forms of
the same header, and remove C from KNOWN_GAPS. Choose the representation;
shared dependency declarations are immutable.
verify: >-
cd gitnexus && npx tsc --noEmit && npx vitest run
test/unit/scope-resolution/c/c-include-provenance.test.ts
test/unit/scope-resolution/external-import-conformance.test.ts
oracle:
command: >-
cd gitnexus && ./node_modules/.bin/vitest run
--config "$GITNEXUS_BENCH_ORACLE_ROOT/vitest.config.mts"
"$GITNEXUS_BENCH_ORACLE_ROOT/inv-bug-c-system-include.oracle.test.ts"
"$GITNEXUS_BENCH_ORACLE_ROOT/inv-feature-c-include-provenance.oracle.test.ts"
files:
- source: vitest.config.mts
target: vitest.config.mts
- source: inv-bug-c-system-include.oracle.test.ts
target: inv-bug-c-system-include.oracle.test.ts
- source: inv-feature-c-include-provenance.oracle.test.ts
target: inv-feature-c-include-provenance.oracle.test.ts
# Investigation-heavy feature: touches tool schema, backend filtering, and
# pagination totals — three seams that must stay consistent.