From 2f0999c93856cfb5ff240037a416868ffa5e4556 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 1 Aug 2026 22:24:17 -0700 Subject: [PATCH] fix(review): include changed features regardless of status --- CHANGELOG.md | 1 + src/review.ts | 7 ++++-- src/selection.ts | 10 ++++++-- src/workflow.test.ts | 58 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d22e5c0..0fa8f91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.7.2 - Unreleased +- Fixed diff-scoped review and CI runs to include changed features regardless of their previous review status, preventing warm-state gates from silently skipping changed code, thanks @youhaowei. - Added Rust seed context for Cargo manifests, paired crate entrypoints, and directly declared modules across crate roots and binary layouts, thanks @Tanmay-008. ## 0.7.1 - 2026-07-20 diff --git a/src/review.ts b/src/review.ts index bcc571a..b2459d9 100644 --- a/src/review.ts +++ b/src/review.ts @@ -124,7 +124,8 @@ export async function reviewCommand( registryPostValidator, allowNonPendingFeatureReview: stringFlag(flags, "feature") !== undefined || - stringFlag(flags, "featureList") !== undefined, + stringFlag(flags, "featureList") !== undefined || + hasFileFilter(flags), }); findingIds.push(...reviewed.findingIds); for (const dropped of reviewed.droppedFindings) { @@ -545,7 +546,9 @@ async function selectReviewFeatures( } return stringFlag(flags, "limit") === undefined ? selected : limitFeatures(selected, flags); } - const candidates = selectReviewCandidates(features, flags); + const candidates = selectReviewCandidates(features, flags, { + ignoreStatus: hasFileFilter(flags), + }); const sinceFiltered = await filterFeaturesByFilesSince(loaded.root, candidates, flags); return limitFeatures(sinceFiltered, flags); } diff --git a/src/selection.ts b/src/selection.ts index 5abf113..96ad422 100644 --- a/src/selection.ts +++ b/src/selection.ts @@ -2,13 +2,19 @@ import { FeatureRecord, FindingRecord } from "./types.js"; type Flags = Record; -export function selectReviewCandidates(features: FeatureRecord[], flags: Flags): FeatureRecord[] { +export function selectReviewCandidates( + features: FeatureRecord[], + flags: Flags, + options: { ignoreStatus?: boolean } = {}, +): FeatureRecord[] { const featureId = stringFlag(flags, "feature"); const projectFilter = stringFlag(flags, "project"); const projectFeatures = filterFeaturesByProject(features, projectFilter); const selected = featureId === undefined - ? projectFeatures.filter((feature) => ["pending", "error"].includes(feature.status)) + ? options.ignoreStatus === true + ? projectFeatures + : projectFeatures.filter((feature) => ["pending", "error"].includes(feature.status)) : projectFeatures.filter((feature) => feature.featureId === featureId); return projectFilter === undefined ? selected : selected.toSorted(featureReviewRank); } diff --git a/src/workflow.test.ts b/src/workflow.test.ts index 65144c4..f9db138 100644 --- a/src/workflow.test.ts +++ b/src/workflow.test.ts @@ -854,6 +854,64 @@ describe("workflow", () => { }); }); + it("selects changed features regardless of their previous review status", async () => { + const root = await sinceFixture("clawpatch-since-status-"); + const context = await makeContext(testOptions(root)); + + await initCommand(context, {}); + await mapCommand(context); + await writeFixture(root, "src/two.ts", "export const two = 'changed';\n"); + await commitAll(root, "change two"); + const paths = statePaths(join(root, ".clawpatch")); + const features = await readFeatures(paths); + const expected = expectedFeatureIds(features, new Set(["src/two.ts"]), true); + const statuses: FeatureRecord["status"][] = ["reviewed", "needs-fix", "fixed"]; + let statusIndex = 0; + for (const feature of features) { + if (!expected.includes(feature.featureId)) { + continue; + } + await writeFeature(paths, { + ...feature, + status: statuses[statusIndex % statuses.length]!, + }); + statusIndex += 1; + } + + const reviewed = await reviewCommand(context, { since: "base", dryRun: true }); + + expect(expected.length).toBeGreaterThan(0); + expect(reviewed).toMatchObject({ dryRun: true, featureIds: expected }); + }); + + it("reviews changed non-pending features through the warm-state CI workflow", async () => { + const root = await sinceFixture("clawpatch-ci-since-status-"); + const context = await makeContext(testOptions(root)); + + await initCommand(context, {}); + await mapCommand(context); + const paths = statePaths(join(root, ".clawpatch")); + const features = await readFeatures(paths); + const touched = expectedFeatureIds(features, new Set(["src/two.ts"]), true); + for (const feature of features) { + if (!touched.includes(feature.featureId)) { + continue; + } + await writeFeature(paths, { ...feature, status: "needs-fix" }); + } + await writeFixture(root, "src/two.ts", "export const two = 'changed';\n"); + await commitAll(root, "change two"); + + const result = await ciCommand(context, { + provider: "mock", + since: "base", + jobs: "1", + }); + + expect(touched.length).toBeGreaterThan(0); + expect(result).toMatchObject({ reviewed: touched.length }); + }); + it("selects review features whose context files overlap the diff range", async () => { const root = await sinceFixture("clawpatch-since-context-"); const context = await makeContext(testOptions(root));