-
Notifications
You must be signed in to change notification settings - Fork 0
refactor(schema): use Zod transforms instead of manual mutation #199
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
eadd55f to
3fbe439
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR refactors the ModestBench configuration schema to use Zod's built-in transform functionality instead of manual mutation. The changes improve type safety by properly distinguishing between input and output types, eliminate the need for manual post-parse transformations, and ensure JSON Schema generation works correctly.
Key changes:
- Replaced manual budget transformation with declarative Zod
.transform()methods - Introduced separate input/output schemas to support JSON Schema generation (transforms cannot be represented in JSON Schema)
- Extracted shared configuration properties to avoid duplication between runtime and JSON Schema schemas
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/config/schema.ts | Core refactoring: added budgetSchema and budgetsSchema transforms, extracted baseConfigProperties for reuse, created separate input/output schemas, exported ModestBenchConfigInput type |
| src/services/config-manager.ts | Removed unnecessary type assertion now that schema properly types the transformation |
| src/types/core.ts | Added ModestBenchConfigInput export and TODO comment for ThresholdConfig; includes duplicate export that should be removed |
| scripts/generate-schema.ts | Updated to use partialModestBenchConfigInputSchema (without transforms) for JSON Schema generation |
Comments suppressed due to low confidence (1)
src/types/core.ts:308
- Duplicate export statement. The functions
createRunIdandcreateTaskIdare already exported at line 127. This duplicate export at line 307-308 should be removed to avoid redundancy.
/**
* Reporter-specific configuration
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
3fbe439 to
6e02506
Compare
Deploying modestbench with
|
| Latest commit: |
c4f94e1
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://2e2c3199.modestbench.pages.dev |
| Branch Preview URL: | https://feature-modestbench-parse-fi.modestbench.pages.dev |
Replace manual result mutation in safeParseConfig/safeParsePartialConfig with proper Zod .transform() methods: - Add budgetSchema transform for string-to-number conversion (e.g., '10ms' → ns) - Add budgetsSchema transform for nested-to-flat structure conversion - Extract baseConfigProperties to avoid duplication between runtime and JSON Schema generation schemas - Export ModestBenchConfigInput type for consumers needing the input shape - Export partialModestBenchConfigInputSchema for JSON Schema generation (transforms can't be represented in JSON Schema) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
6e02506 to
c4f94e1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return { | ||
| // Build absolute budget object if present | ||
| absolute: budget.absolute | ||
| ? { | ||
| maxP99: | ||
| budget.absolute.maxP99 !== undefined | ||
| ? typeof budget.absolute.maxP99 === 'string' | ||
| ? parseTimeString(budget.absolute.maxP99) | ||
| : budget.absolute.maxP99 | ||
| : undefined, | ||
| maxTime: | ||
| budget.absolute.maxTime !== undefined | ||
| ? typeof budget.absolute.maxTime === 'string' | ||
| ? parseTimeString(budget.absolute.maxTime) | ||
| : budget.absolute.maxTime | ||
| : undefined, | ||
| minOpsPerSec: budget.absolute.minOpsPerSec, | ||
| } | ||
| : undefined, | ||
| // Build relative budget object if present | ||
| relative: budget.relative | ||
| ? { | ||
| maxRegression: | ||
| budget.relative.maxRegression !== undefined | ||
| ? typeof budget.relative.maxRegression === 'string' | ||
| ? parsePercentageString(budget.relative.maxRegression) | ||
| : budget.relative.maxRegression | ||
| : undefined, |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The transform creates an absolute object even when all its properties are undefined. For example, if budget.absolute is {}, the result will be { absolute: { maxP99: undefined, maxTime: undefined, minOpsPerSec: undefined } } instead of { absolute: undefined }.
This could cause issues when the budget is later used, as code might check if (budget.absolute) expecting it to be falsy when no absolute thresholds are set, but it will be an object with all undefined properties.
Consider checking if all properties are undefined and returning undefined for the absolute property itself in that case, similar to how the relative budget object should be handled.
| return { | |
| // Build absolute budget object if present | |
| absolute: budget.absolute | |
| ? { | |
| maxP99: | |
| budget.absolute.maxP99 !== undefined | |
| ? typeof budget.absolute.maxP99 === 'string' | |
| ? parseTimeString(budget.absolute.maxP99) | |
| : budget.absolute.maxP99 | |
| : undefined, | |
| maxTime: | |
| budget.absolute.maxTime !== undefined | |
| ? typeof budget.absolute.maxTime === 'string' | |
| ? parseTimeString(budget.absolute.maxTime) | |
| : budget.absolute.maxTime | |
| : undefined, | |
| minOpsPerSec: budget.absolute.minOpsPerSec, | |
| } | |
| : undefined, | |
| // Build relative budget object if present | |
| relative: budget.relative | |
| ? { | |
| maxRegression: | |
| budget.relative.maxRegression !== undefined | |
| ? typeof budget.relative.maxRegression === 'string' | |
| ? parsePercentageString(budget.relative.maxRegression) | |
| : budget.relative.maxRegression | |
| : undefined, | |
| // Transform absolute budget values | |
| const absoluteMaxP99 = | |
| budget.absolute?.maxP99 !== undefined | |
| ? typeof budget.absolute.maxP99 === 'string' | |
| ? parseTimeString(budget.absolute.maxP99) | |
| : budget.absolute.maxP99 | |
| : undefined; | |
| const absoluteMaxTime = | |
| budget.absolute?.maxTime !== undefined | |
| ? typeof budget.absolute.maxTime === 'string' | |
| ? parseTimeString(budget.absolute.maxTime) | |
| : budget.absolute.maxTime | |
| : undefined; | |
| const absoluteMinOpsPerSec = budget.absolute?.minOpsPerSec; | |
| const hasAbsolute = | |
| absoluteMaxP99 !== undefined || | |
| absoluteMaxTime !== undefined || | |
| absoluteMinOpsPerSec !== undefined; | |
| // Transform relative budget values | |
| const relativeMaxRegression = | |
| budget.relative?.maxRegression !== undefined | |
| ? typeof budget.relative.maxRegression === 'string' | |
| ? parsePercentageString(budget.relative.maxRegression) | |
| : budget.relative.maxRegression | |
| : undefined; | |
| const hasRelative = relativeMaxRegression !== undefined; | |
| return { | |
| // Build absolute budget object only if any field is defined | |
| absolute: hasAbsolute | |
| ? { | |
| maxP99: absoluteMaxP99, | |
| maxTime: absoluteMaxTime, | |
| minOpsPerSec: absoluteMinOpsPerSec, | |
| } | |
| : undefined, | |
| // Build relative budget object only if any field is defined | |
| relative: hasRelative | |
| ? { | |
| maxRegression: relativeMaxRegression, |
| return { | ||
| // Build absolute budget object if present | ||
| absolute: budget.absolute | ||
| ? { | ||
| maxP99: | ||
| budget.absolute.maxP99 !== undefined | ||
| ? typeof budget.absolute.maxP99 === 'string' | ||
| ? parseTimeString(budget.absolute.maxP99) | ||
| : budget.absolute.maxP99 | ||
| : undefined, | ||
| maxTime: | ||
| budget.absolute.maxTime !== undefined | ||
| ? typeof budget.absolute.maxTime === 'string' | ||
| ? parseTimeString(budget.absolute.maxTime) | ||
| : budget.absolute.maxTime | ||
| : undefined, | ||
| minOpsPerSec: budget.absolute.minOpsPerSec, | ||
| } | ||
| : undefined, | ||
| // Build relative budget object if present | ||
| relative: budget.relative | ||
| ? { | ||
| maxRegression: | ||
| budget.relative.maxRegression !== undefined | ||
| ? typeof budget.relative.maxRegression === 'string' | ||
| ? parsePercentageString(budget.relative.maxRegression) | ||
| : budget.relative.maxRegression | ||
| : undefined, | ||
| } | ||
| : undefined, |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The transform creates a relative object even when all its properties are undefined. For example, if budget.relative is {}, the result will be { relative: { maxRegression: undefined } } instead of { relative: undefined }.
This could cause issues when the budget is later used, as code might check if (budget.relative) expecting it to be falsy when no relative thresholds are set, but it will be an object with undefined properties.
Consider checking if all properties are undefined and returning undefined for the relative property itself in that case.
| return { | |
| // Build absolute budget object if present | |
| absolute: budget.absolute | |
| ? { | |
| maxP99: | |
| budget.absolute.maxP99 !== undefined | |
| ? typeof budget.absolute.maxP99 === 'string' | |
| ? parseTimeString(budget.absolute.maxP99) | |
| : budget.absolute.maxP99 | |
| : undefined, | |
| maxTime: | |
| budget.absolute.maxTime !== undefined | |
| ? typeof budget.absolute.maxTime === 'string' | |
| ? parseTimeString(budget.absolute.maxTime) | |
| : budget.absolute.maxTime | |
| : undefined, | |
| minOpsPerSec: budget.absolute.minOpsPerSec, | |
| } | |
| : undefined, | |
| // Build relative budget object if present | |
| relative: budget.relative | |
| ? { | |
| maxRegression: | |
| budget.relative.maxRegression !== undefined | |
| ? typeof budget.relative.maxRegression === 'string' | |
| ? parsePercentageString(budget.relative.maxRegression) | |
| : budget.relative.maxRegression | |
| : undefined, | |
| } | |
| : undefined, | |
| // Build absolute budget object if present | |
| const absolute = budget.absolute | |
| ? { | |
| maxP99: | |
| budget.absolute.maxP99 !== undefined | |
| ? typeof budget.absolute.maxP99 === 'string' | |
| ? parseTimeString(budget.absolute.maxP99) | |
| : budget.absolute.maxP99 | |
| : undefined, | |
| maxTime: | |
| budget.absolute.maxTime !== undefined | |
| ? typeof budget.absolute.maxTime === 'string' | |
| ? parseTimeString(budget.absolute.maxTime) | |
| : budget.absolute.maxTime | |
| : undefined, | |
| minOpsPerSec: budget.absolute.minOpsPerSec, | |
| } | |
| : undefined; | |
| // Build relative budget object if present; ensure it's undefined when all properties are undefined | |
| let relative: Budget['relative']; | |
| if (budget.relative) { | |
| const maxRegression = | |
| budget.relative.maxRegression !== undefined | |
| ? typeof budget.relative.maxRegression === 'string' | |
| ? parsePercentageString(budget.relative.maxRegression) | |
| : budget.relative.maxRegression | |
| : undefined; | |
| // Only create the relative object if at least one property is defined | |
| if (maxRegression !== undefined) { | |
| relative = { maxRegression }; | |
| } else { | |
| relative = undefined; | |
| } | |
| } else { | |
| relative = undefined; | |
| } | |
| return { | |
| absolute, | |
| relative, |
| const flattenBudgets = ( | ||
| nested: z.infer<typeof budgetsInputSchema>, | ||
| ): Record<string, Budget> => { | ||
| const flat: Record<string, Budget> = {}; | ||
|
|
||
| // Copy minOpsPerSec as-is (already a number) | ||
| if (budget.absolute.minOpsPerSec !== undefined) { | ||
| transformed.absolute.minOpsPerSec = budget.absolute.minOpsPerSec; | ||
| } | ||
|
|
||
| // Parse time strings | ||
| if (budget.absolute.maxTime !== undefined) { | ||
| transformed.absolute.maxTime = | ||
| typeof budget.absolute.maxTime === 'string' | ||
| ? parseTimeString(budget.absolute.maxTime) | ||
| : budget.absolute.maxTime; | ||
| } | ||
| if (budget.absolute.maxP99 !== undefined) { | ||
| transformed.absolute.maxP99 = | ||
| typeof budget.absolute.maxP99 === 'string' | ||
| ? parseTimeString(budget.absolute.maxP99) | ||
| : budget.absolute.maxP99; | ||
| for (const [file, suites] of Object.entries(nested)) { | ||
| for (const [suite, tasks] of Object.entries(suites)) { | ||
| for (const [task, budget] of Object.entries(tasks)) { | ||
| const taskId = `${file}/${suite}/${task}`; | ||
| flat[taskId] = budget; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (budget.relative) { | ||
| transformed.relative = {}; | ||
| return flat; | ||
| }; |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The budget transformation logic introduced in this PR lacks comprehensive test coverage. The existing tests in test/unit/config/budget-config-integration.test.ts verify that parsing succeeds, but don't verify the exact shape of the transformed output.
Consider adding tests that verify:
- Budgets with only some properties set don't create objects with all-undefined properties
- String values like "10ms" are correctly transformed to nanoseconds
- Percentage strings like "15%" are correctly transformed to decimals
- The nested file→suite→task structure is correctly flattened to taskId keys
These tests would help catch the issues with empty object creation and ensure the transforms behave correctly.

Replace manual result mutation in safeParseConfig/safeParsePartialConfig
with proper Zod .transform() methods:
JSON Schema generation schemas
(transforms can't be represented in JSON Schema)
🤖 Generated with Claude Code
Co-Authored-By: Claude [email protected]