Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions src/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand Down
10 changes: 8 additions & 2 deletions src/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import { FeatureRecord, FindingRecord } from "./types.js";

type Flags = Record<string, string | boolean>;

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);
}
Expand Down
58 changes: 58 additions & 0 deletions src/workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down