mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-02 09:45:39 +08:00
fix(explore): three retrieval regressions found against v1.6.0
The pre-release agent A/B (main vs v1.6.0, 7 README repos, 3 runs each) had main returning 15% less source on vscode for the same queries, with Reads in 2 of 3 runs where v1.6.0 had none. Replaying every query both builds' agents issued traced it to three causes: 1. Named gaps (#1711) displaced source. Every cluster fit measured the gap names, which on a long path run to hundreds of chars, so a class shrunk into its file's room overran it by the names alone and was dropped whole: rpcProtocol.ts emitted 222 of 5,496 funded chars (v1.6.0: 5,194), and the test fixture came back as an empty fence. Fits now measure bare gaps; names are added at assembly from what the file's budget has left. 2. Interface members (#1638) corroborated English words. The NL-stopword guard lets a bare word seed a definition when another query word names a symbol in the same file; `readonly host` in an options interface made "main" seed `main()` in agentHostServerMain.ts, which took the named-first tier from the answer files. Members declared inside an interface no longer count as corroboration. 3. The interface-dispatch note announced dispatch through any common base, declared or not: "`extension` -> runtime dispatch to 2706 types implementing Disposable" opened 9 of 31 vscode answers. A supertype now has to declare the member (it, an ancestor, or a same-named Swift extension). When absence can't be judged, because a protocol's requirements aren't indexed, the note stays as before. Replayed queries, source lines vs v1.6.0 (main -> fixed): vscode 10,744 -> 12,577 (12,617), excalidraw 13,065 -> 13,698 (13,764), django 8,420 -> 8,803 (8,954), tokio 13,618 -> 14,151 (14,600), okhttp 6,511 -> 6,883 (7,090), alamofire 7,019 -> 7,249 (7,120); no query below 60% of v1.6.0. Real dispatch notes (Alamofire adapt/retry/asURLRequest, django, okhttp) are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1cbac0afaa
commit
e5e4921d30
+3
-1
@@ -162,6 +162,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- `codegraph node` now accepts a file reference that carries a line number — `src/app.ts:42`, `src/app.ts:42-80`, `src/app.ts#L42`, `src/app.ts#L42-L80` — instead of reporting the file as not indexed; the line range becomes the window that is read, and an `--offset`/`--limit` you pass yourself still wins. A path that really is named that way is still looked up as written. (#1831)
|
||||
- `codegraph callers`, `codegraph callees` and `codegraph impact` now resolve qualified names, group results and JSON edges by definition, and accept `--file` to narrow ambiguous names; thanks @ferrine. (#1512, #1656)
|
||||
- `codegraph callers`, `codegraph callees` and `codegraph impact` (CLI and MCP) now report missing names with did-you-mean suggestions instead of another symbol's results, and exact matches with no callers stay empty; thanks @uvmplus. (#1473, #1481)
|
||||
- When `codegraph_explore` trims a large file, the gaps in its source now name the symbols that were left out, using only room the file's budget has left, so the names never push out the code itself. (#1711)
|
||||
- `codegraph_explore` no longer reports "runtime dispatch" through a base class that doesn't declare the method, so a common name shared by many unrelated subclasses no longer opens an answer with a list of thousands of implementations.
|
||||
|
||||
#### MCP / indexing
|
||||
|
||||
@@ -327,7 +329,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
- Objective-C headers now index in a project that has no `.m` file. A `.h` file is read as C from its name alone, and only later — once its contents are read — recognized as Objective-C; the grammar for that was never loaded up front, so the file failed with a parser error and nothing in it reached the index. Adding any `.m` file used to make the same header work, which is what made this look arbitrary. Thanks @Juddd. (#1628)
|
||||
|
||||
- TypeScript interface methods and properties are now indexed, so `node`, `callers` and impact can find platform `.d.ts` APIs while declaration-only files keep their lower ranking on flow queries; re-index TypeScript projects after upgrading. (#1638)
|
||||
- TypeScript interface methods and properties are now indexed, so `node`, `callers` and impact can find platform `.d.ts` APIs while declaration-only files keep their lower ranking on flow queries, and an interface's members don't pull unrelated files into answers to plain-English questions; re-index TypeScript projects after upgrading. (#1638)
|
||||
- Lua and Luau function expressions assigned to locals, table members, or keyed table fields are now indexed as callable nodes. Calls from `local f = function() ... end`, `M.f = function() ... end`, and callback tables such as `M.handlers = { onClick = function() ... end }` are attributed to the named function or method instead of collapsing onto the file node, so callers and impact no longer omit these handlers. Re-index after upgrading. (#1616, #1650)
|
||||
- **Functions bound with `const` inside another function are symbols now.** `const handleClear = () => {…}` inside a React component — every handler that skips `useCallback` — was invisible to `callers`, `callees` and impact, answering "Symbol not found" exactly the way a function with no callers would. It is indexed like its module-level twin, contained by the enclosing function, with its own calls. Re-index after upgrading. (#1669)
|
||||
- `codegraph callers`, `callees`, and `query` now clearly report when their result limit hides additional matches, including exact totals in callers/callees JSON output; the `codegraph_callers` and `codegraph_callees` MCP answers carry the same "showing N of M" note. (#1639, #1674)
|
||||
|
||||
@@ -402,6 +402,38 @@ describe('codegraph_explore — interface dispatch', () => {
|
||||
expect(text).not.toContain('**Interface dispatch');
|
||||
});
|
||||
|
||||
// vscode shape: many unrelated classes share a lifecycle base and happen to
|
||||
// share a member name the base never declares. That is not dispatch through
|
||||
// the base — announcing it put "runtime dispatch to 2706 types implementing
|
||||
// Disposable" at the top of every answer whose query said "extension".
|
||||
const lifecycleFamily = () => {
|
||||
const names = ['Editor', 'Terminal', 'Search', 'Debug', 'Scm', 'Chat', 'Notebook', 'Output', 'Tasks', 'Remote'];
|
||||
return [
|
||||
'export abstract class Disposable { dispose(): void {} }',
|
||||
...names.map((nm, i) => [
|
||||
`export class ${nm}Service extends Disposable {`,
|
||||
` get extension(): string { return '${nm.toLowerCase()}'; }`,
|
||||
` dispose(): void { super.dispose(); }`,
|
||||
` describe${nm}() { return this.extension + ${i}; }`,
|
||||
'}',
|
||||
].join('\n')),
|
||||
].join('\n');
|
||||
};
|
||||
|
||||
it('stays SILENT for a shared name the common base never declares', async () => {
|
||||
await setup({ 'services.ts': lifecycleFamily() }, ['**/*.ts']);
|
||||
const res = await handler.execute('codegraph_explore', { query: 'extension describeEditor describeChat' });
|
||||
const text = res.content[0].text as string;
|
||||
expect(text).not.toMatch(/`extension` → runtime dispatch/);
|
||||
});
|
||||
|
||||
it('still announces a member the common base declares', async () => {
|
||||
await setup({ 'services.ts': lifecycleFamily() }, ['**/*.ts']);
|
||||
const res = await handler.execute('codegraph_explore', { query: 'dispose describeEditor describeChat' });
|
||||
const text = res.content[0].text as string;
|
||||
expect(text).toMatch(/`dispose` → runtime dispatch to \*\*10\*\* types implementing `Disposable`/);
|
||||
});
|
||||
|
||||
it('stays SILENT when the interface family is below the polymorphism threshold (3 impls)', async () => {
|
||||
await setup({ 'nodes.ts': nodeFamily(3), 'registry.ts': registry, 'engine.ts': engine }, ['**/*.ts']);
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Naming what a trim skipped (#1711) must never cost a file its source.
|
||||
*
|
||||
* Every cluster fit was measured with the gap names in, and on a long path a
|
||||
* named gap runs to several hundred chars. A class that had to be shrunk into
|
||||
* its file's room then overran the room by its gap names alone and was
|
||||
* dropped whole: vscode's `rpcProtocol.ts` came back as a 3-line stub instead
|
||||
* of ~5,200 chars of the RPCProtocol body, and the fixture below came back as
|
||||
* an empty code fence. Fits now measure bare gaps; names are added at assembly
|
||||
* from whatever the file's budget has left.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import CodeGraph from '../src/index';
|
||||
import { ToolHandler, joinPartsWithNamedGaps } from '../src/mcp/tools';
|
||||
|
||||
describe('joinPartsWithNamedGaps spare budget', () => {
|
||||
const parts = [
|
||||
{ range: { start: 1, end: 5 }, text: 'ONE' },
|
||||
{ range: { start: 40, end: 45 }, text: 'TWO' },
|
||||
{ range: { start: 80, end: 85 }, text: 'THREE' },
|
||||
];
|
||||
const nodes = [
|
||||
{ name: 'first', kind: 'function', startLine: 20, endLine: 25 },
|
||||
{ name: 'second', kind: 'function', startLine: 60, endLine: 65 },
|
||||
];
|
||||
const bare = 'ONE\n\n... (gap) ...\n\nTWO\n\n... (gap) ...\n\nTHREE';
|
||||
|
||||
it('names every gap when the spare is unbounded', () => {
|
||||
const text = joinPartsWithNamedGaps('f.ts', parts, nodes);
|
||||
expect(text).toContain('first (f.ts:20)');
|
||||
expect(text).toContain('second (f.ts:60)');
|
||||
});
|
||||
|
||||
it('leaves every gap bare with no spare', () => {
|
||||
expect(joinPartsWithNamedGaps('f.ts', parts, nodes, 0)).toBe(bare);
|
||||
});
|
||||
|
||||
it('names gaps in order while the spare lasts, then goes bare', () => {
|
||||
const oneNamed = joinPartsWithNamedGaps('f.ts', parts, nodes, 'first (f.ts:20)'.length + 8);
|
||||
expect(oneNamed).toContain('first (f.ts:20)');
|
||||
expect(oneNamed).not.toContain('second (f.ts:60)');
|
||||
expect(oneNamed.length).toBeLessThanOrEqual(bare.length + 'first (f.ts:20)'.length + 8);
|
||||
});
|
||||
});
|
||||
|
||||
describe('explore — gap names never displace a shrunk class', () => {
|
||||
const rel = 'src/workbench/services/extensions/common/protocol/implementation/rpcProtocolHost.ts';
|
||||
let dir: string;
|
||||
let cg: CodeGraph;
|
||||
let response: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-gap-budget-'));
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), '{"name":"gap-budget","version":"1.0.0"}\n');
|
||||
fs.mkdirSync(path.join(dir, path.dirname(rel)), { recursive: true });
|
||||
// A class too big to ship whole but under half its file (so it is not
|
||||
// dropped as an envelope), a tiny entry function far below it, and a deep
|
||||
// path so every named gap is long.
|
||||
const lines = ['export interface MessageSink { accept(payload: string): void }', ''];
|
||||
lines.push('export class RPCProtocolHost {');
|
||||
lines.push(' private readonly pending = new Map<number, string>();');
|
||||
for (let i = 0; i < 70; i++) {
|
||||
lines.push(
|
||||
` receiveIncomingMessageNumber${i}(payload: string): string {`,
|
||||
` const value = payload + ':${i}:' + this.pending.size;`,
|
||||
` this.pending.set(${i}, value);`,
|
||||
" const extra = value.split(':').map((part) => part.toUpperCase()).join('-');",
|
||||
' return (value + extra).trim();',
|
||||
' }',
|
||||
'',
|
||||
);
|
||||
}
|
||||
lines.push('}', '');
|
||||
for (let i = 0; i < 160; i++) {
|
||||
lines.push(`export function unrelatedHelperNumber${i}(n: number): number {`, ` return n * ${i} + 1;`, '}', '');
|
||||
}
|
||||
lines.push(
|
||||
'export function dispatchRequestNow(host: RPCProtocolHost): string {',
|
||||
" return host.receiveIncomingMessageNumber3('x');",
|
||||
'}',
|
||||
'',
|
||||
);
|
||||
fs.writeFileSync(path.join(dir, rel), lines.join('\n'));
|
||||
for (let i = 1; i <= 12; i++) {
|
||||
fs.writeFileSync(path.join(dir, 'src', `noise${i}.ts`), `export const n${i} = ${i};\n`);
|
||||
}
|
||||
|
||||
cg = CodeGraph.initSync(dir);
|
||||
await cg.indexAll();
|
||||
const result = await new ToolHandler(cg).execute('codegraph_explore', {
|
||||
query:
|
||||
'RPCProtocolHost receiveIncomingMessageNumber3 receiveIncomingMessageNumber40 receiveIncomingMessageNumber66 dispatchRequestNow',
|
||||
});
|
||||
response = result.content?.[0]?.text ?? '';
|
||||
}, 120_000);
|
||||
|
||||
afterAll(() => {
|
||||
cg?.destroy();
|
||||
if (dir && fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('renders the named methods inside the class', () => {
|
||||
expect(response, response.slice(0, 3000)).toContain('receiveIncomingMessageNumber3(payload: string): string {');
|
||||
expect(response).toContain('receiveIncomingMessageNumber40(payload: string): string {');
|
||||
expect(response).toContain('export function dispatchRequestNow(host: RPCProtocolHost)');
|
||||
});
|
||||
|
||||
it('never emits an empty source fence for the file', () => {
|
||||
expect(response).not.toMatch(/```typescript\n```/);
|
||||
});
|
||||
|
||||
it('still names a gap when the budget has room for it', () => {
|
||||
expect(response).toMatch(/\.\.\. \(gap: \w+ \([^\s)]+:\d+\)/);
|
||||
});
|
||||
});
|
||||
@@ -123,3 +123,54 @@ describe('codegraph_explore — NL-stopword collision guard', () => {
|
||||
expect(files[0]).toMatch(/updater\.ts$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('codegraph_explore — interface members never corroborate a bare word', () => {
|
||||
let testDir: string;
|
||||
let cg: CodeGraph;
|
||||
|
||||
afterEach(() => {
|
||||
cg?.destroy();
|
||||
if (testDir && fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('an options interface\'s `host` does not seed the same file\'s `main()` above the answer', async () => {
|
||||
// vscode's agentHostServerMain.ts: `interface IServerOptions { readonly host… }`
|
||||
// beside `function main()`. Once interface members were indexed (#1638),
|
||||
// `host` in "extension host … main process" counted as a second query
|
||||
// token named in that file, so the English word "main" seeded `main()` into
|
||||
// the named-first tier and the file outranked the ones about the extension
|
||||
// host itself.
|
||||
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-iface-corroboration-'));
|
||||
const src = path.join(testDir, 'src');
|
||||
fs.mkdirSync(src, { recursive: true });
|
||||
fs.writeFileSync(path.join(src, 'serverMain.ts'),
|
||||
'interface IServerOptions {\n' +
|
||||
' readonly port: number;\n' +
|
||||
' readonly host: string | undefined;\n' +
|
||||
'}\n\n' +
|
||||
'export function main(options: IServerOptions): number {\n' +
|
||||
' return options.port;\n' +
|
||||
'}\n');
|
||||
fs.writeFileSync(path.join(src, 'extensionHostProcess.ts'),
|
||||
'export class ExtensionHostConnection {\n' +
|
||||
' constructor(private readonly send: (msg: string) => void) {}\n' +
|
||||
' connectToMainProcess(): void {\n' +
|
||||
" this.send('hello');\n" +
|
||||
' }\n' +
|
||||
'}\n\n' +
|
||||
'export function startExtensionHostProcess(): ExtensionHostConnection {\n' +
|
||||
' const connection = new ExtensionHostConnection((msg) => console.log(msg));\n' +
|
||||
' connection.connectToMainProcess();\n' +
|
||||
' return connection;\n' +
|
||||
'}\n');
|
||||
for (let i = 1; i <= 12; i++) fs.writeFileSync(path.join(src, `noise${i}.ts`), `export const n${i} = ${i};\n`);
|
||||
cg = CodeGraph.initSync(testDir);
|
||||
await cg.indexAll();
|
||||
|
||||
const res = await new ToolHandler(cg).execute('codegraph_explore', {
|
||||
query: 'how does the extension host talk to the main process',
|
||||
});
|
||||
const files = sourcedFiles(res.content[0].text as string);
|
||||
expect(files[0]).toBe('src/extensionHostProcess.ts');
|
||||
});
|
||||
});
|
||||
|
||||
+104
-11
@@ -932,6 +932,8 @@ export function symbolsNotInRanges(
|
||||
return out;
|
||||
}
|
||||
|
||||
const BARE_GAP_MARKER = '\n\n... (gap) ...\n\n';
|
||||
|
||||
/**
|
||||
* Gap marker between two non-contiguous slices of one file.
|
||||
*
|
||||
@@ -945,7 +947,7 @@ export function formatGapMarker(
|
||||
filePath: string,
|
||||
elided: ReadonlyArray<ElidedSymbolRef>,
|
||||
): string {
|
||||
if (elided.length === 0) return '\n\n... (gap) ...\n\n';
|
||||
if (elided.length === 0) return BARE_GAP_MARKER;
|
||||
const shown = elided.slice(0, ELIDED_SYMBOL_CAP);
|
||||
const more = elided.length - shown.length;
|
||||
const names = shown.map((s) => `${s.name} (${filePath}:${s.startLine})`).join(', ')
|
||||
@@ -953,18 +955,37 @@ export function formatGapMarker(
|
||||
return `\n\n... (gap: ${names}) ...\n\n`;
|
||||
}
|
||||
|
||||
/** Join rendered parts with gap markers that name whatever the trim skipped. */
|
||||
/**
|
||||
* Join rendered parts with gap markers that name whatever the trim skipped.
|
||||
*
|
||||
* `spareChars` is what naming may cost beyond bare markers. Cluster selection
|
||||
* prices every join between clusters as a bare marker, and on a long path each
|
||||
* named gap runs to several hundred chars, so unbounded naming overran the
|
||||
* file's reservation and the ceiling trim then dropped SOURCE to pay for the
|
||||
* names: vscode's `rpcProtocol.ts` went from ~5,200 chars of the RPCProtocol
|
||||
* body to a 222-char stub. A gap the spare can't cover stays bare; the file
|
||||
* header still lists the symbols the trim dropped.
|
||||
*/
|
||||
export function joinPartsWithNamedGaps(
|
||||
filePath: string,
|
||||
parts: ReadonlyArray<{ range: ExploreLineRange; text: string }>,
|
||||
nodes: ReadonlyArray<{ name: string; kind: string; startLine: number; endLine: number }>,
|
||||
spareChars = Infinity,
|
||||
): string {
|
||||
if (parts.length === 0) return '';
|
||||
let out = parts[0]!.text;
|
||||
let spare = spareChars;
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const prev = parts[i - 1]!;
|
||||
const next = parts[i]!;
|
||||
out += formatGapMarker(filePath, symbolsBetweenRanges(nodes, prev.range.end, next.range.start));
|
||||
const named = formatGapMarker(filePath, symbolsBetweenRanges(nodes, prev.range.end, next.range.start));
|
||||
const extra = named.length - BARE_GAP_MARKER.length;
|
||||
if (extra <= spare) {
|
||||
out += named;
|
||||
spare -= extra;
|
||||
} else {
|
||||
out += BARE_GAP_MARKER;
|
||||
}
|
||||
out += next.text;
|
||||
}
|
||||
return out;
|
||||
@@ -3242,10 +3263,62 @@ export class ToolHandler {
|
||||
try { const ce = cg.getIncomingEdges(m.id).find((e) => e.kind === 'contains'); return ce ? cg.getNode(ce.source) : null; }
|
||||
catch { return null; }
|
||||
};
|
||||
// A supertype dispatches only a member it (or an ancestor) declares. Without
|
||||
// this, any name shared by enough subclasses read as dispatch through their
|
||||
// common base: on vscode the query word `extension`, a getter on unrelated
|
||||
// classes that all extend `Disposable`, was announced as "runtime dispatch to
|
||||
// 2706 types implementing Disposable" at the top of 9 of 31 answers.
|
||||
//
|
||||
// A Swift protocol and each of its extensions are separate nodes of one name,
|
||||
// and a conformer's edge may land on any of them (Alamofire's
|
||||
// `RequestInterceptor` conformers point at an extension in OfflineRetrier.swift
|
||||
// that has no `adapt`), so every same-named type is asked. The answer is
|
||||
// `unknown`, and the announcement stays, whenever absence can't be judged:
|
||||
// nothing on the chain has indexed members of the family's sort, or an
|
||||
// interface/protocol on it has none (Swift requirements aren't nodes, so
|
||||
// `URLRequestConvertible` can't be said to lack `asURLRequest`).
|
||||
const memberOfSupertype = (typeId: string, name: string, callable: boolean): 'declared' | 'absent' | 'unknown' => {
|
||||
const counts = (kind: string) => !callable || kind === 'method' || kind === 'function';
|
||||
let sawMembers = false;
|
||||
let opaque = false;
|
||||
const seen = new Set<string>();
|
||||
const visit = (id: string, depth: number): boolean => {
|
||||
if (seen.has(id)) return false;
|
||||
seen.add(id);
|
||||
let node: Node | null = null;
|
||||
let edges: ReturnType<typeof cg.getOutgoingEdges> = [];
|
||||
try { node = cg.getNode(id); edges = cg.getOutgoingEdges(id); } catch { return false; }
|
||||
let own = 0;
|
||||
for (const e of edges) {
|
||||
if (e.kind !== 'contains') continue;
|
||||
let child: Node | null = null;
|
||||
try { child = cg.getNode(e.target); } catch { child = null; }
|
||||
if (!child) continue;
|
||||
if (child.name === name) return true;
|
||||
if (counts(child.kind)) own++;
|
||||
}
|
||||
if (own > 0) sawMembers = true;
|
||||
else if (node && (node.kind === 'interface' || node.kind === 'protocol' || node.kind === 'trait')) opaque = true;
|
||||
if (depth >= 3) return false;
|
||||
return edges.some((e) => (e.kind === 'extends' || e.kind === 'implements') && visit(e.target, depth + 1));
|
||||
};
|
||||
let root: Node | null = null;
|
||||
try { root = cg.getNode(typeId); } catch { root = null; }
|
||||
let namesakes: Node[] = [];
|
||||
try {
|
||||
namesakes = root
|
||||
? cg.getNodesByName(root.name).filter((n) => n.language === root!.language && CLASSY.has(n.kind))
|
||||
: [];
|
||||
} catch { namesakes = []; }
|
||||
if (visit(typeId, 0) || namesakes.some((n) => visit(n.id, 0))) return 'declared';
|
||||
return sawMembers && !opaque ? 'absent' : 'unknown';
|
||||
};
|
||||
const notes: string[] = [];
|
||||
const seenSuper = new Set<string>();
|
||||
for (const { token, family } of candidates) {
|
||||
if (notes.length >= MAX_NOTES) break;
|
||||
const memberName = family[0]?.name ?? token;
|
||||
const callableFamily = family[0]?.kind === 'method' || family[0]?.kind === 'function';
|
||||
// supertype id → how many sampled definers share it + a few example definers
|
||||
const supers = new Map<string, { node: Node; count: number; targets: Node[] }>();
|
||||
for (const m of family.slice(0, SAMPLE)) {
|
||||
@@ -3277,7 +3350,9 @@ export class ToolHandler {
|
||||
// `extends` and a synthesized `implements` is one implementation.
|
||||
const impl = countImplementers(cg, node.id);
|
||||
if (impl < MIN_IMPL) continue;
|
||||
if (!best || impl > best.impl) best = { node, impl, targets };
|
||||
if (best && impl <= best.impl) continue;
|
||||
if (memberOfSupertype(node.id, memberName, callableFamily) === 'absent') continue;
|
||||
best = { node, impl, targets };
|
||||
}
|
||||
if (!best || seenSuper.has(best.node.id)) continue;
|
||||
seenSuper.add(best.node.id);
|
||||
@@ -3813,7 +3888,17 @@ export class ToolHandler {
|
||||
if (!names) {
|
||||
names = new Set<string>();
|
||||
try {
|
||||
for (const n of cg.getNodesInFile(fp)) names.add(n.name.toLowerCase());
|
||||
// An interface's members (#1638) describe a shape; nobody names
|
||||
// them in a question. Counting them let `readonly host: string` in
|
||||
// an options interface corroborate the English word "main" into
|
||||
// seeding the same file's `main()`, which then took the named-first
|
||||
// tier from the answer files (vscode "extension host … main process").
|
||||
const fileNodes = cg.getNodesInFile(fp);
|
||||
const interfaces = fileNodes.filter((n) => n.kind === 'interface');
|
||||
const declaresShape = (n: Node) =>
|
||||
(n.kind === 'property' || n.kind === 'method') &&
|
||||
interfaces.some((i) => n.startLine >= i.startLine && n.endLine <= i.endLine);
|
||||
for (const n of fileNodes) if (!declaresShape(n)) names.add(n.name.toLowerCase());
|
||||
} catch { /* unreadable file entry — treat as uncorroborated */ }
|
||||
fileNameSets.set(fp, names);
|
||||
}
|
||||
@@ -5481,12 +5566,14 @@ export class ToolHandler {
|
||||
// does the slicing; a second function mirroring these window/padding rules
|
||||
// would drift.
|
||||
type SectionPart = { range: ExploreLineRange; text: string };
|
||||
// Named gaps (#1711): a bare `... (gap) ...` hid the answer when the
|
||||
// trim dropped the symbols the query was asking for. Budget estimates
|
||||
// still use GAP_MARKER.length (a lower bound); the final fit test
|
||||
// measures the real joined text.
|
||||
// Every fit decision below measures parts joined by BARE gap markers.
|
||||
// Naming what a gap skipped (#1711) is added once, at assembly, from what
|
||||
// the file's budget has left: measured with the names, a shrunk cluster
|
||||
// could overrun its room by the names alone and be dropped whole — the
|
||||
// RPCProtocol class in vscode's rpcProtocol.ts went out that way, leaving a
|
||||
// 3-line stub where ~5,200 chars of its body had fit.
|
||||
const sectionText = (parts: ReadonlyArray<SectionPart>): string =>
|
||||
joinPartsWithNamedGaps(filePath, parts, fileIndexNodes);
|
||||
joinPartsWithNamedGaps(filePath, parts, fileIndexNodes, 0);
|
||||
const buildSection = (
|
||||
c: { start: number; end: number; hasSpine?: boolean; spineCallLine?: number },
|
||||
): SectionPart[] => {
|
||||
@@ -5901,6 +5988,10 @@ export class ToolHandler {
|
||||
projectedChars += text.length + GAP_MARKER.length;
|
||||
}
|
||||
|
||||
// Gap names (#1711) are paid from what selection left of this file's
|
||||
// budget, never from source: selection measured every gap as bare.
|
||||
const namedGapBudget = Math.max(fileBudget, projectedChars);
|
||||
|
||||
// Emit chosen clusters in source order so the file reads top-to-bottom.
|
||||
// Assembled through a function because it may have to run more than once:
|
||||
// the fit test below trims the weakest cluster and re-assembles rather
|
||||
@@ -5930,7 +6021,9 @@ export class ToolHandler {
|
||||
}
|
||||
}
|
||||
const ranges = parts.map((p) => p.range);
|
||||
const text = joinPartsWithNamedGaps(filePath, parts, fileIndexNodes);
|
||||
const text = joinPartsWithNamedGaps(
|
||||
filePath, parts, fileIndexNodes, Math.max(0, namedGapBudget - sectionText(parts).length),
|
||||
);
|
||||
// Header bias prefers RELEVANT elisions (cluster members the trim cut)
|
||||
// over incidental index filler — otherwise locale-sorted `calls0`…
|
||||
// crowds out the answer methods (#1711).
|
||||
|
||||
Reference in New Issue
Block a user