Merge branch 'main' into feature/CG-35

This commit is contained in:
Colby McHenry
2026-08-06 21:17:56 -05:00
79 changed files with 8946 additions and 169 deletions
+95
View File
@@ -1961,6 +1961,101 @@ export class QueryBuilder {
return (filePath: string) => flagged.has(filePath) || isGeneratedFile(filePath);
}
/**
* Which of `filePaths` are AMBIENT DECLARATION files — they declare nothing
* but types, and nothing in the index depends on them (CG-28). A hand-written
* ambient `.d.ts` of global shims, a vendored typings file, module
* augmentation: reachable only by name, structurally attached to nothing.
*
* Structural, not extension-based, so a hand-written `types.ts` and a `.d.ts`
* are judged by the same rule and a `.d.ts` that does declare a class or a
* const is (correctly) not caught. Four conditions, all required:
*
* 1. it declares at least one symbol — an empty or unparsed file is not a
* declaration file, it is a file we know nothing about;
* 2. EVERY declared symbol is a type-level kind (interface / type alias /
* enum / namespace). The narrowness is deliberate and measured: a rule
* of "no callables" alone flags 118% of a repo, including Kotlin sealed
* classes, Rust `mod.rs` re-exports and django's locale constant tables —
* real source that must not be demoted. This rule flags 04%;
* 3. no symbol in it originates a `calls`/`instantiates` edge — the direct
* evidence that nothing here has a body;
* 4. NOTHING ELSE IN THE INDEX points at it. This is the condition that
* separates an ambient shim from a working type module, and it is why
* the flag is narrow enough to be safe: `displacement-ts`'s pipeline
* `types.ts` passes 13 identically but carries 13 inbound imports and
* 21 references, so the files that answer a query about the pipeline are
* typed BY it — it is part of that answer's structure. An ambient
* `declare global` shim has zero. Deliberately index-wide rather than
* restricted to the candidate list: the file that imports it is usually
* not itself a candidate.
*
* Bounded-lookup like {@link getGeneratedPathsAmong}: callers hold a ranked
* candidate list, so this is a partial-index probe over a handful of paths.
*/
getAmbientDeclarationPathsAmong(filePaths: Iterable<string>): Set<string> {
const unique = [...new Set(filePaths)];
const found = new Set<string>();
if (unique.length === 0) return found;
for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
const placeholders = chunk.map(() => '?').join(',');
// `file`/`import`/`export`/`parameter` are structural bookkeeping, not
// things the file declares, so they neither qualify nor disqualify.
const rows = this.db
.prepare(`
SELECT file_path,
SUM(CASE WHEN kind NOT IN ('file','import','export','parameter')
THEN 1 ELSE 0 END) AS declared,
SUM(CASE WHEN kind IN ('interface','type_alias','enum','enum_member','namespace')
THEN 1 ELSE 0 END) AS typeDeclared
FROM nodes
WHERE file_path IN (${placeholders})
GROUP BY file_path
`)
.all(...chunk) as Array<{ file_path: string; declared: number; typeDeclared: number }>;
let candidates = rows
.filter((r) => r.declared > 0 && r.declared === r.typeDeclared)
.map((r) => r.file_path);
if (candidates.length === 0) continue;
const disqualify = (sql: string): void => {
if (candidates.length === 0) return;
const hit = new Set(
(this.db
.prepare(sql.replace('$IN$', candidates.map(() => '?').join(',')))
.all(...candidates) as Array<{ file_path: string }>).map((r) => r.file_path),
);
candidates = candidates.filter((p) => !hit.has(p));
};
// (3) originates behaviour
disqualify(`
SELECT DISTINCT n.file_path AS file_path
FROM edges e JOIN nodes n ON n.id = e.source
WHERE e.kind IN ('calls','instantiates') AND n.file_path IN ($IN$)
`);
// (4) something outside the file depends on it
disqualify(`
SELECT DISTINCT t.file_path AS file_path
FROM edges e JOIN nodes t ON t.id = e.target JOIN nodes s ON s.id = e.source
WHERE t.file_path IN ($IN$) AND s.file_path <> t.file_path
`);
for (const path of candidates) found.add(path);
}
return found;
}
/**
* A reusable `(path) => boolean` ambient-declaration test over a bounded
* candidate list — the shape a ranking comparator wants: one query up front,
* O(1) per comparison.
*/
ambientDeclarationPredicateFor(filePaths: Iterable<string>): (filePath: string) => boolean {
const flagged = this.getAmbientDeclarationPathsAmong(filePaths);
return (filePath: string) => flagged.has(filePath);
}
/** How many indexed files carry the generated flag. Surfaced by `status`. */
countGeneratedFiles(): number {
const row = this.db
+8
View File
@@ -181,6 +181,14 @@ const GENERATED_CONTENT_PATTERNS: ReadonlyArray<RegExp> = [
// "by" is required — bare "automatically generated" appears in hand-written
// prose ("the table below is automatically generated at runtime").
/\b(?:automatically generated|auto[- ]?generated|autogenerated) by\b/i,
// The "run this command to regenerate" shape: Cloudflare Wrangler
// ("Generated by Wrangler by running `wrangler types` (hash: …)"), and the
// same phrasing used by other CLI-driven emitters. Bare "generated by" is
// deliberately NOT enough — it is ordinary prose — so the reproduction
// instruction is the discriminator: the banner must name a tool AND then
// say `by running`, i.e. TWO separate "by" clauses. That rules out
// "the report is generated by running the nightly job", which has only one.
/\bgenerated by\s+\S.{0,80}?\bby running\b/i,
// Self-declaring in-house banners that name no tool.
/\bthis (?:file|class|code|module) (?:is|was) (?:auto[- ]?)?generated\b/i,
// The reverse ordering: "DO NOT EDIT — this is a generated file".
+13
View File
@@ -1576,6 +1576,19 @@ export class CodeGraph {
return this.queries.generatedPredicateFor(filePaths);
}
/**
* A `(path) => boolean` ambient-declaration test over a BOUNDED candidate
* list: true for a file that declares nothing but types, originates no call
* edge, and that nothing in the index depends on — an ambient `.d.ts` of
* global shims, vendored typings, module augmentation (CG-28). Structural
* rather than extension-based, and deliberately narrow: see
* `QueryBuilder.getAmbientDeclarationPathsAmong` for why each condition is
* there, in particular why a `types.ts` the codebase imports is NOT flagged.
*/
ambientDeclarationFilePredicate(filePaths: Iterable<string>): (filePath: string) => boolean {
return this.queries.ambientDeclarationPredicateFor(filePaths);
}
/** How many indexed files are flagged tool-generated. Reported by `status`. */
getGeneratedFileCount(): number {
return this.queries.countGeneratedFiles();
+66 -2
View File
@@ -66,6 +66,12 @@ export interface ExploreCandidateMeta {
spine: boolean;
lowValue: boolean;
generated: boolean;
/**
* Nothing but type declarations in this file, and nothing in the index
* depends on it (CG-28) — it cannot answer a flow question, so it ranks on
* discounted signals unless the query named one of the types it declares.
*/
ambientDeclaration: boolean;
/**
* Multiplier `rankPenalty` applied to BOTH `score` and `graphScore` (1 = no
* penalty). Generated and test/i18n files rank on discounted signals, so the
@@ -89,9 +95,31 @@ interface FileRecord extends ExploreCandidateMeta {
* it rendered anything. `0` = cliffed; `null` = never reached the allocator.
* The gap between this and `emittedChars` is the whole story of a budget bug:
* reserved-but-unspent means the file had nothing to say, spent-over-reserved
* means an oversize first cluster or the whole-file grace overshot.
* means an oversize first cluster or the whole-file grace overshot — but read
* `spendable` before calling it an overshoot, since inherited slack legitimately
* lifts a file above its reservation.
*/
allowance: number | null;
/**
* What the file could actually SPEND: its reservation plus the slack the
* files above it left on the table (bounded by MAX_SHARE). Every render bound
* reads this, not `allowance`, so it — not the reservation — is what an
* overshoot is measured against. `null` until the render loop reaches the
* file. Reporting only `allowance` makes an ordinary carry-forward look like
* a file spending over its reservation.
*/
spendable: number | null;
/**
* The DISPLACEMENT-GUARDED ceiling (CG-31): the most this file may render
* without spending a reservation still owed to a file the loop has not
* reached AND can still pay. `spendable` is what the file was promised, this
* is what is actually still there to pay it with — every render path is
* bounded by it, so `emittedChars` above it is a bug. Sits ABOVE `spendable`
* when the room is there (the bounded overshoot a big cluster member may
* take) and BELOW it when the files underneath need the bytes. `null` until
* the render loop reaches the file.
*/
funded: number | null;
render?: ExploreRenderMode;
/**
* Source chars this call did NOT re-send because an earlier call in the
@@ -139,6 +167,10 @@ interface BudgetShape {
export interface ExploreDiagnosticFile extends ExploreCandidateMeta {
path: string;
allowance: number | null;
/** Reservation + inherited slack — the bound the render paths actually use. */
spendable: number | null;
/** Render ceiling after holding back what is still owed to unreached files. */
funded: number | null;
render: ExploreRenderMode | null;
skipped: ExploreSkipReason | null;
clipped: boolean;
@@ -362,7 +394,7 @@ export class ExploreDiagnostics {
/** Record one ranked candidate's scoring inputs, in final sort order. */
noteCandidate(path: string, meta: ExploreCandidateMeta): void {
this.files.set(path, {
path, ...meta, allowance: null,
path, ...meta, allowance: null, spendable: null, funded: null,
dedupSavedChars: 0, dedupCovered: [],
emittedChars: 0, finalChars: 0, share: 0, allocatedShare: 0, clipped: false,
});
@@ -391,6 +423,24 @@ export class ExploreDiagnostics {
}
}
/**
* What the render loop will let this file spend — reservation plus inherited
* slack. Called once per file, before any of its render paths run.
*/
recordSpendable(path: string, chars: number): void {
const rec = this.files.get(path);
if (rec) rec.spendable = chars;
}
/**
* What the render loop will let this file spend once the reservations still
* owed BELOW it are held back (CG-31). Called alongside `recordSpendable`.
*/
recordFunded(path: string, chars: number): void {
const rec = this.files.get(path);
if (rec) rec.funded = chars;
}
/** A candidate rendered source into the response. */
recordRender(path: string, render: ExploreRenderMode, sourceChars: number, clipped: boolean): void {
const rec = this.files.get(path);
@@ -535,9 +585,12 @@ export class ExploreDiagnostics {
spine: r.spine,
lowValue: r.lowValue,
generated: r.generated,
ambientDeclaration: r.ambientDeclaration,
penalty: round6(r.penalty),
kinds: r.kinds,
allowance: r.allowance,
spendable: r.spendable,
funded: r.funded,
render: r.render ?? null,
skipped: r.skipped ?? null,
clipped: r.clipped,
@@ -704,6 +757,16 @@ export function renderTable(report: ExploreDiagnosticReport): string {
f.path,
);
out.push(' kinds: ' + (f.kinds || '-'));
// Only when it differs: a file that spent over `reserved` but inside
// `spendable` took inherited slack, not a budget bug.
if (f.spendable !== null && f.allowance !== null && f.spendable !== f.allowance) {
out.push(` spendable: ${num(f.spendable)} (reservation + inherited slack)`);
}
// Only when the displacement guard actually bit: the gap is the overshoot
// this file was refused so the files below it could still be paid.
if (f.funded !== null && f.spendable !== null && f.funded < Math.round(f.spendable * 1.5)) {
out.push(` funded: ${num(f.funded)} (held to this so the files below keep their reservations)`);
}
if (f.dedupSavedChars > 0) {
const spans = f.dedupCovered.slice(0, 6).map(([a, b]) => (a === b ? `${a}` : `${a}-${b}`)).join(',');
const more = f.dedupCovered.length > 6 ? `,+${f.dedupCovered.length - 6}` : '';
@@ -740,5 +803,6 @@ function flagString(f: ExploreDiagnosticFile): string {
if (f.spine) flags.push('spine');
if (f.lowValue) flags.push('low-value');
if (f.generated) flags.push('generated');
if (f.ambientDeclaration) flags.push('ambient-decl');
return flags.join(' ') || '-';
}
+752 -160
View File
File diff suppressed because it is too large Load Diff