diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b1907a..bfb3009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 0.7.2 - Unreleased +- Reordered review prompts so shared instructions and the JSON contract precede feature-specific context, improving provider prompt-cache reuse across feature reviews. + ## 0.7.1 - 2026-07-20 ### Highlights diff --git a/src/prompt.test.ts b/src/prompt.test.ts index 45b2859..599d5e6 100644 --- a/src/prompt.test.ts +++ b/src/prompt.test.ts @@ -10,6 +10,41 @@ import { fixtureRoot, writeFixture } from "./test-helpers.js"; import type { FeatureRecord, FindingRecord, ProjectRecord } from "./types.js"; describe("review prompt provenance", () => { + it("keeps shared review instructions ahead of feature-specific context", async () => { + const root = await fixtureRoot("clawpatch-prompt-cache-prefix-"); + await writeFixture(root, "src/index.ts", "export const value = 1;\n"); + await writeFixture(root, "src/second.ts", "export const second = 2;\n"); + const firstFeature = feature(); + const secondFeature: FeatureRecord = { + ...feature(), + featureId: "feat_second", + title: "Second feature", + summary: "A different semantic review unit", + ownedFiles: [{ path: "src/second.ts", reason: "primary" }], + contextFiles: [], + tests: [], + }; + + const [first, second] = await Promise.all([ + buildReviewPromptBundle(root, project(root), firstFeature, defaultConfig()), + buildReviewPromptBundle(root, project(root), secondFeature, defaultConfig()), + ]); + const firstFeatureIndex = first.prompt.indexOf("Feature:\n"); + const secondFeatureIndex = second.prompt.indexOf("Feature:\n"); + const firstPrefix = first.prompt.slice(0, firstFeatureIndex); + const secondPrefix = second.prompt.slice(0, secondFeatureIndex); + + expect(firstFeatureIndex).toBeGreaterThan(0); + expect(secondFeatureIndex).toBeGreaterThan(0); + expect(firstPrefix).toBe(secondPrefix); + expect(firstPrefix).toContain("Review categories:"); + expect(firstPrefix).toContain("JSON shape:"); + expect(first.prompt.indexOf("JSON shape:")).toBeLessThan(firstFeatureIndex); + expect(first.prompt.indexOf("Valid evidence paths are exactly:")).toBeGreaterThan( + firstFeatureIndex, + ); + }); + it("records included, omitted, and truncated review prompt context", async () => { const root = await fixtureRoot("clawpatch-prompt-provenance-"); await writeFixture(root, "src/index.ts", "export const value = 1;\n"); diff --git a/src/prompt.ts b/src/prompt.ts index 9878942..19b2982 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -198,13 +198,7 @@ ${customPrompt.trim()} Return strict JSON only. No markdown fences. -Project: -${JSON.stringify({ name: project.name, detected: project.detected }, null, 2)} - -Feature: -${JSON.stringify(reviewFeatureView(feature), null, 2)} - -${customBlock}Review categories: +Review categories: - correctness bugs - security issues - race/concurrency bugs @@ -223,7 +217,7 @@ Shell and workflow review: - Focus this rule on captured or parsed values, not human-facing logging fallbacks like some_command || echo "failed". - Recommend separating the primary command capture from fallback assignment, for example if ! status="$(cmd)"; then status="fallback"; fi. -${reviewModeInstructions(mode)}${cudaBlock} +${reviewModeInstructions(mode)} ${languageGuidance} @@ -235,18 +229,13 @@ issues: when the same bug pattern appears in multiple owned files, emit one find with multiple evidence refs instead of separate one-off findings. Avoid speculative low-evidence findings. Evidence must point at included files. -Valid evidence paths are exactly: -${validEvidencePaths.map((path) => `- ${path}`).join("\n")} -Feature metadata paths are not valid evidence unless listed above. +Feature metadata paths are not valid evidence unless listed in the review input below. When providing evidence line ranges, use the line-number gutter in the Files section. Do not inspect files beyond the shown excerpts for evidence. If an excerpt is truncated, only cite lines that appear in the Files section. Set evidence.quote to null; line ranges are enough for validation. -Prompt context: -${JSON.stringify(promptContext, null, 2)} - -JSON shape: +${customBlock}JSON shape: { "findings": [ { @@ -266,6 +255,18 @@ JSON shape: "inspected": {"files":["string"],"symbols":["string"],"notes":["string"]} } +Project: +${JSON.stringify({ name: project.name, detected: project.detected }, null, 2)} + +Feature: +${JSON.stringify(reviewFeatureView(feature), null, 2)} +${cudaBlock} +Valid evidence paths are exactly: +${validEvidencePaths.map((path) => `- ${path}`).join("\n")} + +Prompt context: +${JSON.stringify(promptContext, null, 2)} + Files: ${fileBlocks.join("\n\n")}`; const promptBytes = Buffer.byteLength(prompt, "utf8");