Skip to content
Open
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 @@ -14,6 +14,7 @@
- Improved OpenCode malformed JSON diagnostics with output length, event kinds, and a bounded preview, thanks @rohitjavvadi.
- Fixed Express route mapping for aliased Router imports that follow block comment banners, thanks @rohitjavvadi.
- Fixed Bun package-manager detection to recognize the text `bun.lock` lockfile, thanks @austinm911.
- Fixed review-output schema to tolerate optional `reproduction` and `minimumFixScope` fields and zero-valued evidence line numbers (normalized to `null`), recovering 4 of 28 zod issue patterns observed in run `20260517T190759-3c9e9e` (78 errors over 1000 features) that previously dropped whole-feature output instead of the affected finding.

## 0.3.0 - 2026-05-18

Expand Down
4 changes: 3 additions & 1 deletion src/provider-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ export const revalidateJsonSchema = providerJsonSchema(revalidateOutputSchema);
export const fixPlanJsonSchema = providerJsonSchema(fixPlanOutputSchema);

export function providerJsonSchema(schema: z.ZodType): object {
return stripProviderUnsupportedSchemaKeywords(z.toJSONSchema(schema)) as object;
return stripProviderUnsupportedSchemaKeywords(
z.toJSONSchema(schema, { io: "input", unrepresentable: "any" }),
) as object;
}

function stripProviderUnsupportedSchemaKeywords(value: unknown): unknown {
Expand Down
75 changes: 74 additions & 1 deletion src/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { afterEach, describe, expect, it } from "vitest";
import { ClawpatchError } from "./errors.js";
import { __testing, extractJson, providerByName } from "./provider.js";
import { safeProviderPreview } from "./provider-json.js";
import { revalidateOutputSchema, reviewOutputSchema } from "./types.js";
import { evidenceRefSchema, revalidateOutputSchema, reviewOutputSchema } from "./types.js";

// eslint-disable-next-line no-underscore-dangle
const {
Expand Down Expand Up @@ -569,3 +569,76 @@ describe("providerByName", () => {
expect(providerByName("mock-fail").name).toBe("mock-fail");
});
});

function buildToleranceFinding(overrides: Record<string, unknown> = {}): Record<string, unknown> {
return {
title: "x",
category: "bug",
severity: "low",
confidence: "low",
evidence: [],
reasoning: "r",
reproduction: null,
recommendation: "rec",
whyTestsDoNotAlreadyCoverThis: "",
suggestedRegressionTest: null,
minimumFixScope: "",
...overrides,
};
}

function buildToleranceOutput(finding: Record<string, unknown>): Record<string, unknown> {
return {
findings: [finding],
inspected: { files: [], symbols: [], notes: [] },
};
}

describe("reviewOutputSchema tolerance", () => {
it("accepts findings with null reproduction", () => {
const parsed = reviewOutputSchema.parse(
buildToleranceOutput(buildToleranceFinding({ reproduction: null })),
);
expect(parsed.findings[0]!.reproduction).toBeNull();
});

it("accepts findings with omitted reproduction (becomes null)", () => {
const finding = buildToleranceFinding();
delete finding["reproduction"];
const parsed = reviewOutputSchema.parse(buildToleranceOutput(finding));
expect(parsed.findings[0]!.reproduction).toBeNull();
});

it("accepts findings with omitted minimumFixScope (becomes empty string)", () => {
const finding = buildToleranceFinding();
delete finding["minimumFixScope"];
const parsed = reviewOutputSchema.parse(buildToleranceOutput(finding));
expect(parsed.findings[0]!.minimumFixScope).toBe("");
});
});

describe("evidenceRefSchema tolerance", () => {
it("accepts startLine 0 and normalizes to null", () => {
const parsed = evidenceRefSchema.parse({
path: "src/index.ts",
startLine: 0,
endLine: 5,
symbol: null,
quote: null,
});
expect(parsed.startLine).toBeNull();
expect(parsed.endLine).toBe(5);
});

it("accepts endLine 0 and normalizes to null", () => {
const parsed = evidenceRefSchema.parse({
path: "src/index.ts",
startLine: 5,
endLine: 0,
symbol: null,
quote: null,
});
expect(parsed.startLine).toBe(5);
expect(parsed.endLine).toBeNull();
});
});
24 changes: 20 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,18 @@ export type TrustBoundary = FeatureRecord["trustBoundaries"][number];

export const evidenceRefSchema = z.object({
path: z.string(),
startLine: z.number().int().positive().nullable(),
endLine: z.number().int().positive().nullable(),
startLine: z
.number()
.int()
.min(0)
.nullable()
.transform((v) => (v === 0 ? null : v)),
endLine: z
.number()
.int()
.min(0)
.nullable()
.transform((v) => (v === 0 ? null : v)),
symbol: z.string().nullable(),
quote: z.string().nullable(),
});
Expand Down Expand Up @@ -358,11 +368,17 @@ export const reviewOutputSchema = z.object({
confidence: z.enum(["high", "medium", "low"]),
evidence: z.array(evidenceRefSchema),
reasoning: z.string(),
reproduction: z.string().nullable(),
reproduction: z
.string()
.nullish()
.transform((v) => v ?? null),
recommendation: z.string(),
whyTestsDoNotAlreadyCoverThis: z.string(),
suggestedRegressionTest: z.string().nullable(),
minimumFixScope: z.string(),
minimumFixScope: z
.string()
.nullish()
.transform((v) => v ?? ""),
}),
),
inspected: z.object({
Expand Down