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:
Colby McHenry
2026-08-03 23:13:59 -05:00
co-authored by Claude Opus 5
parent b37f191f5a
commit 16e17495f4
19 changed files with 898 additions and 55 deletions
+14 -6
View File
@@ -15,7 +15,15 @@ import { isGeneratedFile } from '../extraction/generated-detection';
* - Entry points with locations
* - Code blocks only for key symbols
*/
export function formatContextAsMarkdown(context: TaskContext): string {
export function formatContextAsMarkdown(
context: TaskContext,
/**
* Generated-file test. Defaults to the filename convention alone; the
* ContextBuilder passes a DB-backed predicate so files flagged by their
* HEADER at index time (#1500) demote here too.
*/
isGenerated: (filePath: string) => boolean = isGeneratedFile
): string {
const lines: string[] = [];
// Header with query
@@ -26,8 +34,8 @@ export function formatContextAsMarkdown(context: TaskContext): string {
// .pulsar.go, mocks, …) rank LAST — a flow query should lead with the
// hand-written implementation, not protobuf scaffolding.
const orderedEntries = [...context.entryPoints].sort((a, b) => {
const aGen = isGeneratedFile(a.filePath) ? 1 : 0;
const bGen = isGeneratedFile(b.filePath) ? 1 : 0;
const aGen = isGenerated(a.filePath) ? 1 : 0;
const bGen = isGenerated(b.filePath) ? 1 : 0;
return aGen - bGen;
});
if (orderedEntries.length > 0) {
@@ -49,7 +57,7 @@ export function formatContextAsMarkdown(context: TaskContext): string {
// Related Symbols, pure noise that displaced real-flow entries).
const otherSymbols = Array.from(context.subgraph.nodes.values())
.filter(n => !context.entryPoints.some(e => e.id === n.id))
.filter(n => !isGeneratedFile(n.filePath))
.filter(n => !isGenerated(n.filePath))
.slice(0, 10); // Limit to 10 related symbols
if (otherSymbols.length > 0) {
@@ -72,8 +80,8 @@ export function formatContextAsMarkdown(context: TaskContext): string {
// show first (consistent with Entry Points reordering above).
if (context.codeBlocks.length > 0) {
const orderedBlocks = [...context.codeBlocks].sort((a, b) => {
const aGen = isGeneratedFile(a.filePath) ? 1 : 0;
const bGen = isGeneratedFile(b.filePath) ? 1 : 0;
const aGen = isGenerated(a.filePath) ? 1 : 0;
const bGen = isGenerated(b.filePath) ? 1 : 0;
return aGen - bGen;
});
lines.push('### Code\n');
+8 -1
View File
@@ -265,7 +265,14 @@ export class ContextBuilder {
// Return formatted output or raw context
if (opts.format === 'markdown') {
return formatContextAsMarkdown(context)
// Bounded candidate set (entry points + subgraph + code blocks), so the
// DB-backed generated check is one probe, not a per-comparison query.
const isGenerated = this.queries.generatedPredicateFor([
...entryPoints.map((n) => n.filePath),
...Array.from(subgraph.nodes.values(), (n) => n.filePath),
...codeBlocks.map((b) => b.filePath),
]);
return formatContextAsMarkdown(context, isGenerated)
+ this.buildCallPathsSection(subgraph)
+ (subgraph.confidence === 'low' ? this.buildLowConfidenceNote(entryPoints) : '');
} else if (opts.format === 'json') {