Skip to content

fix(provider): tolerate optional/null/zero fields in review output schema#85

Open
coletebou wants to merge 1 commit into
openclaw:mainfrom
coletebou:pr/schema-tolerance
Open

fix(provider): tolerate optional/null/zero fields in review output schema#85
coletebou wants to merge 1 commit into
openclaw:mainfrom
coletebou:pr/schema-tolerance

Conversation

@coletebou
Copy link
Copy Markdown

Summary

reviewOutputSchema (the LLM-input boundary) is stricter than findingRecordSchema (the storage boundary). The asymmetry means LLM output that the store would happily accept gets rejected at parse time. This PR aligns the two by relaxing four fields:

Field Before After
evidenceRefSchema.startLine number().int().positive().nullable() number().int().min(0).nullable().transform(0 → null)
evidenceRefSchema.endLine same same
findings[].reproduction string().nullable() string().nullish().transform(undefined → null)
findings[].minimumFixScope string() string().nullish().transform(undefined → "")

findingRecordSchema is unchanged — it was already tolerant via the existing .transform(...) at L259.

The 0 → null transform on line numbers handles the common LLM pattern of emitting 0 for "unknown line." The downstream validateReviewOutput.assertLineRange already early-returns when both lines are null, so the validator is compatible.

Why

Run 20260517T190759-3c9e9e had 4 zod failure patterns this PR addresses:

Pattern Count
invalid_type @ findings[N].minimumFixScope (got undefined) 2
invalid_type @ findings[N].reproduction (got undefined) 1
too_small @ findings[N].evidence[N].startLine (got 0) 1
too_small @ findings[N].evidence[N].endLine (got 0) 1

After this PR, all four pass.

Files

  • src/types.ts — 4 schema relaxations
  • src/provider-schema.ts — switched z.toJSONSchema to { io: "input", unrepresentable: "any" } because zod v4 refuses to emit JSON Schema for transforms; io: "input" is the right surface for LLM constraint anyway
  • src/provider.test.ts — 5 new cases

Validation

  • pnpm format:check — clean
  • pnpm typecheck — clean
  • pnpm lint — clean
  • pnpm build — clean
  • pnpm test — 546 passed, 1 skipped (+5 new)

Coordination

Composes with the safeparse-partition PR — without partition, even with this tolerance landed, ANY remaining bad finding would still drop its whole feature. With both landed, dropped findings are minimal AND the schema is permissive enough that the model rarely produces them.

…hema

Run 20260517T190759-3c9e9e (1000 features, 78 errors) surfaced 4 zod-issue
patterns inside reviewOutputSchema that dropped whole-feature output when
the model omitted optional fields or returned 0 for "unknown line":

- invalid_type @ findings[N].minimumFixScope (omitted) — 2 occurrences
- invalid_type @ findings[N].reproduction (omitted) — 1 occurrence
- too_small @ findings[N].evidence[N].startLine (0) — 1 occurrence
- too_small @ findings[N].evidence[N].endLine (0) — 1 occurrence

The storage-side findingRecordSchema already tolerated all of these via
.optional()/.nullable() + a transform default, so the LLM-input boundary
was unnecessarily stricter than the persistence boundary.

Relax reviewOutputSchema and the shared evidenceRefSchema to mirror the
record schema:

- evidenceRefSchema.startLine/endLine: accept 0 and normalize to null
  (consumers in reporting.ts already handle null).
- reviewOutputSchema.findings[].reproduction: nullish, default to null.
- reviewOutputSchema.findings[].minimumFixScope: nullish, default to "".

Because these schemas now contain transforms, providerJsonSchema must
emit the input-side JSON Schema; switch z.toJSONSchema to
{ io: "input", unrepresentable: "any" }. The findingRecordSchema is
unchanged.

Composes cleanly with the safeParse partition change: those finding-level
parse failures now pass with sensible defaults instead of dropping the
finding at all.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant