feat(extraction): content-based generated-file detection (CG-5, #1500)
`isGeneratedFile` was path-only, but Go's own convention is a CONTENT marker (`// Code generated by <tool>. DO NOT EDIT.`), not a filename one. A Go monorepo with generated CRUD in ordinarily-named files sitting beside hand-written use-cases was therefore invisible to every generated-file down-rank in the codebase — that is #1500. Measured on kubernetes/client-go (2,453 Go files): the canonical banner appears in 2,001 of them, the path check flags 0, the new content check flags exactly those 2,001 — no false positives, no misses. Design: decide at INDEX time (content is already in memory for parsing), persist on `files.generated`, read from the DB. Explore never reads file headers per request. - `hasGeneratedHeader(content)` recognizes the standard banners — Go's, protoc's, `@generated`, `<auto-generated>`, Thrift, OpenAPI Generator, FlatBuffers, bindgen, ANTLR. Precision-first and fenced three ways: an 8KB/60-line header window, a comment-line requirement (leader or open block comment), and markers tight enough that prose can't trip them. A generator's own source, holding the banner as a string constant in its body, is not flagged; neither is this module itself (pinned by test). - `isGeneratedFile(path)` is unchanged — cheap, sync, still the fallback. - Schema v9 adds `files.generated` + a PARTIAL index. DDL only, no backfill: the flag derives from content the migration cannot see, so rows stay 0 until a re-index and every reader unions the flag with the path check — an un-migrated index keeps pre-#1500 behavior rather than regressing. Re-index required; noted in the CHANGELOG. - `generatedPredicateFor(paths)` gives ranking a bounded probe + O(1) lookups. Bounded, not cached: no invalidation, so a ranking call can never serve a verdict the last sync already replaced. Wired into explore ranking, findSymbolMatches, findAllSymbols, search (MCP + CLI), the context formatter, and the dominant-file/route-file hygiene filters. Cost (acceptance bar was no measurable index-time regression): a single unanchored `/generat/i` test over the header rejects ~every hand-written file before any line splitting. 4.6 µs/file on client-go (worst case — 82% generated). End-to-end `codegraph init` on client-go, n=3 alternating arms: 5.73s median with detection vs 5.76s path-only baseline; the arms cross over between runs, so the difference is inside run-to-run noise. Scope note: generated status remains a stable TIEBREAK at equal score, exactly where it was. Making it a strong negative signal is CG-10, which this unblocks by making the signal correct and available. Two pre-existing tests hard-coded schema version 8; both now track CURRENT_SCHEMA_VERSION (or the migration table) so future migrations don't require editing them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b37f191f5a
commit
16e17495f4
+23
-12
@@ -39,7 +39,6 @@ import {
|
||||
} from 'fs';
|
||||
import { createHash } from 'crypto';
|
||||
import { clamp, validatePathWithinRoot, validateProjectPath, isConfigLeafNode, CONFIG_LEAF_LANGUAGES } from '../utils';
|
||||
import { isGeneratedFile } from '../extraction/generated-detection';
|
||||
import { scanDynamicDispatch } from './dynamic-boundaries';
|
||||
import { getUpdateNotice } from '../upgrade/update-check';
|
||||
import { ExploreDiagnostics } from './explore-diagnostics';
|
||||
@@ -1588,9 +1587,10 @@ export class ToolHandler {
|
||||
// Down-rank generated files within the FTS-returned set so a search
|
||||
// for "Send" surfaces the hand-written keeper before .pb.go stubs
|
||||
// that share the name. Stable: only reorders generated vs. not.
|
||||
const isGen = cg.generatedFilePredicate(results.map((r) => r.node.filePath));
|
||||
const ranked = [...results].sort((a, b) => {
|
||||
const aGen = isGeneratedFile(a.node.filePath) ? 1 : 0;
|
||||
const bGen = isGeneratedFile(b.node.filePath) ? 1 : 0;
|
||||
const aGen = isGen(a.node.filePath) ? 1 : 0;
|
||||
const bGen = isGen(b.node.filePath) ? 1 : 0;
|
||||
return aGen - bGen;
|
||||
});
|
||||
|
||||
@@ -3113,6 +3113,13 @@ export class ToolHandler {
|
||||
!MULTITERM_OFF &&
|
||||
(fileTermHits.get(fp) ?? 0) >= 2 &&
|
||||
(entryFiles.has(fp) || centralFiles.has(fp));
|
||||
|
||||
// One DB probe over the ranked candidates, then O(1) per comparison. Unions
|
||||
// the index-time content-banner flag with the filename convention, so a Go
|
||||
// monorepo's generated CRUD (`payroll.go` carrying a DO-NOT-EDIT banner and
|
||||
// nothing in its name) down-ranks the same way `.pb.go` always has (#1500).
|
||||
const isGeneratedCandidate = cg.generatedFilePredicate(relevantFiles.map(([fp]) => fp));
|
||||
|
||||
const sortedFiles = relevantFiles.sort((a, b) => {
|
||||
const aPath = a[0].toLowerCase();
|
||||
const bPath = b[0].toLowerCase();
|
||||
@@ -3147,8 +3154,8 @@ export class ToolHandler {
|
||||
// the response (the cosmos Q3 explore otherwise leads with
|
||||
// `expected_keepers_mocks.go`, displacing the real `tally.go` content
|
||||
// and forcing the agent to Read tally.go anyway).
|
||||
const aGen = isGeneratedFile(a[0]);
|
||||
const bGen = isGeneratedFile(b[0]);
|
||||
const aGen = isGeneratedCandidate(a[0]);
|
||||
const bGen = isGeneratedCandidate(b[0]);
|
||||
if (aGen !== bGen) return aGen ? 1 : -1;
|
||||
|
||||
if (a[1].score !== b[1].score) return b[1].score - a[1].score;
|
||||
@@ -3233,7 +3240,7 @@ export class ToolHandler {
|
||||
entry: entryFiles.has(fp),
|
||||
spine: group.nodes.some((n) => flow.pathNodeIds.has(n.id)),
|
||||
lowValue: isLowValue(fp),
|
||||
generated: isGeneratedFile(fp),
|
||||
generated: isGeneratedCandidate(fp),
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -4753,7 +4760,8 @@ export class ToolHandler {
|
||||
if (!isQualified) {
|
||||
const exact = cg.getNodesByName(symbol);
|
||||
if (exact.length > 0) {
|
||||
return [...exact].sort((a, b) => (isGeneratedFile(a.filePath) ? 1 : 0) - (isGeneratedFile(b.filePath) ? 1 : 0));
|
||||
const isGen = cg.generatedFilePredicate(exact.map((n) => n.filePath));
|
||||
return [...exact].sort((a, b) => (isGen(a.filePath) ? 1 : 0) - (isGen(b.filePath) ? 1 : 0));
|
||||
}
|
||||
// No exact match — use the single top fuzzy result (e.g. a file basename).
|
||||
const fuzzy = cg.searchNodes(symbol, { limit: 10 });
|
||||
@@ -4781,10 +4789,12 @@ export class ToolHandler {
|
||||
return isQualified ? [] : results[0] ? [results[0].node] : [];
|
||||
}
|
||||
|
||||
// Down-rank generated files (.pb.go, .pulsar.go, _grpc.pb.go, …) so a flow
|
||||
// query prefers the keeper implementation over the protobuf-generated stub.
|
||||
// Down-rank generated files (.pb.go, .pulsar.go, _grpc.pb.go, and anything
|
||||
// whose header declares it generated) so a flow query prefers the keeper
|
||||
// implementation over the generated stub.
|
||||
const isGen = cg.generatedFilePredicate(exactMatches.map((r) => r.node.filePath));
|
||||
return [...exactMatches]
|
||||
.sort((a, b) => (isGeneratedFile(a.node.filePath) ? 1 : 0) - (isGeneratedFile(b.node.filePath) ? 1 : 0))
|
||||
.sort((a, b) => (isGen(a.node.filePath) ? 1 : 0) - (isGen(b.node.filePath) ? 1 : 0))
|
||||
.map((r) => r.node);
|
||||
}
|
||||
|
||||
@@ -4837,9 +4847,10 @@ export class ToolHandler {
|
||||
// Same generated-file down-rank as findSymbol — keeps callers/callees
|
||||
// /impact aggregation aligned (a query against "Send" returns the
|
||||
// hand-written implementations before the protobuf scaffold).
|
||||
const isGen = cg.generatedFilePredicate(exactMatches.map((r) => r.node.filePath));
|
||||
const ranked = [...exactMatches].sort((a, b) => {
|
||||
const aGen = isGeneratedFile(a.node.filePath) ? 1 : 0;
|
||||
const bGen = isGeneratedFile(b.node.filePath) ? 1 : 0;
|
||||
const aGen = isGen(a.node.filePath) ? 1 : 0;
|
||||
const bGen = isGen(b.node.filePath) ? 1 : 0;
|
||||
return aGen - bGen;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user