diff --git a/packages/action/dist/index.mjs b/packages/action/dist/index.mjs index b8090ea..5f160be 100644 --- a/packages/action/dist/index.mjs +++ b/packages/action/dist/index.mjs @@ -976,6 +976,7 @@ var PRESENTATION_CODE_PENALTY = 8; var TYPE_DECLARATION_PENALTY = 4; var BACKUP_COPY_PENALTY = 10; var BUNDLED_OUTPUT_PENALTY = 12; +var GENERATED_TWIN_REASON = "generated build artifact; maintained source counterpart exists"; var BUNDLED_LINE_LENGTH = 400; var MIN_BUNDLE_SAMPLE_BYTES = 2e3; var BUNDLE_MARKERS = [ @@ -1054,6 +1055,9 @@ function rankContextFiles(repo, input, limit = DEFAULT_CONTEXT_FILE_LIMIT, minSc score += EXPLICIT_PATH_BOOST; reasons.push("explicitly named in the task"); } + if (isGeneratedPath(file.path) && maintainedStems.has(moduleStem(file.path))) { + reasons.push(GENERATED_TWIN_REASON); + } const pathTokens = tokenizePath(file.path); const pathOverlap = [...pathTokens].filter((token) => taskTokens.has(token)); if (pathOverlap.length > 0) { @@ -1123,7 +1127,7 @@ function rankContextFiles(repo, input, limit = DEFAULT_CONTEXT_FILE_LIMIT, minSc score -= EXAMPLE_CODE_PENALTY; reasons.push("example or demo code deprioritized for an implementation task"); } - if (file.kind === "code" && isPresentationCodePath(file.path) && !taskTargetsPresentation && !taskTargetsExamples && !isChanged && !mentionedPaths.has(file.path)) { + if (isPresentationSurfacePath(file.path) && !taskTargetsPresentation && !taskTargetsExamples && !isChanged && !mentionedPaths.has(file.path)) { score -= PRESENTATION_CODE_PENALTY; reasons.push("presentation or demo surface deprioritized for a non-UI implementation task"); } @@ -1208,7 +1212,8 @@ function confidenceForEntry(entry, grounding, clustered, shape) { if (entry.isChanged) { return "high"; } - if (entry.reasons.includes("explicitly named in the task") && shape.position === 0 && !shape.leadIsContested) { + const hasMaintainedSourceTwin = entry.reasons.includes(GENERATED_TWIN_REASON); + if (entry.reasons.includes("explicitly named in the task") && shape.position === 0 && !shape.leadIsContested && !hasMaintainedSourceTwin) { return "high"; } let confidence = entry.score >= 14 ? "high" : entry.score >= 8 ? "medium" : "low"; @@ -1219,6 +1224,9 @@ function confidenceForEntry(entry, grounding, clustered, shape) { if (shape.position === 0 && shape.leadIsContested) { confidence = capConfidence(confidence, "medium"); } + if (hasMaintainedSourceTwin) { + confidence = capConfidence(confidence, "medium"); + } const supportedIdentifierCount = grounding.identifiers.filter((identifier) => identifier.status === "exact-definition" || identifier.status === "exact-text" || identifier.status === "partial-definition").length; if (grounding.unresolvedIdentifiers.length > 0) { if (supportedIdentifierCount === 0) { @@ -1302,9 +1310,9 @@ function isAuxiliaryCodePath(path) { const stem = (parts.at(-1) ?? "").replace(/\.[^.]+$/, "").toLowerCase(); return parts.slice(0, -1).some((segment) => AUXILIARY_CODE_DIRS.has(segment.toLowerCase())) || /^(?:demo|example|sample)(?:[-_.]|$)/.test(stem); } -function isPresentationCodePath(path) { +function isPresentationSurfacePath(path) { const name = path.split("/").at(-1)?.toLowerCase() ?? ""; - return /^(?:page|layout|demo|sample-repo)\.[cm]?[jt]sx?$/.test(name); + return /^(?:page|layout|demo|sample-repo)\.[cm]?[jt]sx?$/.test(name) || /\.(?:css|less|sass|scss)$/.test(name); } function tokenizeFileContent(text) { const tokens = tokenizeText(text); diff --git a/packages/core/src/rank.ts b/packages/core/src/rank.ts index 0c5d8e4..a319768 100644 --- a/packages/core/src/rank.ts +++ b/packages/core/src/rank.ts @@ -30,6 +30,7 @@ const PRESENTATION_CODE_PENALTY = 8; const TYPE_DECLARATION_PENALTY = 4; const BACKUP_COPY_PENALTY = 10; const BUNDLED_OUTPUT_PENALTY = 12; +const GENERATED_TWIN_REASON = "generated build artifact; maintained source counterpart exists"; // Bundlers strip newlines; people do not. A file averaging hundreds of characters per // line is machine output, whatever directory it sits in. Repositories commit these — // Next.js keeps pre-bundled dependencies under `src/compiled/` — and because they have @@ -162,6 +163,10 @@ export function rankContextFiles( reasons.push("explicitly named in the task"); } + if (isGeneratedPath(file.path) && maintainedStems.has(moduleStem(file.path))) { + reasons.push(GENERATED_TWIN_REASON); + } + const pathTokens = tokenizePath(file.path); const pathOverlap = [...pathTokens].filter((token) => taskTokens.has(token)); if (pathOverlap.length > 0) { @@ -270,8 +275,7 @@ export function rankContextFiles( } if ( - file.kind === "code" && - isPresentationCodePath(file.path) && + isPresentationSurfacePath(file.path) && !taskTargetsPresentation && !taskTargetsExamples && !isChanged && @@ -413,7 +417,13 @@ function confidenceForEntry( if (entry.isChanged) { return "high"; } - if (entry.reasons.includes("explicitly named in the task") && shape.position === 0 && !shape.leadIsContested) { + const hasMaintainedSourceTwin = entry.reasons.includes(GENERATED_TWIN_REASON); + if ( + entry.reasons.includes("explicitly named in the task") && + shape.position === 0 && + !shape.leadIsContested && + !hasMaintainedSourceTwin + ) { return "high"; } @@ -433,6 +443,9 @@ function confidenceForEntry( if (shape.position === 0 && shape.leadIsContested) { confidence = capConfidence(confidence, "medium"); } + if (hasMaintainedSourceTwin) { + confidence = capConfidence(confidence, "medium"); + } const supportedIdentifierCount = grounding.identifiers.filter((identifier) => identifier.status === "exact-definition" || identifier.status === "exact-text" || @@ -545,9 +558,10 @@ function isAuxiliaryCodePath(path: string): boolean { /^(?:demo|example|sample)(?:[-_.]|$)/.test(stem); } -function isPresentationCodePath(path: string): boolean { +function isPresentationSurfacePath(path: string): boolean { const name = path.split("/").at(-1)?.toLowerCase() ?? ""; - return /^(?:page|layout|demo|sample-repo)\.[cm]?[jt]sx?$/.test(name); + return /^(?:page|layout|demo|sample-repo)\.[cm]?[jt]sx?$/.test(name) || + /\.(?:css|less|sass|scss)$/.test(name); } function tokenizeFileContent(text: string): Set { diff --git a/packages/core/test/rank.test.ts b/packages/core/test/rank.test.ts index c24f88e..e81a39e 100644 --- a/packages/core/test/rank.test.ts +++ b/packages/core/test/rank.test.ts @@ -546,6 +546,60 @@ describe("rankContextFiles", () => { .not.toContain("type declaration deprioritized for a runtime task"); }); + it("keeps stylesheet symptom words below implementation unless the task targets presentation", () => { + const repo: RepoMap = { + root: "/repo", + packageScripts: [], + changedFiles: [], + diffText: "", + packageManager: "npm", + diagnostics: [], + files: [ + { + path: "src/checkout.ts", + extension: ".ts", + sizeBytes: 100, + isSource: true, + isTest: false, + kind: "code", + textSample: "export function applyDiscount(cart: Cart) { return cart.total; }" + }, + { + path: "styles/discount.css", + extension: ".css", + sizeBytes: 100, + isSource: true, + isTest: false, + kind: "code", + textSample: ".discount-banner .discount-total .discount-cart .discount { color: red; }" + }, + { + path: "src/copy.json", + extension: ".json", + sizeBytes: 100, + isSource: true, + isTest: false, + kind: "config", + textSample: "{ \"discount\": \"Discount total on cart\" }" + } + ] + }; + + const implementationTask = rankContextFiles(repo, { + issueText: "discount total on cart is wrong" + }, 8, 0); + expect(implementationTask[0]?.path).toBe("src/checkout.ts"); + expect(implementationTask.find((file) => file.path === "styles/discount.css")?.reasons) + .toContain("presentation or demo surface deprioritized for a non-UI implementation task"); + + const presentationTask = rankContextFiles(repo, { + issueText: "discount banner CSS layout is wrong on the cart page" + }); + expect(presentationTask[0]?.path).toBe("styles/discount.css"); + expect(presentationTask[0]?.reasons) + .not.toContain("presentation or demo surface deprioritized for a non-UI implementation task"); + }); + it("keeps documentation noise below matching code unless the task targets docs", () => { const repo: RepoMap = { root: "/repo", @@ -716,7 +770,49 @@ describe("rankContextFiles", () => { issueText: "dist/color-support.js is stale and disagrees with the source" }); - expect(ranked.map((file) => file.path)).toContain("dist/color-support.js"); + const artifact = ranked.find((file) => file.path === "dist/color-support.js"); + expect(artifact?.reasons).toContain("explicitly named in the task"); + expect(artifact?.reasons).toContain("generated build artifact; maintained source counterpart exists"); + expect(artifact?.confidence).toBe("medium"); + }); + + it("caps confidence when an explicitly named generated path has a maintained twin", () => { + const repo: RepoMap = { + root: "/repo", + packageScripts: [], + changedFiles: [], + diffText: "", + packageManager: "npm", + diagnostics: [], + files: [ + { + path: "packages/action/src/index.ts", + extension: ".ts", + sizeBytes: 100, + isSource: true, + isTest: false, + kind: "code", + textSample: "export function postComment(marker: string) { return marker; }" + }, + { + path: "packages/action/dist/index.mjs", + extension: ".mjs", + sizeBytes: 100, + isSource: true, + isTest: false, + kind: "code", + textSample: "export function postComment(marker) { return marker; }" + } + ] + }; + + const ranked = rankContextFiles(repo, { + issueText: "the bundle in packages/action/dist/index.mjs mishandles the comment marker" + }); + + expect(ranked[0]?.path).toBe("packages/action/dist/index.mjs"); + expect(ranked[0]?.confidence).toBe("medium"); + expect(ranked[0]?.reasons).toContain("generated build artifact; maintained source counterpart exists"); }); it("still credits a task term that most of a focused repository mentions", () => {