test(cfg): pin *-harvest.ts literals to their own grammar in the gate (#2195 U10)

The grammar-literal validation gate scans cfg/visitors/, but a <lang>-harvest.ts
basename was not in BASENAME_LANGS, so fileLanguages() fell it through to the
weak ALL_LANGS valid-if-any bucket — a node-type literal dead in its own grammar
but valid in some other grammar would pass undetected. Strip the -harvest suffix
and reuse the visitor basename map so go-harvest -> Go, c-cpp-harvest -> C+C++,
typescript-harvest -> TS, etc. The two language-agnostic harvesters
(call-site-harvest, scope-tree-harvest) name no grammar and stay valid-if-any.
Also corrects the now-inaccurate mode2Files comment. Adds a fileLanguages unit
test; the existing gate stays green (no harvest file has a dead literal), and a
scratch probe confirmed a bogus go-harvest literal is now caught.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-06-14 17:00:00 +00:00
parent f47e952ba1
commit 85d25c87cf
2 changed files with 43 additions and 6 deletions

View file

@ -139,6 +139,17 @@ function fileLanguages(relPath: string): SupportedLanguages[] {
}
const base = relPath.replace(/\.ts$/, '').split('/').pop() ?? '';
if (BASENAME_LANGS[base]) return BASENAME_LANGS[base];
// A `<lang>-harvest.ts` CFG def/use harvester is validated against the SAME
// grammar(s) as its `<lang>.ts` CFG visitor (go-harvest → Go, c-cpp-harvest →
// C+C++, typescript-harvest → TS, …) — strip the suffix and reuse the visitor
// basename map. The two genuinely language-agnostic harvesters —
// call-site-harvest.ts (pure taint-site mechanism) and scope-tree-harvest.ts
// (shared lexical-scope substrate) — name no grammar, so their stripped base
// (`call-site`, `scope-tree`) misses BASENAME_LANGS and they fall through to
// the valid-if-any ALL_LANGS bucket below (correct: they pin no per-grammar
// literal).
const harvestBase = base.replace(/-harvest$/, '');
if (harvestBase !== base && BASENAME_LANGS[harvestBase]) return BASENAME_LANGS[harvestBase];
// generic / shared / cross-language helpers → any grammar (valid-if-any)
return [...ALL_LANGS];
}
@ -183,12 +194,15 @@ function mode2Files(): string[] {
if (existsSync(cap)) files.push(cap);
}
}
// CFG visitors (cfg/visitors/<lang>.ts) hard-code tree-sitter node-type and
// field literals directly; include them so the gate validates per-language
// CFG literals against the right grammar (basename → grammar via
// `fileLanguages`: c-cpp → C+C++, csharp → C#, java → Java, go → Go,
// typescript[-harvest] → TS). Without this, the gate stays green on dead
// literals in a new visitor — the exact failure KTD5 warns about.
// CFG visitors (cfg/visitors/<lang>.ts) AND their def/use harvesters
// (cfg/visitors/<lang>-harvest.ts) hard-code tree-sitter node-type and field
// literals directly; include them so the gate validates per-language CFG
// literals against the right grammar (basename → grammar via `fileLanguages`:
// c-cpp[-harvest] → C+C++, csharp[-harvest] → C#, go[-harvest] → Go,
// typescript[-harvest] → TS, …; the language-agnostic call-site-harvest.ts and
// scope-tree-harvest.ts carry no grammar literal and stay valid-if-any).
// Without this, the gate stays green on a dead literal in a new visitor or
// harvester — the exact failure KTD5 warns about.
const cfgVisitorsDir = join(INGESTION_DIR, 'cfg', 'visitors');
if (existsSync(cfgVisitorsDir)) walkTs(cfgVisitorsDir, files);
const exportDetection = join(INGESTION_DIR, 'export-detection.ts');

View file

@ -82,6 +82,29 @@ describe('literal-collectors', () => {
});
});
describe('fileLanguages — `*-harvest.ts` map to their grammar (U10)', () => {
const fl = __test.fileLanguages;
it('maps each <lang>-harvest.ts to the same grammar(s) as its <lang>.ts visitor', () => {
expect(fl('cfg/visitors/go-harvest.ts')).toEqual([SupportedLanguages.Go]);
expect(fl('cfg/visitors/csharp-harvest.ts')).toEqual([SupportedLanguages.CSharp]);
expect(fl('cfg/visitors/typescript-harvest.ts')).toEqual([SupportedLanguages.TypeScript]);
expect(fl('cfg/visitors/c-cpp-harvest.ts')).toEqual([
SupportedLanguages.C,
SupportedLanguages.CPlusPlus,
]);
// a harvester pins exactly its visitor's grammar set, not the weak ALL_LANGS
expect(fl('cfg/visitors/go-harvest.ts')).toEqual(fl('cfg/visitors/go.ts'));
});
it('leaves the language-agnostic harvesters valid-if-any (ALL_LANGS)', () => {
const all = fl('cfg/visitors/call-site-harvest.ts');
const scopeTree = fl('cfg/visitors/scope-tree-harvest.ts');
// both name no grammar → fall through to the full gated set, which is far
// larger than any single-/dual-language harvester mapping
expect(all.length).toBeGreaterThan(2);
expect(scopeTree).toEqual(all);
});
});
describe('Mode 4 — registry resolution layer (TypeChecker-gated)', () => {
it('scans the resolution layer and tags literals by language dir', () => {
const { nodeTypes } = __test.collectResolutionLayerLiterals();